工作中我们经常会遇到 Java 常见的加锁方法,本文着重介绍最常见的 synchronized 与 ReentrantLock 的区别,以及说明在动态高并发时为什么推荐 ReentrantLock 而不是 Synchronized?
底层实现上来说,synchronized 是 JVM 层面的锁,是 Java 关键字,通过 monitor 对象来完成(monitorenter 与 monitorexit),对象只有在同步块或同步方法中才能调用 wait / notify 方法,ReentrantLock 是从 jdk1.5 以来(java.util.concurrent.locks.Lock)提供的 API 层面的锁。
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向 OS 申请重量级锁,ReentrantLock 实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和 volatile
保证数据可见性以实现锁的功能。
synchronized (new Object()){
}
new ReentrantLock();
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用。
ReentrantLock 则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过 lock() 和 unlock() 方法配合 try / finally 语句块来完成,使用释放更加灵活。
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private AtomicInteger atomicInteger;
public void increment() throws Exception {
lock.lock();
try {
while (number != 0) {
condition.await();
}
//do something
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
synchronized 是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成。
ReentrantLock 则可以中断,可通过 trylock(long timeout,TimeUnit unit) 设置超时方法;或者将 lockInterruptibly() 放到代码块中,调用 interrupt 方法进行中断。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
synchronized 为非公平锁。
ReentrantLock 则即可以选公平锁也可以选非公平锁,通过构造方法 new ReentrantLock 时传入 boolean 值进行选择,为空默认 false 非公平锁,true 为公平锁。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
synchronized 不能绑定。
ReentrantLock 通过绑定 Condition 结合 await() / singal() 方法实现线程的精确唤醒,而不是像 synchronized 通过 Object 类的 wait() / notify() / notifyAll() 方法要么随机唤醒一个线程要么唤醒全部线程。
示例:用ReentrantLock绑定三个条件实现线程A打印一次1,线程B打印两次2,线程C打印三次3
class Resource {
private int number = 1;//A:1 B:2 C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//1 判断
public void print1() {
lock.lock();
try {
//判断
while (number != 1) {
c1.await();
}
//2 do sth
for (int i = 1; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print2() {
lock.lock();
try {
//判断
while (number != 2) {
c2.await();
}
//2 do sth
for (int i = 1; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print3() {
lock.lock();
try {
//判断
while (number != 3) {
c3.await();
}
//2 do sth
for (int i = 1; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print1();
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print2();
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print3();
}
},"C").start();
}
输出结果为:
A 1 B 2 B 2 C 3 C 3 C 3 A 1 B 2 B 2 C 3 C 3 C 3
synchronzied 锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁 / 争抢锁。
ReentrantLock 锁的是线程,根据进入的线程和 int 类型的 state 标识锁的获得 / 争抢。
Synchronized 和 ReentrantLock 我们都很熟悉了,作为 java 中最常用的本地锁,最初版本中 ReentrantLock 的性能是远远强于 Synchronized 的,后续 java 在一次次的版本迭代中对 Synchronized 进行了大量的优化,直到 jdk1.6 之后,两种锁的性能已经相差无几,甚至 Synchronized 的自动释放锁会更好用。
在面试时被问到 Synchronized 和 ReentrantLock 的使用选择时,很多朋友都脱口而出的说用 Synchronized ,甚至在我面试的时候问候选者,也很少有人能够答出所以然来,下面我们来详细分析下两种的对比。
在 java 代码中 synchronized 的使用是非常简单的。
程序运行期间,Synchronized 那一块儿代码发生么什么?来看一张图:
在多线程运行过程中, 线程会去先抢对象的监视器 ,这个监视器是对象独有的,其实就相当于一把钥匙,抢到了,那你就获得了当前代码块的执行权。
其他没有抢到的线程会进入队列 (SynchronizedQueue) 当中等待,等待当前线程执行完后,释放锁。
最后,当前线程执行完毕后通知出队然后继续重复当前过程。
从 JVM 的角度来看 monitorenter 和 monitorexit 指令代表着代码的执行与结束 。
SynchronizedQueue 是一个比较特殊的队列,它没有存储功能,它的功能就是维护一组线程,其中每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其实没有任何一个元素,或者说容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用 peek 操作,因为只有移除元素时才有元素。
举个例子:
喝酒的时候, 先把酒倒入酒盅,然后再倒入酒杯,这就是正常的队列 。
喝酒的时候, 把酒直接倒入酒杯,这就是 SynchronizedQueue 。
这个例子应该很清晰易懂了,它的好处就是可以直接传递,省去了一个第三方传递的过程。
在 jdk1.6 以前,Synchronized 是一个重量级锁,还是先贴一张图:
这就是为什么说,Synchronized 是一个重量级锁的原因, 因为每一次锁的资源都是直接和 cpu 去申请的,而 cpu 的锁数量是固定的 ,当 cpu 锁资源使用完后还会进行锁等待,这是一个非常耗时的操作。
但是在 jdk1.6,针对代码层面进行了大量的优化,也就是我们常说的锁升级的过程:无锁–》偏向锁–》自旋锁–》重量级锁。
这张图是对象头中 markword 的数据结构 ,锁的信息就是在这里存放的,很清楚的表明了锁在升级的时候锁信息的变动, 其实就是通过二进制的数值,来对对象进行一个标记,每个数值代表一种状态 。
这个问题和我们的题目就有很大的关联了。
在 HotSpot 虚拟机中是有锁降级的, 但是仅仅只发生在 STW 的时候 ,只有垃圾回收线程能够观测到它,也就是说, 在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。
所以题目的答案,你懂了吗?哈哈,我们接着往下走。
ReentrantLock 的使用也是非常简单的,与 Synchronized 的不同就是需要自己去手动释放锁,为了保证一定释放,所以通常都是和 try~finally 配合使用的。
ReentrantLock 意为可重入锁,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是使用 AQS 去实现的。
ReentrantLock 有两种模式,一种是公平锁,一种是非公平锁。
这就是 ReentrantLock 的结构图,我们看这张图其实是很简单的,因为主要的实现都交给 AQS 去做了,我们下面着重聊一下 AQS。
AQS(AbstractQueuedSynchronizer):AQS 可以理解为就是一个可以实现锁的框架。
简单的流程理解:
读完以上的部分相信你对 AQS 已经有了一个比较清楚的概念了,所以我们来聊聊小细节。
AQS 使用 state 同步状态 (0代表无锁,1代表有),并暴露出 getState 、 setState 以及 compareAndSet 操作来读取和更新这个状态,使得仅当同步状态拥有一个期望值的时候,才会被原子地设置成新值。
当有线程获取锁失败后,AQS 是通过一个双向的同步队列来完成同步状态的管理,就被添加到队列末尾。
这是定义头尾节点的代码,我们可以先使用 volatile 去修饰的,就是保证让其他线程可见,AQS 实际上就是修改头尾两个节点来完成入队和出队操作的。
AQS 在锁的获取时,并不一定只有一个线程才能持有这个锁,所以此时有了独占模式和共享模式的区别,我们本篇文章中的 ReentrantLock
使用的就是独占模式,在多线程的情况下只会有一个线程获取锁。
独占模式的流程是比较简单的,就根据 state 是否为 0 来判断是否有线程已经获得了锁,没有就阻塞,有就继续执行后续代码逻辑。
共享模式的流程根据 state 是否大于 0 来判断是否有线程已经获得了锁,如果不大于 0,就阻塞,如果大于 0,通过 CAS 的原子操作来自减 state 的值,然后继续执行后续代码逻辑。
ReentrantLock 和 Synchronized 的区别:
动态高并发时为什么推荐 ReentrantLock 而不是 Synchronized?
答案:Synchronized 升级为重量级锁后无法在正常情况下完成降级,而 ReentrantLock 是通过阻塞来提高性能的,在设计模式上就体现出了对多线程情况的支持。