JUC概述
什么是JUC?
java.util.concurrent包中提供的并发编程的工具类
它提供多种工具类:原子类,线程池,并发集合,同步器
进程与线程:
进程:具备独立功能的程序软件关于某个数据集合的依次运行活动
操作系统动态执行的基本单位
线程:一个进程包含一个或者多个线程
独立运行和调度的基本单位
并行和并发:
并行:多个线程同时跑,干不同的事
并发:多个线程对同一资源的抢占
同步和异步:
同步:任务按顺序依次执行,每一个任务必须等待前一个任务完成
异步:同时发起请求,不需要等待结果,结果回来后
线程的状态:
- 新建(New):线程被创建但尚未启动执行。
- 就绪(Runnable):线程等待CPU时间片以便执行,也就是处于就绪状态。
- 阻塞(Blocked):线程暂停执行,通常是因为等待某些条件满足,例如等待I/O操作完成、等待锁释放等。
- 无限期等待(Waiting):线程无限期地等待某个条件的发生,通常需要其他线程来唤醒它。
- 有限期等待(Timed Waiting):线程等待一段时间,超过指定时间后会自动唤醒。
- 终止(Terminated):线程执行完成或者异常终止,进入终止状态。
枚举没赋值会给它赋初值,从0开始累加
wait和sleep
区别:
wait需要 notify()
或notifyAll()
方法来唤醒 必须在锁环境下使用
sleep是当前线程暂停执行指定时间,时间到唤醒
创建线程:
创建线程常用的两种方式:
1
2
3
4
|
1. 继承Thread:java是单继承,资源宝贵,要用接口方式 extends Thread
2. 实现Runable接口 implements Runnable
3. implements Callable
4. ThreadPoolExecutor
|
*********************************************************************************************视频细节需要补充
继承Thread抽象类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class T1 extends Thread{
@Override
public void run() {
System.out.println("Thread....");
super.run();
}
}
public class ThreadDemo {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
}
}
|
实现Runable接口:
新建类实现runnable接口:这种方法会新增类,有更好的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class T2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" runnable....");
}
}
public class ThreadDemo {
public static void main(String[] args) {
new Thread(new T2(), "线程名").start();
}
}
|
匿名内部类方式:
1
2
3
4
5
6
|
new Thread(new Runnable() {
@Override
public void run() {
// 调用资源方法,完成业务逻辑
}
}, "your thread name").start();
|
lambda表达式方式:
1
2
3
|
new Thread(()->{
// 调用资源方法,完成业务逻辑
}, "your thread name").start();
|
synchronized
多线程编程模板(上):
- 高内聚 低耦合 (将代码进行一定的封装,进行重复,独立使用)
- 线程 操作 资源类
实现步骤:
- 创建资源类
- 资源类里创建同步方法、同步代码块
- 多线程调用
例子:卖票程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
package com.atguigu.demojuc.chap01;
//资源类中内聚操作资源的方法,降低线程操作资源的耦合性
class Ticket{
// 定义一个票数
private int number = 20;
// 定义一个卖票的方法: 出现了资源抢占;
// synchronized: 使用synchronized同步方法解决
public synchronized void sale(){
// 判断
if (number<=0){
System.out.println(Thread.currentThread().getName() + "票已售罄!");
return;
}
try {
System.out.println(Thread.currentThread().getName() + "开始售票,当前票数:" + number);
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + "买票售票,剩余票数:" + --number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
// 创建资源类对象
Ticket ticket = new Ticket();
// 创建线程
new Thread(()->{
for (int i = 0; i < 21; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 21; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 21; i++) {
ticket.sale();
}
},"C").start();
}
}
|
synchronized的8锁问题
synchronized锁的是什么?
java中的每一个对象都可以作为锁。具体表现为以下3种形式:
- 对于普通同步方法,锁是
当前实例对象
。
- 对于
静态
同步方法,锁是当前类的Class对象
。
- 对于同步代码块,锁是Synchonized括号里配置的对象
- 而静态同步方法(Class对象锁)与非静态同步方法(实例对象锁)之间是不会有竞争的(锁都是自己的)。
Lock锁:
lock锁+三个实现类=JUC里面的锁:
1
2
3
4
5
6
7
8
|
ReentrantLock 可重入锁
这两个统一称为读写锁
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
lock是接口,ReenrantLock、ReadLock、WriteLock三个是lock接口的实现类
ReentrantReadWriteLock实现的是ReadWriteLock
|

ReentrantLock可重入锁
ReentrantLock
是可重入的互斥锁,虽然具有与synchronized
相同功能,但是会比synchronized
有更多的方法,因此更加灵活。
1
2
3
4
5
6
7
8
9
|
互斥锁:独占锁、悲观锁
a线程对一个资源加了一把锁,b线程就无法给这个资源加锁
锁可重入:
外层代码块和内层代码块用同一把锁
外层抢到锁后不需要 释放锁再抢内部锁
直接可以进
ReentrantLock和synchronized都具有可重入性
|
1
2
3
4
5
6
|
使用:
ReentrantLock lock = new ReentrantLock();
再:lock.lock();
执行完后
lock.unlock();
为了避免代码出问题,这个方法最好放到finally(try,catch,finally)里
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
class Ticket{
private final ReentrantLock lock = new ReentrantLock();
// 定义一个票数
private int number = 1;
// 定义一个卖票的方法:
public void sale(){
lock.lock();
// 判断
if (number<=0){
System.out.println(Thread.currentThread().getName() + "票已售罄!");
lock.unlock();
System.out.println("return");
return;
}
try {
System.out.println(Thread.currentThread().getName() + "开始买票,当前票数:" + number);
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + "买票结束,剩余票数:" + --number);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally");
lock.unlock();
}
}
}
|
公平锁
1
2
3
4
5
6
7
8
|
ReentrantLock可以实现公平锁
公平锁:
也就是在锁上等待时间最长的线程优先获得锁的使用权。
通俗的理解就是谁排队时间最长谁先获取锁。
(公平锁不允许插队,非公平锁允许插队)
实现:
private ReentrantLock lock = new ReentrantLock(true);
|
限时等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
通过我们的lock.tryLock()方法来实现
synchronized不具有限时等待的功能
有参表示等待指定的时间
无参则表示立即返回锁申请的结果
true表示获取锁成功,false表示获取锁失败。
这种方法可以用来解决死锁问题。
死锁:
两个线程互相抢被对方拿到的锁,都不释放锁,就出现了死锁
tryLock 能拿到锁就干,拿不到就不干了
|
ReentrantLock和synchronized区别
1
2
3
4
5
6
7
8
9
|
1.两个都是独占锁,
但是synchronized加锁解锁是自动 易于操作,不灵活
ReentarantLock加锁解锁需要手动 不易于操作,但灵活
2.两个都是可重入
但因为synchronized加锁解锁是自动,不担心最后是否释放锁
ReentarantLock加锁解锁需要手动,且加锁一次就需要解锁,否则其他线程无法获得锁
3.synchronized不可响应中断,一个线程获取不到锁就一直等着
ReentrantLock可以响应中断(tryLock方法:获取不到锁则返回false)
4. synchronized不具备设置公平锁的特点,ReentrantLock可以成为公平锁。
|
ReentrantReadWriteLock读写锁
1
2
3
|
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
故:一般用于多读写少情况
|
特点:
1
2
3
4
5
6
7
8
|
1. 写写不可并发
两个线程都拿到写锁,一个写的时候另一个不能写
2. 读写不可并发/写读不可并发
一个读时另一个写不了,一个写时另一个也读不了
3. 读读可以并发
两个线程都拿到读锁,两个线程都能读
|
1
|
读写锁保证了写操作的原子性,并且可以进行并发读
|
锁降级
1
2
3
4
5
|
在当前线程拥有写锁的情况下,获取到读锁,随后释放写锁的过程就是锁降级。
为了减少锁的竞争,提高程序的并发性能
锁降级的典型场景是在持有某个写锁时,释放写锁并获取读锁。这样做的好处是在执行读操作期间,其他线程可以同时执行读操作,提高了并发性能。
|
读写锁总结
-
支持公平/非公平策略

