环境:
jdk1.8
摘要说明:
上一大章节主要阐述了原子操作CAS及常用的原子操作类
本章节主要讲述显示锁的使用及显示锁和synchronized关键字的比较
锁是一种通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只有一个线程可以获得锁,对共享资源的所有访问都要求先获得锁。然而,一些锁可能允许对共享资源的并发访问,例如ReadWriteLock的读锁。
纵观java.util.concurrent.locks下有三个接口及其相关实现:
Lock:本质为排他锁,与synchronized关键字效果相同,其特征为获取锁可以被中断,可以超时获取锁尝试获取锁等
ReadWriteLock:读写锁,同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有的读和写都被阻塞,最适宜与读多写少的情况;
Condition:Condition将对象监控器方法(wait、notify和notifyAll)分解为不同的对象,通过将它们与使用任意锁实现相结合,使每个对象具有多个等待集的效果。当锁代替同步方法和语句的使用时,Condition代替对象监视器方法的使用。
步骤:
1.Lock接口的简介及与synchronized的比较
Lock接口与synchronized关键字的比较:
synchronized关键字提供与每个对象关联的隐式监视器锁,但不是所有锁获取和释放发生在结构方式:在获得多个锁时,它们必须被释放在相反的顺序,和所有的锁都必须在相同的词法作用域的。
虽然synchronized关键字的作用域机制使使用监视锁编程变得更容易,并有助于避免涉及锁的许多常见编程错误,但是在某些情况下,您需要以更灵活的方式使用锁。例如,一些遍历并发访问数据结构的算法需要使用“手把手”或“链锁”:先获取节点A的锁,然后获取节点B的锁,然后释放节点A和获取节点C的锁,然后释放节点B和获取节点D的锁,以此类推。Lock接口的实现允许在不同的范围内获取和释放锁,并允许以任意顺序获取和释放多个锁,从而支持使用此类技术。
综上所述(相对于synchronized关键字锁也被成为显示锁):
synchronized关键字:优点是用起来简单优雅,不易出错,缺点是不灵活,不适用所有场景;
Lock:优点是更加灵活,适用场景广,缺点是编程稍微繁琐,会出现错误;通常若需要获取锁可以被中断,超时获取锁,尝试获取锁时一般使用锁;读多写少用读写锁
Lock使用基本范式:
在不同的范围内进行锁定和解锁时,必须小心确保在锁定时执行的所有代码都受到try-finally或try-catch的保护,以确保在必要时释放锁定。
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
Lock接口方法详解:
Lock接口还可以提供与隐式监控器锁完全不同的行为和语义,例如有保证的顺序、不可重入的用法或死锁检测;
下面我们就看看Lock接口各方法的作用:
- void lock():获得锁
- void lockInterruptibly():获取锁,除非当前线程被中断。
- Condition newCondition():返回绑定到此锁定实例的新条件实例。
- boolean tryLock():仅当锁在调用时是空闲的,才获取锁。
- boolean tryLock(long time, TimeUnit unit):如果在给定的等待时间内锁是空闲的,并且当前线程没有被中断,则获取锁。
- void unlock():释放锁。
锁的公平性:
锁在构造时可以指定参数fail,如ReentrantLock(boolean fair);若参数指定为true,则表示这个锁是个公平锁,公平锁在争用项下,锁定优先授予对等待时间最长的线程的访问权。反之,非公平锁不保证任何特定的访问顺序。
默认为非公平锁;
公平锁的吞吐量往往要低于非公平锁的吞吐量,这是因为当线程释放锁时往往等待锁的线程是挂起状态,不能立即获得锁,这时新来一个线程若锁是非公平则可以立即使用,若锁是非公平则需要排队挂起等待唤醒;
2.ReentrantLock
ReentrantLock实现Lock接口;表示可重入互斥锁,具有与使用同步方法和语句访问的synchronized关键字相同的基本行为和语义,但具有扩展功能。
作为可重入锁,ReentrantLock支持同一线程最多2147483647个递归锁。试图超过此限制会导致锁定方法抛出错误。
构造方法:
ReentrantLock():创建一个可重入锁实例,且是非公平锁
ReentrantLock(boolean fair):创建一个可重入锁实例,且可指定锁的公平性,true时为公平锁;
常用方法:
- int getHoldCount():按当前线程查询此锁上的持有数。
- protected Thread getOwner():返回当前拥有此锁的线程,如果不拥有则返回null。
- boolean getAndSet(boolean newValue):原子地设置为给定值并返回前一个值。
- int getqueuelength():返回等待获取此锁的线程数的估计值。
- void lockInterruptibly():获取锁,除非当前线程被中断
- Condition newCondition():返回用于此锁定实例的条件实例。
举例:
package pers.cc.lockAndAQS;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import pers.cc.tools.SleepTools;
public class ReentrantLockTest {
// 公平锁
static ReentrantLock reentrantLock = new ReentrantLock(true);
static AtomicLong time = new AtomicLong();
// 非公平锁
static ReentrantLock failReentrantLock = new ReentrantLock();
static AtomicLong time1 = new AtomicLong();
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
long t = System.currentTimeMillis();
reentrantLock.lock();
try {
SleepTools.ms(1);
}
finally {
reentrantLock.unlock();
System.out.println("公平锁"
+ Thread.currentThread().getId()
+ ":"
+ time.addAndGet(System.currentTimeMillis() - t));
;
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
long t = System.currentTimeMillis();
failReentrantLock.lock();
try {
SleepTools.ms(1);
}
finally {
failReentrantLock.unlock();
System.out.println("非公平锁"
+ Thread.currentThread().getId()
+ ":"
+ time1.addAndGet(System.currentTimeMillis()
- t));
}
}
}).start();
}
}
}
上述我们可以从运行结果中可以看到两种锁的耗时,可以看出非公平锁的效率明显高于公平锁的效率:
非公平锁20009:30720445
公平锁20008:37752013
3.读写锁ReadWriteLock及其实现ReentrantReadWriteLock
不管是Lock接口和synchronized关键字的思想都是排他锁的思想;但在有些情况下会导致效率不高,比如说读多写少的时候;所以就有了读写锁ReadWriteLock;ReadWriteLock维护一对相关锁,一个用于只读操作,一个用于写入。只要没有写入器,多个读取器线程就可以同时持有读锁。写锁是独占的。
所有ReadWriteLock实现都必须保证writeLock操作的内存同步效果(在Lock接口中指定)也与关联的readLock保持一致。也就是说,成功获取读锁的线程将看到在之前释放写锁时所做的所有更新。
读写锁允许在访问共享数据时比互斥锁允许的并发级别更高。它利用的事实是,虽然一次只有一个线程(一个写线程)可以修改共享数据,但在许多情况下,任何数量的线程都可以并发地读取数据(因此读取线程)。从理论上讲,使用读写锁所允许的并发性的增加将导致性能优于使用互斥锁。在实践中,只有当共享数据的访问模式合适时,这种并发性的增加才会在多处理器上完全实现。
读写锁是否会提高性能的使用互斥锁的频率取决于读取数据被修改相比,读和写操作的持续时间,和争用数据——也就是说,线程的数量,将尝试读或写数据在同一时间。例如,最初使用数据填充然后很少修改的集合,而经常搜索的集合(例如某种目录)是使用读写锁的理想候选。然而,如果更新变得频繁,那么数据的大部分时间都被独占锁定,并发性几乎没有增加。此外,如果读操作过短,读写锁实现的开销(本质上比互斥锁更复杂)可能会控制执行成本,特别是在许多读写锁实现仍然通过一小段代码序列化所有线程的情况下。最终,只有分析和度量才能确定读写锁的使用是否适合您的应用程序。
综上所述:
1.读写锁中的写锁和读锁只能同时有一个被线程获取,即写锁被获取,读锁则无法获取;反之亦然
2.读写锁中的读锁同一时间可以被多个线程获取;
3.当线程获取写锁时,后续所有获取读锁或写锁的线程全部处于等待中
ReadWriteLock接口常用方法如下:
- Lock readLock():获取读锁
- Lock writeLock():获取写锁
ReentrantReadWriteLock的常用构造函数如下:
- ReentrantReadWriteLock():创建一个非公平的读写锁实例;
- ReentrantReadWriteLock(boolean fair):创建一个读写锁实例,并指定非公平性;
常用方法如锁的常用方法类似一致,这里就不列举了:
实例:
package pers.cc.lockAndAQS;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import pers.cc.tools.SleepTools;
/**
* 读写锁实例:ReadWriteLock维护一对相关锁,一个用于只读操作,一个用于写入。只要没有写入器,多个读取器线程就可以同时持有读锁。写锁是独占的。
*
* @author cc
*
*/
public class ReentrantReadWriteLockTest {
static ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
static Lock rLock = reentrantReadWriteLock.readLock();
static Lock wLock = reentrantReadWriteLock.writeLock();
static AtomicLong time = new AtomicLong();
static boolean boo = false;
/***
* 获取当前布尔值
*
* @return
*/
public static boolean getL() {
long t = System.currentTimeMillis();
// 多个线程可以共用同一个读锁
rLock.lock();
System.out.println(Thread.currentThread().getId() + "获取读锁"
+ System.currentTimeMillis());
try {
SleepTools.ms(5);
return boo;
}
finally {
System.out.println("总耗时"
+ time.addAndGet(System.currentTimeMillis() - t));
rLock.unlock();
}
}
/***
* 随机修改当前布尔值
*
* @return
*/
public static void setL() {
long t = System.currentTimeMillis();
// 当写锁被获取后,所有想获得读锁和写锁的线程全部等待
wLock.lock();
System.out.println(Thread.currentThread().getId() + "获取写锁"
+ System.currentTimeMillis());
try {
SleepTools.ms(5);
Random r = new Random();
boo = r.nextBoolean();
}
finally {
System.out.println("总耗时"
+ time.addAndGet(System.currentTimeMillis() - t));
wLock.unlock();
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 每20次才修改布尔值
if (Thread.currentThread().getId() % 20 == 0) {
setL();
}
else {
getL();
}
}
}).start();
}
SleepTools.second(100);
}
}
4.等待通知Condition
之前我们学习synchronized关键字的时候有通过wait和notify来实现等待通知模式;显示锁同样也可以完成等待通知模式;
Condtion将对象监控器方法(wait、notify和notifyAll)分解为不同的对象,通过将它们与使用任意锁实现相结合,使每个对象具有多个等待集的效果。当锁代替synchronized关键字的使用时,Condtion代替对象监视器方法的使用。
Condtion(也称为条件队列或条件变量)为一个线程提供了一种挂起执行(“等待”)的方法,直到另一个线程通知某个状态条件现在可能为真。由于对该共享状态信息的访问发生在不同的线程中,因此必须对其进行保护,因此某种形式的锁与该条件相关联。等待条件提供的键属性是,它自动释放关联的锁并挂起当前线程,就像Object.wait一样
Condtion实例本质上绑定到锁。要获取特定锁实例的条件实例,请使用其newCondition()方法。
Condtion常用方法如下:
- void await():使当前线程等待,直到发出信号或中断。
- boolean await(long time, TimeUnit unit):使当前线程等待,直到发出信号或中断,或指定的等待时间结束。
- long awaitNanos(long nanosTimeout):使当前线程等待,直到发出信号或中断,或指定的等待时间结束。
- void awaitUninterruptibly():使当前线程等待,直到发出信号。
- boolean awaitUntil(Date deadline):使当前线程等待,直到发出信号或中断,或指定的截止日期过期。
- void signal():唤醒一个正在等待的线程。
- void signalAll()唤醒所有等待的线程。
实例:
package pers.cc.lockAndAQS;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 控制器测试等待唤醒
*
* @author cc
*
*/
public class ConditionTest {
/**
* 创建一个可重入锁
*/
static Lock lock = new ReentrantLock();
static Condition cond = lock.newCondition();
static AtomicInteger pernum = new AtomicInteger();
/**
* 等待触发,当初上人数满45人出发
*
* @throws InterruptedException
*/
public static void waitGo() throws InterruptedException {
lock.lock();
try {
cond.await();
System.out.println("汽车满员出发");
}
finally {
lock.unlock();
}
}
/**
* 没到达一个人数加1
*/
public static void arrive() {
lock.lock();
try {
int i = pernum.incrementAndGet();
System.out.println("现有人数" + i);
if (i == 45) {
cond.signal();
}
}
finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
// 等待出发
new Thread(new Runnable() {
@Override
public void run() {
try {
waitGo();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
for (int i = 0; i < 45; i++) {
new Thread(new Runnable() {
@Override
public void run() {
arrive();
}
}).start();
}
}
}
运行结果:
现有人数1
现有人数2
现有人数3
......
现有人数42
现有人数43
现有人数44
现有人数45
汽车满员出发
5.源码地址
https://github.com/cc6688211/concurrent-study.git