显示锁
1. 什么是显示锁
- 显示锁是JDK 1.5新增的同步机制,本质是使用
Lock
接口以及相关的实现类,来实现锁的功能; Lock
接口提供了和Synchronized
关键字类似的同步功能,但是和Synchronized
不同的是Lock
拓展了更多的同步方式,使得用锁变得更加的灵活
2. 显示锁的使用
1) Lock显示锁的使用范式
-
如果要使用显示锁,是存在使用范式的,正确的使用显示锁才能保证加锁的有效性
/** * 使用 显示锁 的规范 */ //先定义一个显示锁 Lock lock = new ReentrantLock(); void func1() { // 使用 显示锁 的规范 lock.lock(); try{ count++; } finally { // 打死都要执行 最后一定会执行 lock.unlock(); } }
如上,使用显示锁,必须将解锁操作,放到
finally
代码块中去执行,因为只有放在finally
代码块中,才能保证除了守护线程之外,在其他的正常的工作线程中必定会被执行,从而避免业务代码出错导致的锁未释放,进而造成问题。
2) 显示锁的使用示例
-
显示锁的简单使用见下面的例子:
/** * 使用显示锁的范式 */ public class LockDemo { private int count = 0; // 内置锁 == this private synchronized void test() { } // 内置锁 == LockDemo.class private static synchronized void test2() { } private synchronized void test3() { // 业务逻辑,无法被中断 } // 声明一个显示锁之可重入锁 new 可重入锁 // 非公平锁 private Lock lock = new ReentrantLock(); public void incr(){ // 使用 显示锁 的规范 lock.lock(); try{ count++; } finally { // 打死都要执行 最后一定会执行 lock.unlock(); } } // 可重入锁🔒 意思就是递归调用自己,锁可以释放出来 // synchronized == 天生就是 可重入锁🔒 // 如果是非重入锁🔒 ,就会自己把自己锁死 public synchronized void incr2(){ count++; incr2(); } public static void main(String[] args) { LockDemo lockDemo = new LockDemo(); } }
此处举例的是递归调用同一个函数,但是需要明确的是:锁的出现是为了保证多线程的数据安全,也就是说锁其实针对的是线程,而非某一个函数,如下面的示例:
class test { public Object obj = new Object(); public void test1() { synchronized(obj) { test2(); ...... } } public void test2() { synchronized(obj) { ...... } } public static void main(String[] args) { test t = new test(); test.test1(); } }
如上面例子中,程序也是可以正确执行的,至于为什么,可见下面AQS的分析。
3. Lock接口
1) Lock接口中的函数
-
Lock
接口中的方法描述方法名 说明 lock() 获取锁(不死不休) tryLock() 获取锁(浅尝则止) tryLock(long time, TimeUnit unit) 获取锁(浅尝则止且过时不候) lockInterruptibly() 获取锁(任人摆布) newCondition() 返回一个新创建的Condition对象 unlock() 释放锁
2) Lock
的类关系
Lock
只是个接口,具体使用可以使用JDK提供的派生继承类,或者自己继承Lock
接口来实现,接下来介绍一下JDK提供的类:
- 如上图所示,继承自
Lock
接口的有:ReentrantLock
、ReentrantReadWriteLock
3)Lock
中的newCondition()
方法
-
对于显示锁,我们可以通过
newCondition()
方法来获取一个Condition
对象,那么我们先来看一下Condition
类public interface Condition { void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long var1) throws InterruptedException; boolean await(long var1, TimeUnit var3) throws InterruptedException; boolean awaitUntil(Date var1) throws InterruptedException; void signal(); void signalAll(); }
如上,其实观察
Condition
接口中的方法,也可以得知,Condition
接口和wait
/notify
的作用是一致的,SDK有给出使用示例,如下:class BoundedBuffer { //定义可重入显示锁 final Lock lock = new ReentrantLock(); //创建两个Condition对象 final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) { //当count==length,即存储满了之后,阻塞生产线程 notFull.await(); } items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; //当count != 0,即存在数据时,notEmpty发出signal,唤醒消费线程 notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) //如果当前count==0,即没有数据时,阻塞获取线程,调用await notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; //当消费了数据,即count != length时,notFull发出signal,唤醒生产线程 notFull.signal(); return x; } finally { lock.unlock(); } } }
-
signal()
/signalAll()
-
signal()
/signalAll()
两者之间的差别和notify()
/notifyAll()
之间的区别是一致的,即:signal()
随机唤醒对应Condition
对象上的任意一个等待线程signalAll()
唤醒对应Condition
对象上的所有等待线程
-
signal()
/signalAll()
的使用和notify()
/notifyAll()
一样,同样有使用范式,使用范式如下:final Lock lock = new ReentrantLock(); final Condition Con = lock.newCondition(); //wait: lock.lock(); try { while(条件不满足) { Con.await(); } //业务逻辑 } finally { lock.unlock(); } //signal/signalall lock.lock(); try { //业务逻辑 //do something Con.signal()/signalAll(); } finally { lock.unlock(); }
-
-
Condition
中的await()
和signal()
对锁的持有状态以及对线程状态的改变和wait()
/notify()
是完全一致的
4. 显示锁的原理
- 其实当我们深入去看
ReentrantLock
或者ReentrantReadWriteLock
时,我们会发现,本质上显示锁的实现就是Lock
接口和AQS的结合实现的,那么到底是如何实现的,AQS又是什么,请看AQS解析。
AQS(AbstractQueuedSynchronizer)
1.什么是AQS
AbstractQueuedSynchronizer
是一个抽象类- AQS是队列同步器,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
2. AQS的使用方式
-
在大多场景下,AQS的使用都是在同步工具类的内部,存在一个内部类继承了AQS,并且针对AQS的方法进行了一层封装,所以我们无法直接看到AQS的使用
-
在AQS中,存在一个
int
型的state
变量来代表同步状态,那么在抽象方法的实现过程中,我们肯定会对这个变量进行操作,在AQS中提供了3个方法来进行操作:protected final int getState()
- 获取当前同步状态
protected final void setState(int newState)
- 设置当前同步状态
protected final boolean compareAndSetState(int expect, int update)
- 使用CAS设置当前状态,该方法能够保证状态设置的原子性
我们只能通过这三个方法对
state
变量进行操作,因为这三个方法可以保证状态的改变是安全的。 -
AQS自身是没有实现任务同步接口的,它只是定义了若干同步状态的获取和释放来供自定义同步组件的使用,我们可以通过它实现不同类型的同步组件
-
AQS是实现同步组件(例如锁)的关键,我们可以这样理解这两者的关系:
-
可以这样认为:同步组件和AQS(同步器)很好的隔离了使用者和实现者所需要关注的领域。实现者需要继承同步器(AQS)并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,通过这些模板方法去调用实现者重写的方法。
3. AQS中的设计模式
-
AQS的设计基于模板方法模式
-
模板方法:定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中;模板方法的意义就在于,可以使得子类在不改变一个算法的结构的前提下,即可重定义该算法的某些特定步骤
-
如果我们需要自定义同步组件,那么我们就需要调用AQS提供的模板方法:
- 可被我们重写的方法
-
4. AQS使用实例
/**
* 我们自己实现的同步独占锁需要继承Lock接口,
* 并实现最基本的Lock()和unlock()方法
* -----> 存在缺陷,当前的独占锁是不可重入的!!!!
*/
class UserAQSLock implements Lock {
//在我们的同步组件中,定义一个AQS内部类,并重写所需的方法
private static class UserSync extends AbstractQueuedSynchronizer {
//重写获取锁的方法
@Override
protected boolean tryAcquire(int arg) {
//直接使用原子操作,判断当前state状态值并写入
if (compareAndSetState(0, 1)) {
//将当前线程记录进入
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//重写释放锁的方法
@Override
protected boolean tryRelease(int arg) {
//判断当前同步器状态
if (getState() == 0) {
//如果当前未持有锁,直接返回
throw new IllegalMonitorStateException();
}
//如果当前状态不为0,即锁被持有,则重置状态
setState(0);
//释放占用的线程
setExclusiveOwnerThread(null);
return true;
}
}
//实例化一个UserSync同步器对象,用这个对象来进行同步操作的实现
UserSync sync = new UserSync();
@Override
public void lock() {
// System.out.println("before lock");
/**
* tryAcquire()是我们进行重写的方法,
* 外部调用是使用acquire()方法,在acquire()方法中会调用到tryAcquire()
* 至于acquire()方法中的变量,因为我们在重写的时候并未使用,所以可以随便给值
*/
sync.acquire(0);
// System.out.println("after lock");
}
@Override
public void unlock() {
// System.out.println("before unlock");
/**
* 同 acquire()方法,release()是供给外部进行调用的,而在release()
* 方法中最终是调用 tryrelease()
*/
sync.release(0);
// System.out.println("after unlock");
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
//两个线程实现累加操作
class AqsTest {
public static volatile int count;
public static UserAQSLock lock = new UserAQSLock();
private static class userRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
lock.lock();
count++;
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new userRunable()).start();
new Thread(new userRunable()).start();
Thread.sleep(1500);
System.out.println("count = " + count);
}
}
/**
* 实现我们自定义的一个可重入的独占锁
* 要实现可重入的功能,其实只需要在原来
* 不可重入锁的基础上加一点逻辑就可以了
* 差异就是tryAcquire()和tryRelease()
*/
//重写获取锁的方法
@Override
protected boolean tryAcquire(int arg) {
//直接使用原子操作,判断当前state状态值并写入
if (compareAndSetState(0, 1)) {
//将当前线程记录进入
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
/**
* 如果想要锁可重入,那么在判断有线程持锁后,
* 增加一步判断,判断当前持锁的线程是否是当前线程本身,
* 如果是当前线程本身持锁,那么将State进行加一,并正常返回
*/
setState(getState() + 1);
return true;
}
return false;
}
//重写释放锁的方法
@Override
protected boolean tryRelease(int arg) {
//判断当前同步器状态
if (getState() == 0) {
//如果当前未持有锁,直接返回
throw new IllegalMonitorStateException();
} else {
/**
* 如果是可重入的锁,则State状态可能不为1,即
* 可能存在同一个线程多次持了锁,那么在所有持
* 锁次数减完之前,不能直接将锁直接释放,只能
* 先进行State状态的改变
*/
setState(getState() - 1);
if (getState() == 0) {
/**
* 如果减完状态为0.那么代表当前线程所有递归层级都已释放了锁,
* 那么就可以将锁释放
*/
//释放占用的线程
setExclusiveOwnerThread(null);
}
}
return true;
}
//递归调用可重入锁
class ReetrantTest {
public static UserReentrantLock Sync_B = new UserReentrantLock();
public static void reenter(int x){
Sync_B.lock();
try {
System.out.println(Thread.currentThread().getName()+":递归层级:"+x);
int y = x - 1;
if (y==0) return;
else{
reenter(y);
}
} finally {
Sync_B.unlock();
}
}
public static class UserRunnable implements Runnable {
@Override
public void run() {
reenter(3);
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new UserRunnable()).start();
}
}
5. CLH队列锁
-
什么是CLH队列锁
-
如果我们对某一个代码块进行了加锁操作,那么就意味着在同一时间,只有一个线程可以进入该代码块进行执行,那么其他想要执行该代码块的线程就只能等待获取到锁的线程执行完毕释放锁后再去竞争锁,而记录这些等待锁的线程就是CLH队列锁的工作。
-
CLH队列锁是一种基于链表的可拓展、高性能、公平的自旋锁,申请锁的线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱节点线程释放了锁就结束自旋。
-
CLH队列锁的单个节点至少由以下几个部分构成:
- myPred:用来记录前驱节点
- locked:用来表示当前节点线程是否需要获取锁
- mThread:当前节点代表的线程
由这三部分组成一个QNode节点,而这样的一个个QNode节点串联起来就组成了CLH队列锁。
-
-
CLH队列锁的运行模式:
-
在线程0持锁的前提下,线程1也想要持锁
- 此时会创建一个QNode_1节点,并将locked置为
TRUE
,表示该线程1需要获得锁,myPred指向前置节点,表示对其前驱节点的引用,然后CLH队列尾部引用tail
,调用CASgetAndset()
方法,将QNode_1设置为队列锁的队尾
- 此时会创建一个QNode_1节点,并将locked置为
-
此时又有线程2也想要持锁
- 按照1的流程再来一遍,此时创建的QNode_2节点,locked也是
TRUE
,myPred指向QNode_1,QNode_2成为队尾 - 此时线程2就在线程1的locked字段上旋转,直到前驱节点释放锁(前驱节点的锁值
locked == false
) - 当一个线程需要释放锁时,将当前节点的locked设置为
false
,同时回收其前驱节点
- 按照1的流程再来一遍,此时创建的QNode_2节点,locked也是
-
从队列锁的运行模式可以很清楚的看出,CLH是一个公平锁,因为它必须等到前驱节点locked等于
false
,当前线程节点才能去尝试获取锁 -
AQS其实是CLH队列锁的一种变体实现。
-
-
公平锁和非公平锁
-
公平锁和非公平锁的区别:
- 公平锁即像CLH队列锁这样,每个等待用锁的线程都被记录到链表中,而每一个线程能够获得锁的前提是前一个节点的线程释放锁,这就保证了先等待的线程肯定会先拿锁,所以CLH队列锁是公平锁
- 非公平锁即等待用锁的线程不被记录到链表,直接抢占拿锁,这就是非公平锁
-
公平锁和非公平锁代码差异
-
公平锁和非公平锁代码实现上基本都是相同的,只是公平锁比非公平多了一个判断条件;
ReentrantLock
可重入锁中就有非公平锁和公平锁的实现://公平锁 protected final boolean tryAcquire(int var1) { Thread var2 = Thread.currentThread(); int var3 = this.getState(); if (var3 == 0) { //this.hasQueuedPredecessors()会判断当前队列中是否有线程在等,如果有等待,则不再去尝试拿锁 if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) { this.setExclusiveOwnerThread(var2); return true; } } else if (var2 == this.getExclusiveOwnerThread()) { int var4 = var3 + var1; if (var4 < 0) { throw new Error("Maximum lock count exceeded"); } this.setState(var4); return true; } return false; } //非公平锁 final boolean nonfairTryAcquire(int var1) { Thread var2 = Thread.currentThread(); int var3 = this.getState(); if (var3 == 0) { //非公平锁就没有当前队列是否存在元素的判断,他会直接去尝试拿锁 if (this.compareAndSetState(0, var1)) { this.setExclusiveOwnerThread(var2); return true; } } else if (var2 == this.getExclusiveOwnerThread()) { int var4 = var3 + var1; if (var4 < 0) { throw new Error("Maximum lock count exceeded"); } this.setState(var4); return true; } return false; }
-
-
6. 优秀博客
JMM
1. 计算机原理中的CPU存储
-
根据这个表格我们可以看到,CPU的运行速度是远远超过读取数据的速度,而我们像完成一个任务,很显然并不是只依靠处理器就可以完成的,处理器至少需要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是基本上是无法消除的(无法仅靠寄存器来完成所有运算任务)。早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
- 在计算机系统中,寄存器是L0级缓存,接着依次是L1,L2,L3(接下来是内存,本地磁盘,远程存储)。越往上的缓存存储空间越小,速度越快,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1级缓存的缓存,L1是L2的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集。
- 在现代CPU上,一般来说L0, L1,L2,L3都集成在CPU内部,而L1还分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据和执行数据的指令解码。每个核心拥有独立的运算处理单元、控制器、寄存器、L1、L2缓存,然后一个CPU的多个核心共享最后一层CPU缓存L3
2. 什么是JMM
-
JMM内存模型即 Java Memory Model,它定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作方式。
-
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
-
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
3. 根据JMM内存模型带来的并发数据安全问题
-
根据JMM内存模型定义:
- 每个线程都有自己的私有的工作内存,不同线程之间不允许相互访问(有点类似ThreadLocal)
- 每个线程不允许直接从主内存中获取数据,即当存在一个线程想要主内存中的数据进行操作时,它必须先将主内存中的数据拷贝一份到当前线程所独有的工作内存中,进而才能进行对应操作
-
带来的问题
-
根据JMM内存模型的定义,以下面的实例来说明,JMM内存模型所带来的并发数据安全问题:
-
假设我们当前在线程中执行
count++
的操作,本质上根据JMM内存模型,在计算机中,需要执行这条指令,有很多必须的步骤: -
根据JMM内存模型,线程无法直接操作主内存,所以我们需要先将数据加载到线程内部空间中,除了最后的同步数据以外,本质上,在线程中操作的都是主内存数据的副本,而又因为每个线程都有各自私有的工作内存,是不互通共享的,即数据的变化是不可见的,这就会导致数据的安全问题
-
那么如果将变量声明为
volatile
呢?是否可以解决数据安全问题?显然,答案是否定的!因为volatile
变量只能保证数据的可见性,但是由于线程內部的这些操作并不是原子操作,即他们是可以被诸如上下文切换等操作打断的,而一旦打断,如果在其他线程修改了值,那么数据又会出现不安全问题。-
volatile
可以保证:- 被其定义的数据,使用时每次必须从主内存中重新读取,即不能是直接使用工作内存的值
- 被其定义的数据,一旦发生改变,会被强制立刻写回主内存中
-
volatile变量自身具有下列特性:
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
-
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
-
总结
volatile
:volatile
是最轻量级的同步机制- 只能保证可见性,无法保证原子性
- 适用于一写多读的场景
- 抑制重排序
- 重排序是CPU执行指令的一种高效能运行方式。
-
-
-