文章目录
- 一.多线程基础
- 二.创建线程
- 三.线程的状态
- 四.线程礼让
- 五.中断线程
- 五.守护线程
- 六.线程同步
- 七.同步方法
- Java中没有特殊说明时,一个类`默认是非线程安全的`
- 八.死锁
- 九.使用wait和notify
- 十.使用ReentrantLock
- 十一.使用ReentrantLock + Condition对象来实现wait和notify的功能
- 十二.使用ReadWriteLock
- 十三.乐观锁和悲观锁
- 十四.使用StampedLock
- 十五.使用Concurrent集合
- 十六.使用Atomic(原子类)
- 十七.使用线程池
- 十八.使用Future
- 十九. 使用CompletableFuture
- 二十.使用ForkJoin
- 二十一.使用ThreadLocal
一.多线程基础
1.多任务概念
-
现代操作系统
(Windows,macOS,Linux)
都可以执行多任务。多任务就是同时运行多个任务
,例如:
同时打开ie浏览器/QQ/QQ音乐 -
CPU执行代码都是
一条一条顺序执行
的,即使是单核cpu,也可以同时运行多个任务。 因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行
。例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:
这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样
类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让QQ音乐执行0.001秒,在人看来,CPU就是在同时执行多个任务。 即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。
2.进程和线程的概念
-
在计算机中,我们把
一个任务称为一个进程
,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。 -
某些进程内部还需要同时执行
多个子任务
。 例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。 -
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
-
操作系统调度的
最小任务单位是线程
。 常用的Windows、Linux等操作系统都采用抢占式多任务
,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
因为同一个应用程序,既可以有多个进程,也可以有多个线程
,
因此,实现多任务的方法,有以下几种:
- 多进程模式(每个进程只有一个线程):
- 多线程模式(一个进程有多个线程)
- 多进程+多线程模式(复杂度最高)
3.进程 vs 线程
-
进程和线程是
包含关系
,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。 -
和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销大,尤其是在Windows系统上;
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
- 而多进程的优点在于:
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
4.Java多线程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程
,JVM进程用一个主线程来执行main()方法
,在main()方法内部
,我们又可以启动多个线程
。 此外,JVM还有负责垃圾回收的其他工作线程(守护线程)等。
-
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
-
和单线程相比,多线程编程的特点在于:多线程
经常
需要读写共享数据,并且需要同步
。- 例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,
多线程编程的复杂度高,调试更困难。
- 例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,
-
Java多线程编程的特点又在于:
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
二.创建线程
- Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:
- 方法一:继承Thread类重写run方法:
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 方法二:实现Runnable接口重写run方法,创建Thread实例时,传入一个Runnable实例:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
或者用Java8引入的lambda语法进一步简写为:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
1.线程执行顺序
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
main线程执行的代码有4行,首先打印main start
,然后创建Thread对象
,紧接着调用start()
启动新线程。当start()方法被调用时,JVM就创建了一个新线程
,我们通过实例变量t
来表示这个新线程对象,并开始执行。
接着,main线程
继续执行打印main end
语句,而t线程
在main线程
执行的同时会并发执行
,打印thread run
和thread end
语句。
当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。
我们再来看线程的执行顺序:
main线程
肯定是先打印main start
,再打印main end;
2.t线程肯
定是先打印thread run
,再打印thread end
。- 但是,除了可以肯定,
main start会先打印外
,main end
打印在thread run
之前、thread end
之后或者之间,都无法确定
。因为从t线程
开始运行以后,两个线程就开始同时运行了
,并且由操作系统调度
,程序本身无法确定线程的调度顺序。
2.线程的优先级
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但不能保证优先级高的线程一定会先执行。线程调度由操作系统决定,程序本身无法决定调度顺序;
三.线程的状态
在Java程序中,一个线程对象只能调用一次start()方法
启动新线程,并在新线程中执行run()方法
。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New
:新创建的线程,尚未执行;Runnable
:运行中的线程,正在执行run()方法的Java代码;Blocked
:运行中的线程,因为某些操作被阻塞而挂起;Waiting
:运行中的线程,因为某些操作在等待中;Timed Waiting
:运行中的线程,因为执行sleep()方法正在计时等待;Terminated
:线程已终止,因为run()方法执行完毕。
-
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的原因有:
- 线程正常终止:run()方法执行到return语句返回;
- 线程意外终止:run()方法因为未捕获的异常导致线程终止;
- 调用线程实例的stop()方法强制终止
(强烈不推荐使用)
。
四.线程礼让
- 通过对另一个线程对象调用join()方法可以等待其执行结束;
- 可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
- 对已经运行结束的线程调用join()方法会立刻返回。
- join()方法谁的线程体中哪个线程就会等待当前join()方法对应实例线程执行结束在执行
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}
当main线程
对线程t
调用join()方法时,主线程
将等待线程t
运行结束在继续执行,即join就是指等待该线程结束,然后才继续往下执行自身线程。 所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。
如果线程t
已经结束,对实例t调用join()会立刻返回。
- 此外,
join(long)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待
。
五.中断线程
-
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()
(默认值为false,需要取反判断) 方法,目标线程循环调用interrupted()方法判断自身状态
,如果是,就立刻结束运行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
上述代码 main线程 通过调用·线程t.interrupt()·方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”
,至于t线程是否能立刻响应,要看具体代码。而线程t的while循环会检测isInterrupted()
,所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。
如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedException,例如,t.join()
会让main
线程进入等待状态,此时,如果对main线程调用interrupt(),join()方法会立刻抛出InterruptedException
,因此,目标线程只要捕获到join()方法抛出的InterruptedException`,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
main线程
通过调用t.interrupt()
从而通知t线程中断,而此时线程t正在等待hello线程执行结束
,此方法会立刻结束等待并抛出InterruptedException
。由于我们在线程t中捕获了InterruptedException
,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。
1.设置标志位中断线程
常用的中断线程的方法是设置标志位。我们通常会用一个boolean 类型的标记位来标识线程是否应该继续运行,在外部线程中,通过把它置为false,就可以让线程结束:
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
注意到HelloThread的标志位boolean running
是一个线程间共享的变量
。
- 线程间共享变量需要使用
volatile关键字
标记,确保每个线程都能读取到更新后的变量值。
2.volatile
为什么要对线程间共享的变量用关键字volatile声明?
- 这涉及到
Java的内存模型
。在Java虚拟机中,变量的值保存在主内存
中,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中
。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但这个时间是不确定的!
这会导致一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。
例如,主内存变量a = true,线程1执行a = false时
,它在此刻仅仅是把变量a的副本变成了false
,主内存的变量a还是true
,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true
, 这 就造成了多线程之间共享的变量不一致。
volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
volatile关键字解决的是共享变量在线程间的可见性问题
:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在
x86
的架构下,JVM回写主内存的速度非常快,但是,换成ARM
的架构,就会有显著的延迟。
五.守护线程
-
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。
当所有线程都运行结束时,JVM退出,进程结束
。 -
如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程
:
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
答案是使用守护线程(Daemon Thread)
。
守护线程是指为其他线程服务的线程
。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程
- 在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
六.线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
这个时候,有个单线程模型下不存在的问题就来了:如果多个线程时读写共享变量
,会出现数据不一致的问题。
1.线程同步问题产生
public class Main {
public static void main(String[] args) throws Exception {
AddThread add = new AddThread();
DecThread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count += 1; }
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count -= 1; }
}
}
上面的代码两个线程同时对一个int变量进行操作
,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。
连续执行三次结果
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
实际上执行n = n + 1
并不是一个原子操作,它的执行过程如下:
1. 从主存中读取变量x副本到工作内存
2. 给x加1
3. 将x加1后的值写回主存
我们假设n
的值是100
,如果两个线程同时执行n = n + 1
,得到的结果很可能不是102,而是101
,
原因在于:多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程
- 如果线程1在从主内存将n=100的值同步到工作内存时,此时cpu切换到线程2,线程2也将n=100的值同步到工作内存
- 线程1 n+=1 = 101,然后同步到主内存此时主内存为101
- 线程2 n-=1 = 99,然后同步到主内存此时主内存为99
- 显然由于执行顺序的不同n最终的结果可能为101也可能为99
这说明多线程模型下,要保证逻辑正确,即某一个线程对共享变量进行读写时,其他线程必须等待
2.Synchronized
-
通过加锁和解锁的操作,就能保证在一个线程执行期间,不会有其他线程会进入此代码块。
-
即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此代码块。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种
加锁和解锁之间的代码块
我们称之为临界区(Critical Section)
,任何时候临界区最多只有一个线程能执行。
-
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用
synchronized关键字
对一个对象进行加锁
synchronized(lock) {
n = n + 1;
}
synchronized保证了代码块在任意时刻最多只有一个线程能执行。
public class TestSynchronized {
public static void main(String[] args) throws Exception {
AddThread add = new AddThread();
DecThread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
//计数器
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
//新增线程
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count += 1;} }
}
}
//减少线程
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { synchronized(Counter.lock){ Counter.count -= 1;} }
}
}
执行结果
代码
synchronized(Counter.lock) {//获取锁
}//释放锁
- 它表示用
Counter.lock实例
作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。
执行结束后,在synchronized语句块结束会自动释放锁。
这将会导致对Counter.count变量进行读写就不能同时进行
。论运行多少次,最终结果都是0。
synchronized解决了多线程同步访问共享变量的有序性问题。
但它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外加锁和解锁需要消耗一定的时间
,所以,synchronized会降低程序的执行效率。
如何使用Synchronized
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) { }.
- 在使用synchronized的时候,
不必担心抛出异常
。因为无论是否有异常,都会在synchronized结束处正确释放锁:
public void add(int m) {
synchronized (obj) {
if (m < 0) {
throw new RuntimeException();
}
this.value += m;
} // 无论有无异常,都会在此释放锁
}
3.错误使用Synchronized例子1
public class Main {
public static void main(String[] args) throws Exception {
AddThread add = new AddThread();
DecThread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock1) {
Counter.count += 1;
}
}
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock2) {
Counter.count -= 1;
}
}
}
}
执行结果
结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象! 这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取
。 使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。
4.错误使用Synchronized例子2
public class Main {
public static void main(String[] args) throws Exception {
Thread [] ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
for (Thread t : ts) {
t.start();
}
for (Thread t : ts) {
t.join();
}
System.out.println(Counter.studentCount);
System.out.println(Counter.teacherCount);
}
}
class Counter {
public static final Object lock = new Object();
public static int studentCount = 0;
public static int teacherCount = 0;
}
class AddStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount += 1;
}
}
}
}
class DecStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount -= 1;
}
}
}
}
class AddTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount += 1;
}
}
}
}
class DecTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount -= 1;
}
}
}
}
执行结果
- 上面
4个线程
对两个共享变量
分别进行读写操作,但是使用的锁都是Counter.lock对象
,这就造成了原本可以并发执行的Counter.studentCount += 1和Counter.teacherCount += 1无法并发执行了
,执行效率大大降低。 - 实际上,需要同步的线程可以分成两组:
AddStudentThread和DecStudentThread
,AddTeacherThread和DecTeacherThread
,组之间不存在竞争,因此,应该使用两个不同的锁
public class TestSynchronizedMulti {
public static void main(String[] args) throws Exception {
//创建线程
Thread[] ts = new Thread[]{new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()};
//启动线程
for (Thread t : ts) {
t.start();
}
//优先子线程先执行
for (Thread t : ts) {
t.join();
}
//最后打印执行结果
System.out.println(Counter.studentCount);
System.out.println(Counter.teacherCount);
}
}
//计数器
class Counter {
public static final Object lockTeacher = new Object();//学生线程锁对象
public static final Object lockStudent = new Object();//老师线程锁对象
public static int studentCount = 0;
public static int teacherCount = 0;
}
//增加学生数量线程
class AddStudentThread extends Thread {
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Counter.lockStudent) {
Counter.studentCount += 1;
}
}
}
}
//减少学生数量线程
class DecStudentThread extends Thread {
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Counter.lockStudent) {
Counter.studentCount -= 1;
}
}
}
}
//增加老师数量线程
class AddTeacherThread extends Thread {
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Counter.lockTeacher) {
Counter.teacherCount += 1;
}
}
}
}
//减少老师数量线程
class DecTeacherThread extends Thread {
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Counter.lockTeacher) {
Counter.teacherCount -= 1;
}
}
}
}
执行结果
5.不需要synchronized的操作
JVM规范定义了几种原子操作:
- 基本类型(long和double除外)赋值,例如:int n = m;
- 引用类型赋值,例如:List list = anotherList。
- long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在
x64平台的JVM是把long和double的赋值作为原子操作实现的。
- long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在
- 单条原子操作的语句不需要同步。例如:
public void set(int m) {
synchronized(lock) {
this.value = m;
}
}
就不需要同步
//引用类型赋值
public void set(String s) {
this.value = s;
}
- 如果是多行赋值语句,就必须保证是同步操作
class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) {
this.first = first;
this.last = last;
}
}
}
有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:
class Pair {
int[] pair;
public void set(int first, int last) {
int[] ps = new int[] { first, last };
this.pair = ps;
}
}
就不再需要同步,因为this.pair = ps
是引用赋值的原子操作
。而语句:int[] ps = new int[] { first, last };
,这里的ps是方法内部定义的局部变量
,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。
6.小结
- 多线程
同时读写共享变量时
,会造成逻辑错误,因此需要通过synchronized
同步; - 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
- 注意加锁对象必须是同一个实例;
- 对JVM定义的单个原子操作不需要同步
七.同步方法
Java程序依靠synchronized
对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。
例如,我们编写一个计数器
//计数器
public class Counter {
private int count = 0;
public synchronized void add(int n) {
count += n;
}
public synchronized void dec(int n) {
count -= n;
}
public int get() {
return count;
}
}
//测试方法
class Main {
public static void main(String[] args) throws InterruptedException {
Counter c1 = new Counter();
Counter c2 = new Counter();
// 对c1进行操作的线程:
new Thread(() -> {
c1.add(1);
}).start();
new Thread(() -> {
c1.dec(1);
}).start();
// 对c2进行操作的线程:
new Thread(() -> {
c2.add(1);
}).start();
new Thread(() -> {
c2.dec(1);
}).start();
//主线程休眠20毫秒
Thread.sleep(20);
System.out.println(c1.get());
System.out.println(c2.get());
}
线程调用 add()
、dec()
方法时不必关心同步逻辑,因为synchronized代码块
在add()
、dec()
方法内部。并且synchronized
锁住的对象是this
,即当前实例
,这使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行
-
使用synchronized修饰方法,表示整个方法都必须用this实例加锁
public synchronized void add(int n) { // 锁住this count += n; } //
-
使用synchronized修饰静态方法,锁住的是该类的class实例
static方法没有this实例的,因为static方法是针对类而不是实例。任何一个类都有一个由JVM自动创建的Class实例
,对static方法添加synchronized锁住的是该类的class实例
public class Counter { public static void test(int n) { synchronized(Counter.class) { // ... } } }
Java中没有特殊说明时,一个类默认是非线程安全的
八.死锁
1.什么是可重入锁
Java的线程锁是可重入的锁
- 什么是可重入的锁?
public synchronized void method1(){
System.out.println("sysn method1");
method2();
}
private synchronized void method2() {
System.out.println("syn method2");
}
如果一旦线程执行到add()
方法内部,说明它已经获取了当前实例的this锁
。如果传入的n < 0
,将在add()
方法内部调用dec()
方法。由于dec()方法也需要获取this锁,现在问题来了:
对同一个线程,能否在获取到锁以后继续获取同一个锁?
-
答案是肯定的。JVM允许
同一个线程重复获取同一个锁
,这种能被同一个线程反复获取的锁,就叫做可重入锁
。 -
广义上的可重入锁指的是可重复可递归调用的锁,
在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class)
,这样的锁就叫做可重入锁. -
可重入锁(也叫递归锁):指的是
同一线程
在外层
方法获得锁之后,内层递归方法
仍然可以获取该锁的代码,在同一线程在外层方法获取锁的时候+1
,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
。 -
重入锁以
线程
为单位,当一个线程获取对象锁
之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。ReentrantLock和synchronized都是可重入锁 -
可重入锁的意义便在于
防止死锁!!!
-
由于Java的线程锁是可重入锁,所以获取锁的时,
不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
-
实现原理是通过为
每个锁
关联一个请求计数器
和一个占有它的线程
。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。- 如果同一个线程再次请求这个锁,计数将递增;
- 每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
2.不可重入锁
所谓不可重入锁,即指的是同一线程在外层方法获得锁之后,那么在内层递归方法
中尝试再次获取锁时,就会获取不到被阻塞。
- 不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生
死锁
。
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当前线程执行print()
方法首先获取lock
,接下来执行doAdd()
方法就无法执行doAdd()
中的逻辑,必须先释放锁。
3.死锁
- 什么是死锁
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁
。
线程1和线程2如果分别执行add()和dec()方法时:
- 线程1:进入add(),获得lockA;
- 线程2:进入dec(),获得lockB。
随后:
- 线程1:准备获得lockB,失败,等待中;
- 线程2:准备获得lockA,失败,等待中。
此时两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去
,这就是死锁
。
- 死锁发生后,
没有任何机制能解除死锁,只能强制结束JVM进程
。在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
- 如何避免死锁?
线程获取锁的顺序要一致。即严格按照先获取锁A,再获取锁B的顺序,或者先获取锁B,再获取锁A的顺序
将上面代码中的dec方法获取锁的顺序改成和add()一样即可
public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}
- 避免死锁的3中办法
-
加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 -
加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。 -
死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。-
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
-
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
-
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。
-
关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。
那么当检测出死锁时,这些线程该做些什么呢?
-
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
-
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
九.使用wait和notify
1.什么是多线程协调?
synchronized
解决了多线程竞争
的问题。例如,对于一个任务管理器
,多个线程同时往队列中添加任务,可以用synchronized
加锁:
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
}
但是synchronized
并没有解决多线程协调
的问题。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
上述代码看上去没有问题:getTask()
内部先判断队列是否为空
,如果为空就循环等待
,直到另一个线程往队列中放入了一个任务,while()循环退出
,就可以返回队列的元素了。
- 但实际上
while()循环永远不会退出
。因为线程在执行while()循环时,已经在getTask()入口获取了this锁
,其他线程因为addTask()执行条件也是获取this锁,根本无法调用addTask(),线程会在getTask()中因为死循环而100%占用CPU资源
。
而我们想要的执行效果是:
- 线程1可以调用
addTask()
不断往队列中添加任务; - 线程2可以调用
getTask()
从队列中获取任务。如果队列为空,则getTask()应该等待,直到队列中至少有一个任务时再返回。
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,已唤醒的线程还需要重新获得锁后才能继续执行任务。
2.使用wait()和notify()解决多线程协调?
对于上述TaskQueue,我们先改造getTask()
方法,在条件不满足时,线程进入等待状态:
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}
当一个线程执行到getTask()
方法内部的while循环时,它必定已经获取到了this锁
,此时,线程执行while条件判断,如果条件成立(队列为空),线程将执行this.wait(),进入等待状态,且释放当前占用锁
-
这里的关键是:
wait()
方法必须在当前获取的锁对象上调用,这里获取的是this锁
,因此调用this.wait()
。 -
调用
wait()
方法后,线程进入等待状态
,wait()
方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句。 -
定义在
Object类
的一个native方法
,也就是由JVM的C代码实现的
。其次,必须在synchronized块中才能调用wait()方法
,因为wait()方法调用时,会释放线程获得的锁
,wait()方法返回后,线程又会重新试图获得锁
。
当一个线程在this.wait()等待
时,它就会释放this锁
,从而使得其他线程能够在addTask()方法获得this锁。
如何让等待的线程被重新唤醒,然后从wait()方法返回? 答案是在相同的锁对象上调用notify()方法
。我们修改addTask()
如下:
public synchronized void addTask(String s) {
this.queue.add(s);// 唤醒在this锁等待的线程
this.notify();
}
注意到在往队列中添加任务
后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在等待this锁的线程
(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。
-
wait()、notify()方法属于
Object
中的方法;对于Object中的方法,每个对象都拥有。 -
wait()方法:使当前线程进入等待状态并释放锁,让其他线程可以有机会运行,直到接到通知或者被中断打断为止。在调用wait()方法之前,线程必须要获得该对象的对象级锁;换句话说就是
该方法只能在同步方法或者同步块中调用
,如果没有持有合适的锁的话,线程将会抛出异常IllegalArgumentException
。如果调用成功后,当前线程则释放锁。
-
notify()方法:用来
唤醒处于等待状态获取对象锁的其他线程
。如果有多个线程则线程调度器任意选出一个线程进行唤醒,使其去竞争获取对象锁,但调用notify()的线程并不会马上就释放该对象锁,wait()所在的线程也不能马上获取该对象锁,要程序退出同步块或者同步方法之后,当前线程才会释放锁,wait()所在的线程才可以获取该对象锁。
-
wait()和notify()持有同一把锁 ,wait()方法是释放锁的;notify()方法不释放锁,必须等到所在线程把代码执行完。
3.完整例子
public class TaskQueueMain {
public static void main(String[] args) throws InterruptedException {
TaskQueue taskQueue = new TaskQueue();
List<Thread> threadList = new ArrayList<Thread>();
//创建5个线程用于从队列中不断取任务,如果队列为空,getTask()就会释放当前this锁,进入等待唤醒状态
for (int i = 0; i < 5; i++) {
Thread thread = new Thread() {
@Override
public void run() {
// 执行task:
while (true) {
try {
String s = taskQueue.getTask();
System.out.println("execute task: " + s);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+":InterruptedException");
return;
}
}
}
};
//启动线程
thread.start();
//添加当前线程实例带list中
threadList.add(thread);
}
//创建一个线程循环添加10个任务到队列中,每次添加都会唤醒处理等待状态的任意一个线程
Thread add = new Thread() {
@Override
public void run() {
// 执行task:
for (int i = 0; i < 10; i++) {
// 放入task:
String str = "t-" + Math.random();
System.out.println("add task: " + str);
taskQueue.addTask(str);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
};
//启动线程
add.start();
//阻塞main,必须等待当前线程执行完
add.join();
//休眠100毫秒
Thread.sleep(100);
//中断处于等待状态的线程
//1.如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedExceptio
// 2.因此,目标线程只要捕获到抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
for (Thread thread : threadList) {
thread.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
//唤醒所有等待 this锁共同竞争this锁
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
//释放锁,进入等待状态
this.wait();
// 获取锁,继续执行
}
return queue.remove();
}
}
执行结果:
重点关注addTask()
方法,内部调用了this.notifyAll()
而不是this.notify()
,使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。
- 这是因为
可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒
。通常来说,notifyAll()更安全
。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。 - 注意到wait()方法返回时需要重新获得this锁。假设当前有3个线程被唤醒,唤醒后,首先要等待执行addTask()的线程结束此方法后,才能释放this锁,随后,这3个线程中只能有一个获取到this锁,剩下两个将继续等待。
再注意到我们在while()
循环中调用wait(),而不是if语句
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
如果使用if语句实际上是错误的,因为线程被唤醒时,需要再次获取this锁
。多个线程被唤醒后,只有一个线程能获取this锁
,此时,该线程执行queue.remove()
可以获取到队列的元素,然而,剩下的线程如果获取this锁
后执行queue.remove()
,此刻队列可能已经没有任何元素了。 所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断队列是否为空,如果为空则调用this.wait(),释放锁进入等待状态
:
4.小结
-
wait和notify用于多线程协调运行:
-
在
synchronized内部
可以调用wait()
使线程进入等待状态
; -
必须在
已获得的锁对象
上调用wait()方法; -
在
synchronized内部
可以调用notify()或notifyAll()
唤醒其他等待线程; -
必须在已获得的锁对象上调用notify()或notifyAll()方法;
-
已唤醒的线程还需要重新获得锁后才能继续执行。
十.使用ReentrantLock
1.什么是ReentrantLock?
-
从
Java 5
开始,引入了一个高级的处理并发的java.util.concurrent
包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。 -
Java语言直接提供了
synchronized关键字
用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
-
java.util.concurrent.locks
包提供的ReentrantLock
用于替代synchronized
加锁,
2.java中使用ReentrantLock
- 传统的
synchronized
public class Counter {
private int count;
public void add(int n) {
synchronized(this) {
count += n;
}
}
}
- 如果用
ReentrantLock
替代,可以把代码改造为:
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
因为synchronized
是Java语言层面提供的语法,所以我们不需要考虑异常
,而ReentrantLock
是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。
- 顾名思义,
ReentrantLock是可重入锁
,它和synchronized一样,一个线程可以多次获取同一个锁。
和synchronized不同的是,ReentrantLock可以尝试获取锁
:
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// ...
} finally {
lock.unlock();
}
}
上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。
- 所以,使用
ReentrantLock比直接使用synchronized更安全
,线程在tryLock()失败的时候不会导致死锁。
ReentrantLock的lock(), tryLock(), tryLock(long timeout, TimeUnit unit), lockInterruptibly() 及使用场景示例
3.完整代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread add = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
counter.add(1);
}
}
};
add.start();
add.join();
Thread dec = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
counter.dec(1);
}
}
};
dec.start();
dec.join();
Thread.sleep(100);
System.out.println(counter.getCount());
}
}
class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
public void dec(int n) {
lock.lock();
try {
count -= n;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
4.小结
-
ReentrantLock可以替代synchronized进行同步;
-
ReentrantLock
获取锁更安全
;- 必须先获取到锁,再进入
try {...}
代码块,最后使用finally
保证释放锁; - 可以使用
tryLock()
尝试获取锁。
- 必须先获取到锁,再进入
十一.使用ReentrantLock + Condition对象来实现wait和notify的功能
1.如何使用ReentrantLock + Condition对象来实现wait和notify的功能
使用ReentrantLock
比直接使用synchronized
更安全
,可以替代synchronized进行线程同步
。
synchronized
可以配合wait和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?
- 答案是
使用Condition对象来实现wait和notify的功能。
我们仍然以TaskQueue
为例,把前面用synchronized实现的功能通过ReentrantLock和Condition来实现:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLockConditionMain {
public static void main(String[] args) throws InterruptedException {
TaskQueue taskQueue = new TaskQueue();// 声明任务队列
List<Thread> threadList = new ArrayList<>();// 声明线程集合
//创建5个线程用于从队列中不断取任务,如果队列为空,getTask()就会释放当前this锁,进入等待唤醒状态
for (int i = 0; i < 5; i++) {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
String str = null;
try {
str = taskQueue.getTask();
System.out.println("execute task: " + str);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
return;
}
}
}
};
//启动线程
thread.start();
//添加当前线程实例带list中
threadList.add(thread);
}
//创建一个线程循环添加10个任务到队列中,每次添加都会唤醒处理等待状态的任意一个线程
Thread add = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 放入task:
String str = "t-" + Math.random();
System.out.println("add task: " + str);
taskQueue.addTask(str);
try {
Thread.sleep(100);
} catch (InterruptedException e) { }
}
}
};
//启动线程
add.start();
//阻塞main,必须等待当前线程执行完
add.join();
//休眠100毫秒
Thread.sleep(100);
//中断处于等待状态的线程
//1.如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedExceptio
// 2.因此,目标线程只要捕获到抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
for (Thread thread : threadList) {
thread.interrupt();
}
}
}
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String str) {
lock.lock();
try {
queue.add(str);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
执行结果
使用Condition
时,引用的Condition对象必须从Lock实例的newCondition()
返回,这样才能获得一个绑定了Lock实例的Condition实例。
Condition
提供的await()、signal()、signalAll()
原理和synchronized锁对象
的wait()、notify()、notifyAll()
是一致的,并且其行为也是一样的:await()
会释放当前锁,进入等待状态;signal()
会唤醒某个等待线程;signalAll()
会唤醒所有等待线程;- 唤醒线程从await()返回后需要重新获得锁。
- 和
tryLock()
类似,await()
可以在等待指定时间后
,如果还没有被其他线程通过signal()
或signalAll()唤醒
,可以自己醒来:
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
可见,使用Condition配合Lock
,我们可以实现更灵活的线程同步
。
2.小结
-
Condition可以替代
synchronized + wait和notify
实现线程同步; -
Condition对象必须从Lock对象获取。
十二.使用ReadWriteLock
1.什么是ReadWriteLock?
前面讲到的ReentrantLock保证了只有一个线程可以执行临界区代码:
public class Counter {
private final Lock lock = new ReentrantLock();
private int[] counts = new int[10];
public void inc(int index) {
lock.lock();
try {
counts[index] += 1;
} finally {
lock.unlock();
}
}
public int[] get() {
lock.lock();
try {
return Arrays.copyOf(counts, counts.length);
} finally {
lock.unlock();
}
}
}
但是有些时候,这种保护有点过头。 因为我们发现,任何时刻,只允许一个线程修改
,也就是调用inc()方法
是必须获取锁
,但是,get()方法只读取数据,不修改数据
,它实际上允许多个线程同时调用。
实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待:
读 | 写 | |
---|---|---|
读 | 允许 | 不允许 |
写 | 不允许 | 不允许 |
使用ReadWriteLock
可以解决这个问题,它保证:
- 只允许一个线程写入(
其他线程既不能写入也不能读取
); - 没有写入时,多个线程允许同时读(提高性能)
2.Java中实现ReadWriteLock?
用ReadWriteLock
实现这个功能十分容易。我们需要创建一个ReadWriteLock实例,然后分别获取读锁
和写锁
:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestReadWriteLockMain {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();// 声明任务队列
List<Thread> threadList = new ArrayList<>();// 声明线程集合
//创建5个线程用于从队列中不断取任务,如果队列为空,getTask()就会释放当前this锁,进入等待唤醒状态
for (int i = 0; i < 5; i++) {
Thread readThread = new Thread() {
@Override
public void run() {
while (true) {
counter.get();
System.out.println(Thread.currentThread().getName() + ":get("+ Arrays.toString(counter.get())+")");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
return;
}
}
}
};
//启动线程
readThread.start();
//添加当前线程实例带list中
threadList.add(readThread);
}
//创建一个线程循环添加10个任务到队列中,每次添加都会唤醒处理等待状态的任意一个线程
Thread incThread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
counter.inc(i);
System.out.println(Thread.currentThread().getName() + ":inc("+i+")");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//启动线程
incThread.start();
//阻塞main,必须等待当前线程执行完
incThread.join();
//休眠100毫秒
Thread.sleep(100);
//中断处于等待状态的线程
//1.如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedExceptio
// 2.因此,目标线程只要捕获到抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
for (Thread thread : threadList) {
thread.interrupt();
}
}
}
class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
执行结果
Thread-0:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-2:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-1:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-3:get([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(0)
Thread-2:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-3:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(1)
Thread-1:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(2)
Thread-3:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 0, 0, 0, 0, 0, 0, 0])
Thread-5:inc(3)
Thread-3:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
Thread-5:inc(4)
Thread-3:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
Thread-5:inc(5)
Thread-1:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-1:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-5:inc(6)
Thread-4:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 0, 0, 0])
Thread-5:inc(7)
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 0, 0])
Thread-5:inc(8)
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 0])
Thread-5:inc(9)
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-1:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-2:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-0:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-3:get([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Thread-4:InterruptedException
Thread-2:InterruptedException
Thread-0:InterruptedException
Thread-1:InterruptedException
Thread-3:InterruptedException
-
把
读写操作
分别用读锁
和写锁
来加锁,在读取时多个线程可以同时获得读锁
,这样就大大提高了并发读的执行效率。 -
使用ReadWriteLock时,适用条件是
同一个数据,有大量线程读取,但仅有少数线程修改。
例如: 一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用ReadWriteLock。
3.小结
使用ReadWriteLock可以提高读取
效率:
-
ReadWriteLock
只允许一个线程写入
; -
ReadWriteLock
允许多个线程在没有写入时同时读取
; -
ReadWriteLock
适合读多写少的场景
。
十三.乐观锁和悲观锁
1.什么是悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等
,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
2.什么是乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁
,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据
,可以使用版本号机制
和CAS算
法实现。乐观锁适用于多读
的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制
,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类
就是使用了乐观锁
的一种实现方式CAS
实现的。
3.使用场景:
- 悲观锁适合
写操作多
的场景,先加锁可以保证写操作时数据正确。 - 乐观锁适合
读操作多
的场景,不加锁的特点能够使其读操作的性能大幅提升。
4.什么是CAS
CAS(compare and swap):当多个线程使用CAS获取锁,只能有一个成功,其他线程返回失败,继续尝试获取锁;
- CAS操作中包含三个参数:
V(
需读写的内存位置)+A
(准备用来比较的参数)+B
(准备写入的新值):若A的参数与V的对应的值相匹配,就写入值B;若不匹配,就写入这个不匹配的值而非B;
5.乐观锁常见的两种实现方式
乐观锁一般会使用版本号机制或CAS(Compare-and-Swap,即比较并替换)算法实现。
1.版本号机制
一般是在数据表中加上一个数据版本号version字段
,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子: 假设数据库中帐户信息表中有一个version
字段,当前值为 1 ;而当前帐户余额字段(balance )为 $100
。
- 操作员 A 此时将其读出
( version=1 )
,并从其帐户余额中扣除$50( $100-$50 )
。 - 在操作员 A 操作的过程中,操作员B 也读入此用户信息
( version=1 )
,并从其帐户余额中扣除$20 ( $100-$20 )
。 - 操作员 A 完成了修改工作,将数据版本号加1
( version=2 )
,连同帐户扣除后余额( balance=$50 )
,提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version
更新为 2 。 - 操作员 B 完成了操作,也将版本号加1
( version=2 )
试图向数据库提交数据( balance=$80 )
,但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于version=1
的旧数据修改的结果覆盖操作员A 的操作结果的可能。
2.CAS算法
即compare and swap(比较与交换)
,是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步
,也就是在没有线程被阻塞的情况下实现变量的同步
,所以也叫非阻塞同步(Non-blocking Synchronization)
。CAS算法涉及到三个操作数
-
需要读写的内存值 V
-
进行比较的值 A
-
拟写入的新值 B
-
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个
自旋操作(自旋锁)
,即不断的重试。
十四.使用StampedLock
1.什么是StampedLock
第十二章节讲了 ReadWriteLock
可以解决多线程同时读
,但只有一个线程能写
的问题
- 但是ReadWriteLock有个
潜在的问题
:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
要进一步提升并发执行效率,Java 8
引入了新的读写锁
:StampedLock
。
-
StampedLock和ReadWriteLock
相比,改进之处在于:读的过程中也允许获取写锁后写入!
这样一来,我们读的数据就可能不一致
,所以需要编写一点额外的代码来判断读的过程中是否有写入
,这种读锁是一种乐观锁
。 -
乐观锁的意思: 就是
乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁
。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。
显然乐观锁的并发效率更高
,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
2.Java中使用StampedLock
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.StampedLock;
public class TestStampedLockMain {
public static void main(String[] args) throws InterruptedException {
Point point = new Point();
List<Thread> threadList = new ArrayList<>();// 声明线程集合
//创建5个线程用于不断从point读
for (int i = 0; i < 5; i++) {
Thread readThread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + ":get("+ point.distanceFromOrigin()+")");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
return;
}
}
}
};
//启动线程
readThread.start();
//添加当前线程实例带list中
threadList.add(readThread);
}
//创建一个线程不断往point写
Thread writeThread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
point.move(2,2);
System.out.println(Thread.currentThread().getName() + ":move(2,2)=>"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//启动线程
writeThread.start();
//阻塞main,必须等待当前线程执行完
writeThread.join();
//休眠100毫秒
Thread.sleep(100);
//中断处于等待状态的线程
//1.如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedExceptio
// 2.因此,目标线程只要捕获到抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
for (Thread thread : threadList) {
thread.interrupt();
}
}
}
class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁,并返回版本号
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生,即校验版本号是否一致,不一致重新读取x,y的值
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
执行结果:
Thread-0:get(0.0)
Thread-2:get(0.0)
Thread-1:get(0.0)
Thread-3:get(0.0)
Thread-4:get(0.0)
Thread-5:move(2,2)=>0
Thread-4:get(2.8284271247461903)
Thread-5:move(2,2)=>1
Thread-1:get(2.8284271247461903)
Thread-2:get(2.8284271247461903)
Thread-3:get(2.8284271247461903)
Thread-0:get(2.8284271247461903)
Thread-5:move(2,2)=>2
Thread-4:get(8.48528137423857)
Thread-3:get(8.48528137423857)
Thread-0:get(8.48528137423857)
Thread-2:get(8.48528137423857)
Thread-1:get(8.48528137423857)
Thread-5:move(2,2)=>3
Thread-4:get(11.313708498984761)
Thread-2:get(11.313708498984761)
Thread-0:get(11.313708498984761)
Thread-1:get(11.313708498984761)
Thread-3:get(11.313708498984761)
Thread-5:move(2,2)=>4
Thread-0:get(14.142135623730951)
Thread-3:get(14.142135623730951)
Thread-4:get(14.142135623730951)
Thread-2:get(14.142135623730951)
Thread-1:get(14.142135623730951)
Thread-5:move(2,2)=>5
Thread-4:get(16.97056274847714)
Thread-3:get(16.97056274847714)
Thread-2:get(16.97056274847714)
Thread-0:get(16.97056274847714)
Thread-1:get(16.97056274847714)
Thread-5:move(2,2)=>6
Thread-4:get(19.79898987322333)
Thread-0:get(19.79898987322333)
Thread-1:get(19.79898987322333)
Thread-3:get(19.79898987322333)
Thread-2:get(19.79898987322333)
Thread-5:move(2,2)=>7
Thread-2:get(22.627416997969522)
Thread-0:get(22.627416997969522)
Thread-3:get(22.627416997969522)
Thread-4:get(22.627416997969522)
Thread-1:get(22.627416997969522)
Thread-5:move(2,2)=>8
Thread-3:get(25.45584412271571)
Thread-1:get(25.45584412271571)
Thread-0:get(25.45584412271571)
Thread-4:get(25.45584412271571)
Thread-2:get(25.45584412271571)
Thread-5:move(2,2)=>9
Thread-3:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-3:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-1:get(28.284271247461902)
Thread-4:get(28.284271247461902)
Thread-3:get(28.284271247461902)
Thread-2:get(28.284271247461902)
Thread-0:get(28.284271247461902)
Thread-2:InterruptedException
Thread-1:InterruptedException
Thread-3:InterruptedException
Thread-4:InterruptedException
Thread-0:InterruptedException
-
和ReadWriteLock相比,写入的加锁是完全一样的,
不同的是读取。
-
注意到首先我们通过
tryOptimisticRead()
获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()
去验证版本号,如果在读取
过程中没有写入
,版本号不变
,验证成功
,我们就可以放心地继续后续操作
。如果在读取
过程中有写入
,版本号会发生变化
,验证将失败
。在失败的时候,我们再通过获取悲观读锁再次读取
。由于写入的概率不高
,程序在绝大部分情况下可以通过乐观读锁获取数据
,极少数情况下使用悲观读锁获取数据
。- 可见,StampedLock把
读锁
细分为乐观读
和悲观读
,能进一步提升并发效率。 - 但这也是有代价的:一是代码更加复杂,二是
StampedLock是不可重入锁
,不能在一个线程中反复获取同一个锁
。
- 可见,StampedLock把
StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能
,它主要使用在if-then-update
的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。
3.小结
-
StampedLock提供了
乐观读锁
,可取代ReadWriteLock以进一步提升并发性能; -
StampedLock是不可重入锁。
十五.使用Concurrent集合
1.java.util.concurrent下的并发集合
在前面十一章
已经通过ReentrantLock和Condition
实现了一个BlockingQueue:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLockConditionMain {
public static void main(String[] args) throws InterruptedException {
TaskQueue taskQueue = new TaskQueue();// 声明任务队列
List<Thread> threadList = new ArrayList<>();// 声明线程集合
//创建5个线程用于从队列中不断取任务,如果队列为空,getTask()就会释放当前this锁,进入等待唤醒状态
for (int i = 0; i < 5; i++) {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
String str = null;
try {
str = taskQueue.getTask();
System.out.println("execute task: " + str);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
return;
}
}
}
};
//启动线程
thread.start();
//添加当前线程实例带list中
threadList.add(thread);
}
//创建一个线程循环添加10个任务到队列中,每次添加都会唤醒处理等待状态的任意一个线程
Thread add = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 放入task:
String str = "t-" + Math.random();
System.out.println("add task: " + str);
taskQueue.addTask(str);
try {
Thread.sleep(100);
} catch (InterruptedException e) { }
}
}
};
//启动线程
add.start();
//阻塞main,必须等待当前线程执行完
add.join();
//休眠100毫秒
Thread.sleep(100);
//中断处于等待状态的线程
//1.如果线程处于等待状态,调用当前线程的interrupt()会抛出InterruptedExceptio
// 2.因此,目标线程只要捕获到抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
for (Thread thread : threadList) {
thread.interrupt();
}
}
}
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String str) {
lock.lock();
try {
queue.add(str);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
执行结果:
BlockingQueue(阻塞队列)
的意思就是说:当一个线程调用这个TaskQueue
的getTask()
方法时,该方法内部可能会让线程变成等待状态
, 直到队列条件满足不为空
,线程被唤醒后,getTask()方法才会返回
。
- 因为
BlockingQueue
非常有用,所以我们不必自己编写,可 直接使用Java标准库的java.util.concurrent
包提供的线程安全的集合
:ArrayBlockingQueue。
interface | non-thread-safe | thread-safe |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
使用这些并发集合与使用非线程安全的集合类完全相同
。我们以ConcurrentHashMap为例:
Map<String, String> map = ConcurrentHashMap<>();
// 在不同的线程读写:
map.put("A", "1");
map.put("B", "2");
map.get("A", "1");
因为所有的同步和加锁的逻辑都在集合内部实现
,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样
。即当我们需要多线程访问时,把:
Map<String, String> map = HashMap<>();
//改为
Map<String, String> map = ConcurrentHashMap<>();
java.util.Collections工具类
还提供了一个旧的线程安全集合转换器处理List/Set/Map
语法为 Collections.synchronizedXXX(Collection c)
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
- 它实际上是用一个包装类包装了非线程安全的Map,然后对所有
读写方法
都用synchronized加锁
,这样获得的线程安全集合的性能
比java.util.concurrent集合
要低很多,所以不推荐使用。
2.小结
-
使用
java.util.concurrent
包提供的线程安全的并发集合
可以大大简化多线程编程: -
多线程同时读写并发集合是安全的;
-
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。
十六.使用Atomic(原子类)
1.什么是原子类?
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。
在我们这里 Atomic是指一个操作是不可中断
的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
Java的java.util.concurrent包
除了提供底层锁
、并发集合
外,还提供了一组原子操作的封装类
,它们位于java.util.concurrent.atomic
包。
我们以AtomicInteger
为例,它提供的主要操作有:
- 增加值并返回新值:
int addAndGet(int delta)
- 加1后返回新值:
int incrementAndGet()
- 获取当前值:
int get()
- 用CAS方式设置:
int compareAndSet(int expect, int update)
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问
。它的主要原理是利用了CAS
:Compare and Set。
如果我们自己通过CAS编写incrementAndGet(),它大概长这样:
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return prev;
}
CAS是指,在这个操作中,如果AtomicInteger的当前值是prev
,那么就更新为next
,返回true
。如果AtomicInteger的当前值不是prev,就什么也不干
,返回false
。通过CAS操作
并配合do ... while循环
,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。
我们利用AtomicLong可以编写一个多线程安全的全局唯一ID生成器:
class IdGenerator {
AtomicLong var = new AtomicLong(0);
public long getNextId() {
return var.incrementAndGet();
}
}
-
通常情况下,我们并不需要直接用
do ... while
循环调用compareAndSet
实现复杂的并发操作
,而是用incrementAndGet()
这样的封装好的方法,因此,使用起来非常简单。 -
在高度竞争的情况下,还可以使用Java 8提供的
LongAdder和LongAccumulator
。
2.关于原子类个数说明
在JDK7包括7之前
,java原子类有12个
,图片如下,有些资料说有13个,多出来的是AtomicBooleanArray
类,可是我在JDK8之前的源码里并没有发现有这个类,当然我也没去8以上的版本去看,所以这里不确定这个类到底在哪个版本中存在。
在JDK8时出现了4个原子操作类,分别是如下图片所示
3.原子类的分类:
- 原子更新基本数据类型
- 原子更新数组类型
- 原子更新抽象数据类型
- 原子更新字段
并发包
java.util.concurrent
的原子类都存放在java.util.concurrent.atomic
下,如下图所示。
1.原子更新基本类型类
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
- int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
- boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement(): 以原子的方式将当前值加 1,注意,这里返回的是自增前的值,也就是旧值。
- void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。
代码示例
static AtomicInteger ai =new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());
System.out.println(ai.get());
}
2.原子更新数组
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。
三个类的最常用的方法是如下两个方法:
- get(int index):获取索引为index的元素值。
- compareAndSet(int i, int expect, int update): 如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置为update值。
代码示例
//下面以 AtomicReferenceArray 举例如下
static int[] value =new int[]{1,2};
static AtomicIntegerArray ai =new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0,2);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
3.原子更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个值,如果更新多个值,比如更新一个对象里的值,那么就要用原子更新引用类型提供的类
,Atomic包提供了以下三个类:
- AtomicReference: 原子更新引用类型。
- AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。
代码示例
public static AtomicReference<User> ai = new AtomicReference<User>();
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
ai.set(u1);
User u2 = new User("piKaQiu", 15);
ai.compareAndSet(u1, u2);
System.out.println(ai.get().getAge() + ai.get().getName());
}
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
//输出结果:piKaQiu 15
4.原子更新字段类
如果需要原子的更新类里某个字段时,需要用到原子更新字段类,Atomic包提供了3个类进行原子字段更新:
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
代码示例
//创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater<User> ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
//原子更新年龄,+1
System.out.println(ai.getAndIncrement(u1));
System.out.println(u1.getAge());
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
//输出结果
//18
//19
要想原子地更新字段类需要两步。
- 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用
静态方法newUpdater()
创建一个更新器
,并且需要设置想要更新的类和属性
。 - 第二步,
更新类的字段
必须使用public volatile
修饰。
5.JDK8新增原子类
背景:在高并发的情况下,我们对一个 Integer 类型的整数直接进行
i++
的时候,无法保证操作的原子性
,会出现线程安全的问题。为
此我们会用 JUC下的AtomicInteger
,它是一个提供原子操作的 Interger 类,内部也是通过CAS
实现线程安全的。但当线程同时去访问时,就会因为大量线程执行 CAS 操作失败而进行空旋转
,导致 CPU 资源消耗过多,而且执行效率也不高。
- 于是在 JDK1.8 中对 CAS 进行了优化,提供了 LongAdder ,它是基于
CAS 分段锁
的思想实现的。
- DoubleAdder
- LongAdder
- DoubleAccumulator
- LongAccumulator
线程去读写一个 LongAdder 类型的变量时,流程如下:
LongAdder 也是基于 Unsafe 提供的CAS 操作 +valitale 去实现的
。
- 在 LongAdder 的父类 Striped64 中维护着一个
long base 变量
和一个Cell[] cells 数组
,当多个线程操作一个变量的时候,先会在这个base变量
上进行CAS
操作,当它发现线程增多的时候,就会使用cells 数组
。- 比如当 base 将要更新的时候发现线程增多(
也就是调用 casBase 方法更新 base 值失败
),那么它会自动使用cells数组
,每一个线程对应于一个 Cell,在每一个线程中对该 Cell进行 Cas 操作,这样就可以将单一value 的更新压力分担到多个 value 中去
,降低单个 value 的“热度”
,同时也减少了大量线程的空转
,提高并发效率,分散并发压力。 - 这种分段锁需要
额外维护一个内存空间 cells
,不过在高并发场景下,这点成本几乎可以忽略。分段锁是一种优秀的优化思想, JUC中提供的的 ConcurrentHashMap 在JDK1.7时也是也是基于分段锁保证读写操作的线程安全。
- 比如当 base 将要更新的时候发现线程增多(
下面以 LongAdder 为例介绍一下,并列出使用注意事项
LongAdder类是LongAccumulator
的一个特例
,LongAccumulator提供了比LongAdder更强大的功能
,如下构造函数,其中accumulatorFunction
是一个双目运算器接口,根据输入的2个参数
返回一个计算值,identity则是LongAccumulator累加器的初始值
。
public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
public interface LongBinaryOperator {
//根据两个参数计算返回一个值
long applyAsLong(long left, long right);
}
上面提到LongAdder 其实就是LongAccumulator 的一个特例
,调用LongAdder 相当使用下面的方式调用 LongAccumulator。
LongAdder adder = new LongAdder();
LongAccumulator accumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
return left + right;
}
}, 0);
LongAccumulator相比LongAdder 可以提供累加器初始非0值,后者只能默认为0
,另外前者还可以指定累加规则
,比如: 不是累加而相乘,只需要构造LongAccumulator 时候传入自定义双目运算器
即可,后者则内置累加规则
。
原理:
如下图所示AtomicLong
在高并发下多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,这样导致大大浪费CPU资源,降低了并发性。
既然AtomicLong性能问题是由于过多线程同时去竞争同一个变量的更新而降低的
,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源
,那么性能问题不久迎刃而解了吗?
没错,因此,JDK8 提供的LongAdder就是这个思路。如下图所示
LongAdder则是内部维护多个Cell变量
,每个Cell里面有一个初始值为0的long型变量
,在同等并发量的情况下,争夺单个变量的线程会减少
,这是变相的减少了争夺共享资源的并发量
- 另外多个线程在争夺
同一个原子变量
时候,如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后当获取当前值时候是把所有变量的值累加后再加上base的值返回的
。
常用方法
//构造函数
LongAdder()
//创建初始和为零的新加法器。
//方法摘要
void add(long x) //添加给定的值。
void decrement() //相当于add(-1)。
double doubleValue() //在扩展原始转换之后返回sum()as double。
float floatValue() //在扩展原始转换之后返回sum()as float。
void increment() //相当于add(1)。
int intValue() //返回sum()作为int一个基本收缩转换之后。
long longValue() //相当于sum()。
void reset() //重置将总和保持为零的变量。
long sum() //返回当前的总和。
long sumThenReset() //等同于sum()后面的效果reset()。
String toString() //返回。的字符串表示形式sum()。
4.总结
-
原子操作实现了无锁的线程安全;
-
适用于计数器,累加器等
十七.使用线程池
1.什么是线程池?
Java语言虽然内置了多线程支持
,启动一个新线程非常方便,但创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
如果可以复用一组线程:
那么我们就可以把很多小任务让一组线程来执行
,而不是一个任务对应一个新线程
。这种能接收大量小任务并进行分发处理的就是线程池
。
- 简单地说,
线程池内部维护了若干个线程
,没有任务的时候,这些线程都处于等待状态
。 **如果有新任务,就分配一个空闲线程执行
。如果所有
线程都处于忙碌状态
,新任务要么放入队列等待
,要么增加一个新线程进行处理
。
2.Java中使用线程池
Java标准库提供了ExecutorService接口
表示线程池,它的典型用法如下:
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
因为ExecutorService
只是接口,Java标准库提供的几个常用实现类有:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
FixedThreadPool
创建这些线程池的方法都被封装到Executors
这个类中。我们以FixedThreadPool
为例,看看线程池的执行逻辑:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// 关闭线程池:
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
一次性放入6个任务
,由于线程池只有固定的4个线程,因此,前4个任务会同时执行
,等到有线程空闲后,才会执行后面的两个任务。
线程池在程序结束的时候要关闭
。
shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭
。shutdownNow()
会立刻停止正在执行的任务
-awaitTermination()
则会等待指定的时间让线程池关闭。
CachedThreadPool
如果我们把线程池改为CachedThreadPool
,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行
如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们看Executors.newCachedThreadPool()方法的源码
:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
想创建指定动态范围
的线程池可以这么写:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
ScheduledThreadPool
还有一种任务需要 定期反复执行
,例如:每秒刷新证券价格。
这种任务本身固定,需要反复执行的
,可以使用ScheduledThreadPool
。放入ScheduledThreadPool的任务可以定期反复执行。
创建一个ScheduledThreadPool
仍然是通过Executors
类:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
我们可以提交一次性任务,它会在指定延迟后只执行一次:
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
如果任务以固定的每3秒执行
,我们可以这样写:
// 5秒后开始执行定时任务,每3秒执行一次:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 5, 3, TimeUnit.SECONDS);
如果任务以固定的3秒为间隔执行
,我们可以这样写:
// 3秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
注意FixedRate
和FixedDelay
的区别
-
FixedRate
是指任务总是以固定时间间隔触发,不管任务执行多长时间
:
-
FixedDelay
是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务
:
因此,使用ScheduledThreadPool
时,我们要根据需要选择执行一次
、FixedRate
执行还是FixedDelay
执行。
细心的童鞋还可以思考下面的问题:
- 在
FixedRate
模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行
? - 如果
任务抛出了异常,后续任务是否继续执行
?
Java标准库还提供了一个java.util.Timer
类,这个类也可以定期执行任务
,但是,一个Timer会对应一个Thread
,所以,一个Timer只能定期执行一个任务
,多个定时任务必须启动多个Timer
,
- 一个
ScheduledThreadPoo
l就可以调度多个定时任务
,所以,我们完全可以用ScheduledThreadPool取代旧的Time
r。
3.小结
JDK提供了ExecutorService实现了线程池功能:
- 线程池内部维护一组线程,可以
高效执行大量小任务
;
-Executors
提供了静态方法
创建不同类型的ExecutorService
;
-
必须调用shutdown()关闭ExecutorService
; -
ScheduledThreadPool可以定期调度多个任务
。
Java线程学习体系
Java 中的几种线程池,你之前用对了吗
java中的线程池有哪些,分别有什么作用?
最详细的Java线程池原理解析
java线程池,阿里为什么不允许使用Executors?
java线程池详解
Java线程池详解
十八.使用Future
1.什么是Future?
class Task implements Runnable {
public String result;
public void run() {
this.result = longTimeCalculation();
}
}
Runnable接口
有个问题,它的方法没有返回值。如果任务需要一个返回结果
,那么只能保存到变量
,还要提供额外的方法读取
,非常不便。所以,Java标准库还提供了一个Callable接口
,和Runnable接口比,它多了一个返回值
:并且Callable接口是一个泛型接口
,可以指定返回类型的结果
。
class Task implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
2.Java中使用Future?
现在的问题是,如何获得异步执行的结果?
如果仔细看ExecutorService.submit()方法
,可以看到,它返回了一个Future类型
,一个Future类型
的实例代表一个未来能获取结果的对象
:
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞
- 当我们提交一个
Callable任务
后,我们会同时获得一个Future对象
,然后,我们在主线程某个时刻调用Future对象的get()方法
,就可以获得异步执行的结果
。 - 在
调用get()时
,如果异步任务已经完成
,我们就直接获得结果
。如果异步任务还没有完成
,那么get()会阻塞,直到任务完成后才返回结果。
一个Future<V>接口
表示一个未来可能会返回的结果,它定义的方法有:
get()
:获取结果(可能会等待)get(long timeout, TimeUnit unit)
:获取结果,但只等待指定的时间;cancel(boolean mayInterruptIfRunning)
:取消当前任务;
-isDone()
:判断任务是否已完成。
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void TestFutureMain(String[] args) throws Exception {
ExecutorService es = Executors.newFixedThreadPool(4);
Future<BigDecimal> future = es.submit(new Task("601857"));
System.out.println(future.get());
es.shutdown();
}
}
class Task implements Callable<BigDecimal> {
public Task(String code) {
}
@Override
public BigDecimal call() throws Exception {
Thread.sleep(1000);
double d = 5 + Math.random() * 20;
return new BigDecimal(d).setScale(2, RoundingMode.DOWN);
}
}
3.小结
-
对
线程池
提交一个Callable任务
,可以获得一个Future对象
; -
可以用Future
在将来某个时刻获取结果
。
十九. 使用CompletableFuture
1.什么是CompletableFuture?
-
使用
Future
获得异步执行结果
时,要么调用阻塞方法get()
,要么轮询看isDone()是否为true
,这两种方法都不是很好,因为主线程也会被迫等待
。 -
从
Java 8
开始引入了CompletableFuture
,它针对Future做了改进,可以传入回调对象
,当异步任务完成
或者发生异常时
,自动调用回调对象的回调方法。
以获取股票价格为例,看看如何使用CompletableFuture
import java.util.concurrent.CompletableFuture;
public class TestCompletableFutureMain1{
public static void main(String[] args) throws Exception {
// 创建异步执行任务:
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(TestCompletableFutureMain1::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static Double fetchPrice() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
}
2.Java中使用CompletableFuture?
- 创建一个
CompletableFuture
是通过CompletableFuture.supplyAsync()实现的,它
需要一个实现了Supplier接口的对象:
public interface Supplier<T> {
T get();
}
这里我们用lambda语法
简化了一下,直接传入TestCompletableFutureMain1::fetchPrice
,因为TestCompletableFutureMain1.fetchPrice()静态方法
的签名符合Supplier接口的定义
(除了方法名外)。
- 紧接着,
CompletableFuture
已经被提交给默认的线程池执行了
,我们需要定义的是CompletableFuture完成时和异常时需要回调的实例。
完成时
,CompletableFuture
会调用Consumer对象
:
public interface Consumer<T> {
void accept(T t);
}
异常时
,CompletableFuture
会调用Function对象
:
public interface Function<T, R> {
R apply(T t);
}
!!! 这里我们都用lambda语法简化了代码。
*可见CompletableFuture的优点是:
异步任务结束时
,会自动回调某个对象的方法;异步任务出错时
,会自动回调某个对象的方法;主线程设置好回调后
,不再关心异步任务的执行
。
3.CompletableFuture相比Future的优势
如果只是实现了异步回调机制
,我们还看不出CompletableFuture相比Future的优势。CompletableFuture更强大的功能是,多个
CompletableFuture可以串行执行
例如: 定义两个CompletableFuture,
第一个CompletableFuture根据证券名称查询证券代码,
第二个CompletableFuture根据证券代码查询证券价格,这两个CompletableFuture实现串行操作如下:
import java.util.concurrent.CompletableFuture;
public class TestCompletableFutureMain2 {
public static void main(String[] args) throws Exception {
// 第一个任务:
CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油");
});
// cfQuery成功后继续执行下一个任务:
CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
return fetchPrice(code);
});
// cfFetch成功后打印结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static String queryCode(String name) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
除了串行执行外,多个
CompletableFuture还可以并行执行
。例如,我们考虑这样的场景:
同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格
,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作
:
import java.util.concurrent.CompletableFuture;
public class TestCompletableFutureMain3 {
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
// 两个CompletableFuture执行异步查询:
CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
});
CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://money.163.com/price/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
// 最终结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static String queryCode(String name, String url) {
System.out.println("query code from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code, String url) {
System.out.println("query price from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
上述逻辑实现的异步查询规则实际上是:
除了 anyOf()
可以实现“任意个CompletableFuture只要一个成功”, allOf()
可以实现“所有CompletableFuture都必须成功”,这些组合操作可以实现非常复杂的异步流程控制。
最后我们注意CompletableFuture的命名规则
:
xxx()
:表示该方法将继续在已有的线程中执行;xxxAsync()
:表示将异步在线程池中执行。
4.小结
CompletableFuture可以指定异步处理流程:
thenAccept()
处理正常结果;exceptional()
处理异常结果;thenApplyAsync()
用于串行化另一个CompletableFuture;anyOf()和allOf()
用于并行化多个CompletableFuture。
二十.使用ForkJoin
1.什么是ForkJoin
Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行
。
我们举个例子:如果要计算一个超大数组的和,最简单的做法是 用一个循环在一个线程内完成
:
还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行:
如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行
:
这就是Fork/Join任务的原理:判断一个任务是否足够小,如果就直接计算
,否则就分拆成几个小任务分别计算
。这个过程可以反复“裂变”
成一系列小任务。
2.Java中使用Fork/Join
使用Fork/Join对大数据进行并行求和:
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class TestForkJoinMain {
static Random random = new Random(0);
public static void main(String[] args) throws Exception {
// 创建2000个随机数组成的数组:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
static long random() {
return random.nextInt(10000);
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
private static final long serialVersionUID = 3426594844235690937L;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢计算速度:
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}
执行结果
观察上述代码的执行过程,一个大的计算任务0~2000
首先分裂为两个小任务0~1000
和1000~2000
,这两个小任务仍然太大,继续分裂为更小的0~500
,500~1000
,1000~1500
,1500~200
0,最后,计算结果被依次合并,得到最终结果。
因此,核心代码SumTask
继承自RecursiveTask
,在compute()方法
中,关键是如何“分裂”
出子任务并且提交子任务:
class SumTask extends RecursiveTask<Long> {
protected Long compute() {
// “分裂”子任务:
SumTask subtask1 = new SumTask(...);
SumTask subtask2 = new SumTask(...);
// invokeAll会并行运行两个子任务:
invokeAll(subtask1, subtask2);
// 获得子任务的结果:
Long result1 = fork1.join();
Long result2 = fork2.join();
// 汇总结果:
return result1 + result2;
}
}
Fork/Join线程池
在Java标准库中就有应用。Java标准库提供的java.util.Arrays.parallelSort(array)
可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序
,在多核CPU
上就可以大大提高排序的速度。
3.小结
-
Fork/Join
是一种基于“分治”
的算法:通过分解任
务,并行执行
,最后合并结果
得到最终结果。 -
ForkJoinPool线程池
可以把一个大任务分拆成小任务并行执行
,任务类必须继承
自RecursiveTask或RecursiveAction
。 -
使用
Fork/Join模式
可以进行并行计算
以提高效率。
二十一.使用ThreadLocal
1.什么是ThreadLocal?
多线程是Java实现多任务的基础,Thread对象
代表一个线程,我们可以在代码中调用Thread.currentThread()获取当前线程
。例如,打印日志时,可以同时打印出当前线程的名字:
public class Main {
public static void main(String[] args) throws Exception {
log("start main...");
new Thread(() -> {
log("run task...");
}).start();
new Thread(() -> {
log("print...");
}).start();
log("end main.");
}
static void log(String s) {
System.out.println(Thread.currentThread().getName() + ": " + s);
}
}
对于多任务
,Java标准库提供的线程池
可以方便地执行这些任务,同时复用线程
。Web应用程序就是典型的多任务应用
,每个用户请求页面时,我们都会创建一个任务,类似:
public void process(User user) {
checkPermission();
doWork();
saveStatus();
sendResponse();
}
然后,通过线程池去执行这些任务。
观察process()
方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?
- process()方法需要传递的状态就是
User实例
。有的童鞋会想,简单地传入User就可以了:
public void process(User user) {
checkPermission(user);
doWork(user);
saveStatus(user);
sendResponse(user);
}
但是往往一个方法又会调用其他很多方法,这样会导致User
传递到所有地方:
void doWork(User user) {
queryStatus(user);
checkStatus();
setNewStatus(user);
log();
}
这种在一个线程
中,横跨若干方法调用
,需要传递的对象
,我们通常称之为上下文(Context)
,它是一种状态,可以是用户身份
、任务信息
等。
- 给
每个方法
增加一个context参数
非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。
Java标准库提供了一个特殊的 ThreadLocal
,它可以在一个线程中传递同一个对象。
2.Java中使用ThreadLocal
- ThreadLocal实例通常总是以
静态字段初始化
如下:
static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();
它的典型使用方式如下:
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
通过设置一个User实例
关联到ThreadLocal
中, 在 移除之前
,所有方法都可以随时获取到该User实例
:
void step1() {
User u = threadLocalUser.get();
log();
printUser();
}
void log() {
User u = threadLocalUser.get();
println(u.name);
}
void step2() {
User u = threadLocalUser.get();
checkUser(u.id);
}
- 注意到 普通的方法调用一定是
同一个线程执行的
,所以,step1()
、step2()
以及log()
方法内,threadLocalUser.get()
获取的User对象
是同一个实例。
实际上,可以把ThreadLocal
看成一个全局Map<Thread, Object>
:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key
:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
- 因此,ThreadLocal相当于
给每个线程都开辟了一个独立的存储空间
,各个线程的ThreadLocal关联的实例互不干扰。
最后,特别注意ThreadLocal一定要在finally
中清除:
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}
- 这是因为
当前线程执行完相关代码后
,很可能会被重新放入线程池中
,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去
为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable
接口配合try-catch-resource
,让编译器自动为我们关闭
。
- 例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:
public class UserContext implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public UserContext(String user) {
ctx.set(user);
}
public static String currentUser() {
return ctx.get();
}
@Override
public void close() {
ctx.remove();
}
}
使用的时候,我们借助try (resource) {...}
结构,可以这么写:
try (UserContext ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser():
String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
这样就在UserContext
中完全封装了ThreadLocal
,外部代码在try (resource) {...}内部
可以随时调用UserContext.currentUser()
获取当前线程绑定的用户名
。
实例代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TestThreadLocalMain {
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newFixedThreadPool(3);
String[] users = new String[]{"Bob", "Alice", "Tim", "Mike", "Lily", "Jack", "Bush"};
for (String user : users) {
es.submit(new Task(user));
}
es.awaitTermination(3, TimeUnit.SECONDS);
es.shutdown();
}
}
class UserContext implements AutoCloseable {
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public UserContext(String name) {
userThreadLocal.set(name);
System.out.printf("[%s] init user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
public static String getCurrentUser() {
return userThreadLocal.get();
}
@Override
public void close() {
System.out.printf("[%s] cleanup for user %s...\n", Thread.currentThread().getName(),
UserContext.getCurrentUser());
userThreadLocal.remove();
}
}
class Task implements Runnable {
final String username;
public Task(String username) {
this.username = username;
}
@Override
public void run() {
try (UserContext ctx = new UserContext(this.username)) {
new Task1().process();
new Task2().process();
new Task3().process();
}
}
}
class Task1 {
public void process() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
}
class Task2 {
public void process() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.printf("[%s] %s registered ok.\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
}
class Task3 {
public void process() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.printf("[%s] work of %s has done.\n", Thread.currentThread().getName(),
UserContext.getCurrentUser());
}
}
执行结果
[pool-1-thread-1] init user Bob…
[pool-1-thread-3] init user Tim…
[pool-1-thread-2] init user Alice…
[pool-1-thread-3] check user Tim…
[pool-1-thread-2] check user Alice…
[pool-1-thread-1] check user Bob…
[pool-1-thread-2] Alice registered ok.
[pool-1-thread-3] Tim registered ok.
[pool-1-thread-1] Bob registered ok.
[pool-1-thread-1] work of Bob has done.
[pool-1-thread-2] work of Alice has done.
[pool-1-thread-3] work of Tim has done.
[pool-1-thread-2] cleanup for user Alice…
[pool-1-thread-1] cleanup for user Bob…
[pool-1-thread-2] init user Mike…
[pool-1-thread-3] cleanup for user Tim…
[pool-1-thread-1] init user Lily…
[pool-1-thread-3] init user Jack…
[pool-1-thread-2] check user Mike…
[pool-1-thread-3] check user Jack…
[pool-1-thread-1] check user Lily…
[pool-1-thread-2] Mike registered ok.
[pool-1-thread-3] Jack registered ok.
[pool-1-thread-1] Lily registered ok.
[pool-1-thread-2] work of Mike has done.
[pool-1-thread-2] cleanup for user Mike…
[pool-1-thread-2] init user Bush…
[pool-1-thread-1] work of Lily has done.
[pool-1-thread-3] work of Jack has done.
[pool-1-thread-1] cleanup for user Lily…
[pool-1-thread-3] cleanup for user Jack…
[pool-1-thread-2] check user Bush…
[pool-1-thread-2] Bush registered ok.
[pool-1-thread-2] work of Bush has done.
[pool-1-thread-2] cleanup for user Bush…
3.小结
-
ThreadLocal
表示线程的“局部变量”
,它确保每个线程的ThreadLocal变量
都是各自独立
的; -
ThreadLocal
适合在一个线程的处理流程中保持上下文
(避免了同一参数在所有方法中传递); -
使用
ThreadLocal
要用try ... finally
结构,并在finally
中清除,或者通过AutoCloseable接
口配合try-catch-resource
,让编译器自动为我们关闭
。