一、线程的等待和通知
并发编程中可以通过锁对象控制线程,如:让线程等待,通知线程执行。
注意:线程的等待和通知必须由锁对象来完成,否则出现线程状态异常:IllegalMonitorStateException
任何对象都可以作为锁,方法是 Object 类定义,等待和通知必须是同一个锁对象完成
synchronized的 等待和通知方法
方法名 | 说明 |
wait() weɪt | 让当前持有锁的线程等待,直到被锁的 notify 唤醒(会自动释放锁) |
wait(long) | 线程等待一定时间,到时候会自动唤醒 |
notify() nəʊtɪfaɪ | 随机选择一个等待的线程唤醒 |
notifyAll() | 唤醒所有等待该锁的线程 |
等待和通知线程案例
public class WaitNotifyDemo {
public synchronized static void testWait(){
try {
System.out.println(Thread.currentThread().getName() +"开始等待");
//让线程等待
WaitNotifyDemo.class.wait();
System.out.println(Thread.currentThread().getName() +"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// WaitNotifyDemo demo = new WaitNotifyDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
WaitNotifyDemo.testWait();
}).start();
}
//过5s主线程通知子线程执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知需要上上锁的代码内执行
synchronized (WaitNotifyDemo.class) {
WaitNotifyDemo.class.notifyAll();
}
}
}
线程交替输出: A线程循环输出A,B线程循环输出B,要求两个线程交替输出ABABAB.....
public class ThreadPrintDemo {
public static void main(String[] args) {
new Thread(() -> {
synchronized (ThreadPrintDemo.class) {
for (int i = 0; i < 10; i++) {
System.out.println("A");
try {
//通知对方线程执行
ThreadPrintDemo.class.notify();
//当前线程等待
ThreadPrintDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(() -> {
synchronized (ThreadPrintDemo.class) {
for (int i = 0; i < 10; i++) {
System.out.println("B");
try {
//通知对方线程执行
ThreadPrintDemo.class.notify();
//当前线程等待
ThreadPrintDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
try {
Thread.sleep(500);
synchronized (ThreadPrintDemo.class){
ThreadPrintDemo.class.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
二、生产者和消费者模式
生产者:是生成数据,消费者:是消费数据。
多线程环境下有的线程用于生产数据就属于生产者,有的线程用于使用数据就属于消费者,可能会出现问题:
- 耦合性高,生产者和消费者直接交互,相互影响,不利于代码维护
- 忙闲不均,生产者线程太快消费者来不及消费,数据过量造成资源浪费,返之消费者速度过快,消费者会一直等待。
实现方法:
- 需要定义一个缓冲区,缓冲区有一个临界值
- 生产者将数据存入缓冲区,缓冲区满了,就让生产者等待,通知消费者消费
- 消费者从缓冲区取出数据,缓冲区空了,就让消费者等待,通知生产者生产
/**
* 包子
*/
public class Baozi {
private long id;
public Baozi(long id) {
this.id = id;
}
@Override
public String toString() {
return "包子{" +
"id=" + id +
'}';
}
}
/**
* 包子铺
*/
public class BaoziShop {
//临界值
public static final int MAX_COUNT = 100;
//缓冲区
private List<Baozi> baozis = new ArrayList<>();
/**
* 做包子
*/
public synchronized void makeBaozi(){
//判断缓冲区是否满了
if(baozis.size() >= MAX_COUNT){
System.out.println("包子铺满了,师傅等待 " + Thread.currentThread().getName());
try {
//满了,生产者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//没有满,通知消费者来买
this.notifyAll();
}
//做包子放缓冲区
Baozi baozi = new Baozi(baozis.size());
baozis.add(baozi);
System.out.println("师傅" + Thread.currentThread().getName() + "做了" + baozi);
}
/**
* 卖包子
*/
public synchronized void sellBaozi(){
//判断缓冲区是否空了
if(baozis.size() == 0){
System.out.println("包子铺空了,消费者等待 " + Thread.currentThread().getName());
try {
//空了,消费者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//没有空,通知师傅来做
this.notifyAll();
}
//从缓冲区拿包子,卖给消费者
if(baozis.size() > 0){
Baozi baozi = baozis.remove(0);
System.out.println("消费者" + Thread.currentThread().getName() + "吃了" + baozi);
}
}
}
public class BaoziShopDemo {
public static void main(String[] args) {
BaoziShop shop = new BaoziShop();
//两个师傅分别做了50包子
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 50; j++) {
shop.makeBaozi();
}
}).start();
}
//10个消费者吃10个包子
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
shop.sellBaozi();
}
}).start();
}
}
}
三、JUC 工具类
java.util.concurrent 并发工具包
JUC下提供大量的并发开发工具类
常用的有:
- ReentrantLock 重入锁
- blockingqueue 阻塞队列
- CountDownLatch 类 倒数
- Semaphore 类 信号量
- CyclicBarrier 类 循环栅栏
-
ReentrantLock 的等待和通知
使用Condition接口 ,翻译为条件 kənˈdɪʃ(ə)n
创建方法
Condition condition = Lock对象.newCondition()
下面的方法必须包含在 lock 的 try-finally 中使用
ReentrantLock 方法
方法名 | 说明 |
await() | 让当前持有锁的线程等待,直到被锁的singal唤醒(自动释放锁) |
await(long) | 线程等待一定时间,到时候会自动唤醒 |
singal() | 随机选择一个等待的线程唤醒 |
singalAll() | 唤醒所有等待该锁的线程 |
ReentrantLock 用法
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AwaitNotifyDemo {
// 创建锁对象
private static Lock lock = new ReentrantLock();
// 获得条件对象
private static Condition condition = lock.newCondition();
public static void testAwait() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "开始等待");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
AwaitNotifyDemo.testAwait();
}).start();
}
try {
// 过5s主线程通知子线程执行
Thread.sleep(2000);
// 通知需要在上锁的代码内执行
lock.lock();
// 唤醒所有线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
blockingqueue 阻塞队列
BlockingQueue 是一系列特殊的集合,这种集合会有临界值,达到临界值后自动让线程等待,也会自动唤醒线程。 blɑːkɪŋ kjuː
常用实现类
ArrayBlockingQueue 数组结构的阻塞队列
LinkedBlockingQueue 链表结构的阻塞队列
.....
创建:
new ArrayBlockingQueue(临界值)
阻塞列队 方法
方法 | 说明 |
put(T) | 添加数组到末尾,达到临界值后自动阻塞线程,小于临界值后自动唤醒线程 |
T take() | 从头部删除一个数据,如果空了自动阻塞线程,非空后会自动唤醒线程 |
int Size() | 数据个数 |
阻塞列队 用法
/**
* 包子铺 阻塞队列版
*/
public class BaoziShop2 {
//临界值
public static final int MAX_COUNT = 100;
//阻塞队列
private BlockingQueue<Baozi> baozis = new ArrayBlockingQueue<>(MAX_COUNT);
/**
* 做包子
*/
public void makeBaozi(){
Baozi baozi = new Baozi(baozis.size());
try {
//做包子放缓冲区
baozis.put(baozi);
System.out.println("师傅" + Thread.currentThread().getName() + "做了" + baozi);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 卖包子
*/
public void sellBaozi(){
try {
Baozi baozi = baozis.take();
System.out.println("消费者" + Thread.currentThread().getName() + "吃了" + baozi);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CountDownLatch 类
kaʊnt daʊn lætʃ 倒数
作用:一个或多个线程等待其它线程工作执行完再执行自己的任务
创建:
new CountDownLatch(倒数次数)
CountDownLatch 方法:
方法 | 说明 |
await() | 让当前线程等待 |
countDown() | 倒数一次,次数-1,当次数为0,自动唤醒等待的线程 |
int getCount() | 获得当前倒数次数 |
CountDownLatch 用法
PS:CountDownLatch对象只能使用一次
/**
* 倒数案例
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
//创建倒数对象
CountDownLatch countDownLatch = new CountDownLatch(3);
//创建三个线程,分别倒数一次
Thread thread = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把饭吃了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//倒数一次
countDownLatch.countDown();
});
Thread thread2 = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把垃圾倒了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
Thread thread3 = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把女朋友哄了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
thread.start();
thread2.start();
thread3.start();
try {
System.out.println("等等,我去找我兄弟去!!!");
//阻塞当前线程
countDownLatch.await();
System.out.println("我兄弟都来了!!!上啊!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Semaphore 类
seməfɔːr
信号量,可以用于控制执行任务的数量,主要用于限流(限制并发量) 可以重复使用的
创建方法:
new Semaphore(信号量大小)
new Semaphore(信号量大小,是否公平锁)
Semaphore 方法
方法 | 说明 |
void accquired() eɪ siː siː kwaɪər | 请求信号量,信号量会减1,为0时就会阻塞 |
void release() rɪˈliːs | 释放信号量,信号量会加1,会唤醒一个阻塞的线程 |
Semaphore 限流案例:
/**
* 信号量案例
*/
public class SemaphoneDemo {
public static void main(String[] args) {
//创建信号量对象
Semaphore semaphore = new Semaphore(10);
//限制只有10个线程能执行
for (int i = 0; i < 100; i++) {
final int n = i;
new Thread(() -> {
//消耗一个信号量
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "消耗了信号量" + n);
}).start();
}
try {
Thread.sleep(3000);
//释放信号量
semaphore.release(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CyclicBarrier 类
ˈsaɪklɪk ˈbæriər
循环栏栅,让多个线程等待一个线程,线程准备好后一起执行,类似CountDownLatch,区别是:可以重复使用
创建方法:
// 创建栏栅对象
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("发令枪响了!~ !!!");
});
CyclicBarrier 案例
/**
* 循环栏栅
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
//创建栏栅对象
CyclicBarrier barrier = new CyclicBarrier(3,() -> {
System.out.println("发令枪响了!~!!");
});
// for (int j = 0; j < 2; j++) {
// //重置数量
// barrier.reset();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "准备好了!!");
try {
Thread.sleep(2000);
//开始等待, parties减1,到0就全部唤醒执行
barrier.await();
System.out.println(Thread.currentThread().getName() + "冲啊!!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
// }
}
}
四、AQS
AbstractQueuedSynchronizer 抽象队列同步器
大量并发包中的工具类使用了AQS,基于AQS实现工具类功能的开发
使用了AQS的类:
- CountDownLatch
- Semaphone
- ReentrantLock
AQS是一个抽象类,用于开发并发编程的类,包含:
1) 成员变量
/**
* The synchronization state.
*/
private volatile int state; //同步器状态
对于CountDownLatch来说,state就是倒数数量
对于Semaphore来说,就是信号量数
对于ReentrantLock,是上锁的状态
2) 双向链表
处于等待的线程队列
对于Semaphore和ReentrantLock来说,可以保存公平锁的等待顺序
自学任务:
ThreadLocal是什么?起什么作用?它的原理是什么?
线程局部变量,解决线程同步问题,将变量在每个线程 保存副本,每个线程使用自己的变量互不影响;每个线程内部又一个 map 叫 ThreadLockMap,ThreadLock 帮助线程将变量存放到自己的 map 中。
总结
需要掌握内容: