Java - J.U.C体系进阶
作者:Kerwin
邮箱:806857264@qq.com
说到做到,就是我的忍道!
juc-locks 锁框架
接口说明
Lock接口
类型 | 名称 |
---|---|
void | lock() |
void | lockInterruptibly () |
Condition | newCondition() |
boolean | tryClock() |
boolean | tryClock(Long time, TimeUnit unit) |
void | unlock() |
lock()方法类似于使用synchronized关键字加锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
lockInterruptibly()方法顾名思义,就是如果锁不可用,那么当前正在等待的线程是可以被中断的,这比synchronized关键字更加灵活。
Condition接口
可以看做是Obejct类的wait()、notify()、notifyAll()方法的替代品,与Lock配合使用
核心方法 -> awit() signal() signalAll()
ReadWriteLock接口
核心方法 -> readLock() writeLock() 获取读锁和写锁,注意除非使用Java8新锁,否则读读不互斥,读写是互斥的
ReentrantLock类使用
ReentrantLock的使用非常简单,Demo如下:
/***
* TestDemo
* @author 柯贤铭
* @date 2019年4月22日
* @email 806857264@qq.com
*/
public class ReadWriteLockTest {
private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final WriteLock writeLock = readWriteLock.writeLock();
private static final ReadLock readLock = readWriteLock.readLock();
private static final ExecutorService pool = Executors.newFixedThreadPool(50);
private static int surplusTickets = 100;// 余票量
private static int surplusThread = 500;// 统计进程执行量,在进程都执行完毕后才关闭主线程
/**
* 运行多线程,进行模拟抢票,并计算执行时间
*/
public static void main(String[] args) {
Date beginTime = new Date();
for (int i = 0; i < surplusThread; i++) {
final int runNum = i;
pool.execute(new Runnable() {
public void run() {
boolean getted = takeTicket();
String gettedMsg = "";
if (getted) {
gettedMsg = "has getted";
} else {
gettedMsg = "not getted";
}
System.out.println("thread " + runNum + " " + gettedMsg + ", remain: " + surplusTickets
+ ", line up:" + surplusThread + "..");
}
});
}
while (surplusThread >= 30) {
sleep(100);
}
Date overTime = new Date();
System.out.println("take times:" + (overTime.getTime() - beginTime.getTime()) + " millis.");
}
/**
* 查询当前的余票量
*/
private static int nowSurplus() {
readLock.lock();
int s = surplusTickets;
sleep(30);// 模拟复杂业务
readLock.unlock();
return s;
}
/**
* 拿出一张票
*/
private static boolean takeTicket() {
writeLock.lock();
boolean result = false;
if (nowSurplus() > 0) {
surplusTickets -= 1;
result = true;
}
surplusThread -= 1;
writeLock.unlock();
return result;
}
/**
* 睡觉觉
*/
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
说明: 关键点就在获取其读锁和写锁上,为什么要区分?
因为读写互斥,读读不互斥,所以如果不分清楚的话就会让只读操作性能大大下降
另外: 在频繁互斥情况下,其实Lock的性能和synchronized是一样的
但这仅限于在PC端(用新型编译器和虚拟机),如果是在安卓端,synchronized会慢十几倍
LockSupport工具类
Doug Lea 的神作concurrent包是基于AQS (AbstractQueuedSynchronizer)框架,AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。因此,LockSupport可谓构建concurrent包的基础之一。理解concurrent包,就从这里开始。
归根结底,LockSupport调用的Unsafe中的native代码:
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute, long time);
两个函数声明清楚地说明了操作对象:
park函数是将当前Thread阻塞,而unpark函数则是将另一个Thread唤醒。
与Object类的wait/notify机制相比,park/unpark有两个优点:
- 以thread为操作对象更符合阻塞线程的直观定义;
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性
举个例子,假设现在需要实现一种FIFO类型的独占锁,可以把这种锁看成是ReentrantLock的公平锁简单版本,且是不可重入的,就是说当一个线程获得锁后,其它等待线程以FIFO的调度方式等待获取锁 :
public class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);
private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
public void lock() {
Thread current = Thread.currentThread();
waiters.add(current);
// 如果当前线程不在队首,或锁已被占用,则当前线程阻塞
// NOTE:这个判断的意图其实就是:锁必须由队首元素拿到
while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
LockSupport.park(this);
}
waiters.remove(); // 删除队首元素
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
测试代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
FIFOMutex mutex = new FIFOMutex();
MyThread a1 = new MyThread("a1", mutex);
MyThread a2 = new MyThread("a2", mutex);
MyThread a3 = new MyThread("a3", mutex);
a1.start();
a2.start();
a3.start();
a1.join();
a2.join();
a3.join();
assert MyThread.count == 300;
System.out.print("Finished");
}
}
class MyThread extends Thread {
private String name;
private FIFOMutex mutex;
public static int count;
public MyThread(String name, FIFOMutex mutex) {
this.name = name;
this.mutex = mutex;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
mutex.lock();
count++;
System.out.println("name:" + name + " count:" + count);
mutex.unlock();
}
}
}
park方法的调用一般要方法一个循环判断体里面。
如上述示例中的:while (waiters.peek() != current || !locked.compareAndSet(false, true)) { LockSupport.park(this); }
之所以这样做,是为了防止线程被唤醒后,不进行判断而意外继续向下执行,这其实是一种的多线程设计模式-Guarded Suspension
AbstractQueuedSynchronizer抽象类
AbstractQueuedSynchronizer抽象类是整个JUC体系的核心,一两句话说不清,如果仅限于使用JUC的话,其实也不用看,如果想知道源码层的话,推荐以下几个博文:
核心:抽象类采用模板方法模式主要解决何时,何线程,在何状态下 -> acquire和release的问题 获取资源与释放资源
StampedLock Java8新型锁
ReentrantReadWriteLock锁具有读写锁,问题在于ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的 ,很容易造成写锁获取不到资源
解决的必要问题:读锁采用乐观锁机制,非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程,但是API稍微复杂,因此使用时需要注意
StampedLock的主要特点概括一下,有以下几点:
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。
StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
无论写锁还是读锁,都不支持Conditon等待
/***
* TestStampedLock
* @author 柯贤铭
* @date 2019年4月22日
* @email 806857264@qq.com
*/
public class TestStampedLock {
// StampedLock锁
private static final StampedLock sLock = new StampedLock();
// 模拟500张票
private static Integer total = 500;
// 模拟100个人
private static Integer person = 100;
private static final ExecutorService pool = Executors.newFixedThreadPool(person);
private static final CountDownLatch LATCH = new CountDownLatch(person);
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
for (int i = 0; i < person; i++) {
final Integer index = i;
pool.execute(new Runnable() {
@Override
public void run() {
Integer sheng = TestStampedLock.read();
if (sheng >= 1) {
try {
boolean flag = TestStampedLock.buy();
if (flag) {
System.out.println("线程 " + index + "买到了");
} else {
System.out.println("线程 " + index + "no no no");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("线程 " + index + "no no no");
}
LATCH.countDown();
}
});
}
LATCH.await();
Long end = System.currentTimeMillis();
System.out.println("一共耗时: " + (end - start));
}
/**
* 读剩余还有几张票
* @return
*/
private static Integer read () {
Long stamp = sLock.tryOptimisticRead();
Integer piao = total;
if (!sLock.validate(stamp)) {
stamp = sLock.readLock();
try {
piao = total;
} finally {
sLock.unlockRead(stamp);
}
}
return piao;
}
/***
* 买票
* @throws InterruptedException
*/
private static boolean buy () throws InterruptedException {
Long stamp = sLock.writeLock();
// 模拟复杂操作
Thread.sleep(30);
try {
if (total >= 1) {
total--;
return true;
}
} finally {
sLock.unlockWrite(stamp);
}
return false;
}
}
StampedLock乐观锁操作必要步骤:
// 注意:StampedLock的必要操作流程
// 唯一需要注意的地方就是乐观读锁的地方 - 官方Demo
double distanceFormOrigin() {//只读方法
//试图尝试一次乐观读 返回一个类似于时间戳的邮戳整数stamp
long stamp = s1.tryOptimisticRead();
//读取x和y的值,这时候我们并不确定x和y是否是一致的
double currentX = x, currentY = y;
//判断这个stamp是否在读过程发生期间被修改过,如果stamp没有被修改过,责任无这次读取时有效的,因此就可以直接return了,反之,如果stamp是不可用的,则意味着在读取的过程中,可能被其他线程改写了数据,因此,有可能出现脏读,如果如果出现这种情况,我们可以像CAS操作那样在一个死循环中一直使用乐观锁,知道成功为止
if (!s1.validate(stamp)) {
//也可以升级锁的级别,这里我们升级乐观锁的级别,将乐观锁变为悲观锁, 如果当前对象正在被修改,则读锁的申请可能导致线程挂起.
stamp = s1.readLock();
try {
currentX = x;
currentY = y;
} finally {
s1.unlockRead(stamp);//退出临界区,释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}