JVM实验五:锁
一.并发的本质是什么
锁的出现,是为了保证只有一个线程可以在同一时刻访问临界区。加锁过程如下图所示:
使用锁的过程需要搞清楚三个概念:谁调用了锁?锁是什么?锁什么资源?
(1)案例一:synchronized修饰非静态方法
1 public class Main { 2 int i; 3 // 修饰非静态方法 4 synchronized void A() { 5 // 临界区 6 i++; 7 } 8 public static void main(String[] args) { 9 Main test = new Main(); 10 Thread t1 = new Thread(new Runnable() { 11 @Override 12 public void run() { 13 test.A(); 14 } 15 }); 16 Thread t2 = new Thread(new Runnable() { 17 @Override 18 public void run() { 19 test.A(); 20 } 21 }); 22 t1.start(); 23 t2.start(); 24 } 25 }
案例一中,使用者是线程一和线程二,锁对象是test实例对象(即this对象),共享资源(也就是临界区)是test对象的i变量
(2)案例二:synchronized修饰静态方法
1 public class Main { 2 static int j; 3 // 修饰静态方法 4 synchronized static void B() { 5 // 临界区 6 j++; 7 } 8 public static void main(String[] args) { 9 Thread t1 = new Thread(new Runnable() { 10 @Override 11 public void run() { 12 Main.B(); 13 } 14 }); 15 Thread t2 = new Thread(new Runnable() { 16 @Override 17 public void run() { 18 Main.B(); 19 } 20 }); 21 t1.start(); 22 t2.start(); 23 } 24 }
案例二中,使用者是线程一和线程二,锁对象是Main.class类对象,共享资源(也就是临界区)是Main.class类对象的i静态变量
二.synchronized的原理
以synchronized代码块为例,从字节码的角度,会在synchronized的入口使用monitorenter指令,在出口使用monitorexit指令
当用 synchronized 标记方法时,会看到字节码中方法的访问标记包括 ACC_SYNCHRONIZED
1.加锁的过程
(1)在上一部分中我们已经讲述了,多个线程会争夺一个锁,而这个锁其实是一个锁对象
(2)当线程A竞争到一个锁对象时,会在锁对象的对象头中记录当前线程的线程ID
mark word中会记录线程A的线程ID,当线程B尝试获取锁对象的时候,会被阻塞
2.锁升级
在锁对象头中的标记字段(mark word),它的最后两位被用来表示该锁对象的锁状态。其中,00 代表轻量级锁,01 代表无锁(或偏向锁),10 代表重量级锁,11 则跟垃圾回收算法的标记有关。锁只能升级,不能降级。
(1)无锁---->偏向锁
当锁对象第一次被一个线程获取时(也就是第一次进入synchronized代码块时),会通过CAS操作修改锁对象的锁标志位,而后在mark word中存储当前线程的ID。当线程执行完同步代码块之后,线程不会主动释放偏向锁。因此当同一个线程下一次还是访问到这个锁对象时,会判断之前持有锁的线程是否就是自己,如果是的话,就无需再加锁了。
因此偏向锁就是只有一个线程一直访问一个同步块的场景。
(2)偏向锁---->轻量级锁
如果这个时候有另一个线程访问了这个同步代码块,它通过对比发现之前持有锁的线程ID和自己的线程ID不一致,那么偏向锁升级为轻量级锁。如果一个锁对象长期持有轻量级锁,说明有多个线程访问同一个同步块,但是这些线程不在同一时间执行临界区,因此不存在锁竞争。
(3)轻量级锁---->重量级锁
在轻量级锁期间,如果发生了锁竞争,获取不到锁的线程不会立即阻塞,而是通过自旋的方式尝试获取锁,但是当自旋次数达到一定阈值或者有第三个线程获取当前锁资源,轻量级锁会膨胀为重量级锁,所有未获取到锁的线程被阻塞。
三.ReentrantLock的原理
1.AQS
https://zhuanlan.zhihu.com/p/54297968
2.synchronized和reentrantlock的区别
(1)底层实现
synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法
ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁
(2)公平锁
synchronized为非公平锁
ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
(3)可中断
synchronized不可中断
reentrantlock可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断
(4)可绑定多个condition
synchronized不能绑定
ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程
(5)使用体验上
synchronized不需要手动释放锁
reentrantlock则需要手动释放锁