一、思维导图
首先通过思维导图的形式对Java并发编程的基础知识有个系统化的初步了解;其次要明白并发是基于底层有多个执行单元因此可以原先串行执行的程序进行拆分,将能够并行化执行的并发执行,当然必须串行化执行的部分则因此产生了竞争,为了保障竞争资源的安全性,需要采取保护机制(锁,避免被多方同时修改产生不一致)和同步机制(使得修改后其他方能及时知道或者是协同动作);最后在理解好并发编程的相关理论后,需要对并发场景下的常见问题以优化手段有个大体的掌握,并能在实际中使用。
![](https://img-blog.csdnimg.cn/img_convert/24bf2016aa5cee748bbabe9eee1b6b2d.png)
二、知识要点
1、锁的比较
![](https://img-blog.csdnimg.cn/img_convert/ff82fa7e1896f88498e637ee97cc019b.png)
Lock 锁更加灵活 ,支持绑定多个Condition条件
2、同步队列
/**
* BlockingQueue 单向阻塞队列
* 主要特性:
* 1.阻塞队列是线程安全的,入队/出队互不干扰
* 2.在不满足入队/出队条件时,可以选择阻塞操作线程,或选择快速失败
*/
ArrayBlockingQueue LinkedBlockingQueue
PriorityBlockingQueue
/**
* BlockingDeque 双向阻塞队列
* 除了具备单向阻塞队列的特性,双向阻塞队列还可以选择从队头入队,或从队尾出队。
*/
LinkedBlockingDeque
3、同步工具类
// 有阻塞队列,信号量,闭锁,栅栏
闭锁 CountDownLatch
信号量 Semaphore
栅栏 CyclicBarrier 所有线程必须同时到达栅栏位置,才能继续执行
Exchanger 交换器 双方栅栏
4、多线程
中断
InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
协作
join() wait() notify() notifyAll()
wait() 和 sleep() 的区别
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
wait() 会释放锁,sleep() 不会。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
FutureTask
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
CAS
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
ABA
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
可重入代码(Reentrant Code)
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等
三、应用场景
四、参考资料
《Java并发编程实战》 Brian Goetz 、Doug Lea 等著