通常我们说的并发安全问题,都是由多个线程同时修改公共的资源引起的。由于不同线程同时修改公共资源而导致最终执行的结果不确定。解决这个问题的简单的做法是使用 java 关键字 synchronized 来加锁。关于 synchronized 不是本期的重点,本期我们主要说重入锁。
什么是锁的重入
什么是重入锁?先看下面这段代码:
package com.wuxiaolong.TestConcurrent;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-08-02
*/
public class TestSynchronized {
/**
* 方法A 使用synchronized修饰
*/
public static synchronized void methodA(){
Thread t = Thread.currentThread();
System.out.println(t + "A start");
try {
Thread.sleep(1000);
// 访问带有synchronized修饰的方法B
methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "A end");
}
/**
* 方法B 使用synchronized修饰
*/
public static synchronized void methodB(){
Thread t = Thread.currentThread();
System.out.println(t + "B start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "B end");
}
public static void main(String[] args) {
// 一个线程,内部同时调用方法A,A内部调用方法B
new Thread(){
public void run(){
methodA();
}
}.start();
new Thread(){
public void run(){
methodA();
}
}.start();
}
}
复制代码
这段代码中定义了两个方法:methodA
和methodB
;这两个方法同时都被 synchronized 修饰了。同时methodA
内部调用了methodB
。在main
方法中,新启动了两个子线程,在线程内部调用了methodA
。
这段代码执行的结果是什么样,会出现死锁吗?不看下面的结果,你的答案是什么?
这里是每一个子线程中访问的多个加锁的方法。这两个子线程会有一个先拿到methodA
的同步锁,另一个子线程就会等待。拿到锁的子线程访问第一个方法methodA
时,当执行到methodB
时,methodA
的锁还没释放,methodB
能执行吗?
这里是不会有死锁的。执行结果:
Thread[Thread-0,5,main]A start
Thread[Thread-0,5,main]B start // 入B
Thread[Thread-0,5,main]B end // 出B
Thread[Thread-0,5,main]A end
Thread[Thread-1,5,main]A start
Thread[Thread-1,5,main]B start // 入B
Thread[Thread-1,5,main]B end // 出B
Thread[Thread-1,5,main]A end
复制代码
这里可以看出,Thread-0 在没释放锁时,再次获得了锁进入方法 B。这种现象就是锁的重入。
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
这里可以看出 synchronized 是可重入的锁,如果不可重入,上面的代码就会产生死锁。
可重入锁的意义就在于防止死锁。synchronized 和 ReentrantLock 都是可重入锁。
手写不可重入锁
synchronized 是隐式的加锁;现在我们使用 concurrent 并发包中的 Lock 接口实现自己的不可重入锁和重入锁。先来看一下 Lock 接口的定义。这里我们重点关注 Lock 接口中的lock
和unlock
方法,并实现这两个方法。
实现自己的重入锁的关键就是在 MyLock 类中定义一个成员变量 isLocked,并实现 Lock 接口中的lock
和unlock
方法,通过这两个方法修改 isLocked 的值表示加锁解锁的过程。具体实现如下:
package com.wuxiaolong.TestConcurrent;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-08-02
*/
public class MyLock implements Lock {
/**
* 定义一个变量,标记锁是否被使用
*/
private boolean isLocked = false;
@Override
public void lock() {
// 死循环判断,isLocked是否被使用,如果已经被占用,则进入下一个循环尝试再次获得锁
while(isLocked) {
try {
// 线程进入那个循环体里面,等待50毫秒后,再次尝试获得锁
Thread.sleep(50);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将isLocked变量设置为true,表示本线程已经获得并占用了该锁;其他线程不能再获得锁,必须等待
isLocked = true;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 线程释放锁
isLocked = false;
}
@Override
public Condition newCondition() {
return null;
}
}
复制代码
测试这个不可重入锁:
package com.wuxiaolong.TestConcurrent;
import java.util.concurrent.locks.Lock;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-08-02
*/
public class TestMyLock {
/**
* 先创建一把自己的锁
*/
public static Lock lock = new MyLock();
/**
* 方法A 方法的开始和结束分别手动加锁、解锁
*/
public static void methodA(){
// 手动加锁
lock.lock();
Thread t = Thread.currentThread();
System.out.println(t + "A start");
try {
Thread.sleep(1000);
// 访问带有synchronized修饰的方法B
methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "A end");
// 手动解锁 一般放在finally中
lock.unlock();
}
/**
* 方法B 方法的开始和结束分别手动加锁、解锁
*/
public static void methodB(){
// 手动加锁
lock.lock();
Thread t = Thread.currentThread();
System.out.println(t + "B start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "B end");
// 手动解锁 一般放在finally中
lock.unlock();
}
public static void main(String[] args) {
// 第一个线程,内部同时调用方法A,A内部调用方法B
new Thread(){
public void run(){
methodA();
}
}.start();
// 第二个线程,内部同时调用方法A,A内部调用方法B
new Thread(){
public void run(){
methodA();
}
}.start();
}
}
复制代码
这里在methodA
和methodB
方法的开始和结束分别手动加锁、解锁。
执行结果:
Thread[Thread-0,5,main]A start
// 程序会卡在这里
复制代码
执行的结果可以看到出现了死锁,这就是锁的不可重入导致的。Thread-0 进入methodA
并获得锁,在这个锁没有释放的时候,Thread-0 进入methodB
,这个时候需要再次获得锁,发现锁已经被“别人”拿走了,所以就在这里等啊等,等别人把锁送过来,其实这个锁就在它的兜里,Thread-0 自己不知道,这就会出现死锁。这就是不可重入锁的问题。
对上面的 MyLock 稍加修改就可以实现锁的重入了
手写可重入锁
重入锁的实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为 0 时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM 将记录锁的占有者,并且将请求计数器置为 1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为 0,锁被释放。
按照这个思路,我们修改一下 MyLock:
package com.wuxiaolong.TestConcurrent;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-08-02
*/
public class MyLock2 implements Lock {
/**
* 定义一个变量,标记锁是否被使用
*/
private boolean isLocked = false;
/**
* 第一次线程进来的时候,正在运行的线程为null
*/
private Thread runningThread = null;
/**
* 计数器
*/
private int count = 0;
@Override
public void lock() {
Thread currentThread = Thread.currentThread();
// 死循环判断,isLocked是否被使用,是不是同一个线程在占用,如果已经被占用并且不是同一个线程,则进入下一个循环尝试再次获得锁
while(isLocked && currentThread != runningThread) {
try {
// 线程进入那个循环体里面,等待50毫秒后,再次尝试获得锁
Thread.sleep(50);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将isLocked变量设置为true,表示本线程已经获得并占用了该锁;其他线程不能再获得锁,必须等待
isLocked = true;
// 记录是哪个线程占用了锁
runningThread = currentThread;
// 同一个线程每占用(重入)一次锁,计数器加1
count++;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 只用占用锁的线程才能释放锁
if(runningThread == Thread.currentThread()) {
// 该线程每释放一次锁,计数器减1
count--;
if(count == 0) {
// 计数器为0时,才将锁的状态标志为未占用,正在运行的线程也设置为null
isLocked = false;
runningThread = null;
}
}
}
@Override
public Condition newCondition() {
return null;
}
}
复制代码
使用这个锁的实现在次执行一下上面的 main 方法,结果:
Thread[Thread-0,5,main]A start
Thread[Thread-0,5,main]B start
Thread[Thread-0,5,main]B end
Thread[Thread-0,5,main]A end
Thread[Thread-1,5,main]A start
Thread[Thread-1,5,main]B start
Thread[Thread-1,5,main]B end
Thread[Thread-1,5,main]A end
复制代码
这里可以看出,锁可重入后已经解决了死锁问题。
ReentrantLock 如何实现锁的可重入
其实上面的重入锁在 java 的 concurrent 并发包中已经实现,比如:ReentrantLock、ReentrantReadWriteLock 可以直接拿来使用。下面是 ReentrantLock 的简单使用:
package com.wuxiaolong.TestConcurrent;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-08-02
*/
public class TestLock {
/**
* java的concurrent并发包中的ReentrantLock
*/
public static Lock lock = new ReentrantLock();
/**
* 方法A 方法的开始和结束分别手动加锁、解锁
*/
public static void methodA(){
// 手动加锁
lock.lock();
Thread t = Thread.currentThread();
System.out.println(t + "A start");
try {
Thread.sleep(1000);
// 访问带有synchronized修饰的方法B
methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "A end");
// 手动解锁 一般放在finally中
lock.unlock();
}
/**
* 方法B 方法的开始和结束分别手动加锁、解锁
*/
public static void methodB(){
// 手动加锁
lock.lock();
Thread t = Thread.currentThread();
System.out.println(t + "B start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t + "B end");
// 手动解锁 一般放在finally中
lock.unlock();
}
public static void main(String[] args) {
// 第一个线程,内部同时调用方法A,A内部调用方法B
new Thread(){
public void run(){
methodA();
}
}.start();
// 第二个线程,内部同时调用方法A,A内部调用方法B
new Thread(){
public void run(){
methodA();
}
}.start();
}
}
复制代码
执行的结果和上面我们手写的 MyLock 测试的结果是一样的。
ReentrantLock 的实现机制是 CAS,也就是 compareAndSwap,比较和交换,核心源码如下:
CAS 核心算法:执行函数:CAS(V,E,N)
V 表示准备要被更新的变量
E 表示我们提供的 期望的值
N 表示新值 ,准备更新 V 的值
算法思路:V 是共享变量,我们拿着自己准备的这个 E,去跟 V 去比较,如果 E == V ,说明当前没有其它线程在操作,所以,我们把 N 这个值 写入对象的 V 变量中。如果 E != V ,说明我们准备的这个 E,已经过时了,所以我们要重新准备一个最新的 E ,去跟 V 比较,比较成功后才能更新 V 的值为 N。
在上面的源码中,可以看到 Java 提供了一个 Unsafe 类,其内部方法操作可以像 C 的指针一样直接操作内存,方法都是 native 的。
为了让 Java 程序员能够受益于 CAS 等 CPU 指令,JDK 并发包中有一个 Atomic 包,它们是原子操作类,它们使用的是无锁的 CAS 操作,并且统统线程安全。Atomic 包下的几乎所有的类都使用了这个 Unsafe 类。
关注公众号,输入“java-summary”即可获得源码。
完成,收工!!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。
评论