Lock的核心API
方法 | 描述 |
---|---|
lock | 获取锁的方法,若锁被其他线程获取,则等待(阻塞) |
lockInterruptibly | 在锁的获取过程中可以中断当前线程 |
tryLock | 尝试非阻塞地获取锁,立即返回 |
unlock | 释放锁 |
提示:根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语言。
首先我们根据方法的内容自己来实现一个简单的Lock
public class kfLock implements Lock {
//当前锁的使用者
volatile AtomicReference<Thread> owner = new AtomicReference<>();
// java q 线程安全
volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
public boolean tryLock(){
return owner.compareAndSet(null,Thread.currentThread());
}
public void lock(){
boolean addQ = true;
while (!tryLock()){
if (addQ){
//塞到等待线程队列中
waiters.offer(Thread.currentThread());
addQ = false;
}else {
//挂起这个线程
LockSupport.park();
// 后续,等待其他线程释放锁,收到通知之后继续循环
}
}
waiters.remove(Thread.currentThread());
}
@Override
public void unlock(){
// cas 修改 owner 拥有者
if (owner.compareAndSet(Thread.currentThread(), null)) {
Iterator<Thread> iterator = waiters.iterator();
while (iterator.hasNext()) {
Thread waiter = iterator.next();
LockSupport.unpark(waiter); // 唤醒线程继续 抢锁
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
}
ReentrantLock
特征:独享锁;支持公平锁、非公平锁两种模式;可重入锁。
默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁。
ReentrantLock lock = new ReentrantLock(true);
继续跟进看下源码
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。
公平锁和非公平锁该如何选择?
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。
lockInterruptibly()的作用
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
。该方法可以用来解决死锁问题。
// 可响应中断
public class LockInterruptiblyDemo1 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockInterruptiblyDemo1 demo1 = new LockInterruptiblyDemo1();
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
demo1.test(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(500); // 等待0.5秒,让thread1先执行
thread2.start();
Thread.sleep(2000); // 两秒后,中断thread2
thread2.interrupt();
}
public void test(Thread thread) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ", 想获取锁");
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName() + "得到了锁");
Thread.sleep(10000); // 抢到锁,10秒不释放
} finally {
System.out.println(Thread.currentThread().getName() + "执行finally");
lock.unlock();
System.out.println(thread.getName() + "释放了锁");
}
}
}
获取锁时限时等待
ReentrantLock还给我们提供了获取锁限时等待的方法tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
Condition
Condition由ReentrantLock对象创建,并且可以同时创建多个
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。
- 一个使用condition的简单例子
// condition 实现队列线程安全。
public class QueueDemo {
final Lock lock = new ReentrantLock();
// 指定条件的等待 - 等待有空位
final Condition notFull = lock.newCondition();
// 指定条件的等待 - 等待不为空
final Condition notEmpty = lock.newCondition();
// 定义数组存储数据
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 写入数据的线程,写入进来
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) // 数据写满了
notFull.await(); // 写入数据的线程,进入阻塞
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 唤醒指定的读取线程
} finally {
lock.unlock();
}
}
// 读取数据的线程,调用take
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 线程阻塞在这里,等待被唤醒
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知写入数据的线程,告诉他们取走了数据,继续写入
return x;
} finally {
lock.unlock();
}
}
}
ReadWriteLock
维护一对关联锁,一个用于只读操作,一个用于写入;读锁可以由多个读线程同时持有,写锁是排他的。
适合读取线程比写入线程多的场景,改进互斥锁的性能,示例场景:缓存组件、集合的并发线程安全改造。
锁降级:指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。
写锁是线程独占,读锁是线程共享,所以写 -> 读是升级。(读 -> 写,是不能实现的)
// 读写锁(既保证了读数据的效率,也保证数据的一致性)
public class ReentrantReadWriteLockDemo {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReentrantReadWriteLockDemo readWriteLockDemo = new ReentrantReadWriteLockDemo();
// 多线程同时读/写
new Thread(() -> {
readWriteLockDemo.read(Thread.currentThread());
}).start();
new Thread(() -> {
readWriteLockDemo.read(Thread.currentThread());
}).start();
new Thread(() -> {
readWriteLockDemo.write(Thread.currentThread());
}).start();
}
// 多线程读,共享锁
public void read(Thread thread) {
readWriteLock.readLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行“读”操作");
}
System.out.println(thread.getName() + "“读”操作完毕");
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* 写
*/
public void write(Thread thread) {
readWriteLock.writeLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行“写”操作");
}
System.out.println(thread.getName() + "“写”操作完毕");
} finally {
readWriteLock.writeLock().unlock();
}
}
}
// 缓存示例
public class CacheDataDemo {
// 创建一个map用于缓存
private Map<String, Object> map = new HashMap<>();
private static ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
// 1 读取缓存里面的数据
// cache.query()
// 2 如果换成没数据,则取数据库里面查询 database.query()
// 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
}
public Object get(String id) {
Object value = null;
// 首先开启读锁,从缓存中去取
rwl.readLock().lock();
try {
if (map.get(id) == null) {
// TODO database.query(); 全部查询数据库 ,缓存雪崩
// 必须释放读锁
rwl.readLock().unlock();
// 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
rwl.writeLock().lock(); // 所有线程在此处等待 1000 1 999 (在同步代码里面再次检查是否缓存)
try {
// 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
if (map.get(id) == null) {
// TODO value = ...如果缓存没有,就去数据库里面读取
}
rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性
} finally {
rwl.writeLock().unlock(); // 释放写锁@
}
}
} finally {
rwl.readLock().unlock();
}
return value;
}
}