-
支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁
- 同一读线程在获取了读锁后不可以获取写锁
1
2
3
4
5
|
可重入性都是针对同一线程而言
读锁读锁可以
写锁写锁可以
读锁写锁不可以
|
-
支持锁降级,不支持锁升级
-
读写锁如果使用不当,很容易产生“饥饿”问题:
在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
锁饥饿(Lock Starvation)是指在多线程编程中的一种情况,其中某些线程可能无法获得所需的锁,而一直等待下去,从而无法继续执行,即线程被"饿死"在等待锁的过程中。这可能会导致应用程序的性能问题和不稳定性。
线程间通信
面试题案例
两个线程,一个线程打印1-52,另一个都要弄字母A-Z,打印顺序为12A34B…5152Z,要求用线程间通信
线程间通信模型
- 生产者+消费者
- 通知等待唤醒机制
多线程编程模板(中)
1.判断
2.干活
3.通知
两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
package com.atguigu.demojuc.chap03;
class ShareDataOne{
private Integer number = 0;
/**
* 增加1
*/
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
/**
* 减少1
*/
public synchronized void decrement() throws InterruptedException {
// 1. 判断
if (number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
}
/**
* 现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,10轮。
*
* 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程编程模板(套路)
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程编程模板(套路)
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne shareDataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
}
}
|
部分打印结果:AAA和BBB交互执行,执行结果是1 0 1 0… 一共10轮
1
2
3
4
5
6
7
8
9
|
AAA: 1
BBB: 0
AAA: 1
BBB: 0
AAA: 1
BBB: 0
AAA: 1
BBB: 0
。。。。
|
虚假唤醒
换成4个线程会导致错误,虚假唤醒
**原因:**在java多线程判断时,不能用if,程序出事出在了判断上面。
1
|
进行wait()时判断条件使用if了,导致虚假唤醒
|
**注意:**消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

解决虚假唤醒:查看API,java.lang.Object的wait方法

中断和虚假唤醒是可能产生的,所以要用循环,if只判断一次,while是只要唤醒就要重新再判断一次。
多线程编程模板(下):
防止虚假唤醒(使用while,代替if)
1
2
3
4
5
6
7
8
9
|
// 1. 判断
while (number != 0) {
this.wait();
}
// 1. 判断
while (number != 1) {
this.wait();
}
|
线程通信(Condition)
线程间通信有两套代码开发方式
1
2
3
4
5
6
|
1.wait+notify/notifyall+synchronized
wait+notify/notifyall必须在有锁的环境下使用,所以必须跟synchronized结合做开发
如果使用JUC中的lock锁,
通过Condition来实现通知唤醒(代替wait+notify/notifyall)
2.lock+condition
|

上面案例只需要改动ShareDataOne
并去掉 increment 和 decrement 方法的synchronized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
class ShareDataOne{
private Integer number = 0;
final Lock lock = new ReentrantLock(); // 初始化lock锁
final Condition condition = lock.newCondition(); // 初始化condition对象
/**
* 增加1
*/
public void increment() throws InterruptedException {
lock.lock(); // 加锁
try{
// 1. 判断
while (number != 0) {
// this.wait();
condition.await();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
// this.notifyAll();
condition.signalAll(); //唤醒所有等待的线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 减少1
*/
public void decrement() throws InterruptedException {
lock.lock(); // 加锁
try{
// 1. 判断
while (number != 1) {
// this.wait();
condition.await();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
// this.notifyAll();
condition.signalAll(); //唤醒所有等待的线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
|
1
2
|
condition.signal(); // 唤醒一个等待的线程
condition.signalAll(); //唤醒所有等待的线程
|
定制化调用通信
线程间按顺序通信
面试题
1
2
3
4
5
6
7
8
9
|
多线程之间按顺序调用,实现AA->BB->CC。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
|
1
2
3
4
5
6
7
|
分析实现方式:
1. 有一个锁Lock,3把钥匙Condition(a完了唤醒b,b完了唤醒c)
2. 有顺序通知(切换线程),需要有标识位
3. 判断标志位
4. 输出线程名 + 内容
5. 修改标识符,通知下一个
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
package com.atguigu.juc.chap03;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadOrderAccess {
//声明全局锁对象
private static Lock lock = new ReentrantLock();
//为A,B,C线程声明Condition对象负责不同线程等待,唤醒
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();
//声明全局flag标识,作为线程阻塞,执行条件
private static int flag = 1;
/**
* A线程负责调用打印5次方法
*/
public void print5() {
//1.先获取锁
lock.lock();
try {
//2.判断A线程等待条件(不等于1将当前线程进入等待状态)
while (flag != 1) {
conditionA.await();
}
//3.干活(flag=1则打印5次执行)
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "," + i);
}
//4.将flag标识修改为2,唤醒B线程干活
flag = 2;
conditionB.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//5.将锁释放
lock.unlock();
}
}
/**
* B线程负责调用打印10次方法
*/
public void print10() {
//1.先获取锁
lock.lock();
try {
//2.判断B线程等待条件(不等于2将当前线程进入等待状态)
while (flag != 2) {
conditionB.await();
}
//3.干活(flag=2则打印10次执行)
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "," + i);
}
//4.将flag标识修改为3,唤醒C线程干活
flag = 3;
conditionC.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//5.将锁释放
lock.unlock();
}
}
/**
* C线程负责调用打印15次方法
*/
public void print15() {
//1.先获取锁
lock.lock();
try {
//2.判断C线程等待条件(不等于3将当前线程进入等待状态)
while (flag != 3) {
conditionC.await();
}
//3.干活(flag=3则打印15次执行)
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "," + i);
}
//4.将flag标识修改为1,唤醒A线程干活
flag = 1;
conditionA.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//5.将锁释放
lock.unlock();
}
}
public static void main(String[] args) {
//2.创建资源类对象
ThreadOrderAccess orderAccess = new ThreadOrderAccess();
//1.创建三个线程A,B,C
new Thread(()->{
for (int i = 0; i < 10; i++) {
orderAccess.print5();
}
}, "A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
orderAccess.print10();
}
}, "B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
orderAccess.print15();
}
}, "C").start();
}
}
|
1
2
3
|
在Java中,一个ReentrantLock可以与多个Condition对象一起使用
每个Condition对象可以用于不同的线程协调和通信场景
以便更精细地控制多线程之间的执行顺序和互斥访问。
|
并发容器类
Vector和Synchronized的缺点:
1
2
3
4
|
- Vector:内存消耗比较大,适合一次增量比较大的情况(Vector每次扩容是原来容量的一倍,ArrayList是原来的1.5倍)
//Vector:读取加锁!
- SynchronizedList:迭代器涉及的代码没有加上线程同步代码
//synchronizedList: 读取数据:读取数据没有加锁!
|
CopyOnWrite
什么是CopyOnWrite容器
1
2
3
4
5
6
7
8
9
10
11
|
CopyOnWrite容器(简称COW容器)即写时复制的容器。
通俗的理解:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
好处:
可以对CopyOnWrite容器进行并发的读,而不需要加锁
因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
读写分离:
写的时候不影响读,读写是分开的
|
1
2
3
4
5
6
7
8
|
从JDK1.5开始
Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器
CopyOnWriteArrayList
//是线程安全的 List 实现,它通过复制底层数组的方式保证线程安全,适用于读多写少的场景。
CopyOnWriteArraySet
//是CopyOnWriteArrayList 的 Set 版本
|
应用场景:
1
2
3
4
|
CopyOnWrite并发容器用于 读多写少 的并发场景:白名单,黑名单
场景:
假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。
|
缺点
1
2
3
4
5
6
7
|
1. 内存占用问题。
//写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。
//2倍内存空间
2. 数据一致性问题。
//CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
//所以如果你希望写入的数据,马上能读到,请不要使用CopyOnWrite容器。读的数据可能是就旧数组
|
ConcurrentHashMap
特点
1
2
3
4
5
6
7
8
9
|
1. 并发性:ConcurrentHashMap
//允许多个线程同时访问,读操作不会被阻塞,不需要加锁。
//这意味着多个线程可以并发地读取其中的数据,而不会发生竞争或锁定。
2. 分段锁:ConcurrentHashMap
//ConcurrentHashMap是线程安全的Map容器
//JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
//这种设计允许多个线程同时进行读操作,只有在写操作时才需要锁定相应的段,以确保线程安全。这提高了并发性能,因为不同段之间的操作不会相互阻塞。
|
1
2
3
4
5
|
ConcurrentHashMap是一个用于高并发环境的非常有用的数据结构。
它提供了高效的并发访问支持,允许多个线程同时读取和写入数据,而不需要显式的锁定。
场景:并发编程中需要高效地处理共享数据的情况
|
并发安全集合类都哪些?
1
2
3
4
5
6
7
8
9
10
11
|
ConcurrentHashMap
//是线程安全的哈希表实现,它通过分段锁的机制来实现并发访问的高效性。
CopyOnWriteArrayList
//是线程安全的 List 实现,它通过复制底层数组的方式保证线程安全,适用于读多写少的场景。
CopyOnWriteArraySet
//是 CopyOnWriteArrayList 的 Set 版本
BlockingQueue
//是一个用于在多线程间传递数据的队列接口,它包含多个实现类,如 ArrayBlockingQueue、LinkedBlockingQueue
|
JUC强大的辅助类
- CountDownLatch(倒计数器)
- CyclicBarrier(循环栅栏)
- Semaphore(信号量
CountDownLatch
1
2
3
4
5
6
7
8
9
10
11
|
倒计数器
常用方法:
new CountDownLatch(int count)
//实例化一个倒计数器,count指定初始计数
countDown()
// 每调用一次,计数减一
await()
//等待,当计数减到0时,阻塞线程(可以是一个,也可以是多个)并行执行
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
使用:
ex:
//6个同学(子线程)陆续离开教室后值班同学(主线程)才可以关门
public static void main(String[] args) throws InterruptedException {
//1.创建倒计数器对象
CountDownLatch countDownLatch = new CountDownLatch(6);
//2.循环创建6个子线程分别执行各自业务(完成课后作业)
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+",开始学习");
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+",结束学习");
countDownLatch.countDown();
}, "同学"+i).start();
}
//3.主线程阻塞等待各个子线程执行结束
countDownLatch.await(10, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName()+"班长锁门,走人!");
}
}
|
结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
同学1,开始学习
同学4,开始学习
同学3,开始学习
同学2,开始学习
同学5,开始学习
同学6,开始学习
同学5,结束学习
同学2,结束学习
同学4,结束学习
同学1,结束学习
同学6,结束学习
同学3,结束学习
main班长锁门,走人!
|
CountDownLatch 与 join 方法的区别
1
2
3
4
5
6
7
|
调用一个子线程join()方法后,该线程会一直被阻塞,直到该线程运行完毕
CountDownLatch则使用计数器
允许子线程运行完毕或者运行中时候递减计数
也就是CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;
(await类似wait,不过wait配合synchronized关键字,await配合lock接口)
countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
|
CyclicBarrier
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
循环栅栏
大概的意思就是一个可循环利用的屏障
主要作用是:
在多个线程相互等待达到某个共同点之后再一起继续执行
常用方法:
1. CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,
parties指定参与相互等待的线程数
barrierAction一个可选的Runnable命令,该参数只在每个屏障点运行一次,可以在执行后续业务之前共享状态。该操作由最后一个进入屏障点的线程执行。
2. CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
3. await()
该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,`直到所有线程都到达屏障点`,当前线程才会被唤醒。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
使用:
ex:组队打boss过关卡游戏
共计三个线程(玩家),要求所有线程执行完某一关(到达某个屏障点),才能够继续执行下一关
public static void main(String[] args) {
//1.声明循环栅栏对象 p1:线程数量 p2:所有线程到达屏障点后执行业务-由最后一个到达屏障点线程执行
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println(Thread.currentThread().getName()+"(裁判),所有玩家都完成该关卡,继续...");
});
//2.创建三个线程,业务逻辑,线程间互相等待,全部到达屏障点,才继续
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
try {
//2.1 模拟当前线程过游戏所有关卡
System.out.println(Thread.currentThread().getName() + ",开始过第1关");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + ",第1关,已过");
cyclicBarrier.await();
//2.2 如果当前线程先到达屏障点,其他线程还未到达,将当前线程进入阻塞状态,一直到其他所有线程全部到达屏障点
System.out.println(Thread.currentThread().getName() + ",开始过第2关");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + ",第2关,已过");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + ",开始过第3关");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + ",第3关,已过");
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "玩家" + i).start();
}
}
}
|
结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
玩家1,开始过第1关
玩家3,开始过第1关
玩家2,开始过第1关
玩家3,第1关,已过
玩家1,第1关,已过
玩家2,第1关,已过
玩家2(裁判),所有玩家都完成该关卡,继续...
玩家2,开始过第2关
玩家1,开始过第2关
玩家3,开始过第2关
玩家3,第2关,已过
玩家2,第2关,已过
玩家1,第2关,已过
玩家1(裁判),所有玩家都完成该关卡,继续...
玩家1,开始过第3关
玩家3,开始过第3关
玩家2,开始过第3关
玩家1,第3关,已过
玩家2,第3关,已过
玩家3,第3关,已过
玩家3(裁判),所有玩家都完成该关卡,继续...
|
注:所有的"过关了"都是由最后到达await方法的线程执行打印的
CyclicBarrier和CountDownLatch的区别
1
2
3
4
5
6
7
|
- CountDownLatch允许一个或多个线程等待一组事件的产生
而CyclicBarrier用于等待其他线程运行到栅栏位置
- CountDownLatch的计数器只能使用一次
而CyclicBarrier的计数器可以使用多次
- 所以CyclicBarrier能够处理更为复杂的场景
|
Semaphore
1
2
3
4
5
6
7
8
9
10
11
12
13
|
信号量
Semaphore可以控制同时访问的线程个数
非常适合需求量大,而资源又很紧张的情况
常用方法:
public Semaphore(int permits)
// 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException
// 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
public void release()
// (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
|
两个目的:
1
2
|
1. 多个共享资源的互斥使用。
2. 用于并发线程数的控制。保护一个关键部分不要一次输入超过N个线程。(限流)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
使用:
ex:
//6辆车(线程)要进入到停车场共计有3个车位(信号量)
public static void main(String[] args) {
//1.构建信号量对象
Semaphore semaphore = new Semaphore(3);
//2.循环创建6个线程,拿到信号量才能执行线程业务
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//2.1 每个车辆只有拿到信号才能停车 当前线程会一直阻塞到获取信号为止
semaphore.acquire();
//2.2 线程业务逻辑
System.out.println(Thread.currentThread().getName()+" 抢到了一个停车位!");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
//2.3 当前车辆业务执行完毕,释放信号量
System.out.println(Thread.currentThread().getName()+" 离开停车位!!");
semaphore.release();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "" + i).start();
}
}
}
|
打印结果:
1
2
3
4
5
6
7
8
9
10
11
12
|
0 抢到了一个停车位!!
1 抢到了一个停车位!!
2 抢到了一个停车位!!
1 离开停车位!!
3 抢到了一个停车位!!
2 离开停车位!!
4 抢到了一个停车位!!
0 离开停车位!!
5 抢到了一个停车位!!
5 离开停车位!!
3 离开停车位!!
4 离开停车位!!
|
Callable接口
1
|
Thread类和Runnable接口都`不允许声明检查型异常`,`也不能定义返回值`。
|
1
2
|
从java5开始,提供了Callable接口
`用Call()方法作为线程的执行体,增强了之前的run()方法。`因为call方法可以有返回值,也可以声明抛出异常。
|
Callable和Runnable对比
1
2
3
|
runnable:弊端:没有返回结果,不能声明抛异常 来自java.lang
callable:优势:有返回值,抛异常 来自java.util.concurrent
|
Callable的使用

