Hi!请登陆

咀嚼Lock和Synchronized锁

2020-11-13 26 11/13

1.Synchronized锁

底层是monitor监视器,每一个对象再创建的时候都会常见一个monitor监视器,在使用synchronized代码块的时候,会在代码块的前后产生一个monitorEnter和monitorexit指令,来标识这是一个同步代码块。

1.1 执行流程

线程遇到同步代码块,给这个对象monitor对象加1,当线程退出当前代码块以后,给这个对象的monitor对象减一,如果monitor指令的值为0则当前线程释放锁。

1.2 反编译源码

同步代码块反编译

两次monitorexit的作用是避免同步代码块无法跳出,因此存在两种,正常退出和异常退出

同步方法反编译

可以发现其没有在同步方法前后添加monitor指令,但是在其底层实际上也是通过monitor指令实现的,只不过相较于同步代码块来说,他是隐式的。

1.3 锁升级

在JDK1.5的时候对于synchronzied做了一系列优化操作,增加了诸如:偏向锁,轻量级锁,自旋锁,锁粗化,重量级锁的概念。

1.3.1 偏向锁

在一个线程在执行获取锁的时候,当前线程会在monitor对象中存储指向该线程的ID。当线程再次进入的时候,不需要通过CAS的方法再来进行加锁或者解锁,而是检测偏向锁的ID是不是当前要进行的线程,如果是,直接进入。

偏向锁,适用于一个线程执行任务的情况

在JDK1.6中,默认是开启的。可以通过-XX:-UseBiasedLocking=false参数关闭偏向锁

1.3.2 轻量级锁

轻量级锁是指锁为偏向锁的时候,该锁被其他线程尝试获取,此时偏向锁升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,线程不会阻塞,从而提供性能

升级为轻量级锁的情况有两种:

关闭偏向锁

有多个线程竞争偏向锁的时候

具体实现:

线程进行代码块以后,如果同步对象锁状态为无锁的状态,虚拟机将首先在当前线程的栈帧中创建一个锁记录的空间。这个空间内存储了当前获取锁的对象。

使用情况:

两个线程的互相访问

1.3.3 重量级锁

在有超过2个线程访问同一把锁的时候,锁自动升级为重量级锁,也就是传统的synchronized,此时其他未获取锁的线程会陷入等待状态,不可被中断。

由于依赖于monitor指令,所以其消耗系统资源比较大

上面的三个阶段就是锁升级的过程

1.3.4 锁粗化

当在一个循环中,我们多次使用对同一个代码进行加锁,这个时候,JVM会自动实现锁粗化,即在循环外进行添加同步代码块。

代码案例:

锁粗化之前:

锁粗化之后:

本次关于synchronized的底层原理没有以代码的方式展开,之后笔者会出一篇synchronized底层原理剖析的文章

2. Lock锁

一个类级别的锁,需要手动释放锁。可以选择性的选择设置为公平锁或者不公平锁。等待线程可以被打断。

底层是基于AQS+AOS。AQS类完成具体的加锁逻辑,AOS保存获取锁的线程信息

2.1 ReentrantLock

我们以ReentrantLock为例解析一下其加锁的过程。

2.1.1 lock方法

首先通过ReentrantLock的构造方法的布尔值判断创建的锁是公平锁还是非公平锁。

假设现在创建的是非公平锁,他首先会判断锁有没有被获取,如果没有被获取,则直接获取锁;

如果锁已经被获取,执行一次自旋,尝试获取锁。

如果锁已经被获取,则将当前线程封装为AQS队列的一个节点,然后判断当前节点的前驱节点是不是HEAD节点,如果是,尝试获取锁;如果不是。则寻找一个安全点(线程状态位SIGNAL=-1的节点)。

开始不断自旋。判断前节点是不是HEAD节点,如果是获取锁,如果不是挂起。

源码解读:

非公平锁lock

2.1.2 unlock方法

代码解读:

2.1.3 Node节点

3. Lock锁和Synchronized的区别

Lock锁是API层面,synchronized是CPU源语级别的

Lock锁等待线程可以被中断,synchronized等待线程不可以被中断

Lock锁可以指定公平锁和非公平锁,synchronized只能为非公平锁

Lock锁需要主动释放锁,synchronized执行完代码块以后自动释放锁

相关推荐