深入理解java:2.3.2. 并发编程concurrent包 之重入锁/读写锁/条件锁
重入锁
Java中的重入锁(即ReentrantLock) 与JVM内置锁(即synchronized)一样,是一种排它锁。
ReentrantLock提供了多样化的同步,比如有时间限制的同步(定时锁),可以被Interrupt的同步,即中断锁 (synchronized的同步是不能Interrupt的)等。
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,
但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
Atomic原子操作,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。
激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。
但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
synchronized是在JVM层面上实现的,在代码执行时出现异常,JVM会自动释放锁定。
但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
try{
renentrantLock.lock();
// 用户操作
} finally {
renentrantLock.unlock();
}
ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c) tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到获得锁定,或者当前线程被别的线程中断
重入锁可定义为公平锁或非公平锁,默认实现为非公平锁。
公平锁是指多个线程获取锁被阻塞的情况下,锁变为可用时,最先申请锁的线程获得锁。
在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
可通过在重入锁(RenentrantLock)的构造方法中传入true构建公平锁,如Lock lock = new RenentrantLock(true)
非公平锁是指多个线程等待锁的情况下,锁变为可用状态时,哪个线程获得锁是随机的。
synchonized相当于非公平锁。可通过在重入锁的构造方法中传入false或者使用无参构造方法构建非公平锁。
读写锁
锁可以保证原子性和可见性。
而原子性更多是针对写操作而言。对于读多写少的场景,一个读操作无须阻塞其它读操作,只需要保证读和写 或者 写与写 不同时发生即可。
此时,如果使用重入锁(即排它锁),对性能影响较大。Java中的读写锁(ReadWriteLock)就是为这种读多写少的场景而创造的。
实际上,ReadWriteLock接口并非继承自Lock接口,ReentrantReadWriteLock也只实现了ReadWriteLock接口而未实现Lock接口。
ReadLock和WriteLock,是ReentrantReadWriteLock类的静态内部类,它们实现了Lock接口。
一个ReentrantReadWriteLock实例包含一个ReentrantReadWriteLock.ReadLock实例和一个ReentrantReadWriteLock.WriteLock实例。
通过readLock()和writeLock()方法可分别获得读锁实例和写锁实例,并通过Lock接口提供的获取锁方法获得对应的锁。
读写锁的锁定规则如下:
获得读锁后,其它线程可获得读锁而不能获取写锁
获得写锁后,其它线程既不能获得读锁也不能获得写锁
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println(new Date() + "\tThread 1 started with read lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println(new Date() + "\tThread 1 ended");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println(new Date() + "\tThread 2 started with read lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println(new Date() + "\tThread 2 ended");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
Lock lock = readWriteLock.writeLock();
lock.lock();
try {
System.out.println(new Date() + "\tThread 3 started with write lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(new Date() + "\tThread 3 ended");
} finally {
lock.unlock();
}
}).start();
}
}
执行结果如下
Sat Jun 18 21:33:46 CST 2016 Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016 Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016 Thread 2 ended
Sat Jun 18 21:33:48 CST 2016 Thread 1 ended
Sat Jun 18 21:33:48 CST 2016 Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016 Thread 3 ended
从上面的执行结果可见,thread 1和thread 2都只需获得读锁,因此它们可以并行执行。
而thread 3因为需要获取写锁,必须等到thread 1和thread 2释放锁后才能获得锁。
条件锁
条件锁只是一个帮助用户理解的概念,实际上并没有条件锁这种锁。
对于每个重入锁,都可以通过newCondition()方法绑定若干个条件对象。
重入锁可以创建若干个条件对象,signal()和signalAll()方法只能唤醒相同条件对象的等待。
一个重入锁上可以生成多个条件变量,不同线程可以等待不同的条件,从而实现更加细粒度的的线程间通信。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
原理
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,抽象队列同步器,简称AQS,是java.util.concurrent的核心。
ReentrantLock中有一个抽象类Sync继承了AQS。
我们知道Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。
而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。
但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:
1. 线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。
2. 线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。
3. 锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
4. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。
5. signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。
所以:
发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。
只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。
可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,
Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。
signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,
而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。
尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。
阻塞当前的线程:调用JNI的 unsafe.park(false, 0L)
操作队列时,调用JNI的unsafe.compareAndSwapInt(),循环CAS。
| compareAndSetHead(Node update) | 利用CAS设置头Node |
| compareAndSetTail(Node expect, Node update) | 利用CAS设置尾Node |
| compareAndSetWaitStatus(Node node, int expect, int update) | 利用CAS设置某个Node中的等待状态 |
深入理解java:2.3.2. 并发编程concurrent包 之重入锁/读写锁/条件锁的更多相关文章
- 并发编程-concurrent指南-Lock-可重入锁(ReentrantLock)
可重入和不可重入的概念是这样的:当一个线程获得了当前实例的锁,并进入方法A,这个线程在没有释放这把锁的时候,能否再次进入方法A呢? 可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法 ...
- Python并发编程-concurrent包
Python并发编程-concurrent包 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.concurrent.futures包概述 3.2版本引入的模块. 异步并行任务编程 ...
- Java并发编程原理与实战十八:读写锁
ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程. 基本规则: 读读不互斥 ...
- 深入理解java:2.3.4. 并发编程concurrent包 之容器ConcurrentLinkedQueue(非阻塞的并发队列---循环CAS)
1. 引言 在并发编程中我们有时候需要使用线程安全的队列. 如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法. 使用阻塞算法的队列可以用一个锁(入队和出 ...
- 深入理解java:2.3.3. 并发编程concurrent包 之容器ConcurrentHashMap
线程不安全的HashMap 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap. 效率低下的HashTable容器 H ...
- 深入理解java:2.3. 并发编程 java.util.concurrent包
JUC java.util.concurrent包, 这个包是从JDK1.5开始引入的,在此之前,这个包独立存在着,它是由Doug Lea开发的,名字叫backport-util-concurrent ...
- 【java并发编程】ReentrantLock 可重入读写锁
目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 欢迎关注我的博客,更多精品知识合集 一.ReentrantLock可重入锁 可 ...
- 深入理解java:2.3.6. 并发编程concurrent包 之管理类---线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁 ...
- 深入理解java:2.3.5. 并发编程concurrent包 之容器BlockingQueue(阻塞队列)
1. 什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列. 这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空. 当队列满时,存储元素的线程会等待队列 ...
随机推荐
- gd_t结构 bd_t结构
gd_t在u-boot-2018.07-fmxx/include/asm-generic/global_data.h中定义 typedef struct global_data { bd_t * ...
- QT中获取选中的radioButton的两种方法
QT中要获取radioButton组中被选中的那个按钮,可以采用两种如下两种办法进行: 方法一:采用对象名称进行获取 代码: 1 QRadioButton* pbtn = qobject_cast&l ...
- 一个IP,一个linux服务器,两个项目,两个域名;如何将两个域名配置到同一个IP的两个项目中。
一.现有资源: 1.阿里云centOS6.5服务器: 2.安装tomcat8.0+JDK: 3.两个不同maven项目的war包,项目名分别为cloud.am: 4.两个域名http://www.lu ...
- Codeforces Round #287 (Div. 2) E. Breaking Good 路径记录!!!+最短路+堆优化
E. Breaking Good time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...
- codevs 1002 搭桥x
题目描述 Description 有一矩形区域的城市中建筑了若干建筑物,如果某两个单元格有一个点相联系,则它们属于同一座建筑物.现在想在这些建筑物之间搭建一些桥梁,其中桥梁只能沿着矩形的方格的边沿搭建 ...
- RPN
训练: 特征图是51x39x256,对该图像的每点考虑9个窗口:三种候选面积(128,256,512) x 三种尺度(1:1,1:2,2:1).这些候选窗口称为anchors.如下图: 如果图片尺寸w ...
- qtp识别验证码
花了两天时间才完整的完成识别验证码的登录操作,在网上看到很多关于验证码识别的方法,但是我用的qtp版本比较高级,所以还是要自己花心思研究.po上我的识别验证码的详细历程: 一.读取浏览器中的图片验证码 ...
- linux-解决添加的网卡无法识别的问题(转载)
添加网卡之后,网卡无法被正确的识别和使用排错方法查看/etc/udev/rules.d/70-persistent-net.rules的内容,该文件中可以查看到新添加的网卡的MAC地址修改/etc/s ...
- SPFA算法的SLF优化 ——loj#10081. 「一本通 3.2 练习 7」道路和航线
今天做到一道最短路的题,原题https://loj.ac/problem/10081 题目大意为给一张有n个顶点的图,点与点之间有m1条道路,m2条航线,道路是双向的,且权值非负,而航线是单向的,权值 ...
- 函数式接口和Lambda表达式
函数式接口(一般标有@FunctionalInterface)就是只定义一个抽象方法的接口. 一个接口,如果满足函数式接口的定义,那么即使不标注为 @FunctionalInterface, 编译器依 ...