FutureTask其实可以充当了一个中间人的角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
class MyRunnableThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Runnable");
}
}
/**
* 1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
*/
class MyCallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i;
for (i = 0; i < 10; i++) {
Thread.sleep(300);
System.out.println(Thread.currentThread().getName() + " 执行了!" + i);
}
return i;
}
}
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建多线程
new Thread(new MyRunnableThread(), "threadName").start();
//new Thread(new MyCallableThread(), "threadName").start();
// 2. 创建Callable的实例,并用FutureTask类来包装Callable对象
// 3. 创建FutureTask对象,需要一个Callable类型的参数
FutureTask task = new FutureTask<>(new MyCallableThread());
// 4. 创建多线程,由于FutureTask的本质是Runnable的实现类,所以第一个参数可以直接使用task
new Thread(task, "MyCallableThread").start();
//取消任务
// Thread.sleep(1000);
// task.cancel(true); //线程运行时可以被打断吗
// boolean cancelled = task.isCancelled();
// System.out.println("cancelled " + cancelled);
//等待任务执行完毕
// while (!task.isDone()) { //也可以使用task.isDone()判断子线程是否执行完毕
// Thread.sleep(100);
// System.out.println("wait...");
// }
//获取结果
System.out.println(task.get());//get方法阻塞主线程,因为需要返回子线程的结果
System.out.println(Thread.currentThread().getName() + " over!");
}
}
|
FutureTask
1
2
3
4
|
FutureTask是 Java 中的一个类,它实现了Future和Runnable接口,用于表示一个可取消的异步计算任务。
FutureTask 的主要作用是:
一个线程中计算结果,另一个线程中获取计算机的结果,同时还支持任务的取消操作
|
主要作用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
异步计算:
允许开发者在一个线程中执行耗时的计算任务而不会阻塞主线程
获取计算结果:
调用get()方法,获取计算结果。如果计算尚未完成,get()会阻塞当前线程,直到计算完成
取消任务
调用cancel(boolean mayInterruptIfRunning)方法来取消任务的执行。 可以选择是否允许在任务运行时中断任务。
任务状态查询
FutureTask提供了一些方法来查询任务的状态
isDone()用于检查任务是否已完成
isCancelled()用于检查任务是否已被取消。
异常处理
如果异步任务抛出了异常,FutureTask会捕获异常并在后续调用 get()方法时重新抛出。
获取计算结果时处理可能的一场抢矿
|
注:为了防止主线程阻塞,建议get方法放到最后
只计算一次,FutureTask会复用之前计算过的结果

