Java并发编程:自己动手写一把可重入锁
关于线程安全的例子,我前面的文章Java并发编程:线程安全和ThreadLocal里面提到了,简而言之就是多个线程在同时访问或修改公共资源的时候,由于不同线程抢占公共资源而导致的结果不确定性,就是在并发编程中经常要考虑的线程安全问题。前面的做法是使用同步语句synchronized来隐式加锁,现在我们尝试来用Lock显式加锁来解决线程安全的问题,先来看一下Lock接口的定义:
public interface Lock
Lock接口有几个重要的方法:
//获取锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
void lock()
//释放锁,
void unlock()
lock()和unlock()是Lock接口的两个重要方法,下面的案例将会使用到它俩。Lock是一个接口,实现它的子类包括:可重入锁:ReentrantLock, 读写锁中的只读锁:ReentrantReadWriteLock.ReadLock和读写锁中的只写锁:ReentrantReadWriteLock.WriteLock 。我们先来用一用ReentrantLock可重入锁来解决线程安全问题,如何还不明白什么是线程安全的同学可以回头看我文章开头给的链接文章。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class MyThread implements Runnable {private int number = 5; //公共变量,5个线程都会访问和修改该变量private Lock lock = new ReentrantLock(); //可重入锁@Overridepublic void run() {lock.lock(); //进方法的第一件事就是锁住该方法,不能让其他线程进来try {number--;System.out.println("线程 : " + Thread.currentThread().getName() + "获取到了公共资源,number = " + number);Thread.sleep((long)(Math.random()*1000));} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); //释放锁}}public static void main(String[] args) {//起5个线程MyThread mt = new MyThread();Thread t1 = new Thread(mt, "t1");Thread t2 = new Thread(mt, "t2");Thread t3 = new Thread(mt, "t3");Thread t4 = new Thread(mt, "t4");Thread t5 = new Thread(mt, "t5");t1.start();t2.start();t3.start();t4.start();t5.start();}
}
控制台输出:
线程 : t1获取到了公共资源,number = 4
线程 : t2获取到了公共资源,number = 3
线程 : t3获取到了公共资源,number = 2
线程 : t4获取到了公共资源,number = 1
线程 : t5获取到了公共资源,number = 0
程序中创建了一把锁,一个公共变量的资源,和5个线程,每起一个线程就会对公共资源number做自减操作,从上面的输出可以看到程序中的5个线程对number的操作得到正确的结果。需要注意的是,在你加锁的代码块的finaly语句一定要释放锁,就是调用一下lock的unlock()方法。
现在来看一下什么是可重入锁 ,可重入锁就是同一个线程多次尝试进入同步代码块的时候,能够顺利的进去并执行。实例代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class MyThread implements Runnable {private int number = 5; //公共变量,5个线程都会访问和修改该变量private Lock lock = new ReentrantLock(); //可重入锁public void sayHello(String threadName) {lock.lock();System.out.println("Hello!线程: " + threadName);lock.unlock();}@Overridepublic void run() {lock.lock(); //进方法的第一件事就是锁住该方法,不能让其他线程进来try {number--;System.out.println("线程 : " + Thread.currentThread().getName() + "获取到了公共资源,number = " + number);Thread.sleep((long)(Math.random()*1000));sayHello(Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); //释放锁}}public static void main(String[] args) {//起5个线程MyThread mt = new MyThread();Thread t1 = new Thread(mt, "t1");Thread t2 = new Thread(mt, "t2");Thread t3 = new Thread(mt, "t3");Thread t4 = new Thread(mt, "t4");Thread t5 = new Thread(mt, "t5");t1.start();t2.start();t3.start();t4.start();t5.start();}
}
上述代码什么意思呢?意思是每起一个线程的时候,线程运行run方法的时候,需要去调用sayHello()方法,那个sayHello()也是一个需要同步的和保证安全的方法,方法的第一行代码一来就给方法上锁,然后做完自己的工作之后再释放锁,工作期间,禁止其他线程进来,除了本线程除外。上面代码输出:
线程 : t1获取到了公共资源,number = 4
Hello!线程: t1
线程 : t2获取到了公共资源,number = 3
Hello!线程: t2
线程 : t3获取到了公共资源,number = 2
Hello!线程: t3
线程 : t4获取到了公共资源,number = 1
Hello!线程: t4
线程 : t5获取到了公共资源,number = 0
Hello!线程: t5
实现一把简单的锁
如果你明白了上面几个例子是用来干嘛的,好,我们可以继续进行下去了,我们来实现一把最简单的锁。先不考虑这把锁的公平性和可重入性,只要求达到当使用这把锁的时候我们的代码快安全即可。
我们先来定义自己的一把锁MyLock。
public class MyLock implements Lock {@Overridepublic void lock() {}@Overridepublic void lockInterruptibly() throws InterruptedException {}@Overridepublic boolean tryLock() {return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return false;}@Overridepublic void unlock() {}@Overridepublic Condition newCondition() {return null;}
}
定义自己的锁需要实现Lock接口,而上面是Lock接口需要实现的方法,我们抛开其他因素,只看lock()和unlock()方法。
public class MyLock implements Lock {private boolean isLocked = false; //定义一个变量,标记锁是否被使用@Overridepublic synchronized void lock() {while(isLocked) { //不断的重复判断,isLocked是否被使用,如果已经被占用,则让新进来想尝试获取锁的线程等待,直到被正在运行的线程唤醒try {wait();}catch (InterruptedException e) {e.printStackTrace();}}//进入该代码块有两种情况:// 1.第一个线程进来,此时isLocked变量的值为false,线程没有进入while循环体里面// 2.线程进入那个循环体里面,调用了wait()方法并经历了等待阶段,现在已经被另一个线程唤醒,// 唤醒它的线程将那个变量isLocked设置为true,该线程才跳出了while循环体//跳出while循环体,本线程做的第一件事就是赶紧占用线程,并告诉其他线程说:嘿,哥们,我占用了,你必须等待isLocked = true; //将isLocked变量设置为true,表示本线程已经占用}@Overridepublic void lockInterruptibly() throws InterruptedException {}@Overridepublic boolean tryLock() {return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return false;}@Overridepublic synchronized void unlock() {//线程释放锁,释放锁的过程分为两步//1. 将标志变量设置为true,告诉其他线程,你可以占用了,不必死循环了//2. 唤醒正在等待中的线程,让他们去强制资源isLocked = false;notifyAll(); //通知所有等待的线程,谁抢到我不管}@Overridepublic Condition newCondition() {return null;}
}
从上面代码可以看到,这把锁还是照样用到了同步语句synchronized,只是同步的过程我们自己来实现,用户只需要调用我们的锁上锁和释放锁就行了。其核心思想是用一个公共变量isLocked来标志当前锁是否被占用,如果被占用则当前线程等待,然后每被唤醒一次就尝试去抢那把锁一次(处于等待状态的线程不止当前线程一个),这是lock方法里面使用那个while循环的原因。当线程释放锁时,首先将isLocked变量置为false,表示锁没有被占用,其实线程可以使用了,并调用notifyAll()方法唤醒正在等待的线程,至于谁抢到我不管,不是本宝宝份内的事。
那么上面我们实现的锁是不是一把可重入的锁呢?我们来调用sayHello()方法看看:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;public class MyThread implements Runnable {private int number = 5; //公共变量,5个线程都会访问和修改该变量private Lock lock = new MyLock(); //创建一把自己的锁public void sayHello(String threadName) {System.out.println(Thread.currentThread().getName() + "线程进来,需要占用锁");lock.lock();System.out.println("Hello!线程: " + threadName);lock.unlock();}@Overridepublic void run() {lock.lock(); //进方法的第一件事就是锁住该方法,不能让其他线程进来try {number--;System.out.println("线程 : " + Thread.currentThread().getName() + "获取到了公共资源,number = " + number);Thread.sleep((long)(Math.random()*1000));sayHello(Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); //释放锁}}public static void main(String[] args) {//起5个线程MyThread mt = new MyThread();Thread t1 = new Thread(mt, "t1");Thread t2 = new Thread(mt, "t2");Thread t3 = new Thread(mt, "t3");Thread t4 = new Thread(mt, "t4");Thread t5 = new Thread(mt, "t5");t1.start();t2.start();t3.start();t4.start();t5.start();}
}
为了特意演示效果,我在sayHello方法加锁之前打印一下当前线程的名称,现在控制台输出如下:
线程 : t1获取到了公共资源,number = 4
t1线程进来,需要占用锁
如上所述,t1线程启动并对公共变量做自减的时候,调用了sayHello方法。同一个线程t1,在线程启动的时候获得过一次锁,再在调用sayHello也想要获取这把锁,这样的需求我们是可以理解的,毕竟sayHello方法也时候也需要达到线程安全效果嘛。可问题是痛一个线程尝试获取锁两次,程序就被卡住了,t1在run方法的时候获得过锁,在sayHello方法想再次获得锁的时候被告诉说:唉,哥们,该锁被使用了,至于谁在使用我不管(虽然正在使用该锁线程就是我自己),你还是等等吧!所以导致结果就是sayHello处于等待状态,而run方法则等待sayHello执行完。控制台则一直处于运行状态。
如果你不理解什么是可重入锁和不可重入锁,对比一下上面使用MyLock的例子和使用J.U.C.包下的ReentrantLock俩例子的区别,ReentrantLock是可重入的,而MyLock是不可重入的。
实现一把可重入锁
现在我们来改装一下这把锁,让他变成可重入锁,也就是说:如果我已经获得了该锁并且还没释放,我想再进来几次都行。核心思路是:用一个线程标记变量记录当前正在执行的线程,如果当前想尝试获得锁的线程等于正在执行的线程,则获取锁成功。此外还需要用一个计数器来记录一下本线程进来过多少次,因为如果同步方法调用unlock()时,我不一定就要释放锁,只有本线程的所有加锁方法都释放锁的时候我才真正的释放锁,计数器就起到这个功能。
改装过后的代码如下:
public class MyLock implements Lock {private boolean isLocked = false; //定义一个变量,标记锁是否被使用private Thread runningThread = null; //第一次线程进来的时候,正在运行的线程为nullprivate int count = 0; //计数器@Overridepublic synchronized void lock() {Thread currentThread = Thread.currentThread();//不断的重复判断,isLocked是否被使用,如果已经被占用,则让新进来想尝试获取锁的线程等待,直到被正在运行的线程唤醒//除了判断当前锁是否被占用之外,还要判断正在占用该锁的是不是本线程自己while(isLocked && currentThread != runningThread) { //如果锁已经被占用,而占用者又是自己,则不进入while循环try {wait();}catch (InterruptedException e) {e.printStackTrace();}}//进入该代码块有三种情况:// 1.第一个线程进来,此时isLocked变量的值为false,线程没有进入while循环体里面// 2.线程进入那个循环体里面,调用了wait()方法并经历了等待阶段,现在已经被另一个线程唤醒,// 3.线程不是第一次进来,但是新进来的线程就是正在运行的线程,则直接来到这个代码块// 唤醒它的线程将那个变量isLocked设置为true,该线程才跳出了while循环体//跳出while循环体,本线程做的第一件事就是赶紧占用线程,并告诉其他线程说:嘿,哥们,我占用了,你必须等待,计数器+1,并设置runningThread的值isLocked = true; //将isLocked变量设置为true,表示本线程已经占用runningThread = currentThread; //给正在运行的线程变量赋值count++; //计数器自增}@Overridepublic void lockInterruptibly() throws InterruptedException {}@Overridepublic boolean tryLock() {return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return false;}@Overridepublic synchronized void unlock() {//线程释放锁,释放锁的过程分为三步//1. 判断发出释放锁的请求是否是当前线程//2. 判断计数器是否归零,也就是说,判断本线程自己进来了多少次,是不是全释放锁了//3. 还原标志变量if(runningThread == Thread.currentThread()) {count--;//计数器自减if(count == 0) { //判断是否归零isLocked = false; //将锁的状态标志为未占用runningThread = null; //既然已经真正释放了锁,正在运行的线程则为nullnotifyAll(); //通知所有等待的线程,谁抢到我不管}}}@Overridepublic Condition newCondition() {return null;}
}
如代码注释所述,这里新增了两个变量runningThread和count,用于记录当前正在执行的线程和当前线程获得锁的次数。代码的关键点在于while循环判断测试获得锁的线程的条件,之前是只要锁被占用就让进来的线程等待,现在的做法是,如果锁已经被占用,则判断一下正在占用这把锁的就是我自己,如果是,则获得锁,计数器+1;如果不是,则新进来的线程进入等待。相应的,当线程调用unlock()释放锁的时候,并不是立马就释放该锁,而是判断当前线程还有没有其他方法还在占用锁,如果有,除了让计数器减1之外什么事都别干,让最后一个释放锁的方法来做最后的清除工作,当计数器归零时,才表示真正的释放锁。
我知道你在怀疑这把被改造过后的锁是不是能满足我们的需求,现在就让我们来运行一下程序,控制台输出如下:
线程 : t1获取到了公共资源,number = 4
t1线程进来,需要占用锁
Hello!线程: t1
线程 : t5获取到了公共资源,number = 3
t5线程进来,需要占用锁
Hello!线程: t5
线程 : t2获取到了公共资源,number = 2
t2线程进来,需要占用锁
Hello!线程: t2
线程 : t4获取到了公共资源,number = 1
t4线程进来,需要占用锁
Hello!线程: t4
线程 : t3获取到了公共资源,number = 0
t3线程进来,需要占用锁
Hello!线程: t3
嗯,没错,这就是我们想要的结果。
好了,自己动手写一把可重入锁就先写到这了,后面有时间再写一篇用AQS实现的可重入锁,毕竟ReentrantLock这哥们就是用AQS实现的可重入锁,至于什么是AQS以及如何用AQS实现一把可重入锁,且听我慢慢道来。如果你看懂这篇文章的思路或者如果是你看完了这篇文章有动手写一把可重入锁的冲动,麻烦点个赞哦,毕竟大半夜的写文章挺累的,是吧?
Java并发编程:自己动手写一把可重入锁相关推荐
- java 变量锁_并发编程高频面试题:可重入锁+线程池+内存模型等(含答案)
对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一.因为并发编程是Java语言中最为晦涩的知识点,它涉及操作系统.内存.CPU.编程语言等多方面的基础能力,更为考验一个程序 ...
- Java并发编程(1):可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- java中的账户冻结原理_java可重入锁(ReentrantLock)的实现原理
前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...
- java并发编程(三十五)——公平与非公平锁实战
前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...
- 深入Lock锁底层原理实现,手写一个可重入锁
synchronized与lock lock是一个接口,而synchronized是在JVM层面实现的.synchronized释放锁有两种方式: 获取锁的线程执行完同步代码,释放锁 . 线程执行发生 ...
- java并发编程(二十六)——单例模式的双重检查锁模式为什么必须加 volatile?
前言 本文我们从一个问题出发来进行探究关于volatile的应用. 问题:单例模式的双重检查锁模式为什么必须加 volatile? 什么是单例模式 单例模式指的是,保证一个类只有一个实例,并且提供一个 ...
- Java并发编程|第二篇:线程生命周期
文章目录 系列文章 1.线程的状态 2.线程生命周期 3.状态测试代码 4.线程终止 4.1 线程执行完成 4.2 interrupt 5.线程复位 5.1interrupted 5.2抛出异常 6. ...
- 教你“强人锁男”——java并发编程的常用锁类型
Java 并发编程不可不知的七种锁类型与注意事项 锁是java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息.锁是解决并发冲突的重要工具.在开发 ...
- Java并发编程的艺术_Conc
Java并发编程的艺术 1 并发编程的挑战 1.1 上下文切换 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制.时间片是CPU分配给各个线程的时间,因为时间片 ...
- 学习笔记:Java 并发编程⑥_并发工具_JUC
若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 配套资料: ...
最新文章
- openfiler setup一,安装
- 发送结构化的网络消息
- python基础(part11)-作用域LEGB
- WMAP环境上传图片报错【找不到临时文件夹】解决方案
- Eclipse : Unresolved inclusion
- r语言 小树转化百分数_“小树”机器人1.0新品发布会
- LeetCode 1721. 交换链表中的节点(快慢指针)
- 跳一跳python开挂_微信跳一跳物理外挂—教​你用 Python 来玩微信跳一跳
- 雷神开机logo更改_黑武士再度来袭 雷神第三代911黑武士游戏台式机评测
- 完全不相关的结果集,拼成一个sql
- Mozart Update 1(杯具额…)
- 一个手机只能连接一个热点吗_两个手机怎么连接热点
- Git-第一篇认识git,核心对象,常用命令
- 高通笔记本装linux,在华硕畅370(TP370QL)骁龙笔记本上安装Ubuntu 18.04 ARM64的方法...
- HBase项目之微博系统
- A better Tooltip with jQuery
- .Net Core裁剪图片并存入数据库
- 【Redis】客户端RedisClient
- 数据库基本知识、操作
- PHP来客在线客服系统源码 带安装教程