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则需要手动释放锁

posted @ 2021-02-03 20:57  kozz  阅读(122)  评论(0编辑  收藏  举报