callable接口与runnable接口的区别?
1
2
3
4
5
6
7
|
- 相同点:
都是接口,都可以编写多线程程序,都采用 Thread.start()启动线程
- 不同点:
1. 具体方法不同:一个是run,一个是call
2. Runnable没有返回值;Callable可以返回执行结果,是个泛型
3. Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛
|
获取多线程的方法
1
2
3
4
5
6
|
传统:
继承thread类
runnable接口
java5以后又有
实现callable接口和java的线程池
|
阻塞队列(BlockingQueue)
什么是BlockingQueue
1
|
所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
|
BlockingQueue即阻塞队列,是java.util.concurrent下的一个接口
BlockingQueue是为了解决多线程中数据高效安全传输而提出的。
1
2
3
4
5
6
7
8
|
被阻塞情况主要有如下两种:
1. 当队列满了的时候,依然进行入队列操作
2. 当队列空了的时候,依然进行出队列操作
当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;
当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
|

BlockingQueue接口
主要有以下7个实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
2. LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
5. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
6. LinkedTransferQueue:由链表组成的无界阻塞队列。
7. LinkedBlockingDeque:由链表组成的双向阻塞队列。
|
阻塞队列提供以下4种处理方法:

抛出异常
1
2
3
4
5
6
7
8
|
add正常执行返回true
element(不删除)和remove(删除)返回阻塞队列中的第一个元素
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再从队列里remove移除元素会抛NoSuchElementException
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
|
特殊值
1
2
3
|
插入方法,成功ture失败false
移除方法,成功返回出队列的元素,队列里没有就返回null
检查方法,成功返回队列中的元素,没有返回null
|
阻塞
1
2
3
4
5
|
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程,直到put数据or响应中断退出
当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程,直到队列可用
|
超时
1
2
3
|
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
返回一个特定值以告知该操作是否成功(典型的是 true / false)。
|
阻塞队列操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//创建有界阻塞队列 队列长度为3
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 第一组方法:add remove element
System.out.println(queue.add("a")); //入队,正常则返回true
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
// System.out.println(queue.add("d")); //队列已满仍然入队,报异常 IllegalStateException异常
System.out.println(queue.element()); //获取队列中的第一个元素,并返回
System.out.println(queue.remove()); //出队第一个元素,返回出队元素
System.out.println(queue.remove());
System.out.println(queue.remove());
//System.out.println(queue.remove()); //队列已空仍然出队,报异常 NoSuchElementException 没有这样的元素异常
//System.out.println(queue.element()); //队列已空仍然获取元素,报异常 NoSuchElementException 没有这样的元素异常
-------------------------------------------------------
// 第二组:offer poll peek
/*System.out.println(queue.offer("a")); //入队,正常则返回true
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println(queue.offer("d")); //队列已满仍然入队, 返回false
System.out.println(queue.peek()); //获取队列中的第一个元素,并返回
System.out.println(queue.poll()); //出队第一个元素,返回出队元素
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll()); //队列已空仍然出队,返回null
System.out.println(queue.peek()); //队列已空仍然获取元素,返回null*/
---------------------------------------------------
// 第三组:put take ************
/*queue.put("a"); //入队
queue.put("b");
queue.put("c");
System.out.println(queue.take()); //出队第一个元素,返回出队元素,则后面代码不会阻塞
queue.put("d"); //队列已满仍然入队, 发生阻塞
System.out.println(queue.take()); //出队第一个元素,返回出队元素
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take()); //队列已空仍然出队,发生阻塞*/ 阻塞:程序没终止,卡住不动
----------------------------------------------------
// 第四组:offer poll
/*System.out.println(queue.offer("a")); //入队,正常则返回true
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println(queue.offer("d", 5, TimeUnit.SECONDS)); //队列已满仍然入队,超时返回false
System.out.println(queue.poll()); //出队第一个元素,返回出队元素
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll(5, TimeUnit.SECONDS)); //队列已空仍然出队,,超时返回null*/
}
}
|
生产者消费者案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class BlockingQueueDemo2 {
public static void main(String[] args) {
//创建无界阻塞队列
//BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
//创建有界阻塞队列:当消费的进度较慢,生产进度较快,而且队列放不下的时候,生产会被自动阻塞,等待消费
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
//生产者
new Thread(()->{
try {
for (int i = 1; i <= 10; i++) {
queue.put(i);
System.out.println("生产第" + i + "个馒头," + "目前还剩" + queue.size() + "个馒头");
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "生产者").start();
//消费者
new Thread(()->{
try {
for (int i = 1; i <= 10; i++) {
System.out.println("消费第" + queue.take() + "个馒头" + "目前还剩" +queue.size() + "个馒头");
TimeUnit.SECONDS.sleep(3);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "消费者").start();
}
}
|
SynchronousQueue
作用场景:生产并直接交付
详情看笔记
线程池
1
2
3
4
|
异步调用:做一些远程调用,微服务调用,这个时候需要创建线程池,通过线程池来执行远程调用。
异步方式进行远程调用:
6个远程接口没有业务上先后顺序的要求,可以6个请求一起发送,6个结果一起回来,然后组装数据,返回前端
ex:1s 但同步:6s
|
线程池是通过Executor框架实现的
该框架用了
1
2
3
4
5
|
Executor:execute(runnable command)执行任务的方法
ExecutorService:继承Executor submit
ThreadPoolExecutor:
|
线程池框架

创建一个线程池对象
面试题:execute和submit的区别
1
2
3
4
5
6
7
8
9
10
11
|
1. execute是Executor接口的方法。
submit是ExecutorService的方法
ExecutorService接口继承了Executor接口。
2. execute只接受Runnable参数
没有返回值
submit可以接受Runnable参数和Callable参数
返回了Future对象,可以进行任务取消、获取任务结果、判断任务是否执行完毕/取消等操作。
3. 通过execute方法提交的任务无法获取具体的异常信息;
submit方法可以通过Future对象获取异常信息。
|
Executors(禁用)


线程池工作原理
如何创建线程池?
使用Executors工厂
使用ThreadPoolExecutor构造函数
7个参数
ThreadPoolExecutor的七个参数:
corePoolSize(核心池大小)不会被回收
maximumPoolSize(最大池大小)会被回收
keepAliveTime(线程空闲存活时间)
unit(时间单位)
workQueue(工作队列)
LinkedBlockingQueue大小无限制
ArrayBlockingQueue大小有限,必须指定容量
SynchronousQueue不存储任务,直接给线程池
threadFactory(线程工厂)
handler(拒绝策略)
AbortPolicy: 直接抛出异常
CallerRunsPolicy:返回给调用者线程执行该任务
DiscardPolicy:直接丢弃该任务
DiscardOldestPolicy:丢弃最旧的一个任务

线程池工作原理

1
2
3
4
5
|
工作原理:
提交新任务时,线程池进行判断是否有空闲线程,有就直接执行,没有就查看是否到达核心线程数,没有就创建新的线程执行,
如果达到核心线程数但任务队列有空间,进入任务队列,
如果任务队列也已经满了,查看是否到达最大线程数,没有到达最大线程数就创建新的线程,若到达最大线程数,执行拒绝策略
当队列中的任务执行完,线程空闲时间到达预设时间,销毁超出核心线程数的线程
|
1
2
3
4
5
6
7
8
9
10
11
|
虽然也是有请求就创建,但它会判断,如果满了就去判断任务队列,任务队列如果满了会取判断最大线程池,最后还是不够会交给拒绝策略处理
并不是无所节制的创建线程**
最大线程数包括核心线程数
注意,队列若满,最大线程数有空,会直接创建新线程执行,并不会让对头出队
比如13来了,去创建13的
为什么?因为这样比出队相率更高
|

拒绝策略

以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口
,定义自己的拒绝策略
实际业务哪个都基本不用,最常用的是自定义拒绝策略(线程睡一觉等自定义饱和拒绝策略)
1
2
3
4
5
6
7
|
存储任务,将来人工处理
发邮件、发短信警告
睡一会,再次将任务提交线程池处理
等等等
|
自定义线程池

案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package com.atguigu.demojuc.chap08;
class MyRunnable implements Runnable{
private int param;
public MyRunnable(int param) {
this.param = param;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Runnable......" + param);
}
}
public class CustomizeThreadPoolDemo {
public static void main(String[] args) {
// 自定义连接池
ExecutorService threadPool = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
2,//等待空闲时间,时间到达会销毁非核心线程数。核心线程不销毁 0就是不销毁
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//任务等待队列
Executors.defaultThreadFactory(),//线程工厂
new ThreadPoolExecutor.AbortPolicy() //饱和拒绝策略
//丢弃任务并抛出异常
//new ThreadPoolExecutor.CallerRunsPolicy() //由调用线程处理该任务,谁调用谁处理
//new ThreadPoolExecutor.DiscardOldestPolicy() //丢弃队列中等待最久的任务,添加新任务
//new ThreadPoolExecutor.DiscardPolicy() //也是丢弃任务,但是不抛出异常。
/*new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定义拒绝策略");
}
}*/
);
try {
for (int i = 0; i < 9; i++) {
//execute()方法只能接收Runnable参数
threadPool.execute(() -> {
//submit()方法可以接收Runnable或Callable 推荐
//threadPool.submit(() -> { //这里也可以使用submit
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();//关闭线程池,但实际业务基本不关
}
}
}
|
总结
线程池主要作用:
1
|
有效地管理和复用线程,以提高多线程应用程序的性能和资源利用率。
|
优点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
1. 线程复用:线程池会在池中维护一组可重用的线程,这些线程可以反复执行任务。
线程的复用减少了线程创建和销毁的开销,提高了执行任务的效率。
//---------------------------
2. 任务队列:线程池通常与任务队列结合使用,将待执行的任务排队等待执行。
这允许任务按顺序执行,控制并发度,防止任务过多导致资源耗尽。
//-------------------------
3. 线程生命周期管理:线程池可以管理线程的生命周期,包括线程的创建、销毁、超时处理等。(线程工厂管线程生命周期管理)
这有助于减少资源泄漏和提高系统的稳定性。
//----------------------------
4. 可控性:线程池允许您控制线程的数量、最大并发数、线程优先级等参数,以满足不同应用场景的需求。
弹性伸缩,少了-->最小 多了-->最大,再多就拒绝策略处理
|
多线程高并发底层原理
Java内存模型(JMM)
计算机硬件
1
2
3
4
5
|
CPU 通过CPU缓存和CPU寄存器来提高数据访问速度,减少对RAM的访问次数,提高对RAM的访问效率,从而提高计算机的性能。
(ROM只读不能写)
(RAM随机存取存储器 能读能写)
|
1
2
|
JMM屏蔽了屏蔽系统和硬件的差异
故java一次编译到处运行
|
JMM规定了内存主要划分为主内存和工作内存两种。
主内存对应的是硬件的物理内存
工作内存对应的是寄存器和高速缓存。

-
共享变量:
1
2
3
4
5
6
7
8
|
如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。
数据放主内存中,为什么是共享:
因为数据被多个线程共享
但是因为写操作无法并发,
故线程要操作共享变量,会将共享变量拷贝一份,放到当前线程的本地内存当中来进行数据的处理
数据改了后再写回主内存,这叫做数据的同步
|
-
主内存:
-
工作内存:
1
|
每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。
|
JMM对共享内存的操作做出了如下两条规定:
1
2
3
4
5
6
|
- 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
数据改了后再写回主内存,这叫做数据的同步
- 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。
只有将改后数据写回到主内存中,另一个线程将其从主内存中读走才能获得最新数据
|
ava内存模型的三大特性
volatile关键字
可见性

1
2
3
4
5
6
7
|
使用volatile关键字可以确保共享变量的可见性
当一个变量被声明为volatile时
每次访问该变量时都会从主内存中读取最新的值
并且对该变量的修改会立即写入主内存,而不是先写入缓存
避免了在多线程环境下出现数据不一致的问题
因为任何对volatile变量的修改都会立即可见于其他线程
|
有序性

1
2
3
|
将变量声明为volatile
可以避免指令重排的优化机制,保证指令的执行顺序与源代码中的顺序一致
以避免多线程环境下的错误
|
原子性
从0到1000
1000个线程执行++number操作,如果++number操作具备原子性,最后的值应该是1000。说明++number不具备原子性。
给number添加volatile关键字
测试结果依然不是1000
尝试给++number方法加同步锁
结果达成
说明volatile关键字不能保证原子性。
1
2
3
|
volatile关键字只能保证单个变量的可见性,并不能解决原子性问题。
对于原子性需求,需要使用更强大的同步机制,如锁、原子操作等。
|
保证三大特性
1
2
3
4
5
6
7
8
|
可见性:
使用volatile关键字声明变量,确保修改对于所有线程立即可见
有序性:
volatile保证对该变量的读写操作是有序的,禁止重排序
原子性:
synchronized同步锁确保对共享变量的操作是原子的
atomic提供的原子类AtomicInteger、AtomicLong等
|
CAS
AQS
1
2
3
4
5
6
7
8
9
|
AbstractQueuedSynchronizer抽象队列同步器简称AQS
是整个java.util.concurrent包的核心
AQS框架提供了一套通用的机制来
管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。
JUC下的Lock(ReentrantLock、ReentrantReadWriteLock等)以及并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS实现的
|
什么是AQS
1
2
3
4
5
6
7
|
一个状态值+双端队列(FIFO)(左进右出,右进左出)
- FIFO:意味着等待时间最长的线程将首先获得锁或资源(公平锁排队)。
- 双向队列:意味着允许多个线程同时从队列的两端进行插入、删除和查找操作(非公平锁插队)。
队列里放等待的线程
|

1
2
3
4
5
6
7
8
9
10
11
12
|
请求获取锁->入队
多线程并发执行任务,加锁
怎么加:
线程去抢AQS类里state属性(资源),初始为0表示锁没人用,抢到了后0变为1.变为1后其他线程就不能抢,得在队列里排队,直到state变为0(线程任务执行完了 也就是释放锁)
公平锁:排队使用 公平锁指的是按照线程请求的顺序,来分配锁
非公平锁: 一定情况下,可以允许插队
当前线程在请求获取锁的时候,恰巧前一个持有锁的线程释放了这把锁,那么当前申请锁的线程就可以不顾已经等待的线程而选择立刻插队。
但是如果当前线程请求的时候,前一个线程并没有在那一时刻释放锁,那么当前线程还是一样会进入等待队列。
|
state 属性:表示资源的状态
1
2
3
|
- 对于ReentrantLock来说,state=1,表示资源被占用;state=0,表示资源没有被占用。
- 对于CountDownLatch来说,state=0,表示计数器归零,所有线程都可以访问资源;state为N表示计数器未归零,所有线程都需要阻塞。
|
ReentrantLock
默认非公平锁

在ReentrantLock类中包含了3个AQS的实现类:
- 抽象类Sync
- 非公平锁实现类NonfaireSync
- 公平锁实现类FairSync
上锁,调试公平锁,解锁
详情看笔记
AQS的底层原理
1
2
3
4
5
6
7
|
AQS使用一个volatile成员变量state来表示锁是否已被持有,通过内置的FIFO队列存储被阻塞的线程。
AQS使用CAS机制对state进行原子操作从而对state的值进行修改。
如果state的值为0,表示锁未被持有,则将当前线程设置为工作线程(即获取到了锁),并将state的值设置为1,返回成功获取到锁。
如果未能成功获取到锁,AQS先自旋获取锁,如果一直获取不到,则将当前获取不到锁的线程加入到FIFO队列中
|
1
2
3
4
5
6
7
8
9
10
11
12
|
构成:
AQS使用一个volatile成员变量state来表示锁是否已被持有(0/1),通过内置的FIFO队列存储被阻塞的线程
怎么改state:
AQS使用CAS机制对state进行原子操作从而对state的值进行修改
怎么取到锁:
如果state的值为0,表示锁未被持有,则将当前线程设置为工作线程(即获取到了锁),并将state的值设置为1,返回成功获取到锁。
取不到怎么办:
如果未能成功获取到锁,AQS先自旋获取锁
如果一直获取不到,则将当前获取不到锁的线程加入到FIFO队列中
|
注:FIFO公平锁 双向队列:非公平锁
自旋锁
1
2
3
4
5
6
7
8
|
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
这样获取锁的线程一直处于活跃状态
但是并没有执行任何有效的任务
长时间使用这种锁会造成系统负载很大,耗费性能,阻止了其他线程的运行和调度
自旋的线程保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。
|
- 对于ReentrantLock来说,自旋的过程就是不断尝试 调用lock()的过程(使用CAS的方式修改state值)
- 对于原子操作类来说,自旋的过程就是比较和交换失败后再次尝试比较和交换的过程(CAS)
自旋锁优点
1
2
3
4
5
|
1.自旋锁尽可能的减少线程的阻塞
这对于锁的竞争不激烈,且占用锁时间非常短的代码块 来说性能能大幅度的提升
因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗
2.不使用自旋,并且获取不到锁的时候直接进入阻塞状态
|
自旋锁缺点
1
2
3
4
5
6
7
|
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块
这种情况不适合使用自旋锁
因为自旋锁在获取锁前一直都是占用 cpu 做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。
所以这种情况下我们要关闭自旋锁。
|
synchronized锁升级过程
-
无锁状态:没有线程来申请锁
-
偏向锁状态:只有一个线程来申请锁,【没有竞争】,不需要做加锁、解锁的操作
- 从无锁状态到当前状态,每次来申请锁的都是这同一个线程,中间没有别的线程来申请过
-
轻量级锁状态:只有一个线程来申请锁,【没有竞争】,不需要做加锁、解锁的操作
- 从上一个状态到现在状态,锁对象被不同线程申请过,只不过每次都是只有一个线程来申请
-
重量级锁状态:同时有多个线程来申请锁,【有竞争】,需要做加锁、解锁的操作