Lock接口
与Sychronized
利用JVM指令级别的monitor锁,来实现线程安全不同的是,Lock接口实现线程安全则是代码级别实现的,Lock接口
是 Java并发编程中很重要的一个接口,当程序发生异常时,Sychronized
可以自动释放锁,但Lock必须需要手动解锁。与 Lock
关联密切的锁有 ReetrantLock
和 ReadWriteLock
。我们以ReentrantLock
切入,来看看其底层涉及到的原理。
初识ReentrantLock
ReentrantLock
也叫重入锁,我们首先得了解ReentrantLock的一般使用方法:
Lock lock = new ReentrantLock(false);
lock.lock();
try{
//临界区,执行一些具体操作。。
}finally{
lock.unlock();
}
代码层次必须要手动解锁。
公平锁和非公平锁
查看ReentrantLock
的源码可以发现:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当参数fair为true表示公平锁,创建的是FairSync类
;false为非公平锁,创建的是NonfairSync
类。如果该参数不填,则默认是非公平锁,调用另一个构造方法。
-
那什么是公平锁和非公平锁?
-
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁,遵循先来先得的规则。
-
非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁
我们接下来举个例子来看看:首先创建公平锁,开启6个线程执行,分别加锁和释放锁并打印线程名的操作:
public class FairReentrantLockTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 6; i++) {
new Thread(() -> {
lock.lock();
System.out.println("临界区的当前线程名称:" + Thread.currentThread().getName());
lock.unlock();
}).start();
}
}
}
结果:
临界区的当前线程名称:Thread-0
临界区的当前线程名称:Thread-1
临界区的当前线程名称:Thread-2
临界区的当前线程名称:Thread-3
临界区的当前线程名称:Thread-4
临界区的当前线程名称:Thread-5
如果我们把公平锁换成非公平锁的话,static Lock lock = new ReentrantLock(false)
,再执行一遍结果为:
临界区的当前线程名称:Thread-0
临界区的当前线程名称:Thread-5
临界区的当前线程名称:Thread-1
临界区的当前线程名称:Thread-2
临界区的当前线程名称:Thread-3
临界区的当前线程名称:Thread-4
我们可以发现:当使用公平锁,线程获取锁的话,线程进入"等待队列"的队尾,得排队,依次获取锁,先到先得。如果使用的是非公平锁,那就直接尝试竞争锁,竞争得到,就获得锁,获取锁的顺序是随机的。
-
公平锁和非公平锁的优缺点?
-
公平锁,其优点:所有的线程都能得到资源,不会饿死在队列中;缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,线程 每次从阻塞恢复到运行状态 都需要从用户态转换成内核态,而这个状态的转换是比较慢的,因此公平锁的执行速度会比较慢,而且CPU唤醒阻塞线程的开销会很大。
-
非公平锁,其优点:不遵守先到先得的原则,CPU不必取唤醒所有线程,会减少唤起线程的数量,可以减少CPU唤醒线程的开销,整体的吞吐效率会高点。缺点:但这样也可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致"饿死"。
我们这里贴一下公平锁和非公平锁的性能测试结果图,来源于《Java并发编程实战》:
从上述结果可以看出,使用非公平锁的性能(吞吐率)普遍比公平锁高很多。