CS-Notes/notes/Java 并发.md

1637 lines
60 KiB
Java
Raw Permalink Normal View History

2020-11-17 00:32:18 +08:00
# Java 并发
2019-04-25 18:24:51 +08:00
<!-- GFM-TOC -->
2020-11-17 00:32:18 +08:00
* [Java 并发](#java-并发)
* [使用线程](#一使用线程)
2020-11-18 03:00:45 +08:00
* [实现 Runnable 接口](#实现-runnable-接口)
* [实现 Callable 接口](#实现-callable-接口)
* [继承 Thread ](#继承-thread-)
* [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
2020-11-17 00:32:18 +08:00
* [基础线程机制](#二基础线程机制)
2020-11-18 03:00:45 +08:00
* [Executor](#executor)
* [Daemon](#daemon)
* [sleep()](#sleep)
* [yield()](#yield)
2020-11-17 00:32:18 +08:00
* [中断](#三中断)
2020-11-18 03:00:45 +08:00
* [InterruptedException](#interruptedexception)
* [interrupted()](#interrupted)
* [Executor 的中断操作](#executor-的中断操作)
2020-11-17 00:32:18 +08:00
* [互斥同步](#四互斥同步)
2020-11-18 03:00:45 +08:00
* [synchronized](#synchronized)
* [ReentrantLock](#reentrantlock)
* [比较](#比较)
* [使用选择](#使用选择)
2020-11-17 00:32:18 +08:00
* [线程之间的协作](#五线程之间的协作)
2020-11-18 03:00:45 +08:00
* [join()](#join)
* [wait() notify() notifyAll()](#wait-notify-notifyall)
* [await() signal() signalAll()](#await-signal-signalall)
2020-11-17 00:32:18 +08:00
* [线程状态](#六线程状态)
2020-11-18 03:00:45 +08:00
* [新建NEW](#新建new)
* [可运行RUNABLE](#可运行runable)
* [阻塞BLOCKED](#阻塞blocked)
* [无限期等待WAITING](#无限期等待waiting)
* [限期等待TIMED_WAITING](#限期等待timed_waiting)
* [死亡TERMINATED](#死亡terminated)
2020-11-17 00:32:18 +08:00
* [J.U.C - AQS](#七juc---aqs)
2020-11-18 03:00:45 +08:00
* [CountDownLatch](#countdownlatch)
* [CyclicBarrier](#cyclicbarrier)
* [Semaphore](#semaphore)
2020-11-17 00:32:18 +08:00
* [J.U.C - 其它组件](#八juc---其它组件)
2020-11-18 03:00:45 +08:00
* [FutureTask](#futuretask)
* [BlockingQueue](#blockingqueue)
* [ForkJoin](#forkjoin)
2020-11-17 00:32:18 +08:00
* [线程不安全示例](#九线程不安全示例)
* [Java 内存模型](#十java-内存模型)
2020-11-18 03:00:45 +08:00
* [主内存与工作内存](#主内存与工作内存)
* [内存间交互操作](#内存间交互操作)
* [内存模型三大特性](#内存模型三大特性)
* [先行发生原则](#先行发生原则)
2020-11-17 00:32:18 +08:00
* [十一线程安全](#十一线程安全)
2020-11-18 03:00:45 +08:00
* [不可变](#不可变)
* [互斥同步](#互斥同步)
* [非阻塞同步](#非阻塞同步)
* [无同步方案](#无同步方案)
2020-11-17 00:32:18 +08:00
* [十二锁优化](#十二锁优化)
2020-11-18 03:00:45 +08:00
* [自旋锁](#自旋锁)
* [锁消除](#锁消除)
* [锁粗化](#锁粗化)
* [轻量级锁](#轻量级锁)
* [偏向锁](#偏向锁)
2020-11-17 00:32:18 +08:00
* [十三多线程开发良好的实践](#十三多线程开发良好的实践)
* [参考资料](#参考资料)
2019-04-25 18:24:51 +08:00
<!-- GFM-TOC -->
2020-11-17 00:32:18 +08:00
## 使用线程
2019-04-25 18:24:51 +08:00
有三种使用线程的方法
- 实现 Runnable 接口
- 实现 Callable 接口
- 继承 Thread
2020-01-09 00:52:33 +08:00
实现 Runnable Callable 接口的类只能当做一个可以在线程中运行的任务不是真正意义上的线程因此最后还需要通过 Thread 来调用可以理解为任务是通过线程驱动从而执行的
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
### 实现 Runnable 接口
2019-04-25 18:24:51 +08:00
2020-01-09 00:52:33 +08:00
需要实现接口中的 run() 方法
2019-04-25 18:24:51 +08:00
```java
public class MyRunnable implements Runnable {
2020-01-09 00:52:33 +08:00
@Override
2019-04-25 18:24:51 +08:00
public void run() {
// ...
}
}
```
2020-01-09 00:52:33 +08:00
使用 Runnable 实例再创建一个 Thread 实例然后调用 Thread 实例的 start() 方法来启动线程
2019-04-25 18:24:51 +08:00
```java
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
```
2020-11-17 00:32:18 +08:00
### 实现 Callable 接口
2019-04-25 18:24:51 +08:00
Runnable 相比Callable 可以有返回值返回值通过 FutureTask 进行封装
```java
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
```
```java
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
```
2020-11-17 00:32:18 +08:00
### 继承 Thread
2019-04-25 18:24:51 +08:00
同样也是需要实现 run() 方法因为 Thread 类也实现了 Runable 接口
当调用 start() 方法启动一个线程时虚拟机会将该线程放入就绪队列中等待被调度当一个线程被调度时会执行该线程的 run() 方法
```java
public class MyThread extends Thread {
public void run() {
// ...
}
}
```
```java
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
```
2020-11-17 00:32:18 +08:00
### 实现接口 VS 继承 Thread
2019-04-25 18:24:51 +08:00
实现接口会更好一些因为
- Java 不支持多重继承因此继承了 Thread 类就无法继承其它类但是可以实现多个接口
- 类可能只要求可执行就行继承整个 Thread 类开销过大
2020-11-17 00:32:18 +08:00
## 基础线程机制
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
### Executor
2019-04-25 18:24:51 +08:00
Executor 管理多个异步任务的执行而无需程序员显式地管理线程的生命周期这里的异步是指多个任务的执行互不干扰不需要进行同步操作
主要有三种 Executor
- CachedThreadPool一个任务创建一个线程
- FixedThreadPool所有任务只能使用固定大小的线程
- SingleThreadExecutor相当于大小为 1 FixedThreadPool
```java
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
```
2020-11-17 00:32:18 +08:00
### Daemon
2019-04-25 18:24:51 +08:00
守护线程是程序运行时在后台提供服务的线程不属于程序中不可或缺的部分
当所有非守护线程结束时程序也就终止同时会杀死所有守护线程
main() 属于非守护线程
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程
2019-04-25 18:24:51 +08:00
```java
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
```
2020-11-17 00:32:18 +08:00
### sleep()
2019-04-25 18:24:51 +08:00
Thread.sleep(millisec) 方法会休眠当前正在执行的线程millisec 单位为毫秒
sleep() 可能会抛出 InterruptedException因为异常不能跨线程传播回 main() 因此必须在本地进行处理线程中抛出的其它异常也同样需要在本地进行处理
```java
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
2020-11-17 00:32:18 +08:00
### yield()
2019-04-25 18:24:51 +08:00
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分可以切换给其它线程来执行该方法只是对线程调度器的一个建议而且也只是建议具有相同优先级的其它线程可以运行
```java
public void run() {
Thread.yield();
}
```
2020-11-17 00:32:18 +08:00
## 中断
2019-04-25 18:24:51 +08:00
一个线程执行完毕之后会自动结束如果在运行过程中发生异常也会提前结束
2020-11-17 00:32:18 +08:00
### InterruptedException
2019-04-25 18:24:51 +08:00
通过调用一个线程的 interrupt() 来中断该线程如果该线程处于阻塞限期等待或者无限期等待状态那么就会抛出 InterruptedException从而提前结束该线程但是不能中断 I/O 阻塞和 synchronized 锁阻塞
对于以下代码 main() 中启动一个线程之后再中断它由于线程中调用了 Thread.sleep() 方法因此会抛出一个 InterruptedException从而提前结束线程不执行之后的语句
```java
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
```java
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
```
```html
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
```
2020-11-17 00:32:18 +08:00
### interrupted()
2019-04-25 18:24:51 +08:00
如果一个线程的 run() 方法执行一个无限循环并且没有执行 sleep() 等会抛出 InterruptedException 的操作那么调用线程的 interrupt() 方法就无法使线程提前结束
但是调用 interrupt() 方法会设置线程的中断标记此时调用 interrupted() 方法会返回 true因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态从而提前结束线程
```java
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
}
```
```java
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();
}
```
```html
Thread end
```
2020-11-17 00:32:18 +08:00
### Executor 的中断操作
2019-04-25 18:24:51 +08:00
调用 Executor shutdown() 方法会等待线程都执行完毕之后再关闭但是如果调用的是 shutdownNow() 方法则相当于调用每个线程的 interrupt() 方法
以下使用 Lambda 创建线程相当于创建了一个匿名内部线程
```java
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow();
System.out.println("Main run");
}
```
```html
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
```
2020-11-17 00:32:18 +08:00
如果只想中断 Executor 中的一个线程可以通过使用 submit() 方法来提交一个线程它会返回一个 Future\<?\> 对象通过调用该对象的 cancel(true) 方法就可以中断线程
2019-04-25 18:24:51 +08:00
```java
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
```
2020-11-17 00:32:18 +08:00
## 互斥同步
2019-04-25 18:24:51 +08:00
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问第一个是 JVM 实现的 synchronized而另一个是 JDK 实现的 ReentrantLock
2020-11-17 00:32:18 +08:00
### synchronized
2019-04-25 18:24:51 +08:00
2019-11-02 12:07:41 +08:00
**1. 同步一个代码块**
2019-04-25 18:24:51 +08:00
```java
public void func() {
synchronized (this) {
// ...
}
}
```
它只作用于同一个对象如果调用两个对象上的同步代码块就不会进行同步
对于以下代码使用 ExecutorService 执行了两个线程由于调用的是同一个对象的同步代码块因此这两个线程会进行同步当一个线程进入同步语句块时另一个线程就必须等待
```java
public class SynchronizedExample {
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
```
```java
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
```
```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
对于以下代码两个线程调用了不同对象的同步代码块因此这两个线程就不需要同步从输出结果可以看出两个线程交叉执行
```java
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
```
```html
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
```
2019-11-02 12:07:41 +08:00
**2. 同步一个方法**
2019-04-25 18:24:51 +08:00
```java
public synchronized void func () {
// ...
}
```
它和同步代码块一样作用于同一个对象
2019-11-02 12:07:41 +08:00
**3. 同步一个类**
2019-04-25 18:24:51 +08:00
```java
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
```
作用于整个类也就是说两个线程调用同一个类的不同对象上的这种同步语句也会进行同步
```java
public class SynchronizedExample {
public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
```
```java
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
```
```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
2019-11-02 12:07:41 +08:00
**4. 同步一个静态方法**
2019-04-25 18:24:51 +08:00
```java
public synchronized static void fun() {
// ...
}
```
作用于整个类
2020-11-17 00:32:18 +08:00
### ReentrantLock
2019-04-25 18:24:51 +08:00
ReentrantLock java.util.concurrentJ.U.C包中的锁
```java
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
```
```java
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
```
```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
2020-11-17 00:32:18 +08:00
### 比较
2019-04-25 18:24:51 +08:00
2019-11-02 12:07:41 +08:00
**1. 锁的实现**
2019-04-25 18:24:51 +08:00
synchronized JVM 实现的 ReentrantLock JDK 实现的
2019-11-02 12:07:41 +08:00
**2. 性能**
2019-04-25 18:24:51 +08:00
新版本 Java synchronized 进行了很多优化例如自旋锁等synchronized ReentrantLock 大致相同
2019-11-02 12:07:41 +08:00
**3. 等待可中断**
2019-04-25 18:24:51 +08:00
当持有锁的线程长期不释放锁的时候正在等待的线程可以选择放弃等待改为处理其他事情
ReentrantLock 可中断 synchronized 不行
2019-11-02 12:07:41 +08:00
**4. 公平锁**
2019-04-25 18:24:51 +08:00
公平锁是指多个线程在等待同一个锁时必须按照申请锁的时间顺序来依次获得锁
synchronized 中的锁是非公平的ReentrantLock 默认情况下也是非公平的但是也可以是公平的
2019-11-02 12:07:41 +08:00
**5. 锁绑定多个条件**
2019-04-25 18:24:51 +08:00
一个 ReentrantLock 可以同时绑定多个 Condition 对象
2020-11-17 00:32:18 +08:00
### 使用选择
2019-04-25 18:24:51 +08:00
除非需要使用 ReentrantLock 的高级功能否则优先使用 synchronized这是因为 synchronized JVM 实现的一种锁机制JVM 原生地支持它 ReentrantLock 不是所有的 JDK 版本都支持并且使用 synchronized 不用担心没有释放锁而导致死锁问题因为 JVM 会确保锁的释放
2020-11-17 00:32:18 +08:00
## 线程之间的协作
2019-04-25 18:24:51 +08:00
当多个线程可以一起工作去解决某个问题时如果某些部分必须在其它部分之前完成那么就需要对线程进行协调
2020-11-17 00:32:18 +08:00
### join()
2019-04-25 18:24:51 +08:00
在线程中调用另一个线程的 join() 方法会将当前线程挂起而不是忙等待直到目标线程结束
对于以下代码虽然 b 线程先启动但是因为在 b 线程中调用了 a 线程的 join() 方法b 线程会等待 a 线程结束才继续执行因此最后能够保证 a 线程的输出先于 b 线程的输出
```java
public class JoinExample {
private class A extends Thread {
@Override
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
```
```java
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
```
```
A
B
```
2020-11-17 00:32:18 +08:00
### wait() notify() notifyAll()
2019-04-25 18:24:51 +08:00
调用 wait() 使得线程等待某个条件满足线程在等待时会被挂起当其他线程的运行使得这个条件满足时其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程
它们都属于 Object 的一部分而不属于 Thread
只能用在同步方法或者同步控制块中使用否则会在运行时抛出 IllegalMonitorStateException
使用 wait() 挂起期间线程会释放锁这是因为如果没有释放锁那么其它线程就无法进入对象的同步方法或者同步控制块中那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程造成死锁
```java
public class WaitNotifyExample {
public synchronized void before() {
System.out.println("before");
notifyAll();
}
public synchronized void after() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after");
}
}
```
```java
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotifyExample example = new WaitNotifyExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
```
```html
before
after
```
2019-11-02 12:07:41 +08:00
**wait() sleep() 的区别**
2019-04-25 18:24:51 +08:00
- wait() Object 的方法 sleep() Thread 的静态方法
- wait() 会释放锁sleep() 不会
2020-11-17 00:32:18 +08:00
### await() signal() signalAll()
2019-04-25 18:24:51 +08:00
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调可以在 Condition 上调用 await() 方法使线程等待其它线程调用 signal() signalAll() 方法唤醒等待的线程
相比于 wait() 这种等待方式await() 可以指定等待的条件因此更加灵活
使用 Lock 来获取一个 Condition 对象
```java
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
```
```java
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
```
```html
before
after
```
2020-11-17 00:32:18 +08:00
## 线程状态
2019-12-13 00:48:43 +08:00
一个线程只能处于一种状态并且这里的线程状态特指 Java 虚拟机的线程状态不能反映线程在特定操作系统下的状态
2020-11-17 00:32:18 +08:00
### 新建NEW
2019-12-13 00:48:43 +08:00
创建后尚未启动
2020-11-17 00:32:18 +08:00
### 可运行RUNABLE
2019-12-13 00:48:43 +08:00
正在 Java 虚拟机中运行但是在操作系统层面它可能处于运行状态也可能等待资源调度例如处理器资源资源调度完成就进入运行状态所以该状态的可运行是指可以被运行具体有没有运行要看底层操作系统的资源调度
2020-11-17 00:32:18 +08:00
### 阻塞BLOCKED
2019-12-13 00:48:43 +08:00
请求获取 monitor lock 从而进入 synchronized 函数或者代码块但是其它线程已经占用了该 monitor lock所以出于阻塞状态要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock
2020-11-17 00:32:18 +08:00
### 无限期等待WAITING
2019-12-13 00:48:43 +08:00
等待其它线程显式地唤醒
阻塞和等待的区别在于阻塞是被动的它是在等待获取 monitor lock而等待是主动的通过调用 Object.wait() 等方法进入
| 进入方法 | 退出方法 |
| --- | --- |
| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
| LockSupport.park() 方法 | LockSupport.unpark(Thread) |
2020-11-17 00:32:18 +08:00
### 限期等待TIMED_WAITING
2019-12-13 00:48:43 +08:00
无需等待其它线程显式地唤醒在一定时间之后会被系统自动唤醒
| 进入方法 | 退出方法 |
| --- | --- |
| Thread.sleep() 方法 | 时间结束 |
| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
调用 Thread.sleep() 方法使线程进入限期等待状态时常常用使一个线程睡眠进行描述调用 Object.wait() 方法使线程进入限期等待或者无限期等待时常常用挂起一个线程进行描述睡眠和挂起是用来描述行为而阻塞和等待用来描述状态
2020-11-17 00:32:18 +08:00
### 死亡TERMINATED
2019-12-13 00:48:43 +08:00
可以是线程结束任务之后自己结束或者产生了异常而结束
[Java SE 9 Enum Thread.State](https://docs.oracle.com/javase/9/docs/api/java/lang/Thread.State.html)
2020-11-17 00:32:18 +08:00
## J.U.C - AQS
2019-04-25 18:24:51 +08:00
java.util.concurrentJ.U.C大大提高了并发性能AQS 被认为是 J.U.C 的核心
2020-11-17 00:32:18 +08:00
### CountDownLatch
2019-04-25 18:24:51 +08:00
2019-07-13 23:50:08 +08:00
用来控制一个或者多个线程等待多个线程
2019-04-25 18:24:51 +08:00
维护了一个计数器 cnt每次调用 countDown() 方法会让计数器的值减 1减到 0 的时候那些因为调用 await() 方法而在等待的线程就会被唤醒
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ba078291-791e-4378-b6d1-ece76c2f0b14.png" width="300px"> </div><br>
2019-04-25 18:24:51 +08:00
```java
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
```
```html
run..run..run..run..run..run..run..run..run..run..end
```
2020-11-17 00:32:18 +08:00
### CyclicBarrier
2019-04-25 18:24:51 +08:00
用来控制多个线程互相等待只有当多个线程都到达时这些线程才会继续执行
CountdownLatch 相似都是通过维护计数器来实现的线程执行 await() 方法之后计数器会减 1并进行等待直到计数器为 0所有调用 await() 方法而在等待的线程才能继续执行
CyclicBarrier CountdownLatch 的一个区别是CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用所以它才叫做循环屏障
CyclicBarrier 有两个构造函数其中 parties 指示计数器的初始值barrierAction 在所有线程都到达屏障的时候会执行一次
```java
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
```
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f71af66b-0d54-4399-a44b-f47b58321984.png" width="300px"> </div><br>
2019-04-25 18:24:51 +08:00
```java
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
```
```html
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
```
2020-11-17 00:32:18 +08:00
### Semaphore
2019-04-25 18:24:51 +08:00
Semaphore 类似于操作系统中的信号量可以控制对互斥资源的访问线程数
以下代码模拟了对某个服务的并发请求每次只能有 3 个客户端同时访问请求总数为 10
```java
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
```
```html
2 1 2 2 2 2 2 1 2 2
```
2020-11-17 00:32:18 +08:00
## J.U.C - 其它组件
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
### FutureTask
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
在介绍 Callable 时我们知道它可以有返回值返回值通过 Future\<V\> 进行封装FutureTask 实现了 RunnableFuture 接口该接口继承自 Runnable Future\<V\> 接口这使得 FutureTask 既可以当做一个任务执行也可以有返回值
2019-04-25 18:24:51 +08:00
```java
public class FutureTask<V> implements RunnableFuture<V>
```
```java
public interface RunnableFuture<V> extends Runnable, Future<V>
```
FutureTask 可用于异步获取执行结果或取消执行任务的场景当一个计算任务需要执行很长时间那么就可以用 FutureTask 来封装这个任务主线程在完成自己的任务之后再去获取结果
```java
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
otherThread.start();
System.out.println(futureTask.get());
}
}
```
```html
other task is running...
4950
```
2020-11-17 00:32:18 +08:00
### BlockingQueue
2019-04-25 18:24:51 +08:00
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现
2019-11-02 12:07:41 +08:00
- **FIFO 队列** LinkedBlockingQueueArrayBlockingQueue固定长度
- **优先级队列** PriorityBlockingQueue
2019-04-25 18:24:51 +08:00
提供了阻塞的 take() put() 方法如果队列为空 take() 将阻塞直到队列中有内容如果队列为满 put() 将阻塞直到队列有空闲位置
2019-11-02 12:07:41 +08:00
**使用 BlockingQueue 实现生产者消费者问题**
2019-04-25 18:24:51 +08:00
```java
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
```
```java
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
```
```html
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
```
2020-11-17 00:32:18 +08:00
### ForkJoin
2019-04-25 18:24:51 +08:00
主要用于并行计算中 MapReduce 原理类似都是把大的计算任务拆分成多个小任务并行计算
```java
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;
}
}
```
```java
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
```
ForkJoin 使用 ForkJoinPool 来启动它是一个特殊的线程池线程数量取决于 CPU 核数
```java
public class ForkJoinPool extends AbstractExecutorService
```
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率每个线程都维护了一个双端队列用来存储需要执行的任务工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行窃取的任务必须是最晚的任务避免和队列所属线程发生竞争例如下图中Thread2 Thread1 的队列中拿出最晚的 Task1 任务Thread1 会拿出 Task2 来执行这样就避免发生竞争但是如果队列中只有一个任务时还是会发生竞争
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e42f188f-f4a9-4e6f-88fc-45f4682072fb.png" width="300px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
## 线程不安全示例
2019-04-25 18:24:51 +08:00
如果多个线程对同一个共享数据进行访问而不采取同步操作的话那么操作的结果是不一致的
以下代码演示了 1000 个线程同时对 cnt 执行自增操作操作结束之后它的值有可能小于 1000
```java
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
```
```java
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
```
```html
997
```
2020-11-17 00:32:18 +08:00
## Java 内存模型
2019-04-25 18:24:51 +08:00
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异以实现让 Java 程序在各种平台下都能达到一致的内存访问效果
2020-11-17 00:32:18 +08:00
### 主内存与工作内存
2019-04-25 18:24:51 +08:00
处理器上的寄存器的读写的速度比内存快几个数量级为了解决这种速度矛盾在它们之间加入了高速缓存
加入高速缓存带来了一个新的问题缓存一致性如果多个缓存共享同一块主内存区域那么多个缓存的数据可能会不一致需要一些协议来解决这个问题
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/942ca0d2-9d5c-45a4-89cb-5fd89b61913f.png" width="600px"> </div><br>
2019-04-25 18:24:51 +08:00
所有的变量都存储在主内存中每个线程还有自己的工作内存工作内存存储在高速缓存或者寄存器中保存了该线程使用的变量的主内存副本拷贝
线程只能直接操作工作内存中的变量不同线程之间的变量值传递需要通过主内存来完成
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/15851555-5abc-497d-ad34-efed10f43a6b.png" width="600px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
### 内存间交互操作
2019-04-25 18:24:51 +08:00
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8b7ebbad-9604-4375-84e3-f412099d170c.png" width="450px"> </div><br>
2019-04-25 18:24:51 +08:00
- read把一个变量的值从主内存传输到工作内存中
- load read 之后执行 read 得到的值放入工作内存的变量副本中
- use把工作内存中一个变量的值传递给执行引擎
- assign把一个从执行引擎接收到的值赋给工作内存的变量
- store把工作内存的一个变量的值传送到主内存中
- write store 之后执行 store 得到的值放入主内存的变量中
- lock作用于主内存的变量
- unlock
2020-11-17 00:32:18 +08:00
### 内存模型三大特性
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 1. 原子性
2019-04-25 18:24:51 +08:00
Java 内存模型保证了 readloaduseassignstorewritelock unlock 操作具有原子性例如对一个 int 类型的变量执行 assign 赋值操作这个操作就是原子性的但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据longdouble的读写操作划分为两次 32 位的操作来进行 loadstoreread write 操作可以不具备原子性
有一个错误认识就是int 等原子性的类型在多线程环境中不会出现线程安全问题前面的线程不安全示例代码中cnt 属于 int 类型变量1000 个线程对它进行自增操作之后得到的值为 997 而不是 1000
为了方便讨论将内存间的交互操作简化为 3 loadassignstore
下图演示了两个线程同时对 cnt 进行操作loadassignstore 这一系列操作整体上看不具备原子性那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存T2 依然可以读入旧值可以看出这两个线程虽然执行了两次自增运算但是主内存中 cnt 的值最后为 1 而不是 2因此对 int 类型读写操作满足原子性只是说明 loadassignstore 这些单个操作具备原子性
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2797a609-68db-4d7b-8701-41ac9a34b14f.jpg" width="300px"> </div><br>
2019-04-25 18:24:51 +08:00
AtomicInteger 能保证多个线程修改的原子性
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dd563037-fcaa-4bd8-83b6-b39d93a12c77.jpg" width="300px"> </div><br>
2019-04-25 18:24:51 +08:00
使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现
```java
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
public int get() {
return cnt.get();
}
}
```
```java
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // 只修改这条语句
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
```
```html
1000
```
除了使用原子类之外也可以使用 synchronized 互斥锁来保证操作的原子性它对应的内存间交互操作为lock unlock在虚拟机实现上对应的字节码指令为 monitorenter monitorexit
```java
public class AtomicSynchronizedExample {
private int cnt = 0;
public synchronized void add() {
cnt++;
}
public synchronized int get() {
return cnt;
}
}
```
```java
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
```
```html
1000
```
2020-11-17 00:32:18 +08:00
#### 2. 可见性
2019-04-25 18:24:51 +08:00
可见性指当一个线程修改了共享变量的值其它线程能够立即得知这个修改Java 内存模型是通过在变量修改后将新值同步回主内存在变量读取前从主内存刷新变量值来实现可见性的
主要有三种实现可见性的方式
- volatile
- synchronized对一个变量执行 unlock 操作之前必须把变量值同步回主内存
- final final 关键字修饰的字段在构造器中一旦初始化完成并且没有发生 this 逃逸其它线程通过 this 引用访问到初始化了一半的对象那么其它线程就能看见 final 字段的值
对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰不能解决线程不安全问题因为 volatile 并不能保证操作的原子性
2020-11-17 00:32:18 +08:00
#### 3. 有序性
2019-04-25 18:24:51 +08:00
有序性是指在本线程内观察所有操作都是有序的在一个线程观察另一个线程所有操作都是无序的无序是因为发生了指令重排序 Java 内存模型中允许编译器和处理器对指令进行重排序重排序过程不会影响到单线程程序的执行却会影响到多线程并发执行的正确性
volatile 关键字通过添加内存屏障的方式来禁止指令重排即重排序时不能把后面的指令放到内存屏障之前
也可以通过 synchronized 来保证有序性它保证每个时刻只有一个线程执行同步代码相当于是让线程顺序执行同步代码
2020-11-17 00:32:18 +08:00
### 先行发生原则
2019-04-25 18:24:51 +08:00
上面提到了可以用 volatile synchronized 来保证有序性除此之外JVM 还规定了先行发生原则让一个操作无需控制就能先于另一个操作完成
2020-11-17 00:32:18 +08:00
#### 1. 单一线程原则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Single Thread rule
2019-04-25 18:24:51 +08:00
在一个线程内在程序前面的操作先行发生于后面的操作
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/874b3ff7-7c5c-4e7a-b8ab-a82a3e038d20.png" width="180px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 2. 管程锁定规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Monitor Lock Rule
2019-04-25 18:24:51 +08:00
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8996a537-7c4a-4ec8-a3b7-7ef1798eae26.png" width="350px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 3. volatile 变量规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Volatile Variable Rule
2019-04-25 18:24:51 +08:00
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/942f33c9-8ad9-4987-836f-007de4c21de0.png" width="400px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 4. 线程启动规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Thread Start Rule
2019-04-25 18:24:51 +08:00
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6270c216-7ec0-4db7-94de-0003bce37cd2.png" width="380px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 5. 线程加入规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Thread Join Rule
2019-04-25 18:24:51 +08:00
Thread 对象的结束先行发生于 join() 方法返回
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/233f8d89-31d7-413f-9c02-042f19c46ba1.png" width="400px"> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
#### 6. 线程中断规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Thread Interruption Rule
2019-04-25 18:24:51 +08:00
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生可以通过 interrupted() 方法检测到是否有中断发生
2020-11-17 00:32:18 +08:00
#### 7. 对象终结规则
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Finalizer Rule
2019-04-25 18:24:51 +08:00
一个对象的初始化完成构造函数执行结束先行发生于它的 finalize() 方法的开始
2020-11-17 00:32:18 +08:00
#### 8. 传递性
2019-04-25 18:24:51 +08:00
2020-11-18 03:00:45 +08:00
> Transitivity
2019-04-25 18:24:51 +08:00
如果操作 A 先行发生于操作 B操作 B 先行发生于操作 C那么操作 A 先行发生于操作 C
2020-11-17 00:32:18 +08:00
## 十一线程安全
2019-04-25 18:24:51 +08:00
多个线程不管以何种方式访问某个类并且在主调代码中不需要进行同步都能表现正确的行为
线程安全有以下几种实现方式
2020-11-17 00:32:18 +08:00
### 不可变
2019-04-25 18:24:51 +08:00
不可变Immutable的对象一定是线程安全的不需要再采取任何的线程安全保障措施只要一个不可变的对象被正确地构建出来永远也不会看到它在多个线程之中处于不一致的状态多线程环境下应当尽量使对象成为不可变来满足线程安全
不可变的类型
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类 Long Double 等数值包装类型BigInteger BigDecimal 等大数据类型但同为 Number 的原子类 AtomicInteger AtomicLong 则是可变的
对于集合类型可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合
```java
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", 1);
}
}
```
```html
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)
```
Collections.unmodifiableXXX() 先对原始的集合进行拷贝需要对集合进行修改的方法都直接抛出异常
```java
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
```
2020-11-17 00:32:18 +08:00
### 互斥同步
2019-04-25 18:24:51 +08:00
synchronized ReentrantLock
2020-11-17 00:32:18 +08:00
### 非阻塞同步
2019-04-25 18:24:51 +08:00
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题因此这种同步也称为阻塞同步
互斥同步属于一种悲观的并发策略总是认为只要不去做正确的同步措施那就肯定会出现问题无论共享数据是否真的会出现竞争它都要进行加锁这里讨论的是概念模型实际上虚拟机会优化掉很大一部分不必要的加锁用户态核心态转换维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作
随着硬件指令集的发展我们可以使用基于冲突检测的乐观并发策略先进行操作如果没有其它线程争用共享数据那操作就成功了否则采取补偿措施不断地重试直到成功为止这种乐观的并发策略的许多实现都不需要将线程阻塞因此这种同步操作称为非阻塞同步
2020-11-17 00:32:18 +08:00
#### 1. CAS
2019-08-11 23:10:46 +08:00
2019-04-25 18:24:51 +08:00
乐观锁需要操作和冲突检测这两个步骤具备原子性这里就不能再使用互斥同步来保证了只能靠硬件来完成硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCASCAS 指令需要有 3 个操作数分别是内存地址 V旧的预期值 A 和新值 B当执行操作时只有当 V 的值等于 A才将 V 的值更新为 B
2020-11-17 00:32:18 +08:00
#### 2. AtomicInteger
2019-04-25 18:24:51 +08:00
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作
以下代码使用了 AtomicInteger 执行了自增的操作
```java
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
```
以下代码是 incrementAndGet() 的源码它调用了 Unsafe getAndAddInt()
```java
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
```
以下代码是 getAndAddInt() 源码var1 指示对象内存地址var2 指示该字段相对对象内存地址的偏移var4 指示操作需要加的数值这里为 1通过 getIntVolatile(var1, var2) 得到旧的预期值通过调用 compareAndSwapInt() 来进行 CAS 比较如果该字段内存地址中的值等于 var5那么就更新内存地址为 var1+var2 的变量为 var5+var4
可以看到 getAndAddInt() 在一个循环中进行发生冲突的做法是不断的进行重试
```java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
```
2020-11-17 00:32:18 +08:00
#### 3. ABA
2019-04-25 18:24:51 +08:00
如果一个变量初次读取的时候是 A 它的值被改成了 B后来又被改回为 A CAS 操作就会误认为它从来没有被改变过
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题它可以通过控制变量值的版本来保证 CAS 的正确性大部分情况下 ABA 问题不会影响程序并发的正确性如果需要解决 ABA 问题改用传统的互斥同步可能会比原子类更高效
2020-11-17 00:32:18 +08:00
### 无同步方案
2019-04-25 18:24:51 +08:00
要保证线程安全并不是一定就要进行同步如果一个方法本来就不涉及共享数据那它自然就无须任何同步措施去保证正确性
2020-11-17 00:32:18 +08:00
#### 1. 栈封闭
2019-04-25 18:24:51 +08:00
多个线程访问同一个方法的局部变量时不会出现线程安全问题因为局部变量存储在虚拟机栈中属于线程私有的
```java
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
```
```java
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
```
```html
100
100
```
2020-11-17 00:32:18 +08:00
#### 2. 线程本地存储Thread Local Storage
2019-04-25 18:24:51 +08:00
如果一段代码中所需要的数据必须与其他代码共享那就看看这些共享数据的代码是否能保证在同一个线程中执行如果能保证我们就可以把共享数据的可见范围限制在同一个线程之内这样无须同步也能保证线程之间不出现数据争用的问题
符合这种特点的应用并不少见大部分使用消费队列的架构模式生产者-消费者模式都会将产品的消费过程尽量在一个线程中消费完其中最重要的一个应用实例就是经典 Web 交互模型中的一个请求对应一个服务器线程Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能
对于以下代码thread1 中设置 threadLocal 1 thread2 设置 threadLocal 2过了一段时间之后thread1 读取 threadLocal 依然是 1不受 thread2 的影响
```java
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
```
```html
1
```
为了理解 ThreadLocal先看以下代码
```java
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2);
});
thread1.start();
thread2.start();
}
}
```
它所对应的底层结构图为
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6782674c-1bfe-4879-af39-e9d722a95d39.png" width="500px"> </div><br>
2019-04-25 18:24:51 +08:00
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象
```java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
```
2020-11-17 00:32:18 +08:00
当调用一个 ThreadLocal set(T value) 方法时先得到当前线程的 ThreadLocalMap 对象然后将 ThreadLocal-\>value 键值对插入到该 Map
2019-04-25 18:24:51 +08:00
```java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
```
get() 方法类似
```java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
```
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的因为根本不存在多线程竞争
在一些场景 (尤其是使用线程池) 由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况应该尽可能在每次使用 ThreadLocal 后手动调用 remove()以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险
2020-11-17 00:32:18 +08:00
#### 3. 可重入代码Reentrant Code
2019-04-25 18:24:51 +08:00
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误
可重入代码有一些共同的特征例如不依赖存储在堆上的数据和公用的系统资源用到的状态量都由参数中传入不调用非可重入的方法等
2020-11-17 00:32:18 +08:00
## 十二锁优化
2019-04-25 18:24:51 +08:00
这里的锁优化主要是指 JVM synchronized 的优化
2020-11-17 00:32:18 +08:00
### 自旋锁
2019-04-25 18:24:51 +08:00
互斥同步进入阻塞状态的开销都很大应该尽量避免在许多应用中共享数据的锁定状态只会持续很短的一段时间自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环自旋一段时间如果在这段时间内能获得锁就可以避免进入阻塞状态
自旋锁虽然能避免进入阻塞状态从而减少开销但是它需要进行忙循环操作占用 CPU 时间它只适用于共享数据的锁定状态很短的场景
JDK 1.6 中引入了自适应的自旋锁自适应意味着自旋的次数不再固定了而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定
2020-11-17 00:32:18 +08:00
### 锁消除
2019-04-25 18:24:51 +08:00
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除
锁消除主要是通过逃逸分析来支持如果堆上的共享数据不可能逃逸出去被其它线程访问到那么就可以把它们当成私有数据对待也就可以将它们的锁进行消除
对于一些看起来没有加锁的代码其实隐式的加了很多锁例如下面的字符串拼接代码就隐式加了锁
```java
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
```
String 是一个不可变的类编译器会对 String 的拼接自动优化 JDK 1.5 之前会转化为 StringBuffer 对象的连续 append() 操作
```java
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
```
每个 append() 方法中都有一个同步块虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部也就是说sb 的所有引用永远不会逃逸到 concatString() 方法之外其他线程无法访问到它因此可以进行消除
2020-11-17 00:32:18 +08:00
### 锁粗化
2019-04-25 18:24:51 +08:00
如果一系列的连续操作都对同一个对象反复加锁和解锁频繁的加锁操作就会导致性能损耗
上一节的示例代码中连续的 append() 方法就属于这类情况如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁将会把加锁的范围扩展粗化到整个操作序列的外部对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后这样只需要加锁一次就可以了
2020-11-17 00:32:18 +08:00
### 轻量级锁
2019-04-25 18:24:51 +08:00
JDK 1.6 引入了偏向锁和轻量级锁从而让锁拥有了四个状态无锁状态unlocked偏向锁状态biasble轻量级锁状态lightweight locked和重量级锁状态inflated
以下是 HotSpot 虚拟机对象头的内存布局这些数据被称为 Mark Word其中 tag bits 对应了五个状态这些状态在右侧的 state 表格中给出除了 marked for gc 状态其它四个状态已经在前面介绍过了
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="500"/> </div><br>
2019-04-25 18:24:51 +08:00
下图左侧是一个线程的虚拟机栈其中有一部分称为 Lock Record 的区域这是在轻量级锁运行过程创建的用于存放锁对象的 Mark Word而右侧就是一个锁对象包含了 Mark Word 和其它信息
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/051e436c-0e46-4c59-8f67-52d89d656182.png" width="500"/> </div><br>
2019-04-25 18:24:51 +08:00
轻量级锁是相对于传统的重量级锁而言它使用 CAS 操作来避免重量级锁使用互斥量的开销对于绝大部分的锁在整个同步周期内都是不存在竞争的因此也就不需要都使用互斥量进行同步可以先采用 CAS 操作进行同步如果 CAS 失败了再改用互斥量进行同步
当尝试获取一个锁对象时如果锁对象标记为 0 01说明锁对象的锁未锁定unlocked状态此时虚拟机在当前线程的虚拟机栈中创建 Lock Record然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针如果 CAS 操作成功了那么线程就获取了该对象上的锁并且对象的 Mark Word 的锁标记变为 00表示该对象处于轻量级锁状态
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="400"/> </div><br>
2019-04-25 18:24:51 +08:00
如果 CAS 操作失败了虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈如果是的话说明当前线程已经拥有了这个锁对象那就可以直接进入同步块继续执行否则说明这个锁对象已经被其他线程线程抢占了如果有两条以上的线程争用同一个锁那轻量级锁就不再有效要膨胀为重量级锁
2020-11-17 00:32:18 +08:00
### 偏向锁
2019-04-25 18:24:51 +08:00
偏向锁的思想是偏向于让第一个获取锁对象的线程这个线程在之后获取该锁就不再需要进行同步操作甚至连 CAS 操作也不再需要
当锁对象第一次被线程获得的时候进入偏向状态标记为 1 01同时使用 CAS 操作将线程 ID 记录到 Mark Word 如果 CAS 操作成功这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作
当有另外一个线程去尝试获取这个锁对象时偏向状态就宣告结束此时撤销偏向Revoke Bias后恢复到未锁定状态或者轻量级锁状态
2019-12-06 10:11:23 +08:00
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg" width="600"/> </div><br>
2019-04-25 18:24:51 +08:00
2020-11-17 00:32:18 +08:00
## 十三多线程开发良好的实践
2019-04-25 18:24:51 +08:00
- 给线程起个有意义的名字这样可以方便找 Bug
- 缩小同步范围从而减少锁争用例如对于 synchronized应该尽量使用同步块而不是同步方法
- 多用同步工具少用 wait() notify()首先CountDownLatch, CyclicBarrier, Semaphore Exchanger 这些同步类简化了编码操作而用 wait() notify() 很难实现复杂控制流其次这些同步类是由最好的企业编写和维护在后续的 JDK 中还会不断优化和完善
- 使用 BlockingQueue 实现生产者消费者问题
- 多用并发集合少用同步集合例如应该使用 ConcurrentHashMap 而不是 Hashtable
- 使用本地变量和不可变类来保证线程安全
- 使用线程池而不是直接创建线程这是因为创建线程代价很高线程池可以有效地利用有限的线程来启动任务
2020-11-17 00:32:18 +08:00
## 参考资料
2019-04-25 18:24:51 +08:00
- BruceEckel. Java 编程思想: 4 [M]. 机械工业出版社, 2007.
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
- [线程通信](http://ifeve.com/thread-signaling/#missed_signal)
- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
- [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html)
- [thread state java](https://stackoverflow.com/questions/11265289/thread-state-java)
- [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN)
- [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html)
- [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization)
- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015)
- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
- [聊聊并发Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)