CyclicBarrier可以实现让一组线程等待至某个状态后,再全部同时执行。为什么叫Cyclic呢,当所有线程都被释放结束后,CyclicBarrier可以被重用。当调用await()方法后,所有线程都位于barrier,也就是屏障位置。
/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/8/16 11:49
*/
public class CyclicBarrierTest {
/**
* 通过CyclicBarrier可以实现让一组线程等待至某个状态后,再全部同时执行。
* 为什么叫Cyclic,当所有线程都被释放结束后,CyclicBarrier可以被重用。
* 当调用await()方法后,所有线程都位于barrier,也就是屏障位置。
* @param args
*/
public static void main(String[] args) {
// test1();
// test2();
// test3();
test4();
}
/**
* 挂起当前线程,直到所有线程到达屏障状态后,再同时执行后续任务
*/
public static void test1() {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
ExecutorService executorService = new CustomThreadPoolExecutor(2,
2, 0L,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue(10));
for (int i = 0; i < 2; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(
new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + Thread.currentThread().getName()
+ "正在写入数据...");
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("线程" + Thread.currentThread().getName()
+ "写入数据完毕...");
cyclicBarrier.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
} catch (BrokenBarrierException ex) {
ex.printStackTrace();
}
System.out.println("所有线程执行完毕...继续执行其他任务...");
}
}
, "success");
executorService.submit(task);
}
executorService.shutdown();
}
/**
* 挂起当前线程,直到所有线程到达屏障状态后,再执行后续任务。
* 当所有线程到达屏障状态后,还可以执行额外的操作。
*/
public static void test2() {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("选择线程" + Thread.currentThread().getName() + "...");
System.out.println("执行额外的骚操作...");
}
});
ExecutorService executorService = new CustomThreadPoolExecutor(2,
2, 0L,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue(10));
for (int i = 0; i < 2; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(
new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + Thread.currentThread().getName()
+ "正在写入数据...");
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("线程" + Thread.currentThread().getName()
+ "写入数据完毕...");
cyclicBarrier.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
} catch (BrokenBarrierException ex) {
ex.printStackTrace();
}
System.out.println("所有线程执行完毕...继续执行其他任务...");
}
}
, "success");
executorService.submit(task);
}
executorService.shutdown();
}
/**
* 还可以让其他线程等待一段时间,如果还有线程没有到达屏障状态的话,就直接进行执行后续任务。
*/
public static void test3() {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
ExecutorService executorService = new CustomThreadPoolExecutor(2,
2, 0L,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue(10));
for (int i = 0; i < 2; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(
new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + Thread.currentThread().getName()
+ "正在写入数据...");
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println("线程" + Thread.currentThread().getName()
+ "写入数据完毕...");
cyclicBarrier.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace();
// 一个线程试图在一个处于崩溃状态的屏障等待其他线程
} catch (BrokenBarrierException ex) {
ex.printStackTrace();
} catch (TimeoutException ex) {
System.out.println("等待超时...");
}
System.out.println("所有线程执行完毕...继续执行其他任务...");
}
}
, "success");
if (i != 0) {
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
executorService.submit(task);
}
executorService.shutdown();
System.out.println("132323");
}
/**
* 重用CyclicBarrier,而CountDownLatch无法做到
*/
public static void test4() {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
ExecutorService executorService = new CustomThreadPoolExecutor(2,
2, 0L,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue(10));
for (int j = 0; j < 2; j++) {
List futureList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(
new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + Thread.currentThread().getName()
+ "正在写入数据...");
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("线程" + Thread.currentThread().getName()
+ "写入数据完毕...");
cyclicBarrier.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
} catch (BrokenBarrierException ex) {
ex.printStackTrace();
}
System.out.println("所有线程执行完毕...继续执行其他任务...");
}
}
, "success");
futureList.add(executorService.submit(task));
}
while (true) {
boolean isShutDown = true;
for (Future future : futureList) {
if (!future.isDone()) {
isShutDown = false;
}
}
if (isShutDown) {
System.out.println("第" + (j + 1) + "波CyclicBarrier完毕...");
break;
}
}
}
executorService.shutdown();
System.out.println(executorService.toString());
}
}
Semaphore用于控制对某组变量资源的访问权限。
/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/8/17 17:57
*/
public class SemaphoreTest {
public static void main(String[] args) {
test1();
}
public static void test1() {
final Semaphore semaphore = new Semaphore(5);
CustomThreadPoolExecutor customThreadPoolExecutor = new CustomThreadPoolExecutor(
10, 10, 0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue(100)
);
for (int i = 0; i < 9; i++) {
CustomThreadPoolExecutor.CustomTask task =
new CustomThreadPoolExecutor.CustomTask(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("thread:" + Thread.currentThread().getName() + "acquire one permit");
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("thread:" +Thread.currentThread().getName() + "release one permit");
semaphore.release();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}, "success");
customThreadPoolExecutor.submit(task);
}
customThreadPoolExecutor.shutdown();
}
}
CountDownLatch之前写过一篇文章分析它的源码,这里简单过一下。它类似计数器的功能,比如有一个任务A,需要等待其他5个任务执行完毕后才能执行,就可以用CountDownLatch。
/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/8/16 9:54
*/
public class CountDownLatchTest {
/**
* 类似计数器的功能,比如有一个任务A,需要等待其他5个任务执行完毕后才能执行
* ,就可以用CountDownLatch
*
* @param args
*/
public static void main(String[] args) {
test1();
}
public static void test1() {
final CountDownLatch countDownLatch = new CountDownLatch(2);
ExecutorService executorService = new CustomThreadPoolExecutor(2,
2, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(10));
for (int i = 0; i < 2; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("子线程" + Thread.currentThread().getName()
+ "正在执行...");
System.out.println("子线程" + Thread.currentThread().getName()
+ "执行完毕...");
countDownLatch.countDown();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}, "success");
executorService.submit(task);
}
try {
System.out.println("等待2个线程...");
countDownLatch.await();
executorService.shutdown();
System.out.println("2个线程执行完毕...");
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
public static void test2() {
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(10);
ExecutorService executorService = new CustomThreadPoolExecutor(10,
10, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(10));
for (int i = 0; i < 10; i++) {
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(new Runnable() {
@Override
public void run() {
try {
System.out.println("子线程" + Thread.currentThread().getName()
+ "正在执行...");
start.await();
System.out.println("子线程" + Thread.currentThread().getName()
+ "执行完毕...");
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
end.countDown();
}
}
}, "success");
executorService.submit(task);
}
start.countDown();
try {
System.out.println("等待10个线程...");
end.await();
executorService.shutdown();
System.out.println("10个线程执行完毕...");
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
多个用户在同一时刻可以同时读取统一资源,而且互不干扰。而写锁则是排它的。也就是说写锁是排它的。
读锁可以允许多个reader线程同时持有,而写入锁最多有一个writer线程持有。使用场景:读取共享数据的频率远大于修改共享数据的频率。在上述场景下,使用读写锁控制共享资源的访问,就可以提高并发性能。
如果一个线程已经持有了写入锁,则可以再持有读取锁。相反,如果一个线程已经持有了读取锁,则在释放读取锁之前,不能再持有写入锁了。
重入锁最大的作用是避免死锁。重入是指任意线程在获取锁之后能够再次获取该锁而不被阻塞。
- 线程再次获取锁,锁要去识别获取锁的线程是否为当前线程,如果是,则再次获取锁成功。
- 锁被释放时,计数自减,当计数等于0时,表示锁已经成功释放。
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁。这样就从写入锁变成了读取锁,从而完成了锁降级。
锁升级:读取锁是不能直接升级为写锁的。因为一个写入锁是需要释放所有的读取锁。如果有2个读取锁,试图获取写入锁,且都不释放读取锁,就会发生死锁。
Condition条件变量:写入锁提供了条件变量支持,但是读取锁却不允许获取条件变量,会抛出UnsupportedOperationException
异常。
JDK1.5引入了Lock接口,下面这几个类实现了它:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
互斥同步最重要的问题就是进行线程阻塞和唤醒带来的性能开销问题,因为这种同步又称之为阻塞同步,它属于一种悲观的并发问题,即线程获取的是独占锁。独占锁意味着就是其他线程只能依靠阻塞来等待线程释放当前的锁。而在CPU转换线程阻塞时会引用线程上下文的切换。当有很多线程去竞争锁的时候,会引发CPU频繁的上下文切换,导致效率很低,Synchronized就是用这个策略(还好JDK1.6优化了)。
如果没有其他线程争用共享数据的话,那操作就完美成功了。如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断的重试,直到成功为止)。这种乐观的并发策略的很多实现都不需要把线程挂起,因此这种同步又被称之为非阻塞同步。大名鼎鼎的ReentrantLock采用的就是这种并发策略。
ReentrantLock与synchronized区别:
1.可重入性。2个都可以重入。
2.锁的实现。synchronized是基于JVM实现的,ReentrantLock是基于JDK实现。
3.性能区别。synchronized在优化之前,性能很差。JDK1.6后对synchronized进行优化后,能够减少获得锁和释放锁带来的性能消耗,主要是引入了偏向锁和轻量级锁。
4.功能区别。synchronized使用比较简单,是通过编译器来保证加锁和释放锁。ReentrantLock是要手动加锁和释放锁(如果忘记释放锁了就翻车了)。
ReentrantLock是独占锁,而AQS的ConditionObject只能和ReentrantLock一起用,它是为了支持条件队列的锁更加方便。ConditionObject的signal和await方法都是基于独占锁的。
ReentrantLock去尝试释放锁时,会判断当前线程是不是独占锁的线程。如果不是,会抛出IllegalMonitorStateException
独占锁是一种悲观保守的加锁策略,它避免了读和读之间的冲突。如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。独占的意思就是同一时刻只能有一个线程获取锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能获取锁。
共享锁就是一种乐观锁,放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。ReentrantReadWriteLock允许一个资源可以被多个线程访问的,但是写操作访问,两者不能同时进行哟。
LockSupport的park()可以挂起线程,unPark()唤醒线程。这里不需要获取对象的监听器。
- wait和notify/notifyAll方法只能在同步块里面使用
- LockSupport不需要在同步块里面,线程间也不需要维护一个共享的同步对象了,实现线程之间的解耦。
- unPark()可以优先于park()调用,不用担心线程之间的执行的先后顺序。
BlockingQueue中的take()实际是调用了condition.await(继续调用了LockSupport.park)。await会报告中断异常。checkInterruptWhileWaiting(node)
中会取判断是signal前被中断还是signal后被中断。如果是signal前被中断会抛出InterruptedException。如果是signal后被中断会执行Thread.currentThread().interrupt()
这里要提一下AQS中waitStatus为SIGNAL。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或者cancel掉,因此需要唤醒当前线程的后继线程。
Condition.await()执行的具体步骤:
1.构造一个新的等待队列节点加入到等待队列尾部。
2.释放锁,将同步队列节点从头部移除掉。
3.自旋,直到它在等待队列上的节点移动到了同步队列(其他线程调用signal()或者被中断)。
4.通过LockSupport.park(this)
挂起当前节点的线程,直到它获取了锁。也就是它当前节点加入到同步队列且处于头部。
Condition.signal()执行的具体步骤:
1.首先从等待队列中的头部节点开始尝试唤醒操作。如果当前节点处于CONDITION,则将waitStatus CAS为0准备加入到同步队列,如果当前状态不为CONDITION,说明该节点等待已被中断会返回false。doSignal()方法会继续尝试唤醒当前节点的后继节点。
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
- 将节点加入到同步队列,返回的p是节点在同步队列中的先驱节点。
3.如果先驱节点的状态为CANCELLED或者设置先驱节点的状态为SIGNAL失败,那么就唤醒当前节点对应的线程。
4.如果成功把先驱节点的状态设置为SIGNAL,那么就不用立即唤醒。等到先驱节点成为同步队列的头部节点并释放了同步状态的,会自动唤醒当前节点对应的线程。
同步队列和等待作用是不同的,最重要的区别就是每个线程只能存在于同步队列和等待队列中的一个。
来讲讲CAS和AQS的基本概念把
CAS(Compare And Swap)是比较并交换,是解决多线程并行情况下使用锁造成性能损耗的一种机制。CAS操作包括3个操作数:内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置的值更新为新值,否则处理器不会进行任何操作。
在Java中,sun.misc.Unsafe类提供了硬件级别的原子操作来实现这个CAS。java.util.concurrent.atomic包下的类大多是使用CAS操作来实现。
AQS(AbstractQueuedSynchronizer)是JDK下提供的一套用于实现基于FIFO等待队列、阻塞锁和相关同步器的一个同步框架。这个抽象类被设计为作为一些可用原子值来表示状态的同步器的基类。CountDownLatch和Semaphore都用到了AQS。
如果要实现独占锁, 同步器要实现
boolean tryAcquire(int arg)
boolean tryRelease(int arg)
boolean isHeldExclusively()
如果要实现共享锁,同步器要实现
int tryAcquireShared(int arg)
boolean tryReleaseShared(int arg)
AQS维护的队列是当前等待资源的队列(也就是同步队列)。当前线程获取同步状态失败后,同步器会把当前线程以及等待状态等信息构造成一个节点并加入到同步队列,同时会阻塞当前线程。当同步状态释放后,会把头节点中的后继节点唤醒,使其再次尝试获取同步状态。
记住Object的wait()和Condition的await()都会释放当前对象持有的锁。
ReentrantLock独占锁的获取:调用AQS的acquire(int arg)方法可以获取到同步状态,如果获取同步失败,调用addWaiter(Node.EXCLUSIVE), arg)
构造同步节点,加入到同步队列的尾部。调用acquireQueued
,让线程在同步队列中等待获取锁。
记住acquire(int arg)
方法对中断不敏感,即线程获取同步状态失败后进入到同步队列,后续对线程进行中断操作时,线程不会从同步队列中移除。
selfInterrupt
:如果线程在等待过程中被中断,会在这里处理中断。也就是线程在等待过程中不响应中断的,只有获取锁之后才能自我中断。
附上LockSupport总结图
ReentrantLock独有的功能:
1.可指定是公平锁还是非公平锁。
2.提供了一个Condition类,可以分组唤醒需要唤醒的线程。
3.提供能够中断等待锁的线程的机制,lock.lockInterruptibly()。ReentrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁操作。它的性能好的原因在于避免了线程进入内核态的阻塞状态。
我们来回忆优化后的synchronized锁的分类,级别从低到高依次是:
无锁状态=》偏向锁状态=》轻量级锁状态=》重量级锁状态
记住锁可以升级,但是不能降级。
偏向锁对于一个线程来说,线程获取锁之后就不会再有解锁等操作了,节省了很多不必要的开销。如果有2个线程来竞争锁的话,那么偏向锁就失效了,进而升级成轻量级锁了。在大部分情况下,都会是同一个线程进入同一个同步代码块,这就是为什么会有偏向锁出现的原因。
偏向锁的加锁和撤销:
当一个线程访问同步块获取锁时,会在锁对象的对象头和栈帧中的锁记录里面存储锁偏向的线程ID。以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁。只需要测试锁对象的对象头的markword里面是否存储指向当前线程的偏向锁,如果测试成功,表示线程已经获取了锁。如果测试失败,则需要再测试一下markword中偏向锁的标识是否设置成1。如果没有设置,则使用CAS竞争锁。如果设置了,则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在指向的字节码)。首先会暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否存活。如果线程不处于活动状态,则将锁对象的对象头设置为无锁状态。如果线程仍然存活,则锁对象中对象头的markword和栈帧中的锁记录要么重新偏向于其他线程要么恢复到无锁状态,最后唤醒暂停的线程。
当出现有2个线程来竞争锁的话,那么偏向锁就失效了。此时锁就会升级为轻量级锁。轻量级锁加锁和解锁过程如下:
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用户存储锁记录的空间,并将markword复制到锁记录中,然后线程尝试使用CAS将markword替换成指向锁记录的指针。如果成功,当前线程获取锁。如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。之后再来的线程发现是轻量级锁,就开始进行自旋。
轻量级锁解锁时,会使用CAS操作将当前线程的锁记录替换回对象头。如果成功,没有竞争关系。如果失败,当前锁存在竞争,锁就会膨胀成重量级锁。
我们来捋一捋思路。假设有线程A和线程B来竞争对象C的锁。这时候线程A和线程B同时将对象C的markword复制到自己的锁记录中,两者竞争去获取锁。假设线程A成功获取锁,并且将对象C的markword(如果要说细一点,就是线程ID)修改指向自己的锁记录的指针。这时候线程B通过CAS获取锁会失败,然后开始自旋。超过自旋的最大次数限制还没获取锁,此时对象C的markword中的记录就会被修改成重量级锁,然后线程B就会被挂起。后面有一个线程C也来获取锁时,看到对象C的markword中的是重量级锁的指针时,说明竞争异常残酷,直接挂起。
最后附上锁比较图,方便记忆。
再说一下StampedLock是Java8引入的一种新机制。它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的。读锁会完全阻塞写锁,它使用的是悲观的锁策略,如果有大量的读线程,它会可能引起写线程的饥饿。
StampedLock提供了一种乐观的读策略,使得乐观锁完全不会阻塞写线程。我们来看官方提供的例子:
public class StampedLockTest {
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
stamp = sl.readLock(); //如果有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
if (ws != 0L) { //这是确认转为写锁是否成功
stamp = ws; //如果成功 替换票据
x = newX; //进行状态改变
y = newY; //进行状态改变
break;
} else { //如果不能成功转换为写锁
sl.unlockRead(stamp); //我们显式释放读锁
stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
}
}
} finally {
sl.unlock(stamp); //释放读锁或写锁
}
}
}
}
StampedLock中有一个BUG。其内部使通过死循环+CAS操作来修改状态位。但是没有处理中断的逻辑,会导致挂起的线程(通过Unsafe.park挂起线程。但是对于中断的线程,Unsafe.park会直接返回)中断后,一直处于死循环(直到满足终止条件)。因此整个过程会使CPU暴涨。我们来复现一下BUG。
我们发现带有3个中断状态的线程去参与对锁的竞争被阻塞了6秒,直到另一个线程释放了锁。
public class StampedLockBugTest {
public static StampedLock stampedLock = new StampedLock();
public static Unsafe unsafe;
static {
try {
Constructor constructor = Unsafe.class.getDeclaredConstructor();
ReflectionUtils.makeAccessible(constructor);
unsafe = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread parkThread= new Thread(new Runnable() {
@Override
public void run() {
Long stamp = stampedLock.writeLock();
unsafe.park(true, System.currentTimeMillis() + 6000L);
stampedLock.unlockWrite(stamp);
}
});
TimeUnit.SECONDS.sleep(1L);
parkThread.start();
Long start = System.currentTimeMillis();
List threadList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new MyThread());
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
Long end = System.currentTimeMillis();
System.out.println("3个线程阻塞了:" + (end - start) + "ms");
}
public static class MyThread implements Runnable {
@Override
public void run() {
Thread.currentThread().interrupt();
Long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + " read lock");
stampedLock.unlockRead(stamp);
}
}
}
Thread-1 read lock
Thread-2 read lock
Thread-3 read lock
Disconnected from the target VM, address: '127.0.0.1:56256', transport: 'socket'
3个线程阻塞了:6003ms
这个BUG其实在于StampedLock中的
long acquireRead(boolean interruptible, long deadline)
acquireWrite(boolean interruptible, long deadline)
没有添加保存/复原中断状态的机制。
我们只用在涉及到死循环的方法中添加这种机制就行了,以acquireWrite为例。
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大人物分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
采用的是工作窃取算法(work-stealing),思想是分而治之(快速排序也用到了此思想)。该算法优点是利用线程进行并行计算,并减少了线程间的竞争。被窃取任务的线程从双端队列的头部拿任务,窃取任务的线程从双端队列尾部拿任务。
使用Fork/Join框架的任务有一些局限性:
1.只能使用fork(把任务推入当前工作线程的工作队列里),join(等待该任务的处理线程处理完毕,获得返回值)来进行同步机制。比如调用了sleep使任务进入睡眠了,那么工作线程不能执行其他任务了。
2.不能执行IO操作。
3.任务不能抛出检查异常。
ForkJoinPool的每个工作线程都维护着一个workQueue(是一个双端队列Deque),里面存放的对象是ForkJoinTask。假设采用的是异步模式,每个工作线程在运行中产生新的任务(通常是调用fork())。新的任务会放入到工作队列的尾部。并且工作线程在处理自己队列中的任务时,使用的是FIFO方式,也就是每次从头部取出任务来执行。每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(来自刚刚提交到pool的任务,或者是其他工作线程的工作队列中的任务)。窃取的任务位于其他线程的工作队列的尾部,使用的是LIFO方式。
public class ForkJoinTest extends RecursiveTask {
public static final int threshold = 2;
private int start;
private int end;
public ForkJoinTest(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// 如果任何足够小就计算任务
boolean canCompute = (end - start) <= threshold;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果任务大于阈值, 就分裂成2个子任务计算
int middle = (start + end) / 2;
ForkJoinTest leftTask = new ForkJoinTest(start, middle);
ForkJoinTest rightTask = new ForkJoinTest(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待2个任务执行结束后合并结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
// 合并子任务
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTest forkJoinTest = new ForkJoinTest(1, 100);
Future result = forkJoinPool.submit(forkJoinTest);
System.out.println("result:" + result.get());
/**
* 1-----ForkJoinPool 使用submit 或 invoke 提交的区别:invoke是同步执行,
* 调用之后需要等待任务完成,才能执行后面的代码;submit是异步执行,
* 只有在Future调用get的时候会阻塞。
2-----这里继承的是RecursiveTask,还可以继承RecursiveAction。
前者适用于有返回值的场景,而后者适合于没有返回值的场景
3-----其实这里执行子任务调用fork方法并不是最佳的选择,最佳的选择是invokeAll方法。
*/
}