线程的等待和通知
Object类中的方法:
wait() | 让当前线程进入等待状态,直到被通知为止 |
wait(long) | 让当前线程进入等待状态,同时设置时间,一直到被通知为止或者时间结束 |
notify() | 随机通知一个等待线程(随机无顺序) |
notifyAll() | 通知所有的等待线程 |
需要注意的点:等待和通知方法必须是锁对象,否则会抛出IllegalMonitorStateException
例子:通过锁对象将线程等待,经过5秒通知该线程来执行
/**
* 通过锁对象将线程等待,经过5秒通知该线程来执行
*/
public class WaitDemo {
public synchronized void print() throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" +i);
if(i == 50){
//让当前线程等待
this.wait();
}
}
}
public synchronized void notifyTest(){
//让等待的线程执行
this.notifyAll();
}
public static void main(String[] args) {
WaitDemo waitDemo = new WaitDemo();
new Thread(()->{
try {
waitDemo.print();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitDemo.notifyTest();
}
}
wait()和sleep()的区别
调用对象不同 | 锁使用不同 | 唤醒机制不同 | |
wait() | 由锁对象调用 | 执行wait后 自动释放锁 | 执行wait后,可以被唤醒通知 |
sleep() | 由线程调用 | 执行sleep后 不会释放锁 | 执行sleep后,只能等待时间结束后,自动唤醒 |
生产者消费者模式 (一种设计模式,不属于GOF23)
生产者和消费者:
生产者:某些程序、进程、线程负责生产数据就属于生产者
消费者:某些程序、进程、线程负责使用数据就属于消费者
生产者和消费者之间的问题:
1.耦合性高,生产者和消费者联系紧密,不利于系统的维护和拓展
2.并发性低,同时能处理的请求量少
3.忙闲不均,生产者和消费者的速度不一致,带来系统资源的浪费
实现的过程:(就像包子铺,生产者把包子放入笼屉中,消费者不必与生产者密切联系,而是通过第三者进行操作)
1.通过添加缓冲区,设置上限
2.生产者生产出数据,放入缓存区,如果满了 则生产者进入等待,直到缓冲区有空的位置通知生产者生产;
3.消费者从缓冲区取数据进行消费,如果空了,消费者进入等待,直到缓冲区有数据再通知消费者消费。
解决问题:
1.解耦
2.提高并发性能
3.解决,忙闲不均
包子铺例子:
/**
* 包子铺
*/
public class BaoziShop {
/**
* 包子
*/
class Baozi{
private int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子--" + id;
}
}
//上限
public static final int MAX_COUNT = 100;
//缓冲区 存放数据
private List<Baozi> baozis = new ArrayList<>();
/**
* 做包子
*/
public synchronized void makeBaozi() throws InterruptedException {
//判断缓冲区是否满了
if(baozis.size() == MAX_COUNT){
System.out.printf("缓冲区满了,%s等待%n",Thread.currentThread().getName());
//让生产者线程等待
this.wait();
}else{
//通知生产者线程生产
this.notifyAll();
}
//创建包子
Baozi baozi = new Baozi(baozis.size() + 1);
System.out.println(Thread.currentThread().getName()+"做了"+baozi);
//保存到缓冲区
baozis.add(baozi);
}
/**
* 拿包子
*/
public synchronized void takeBaozi() throws InterruptedException {
//判断缓冲区是否空了
if(baozis.size() == 0){
System.out.printf("缓冲区空了,%s等待%n", Thread.currentThread().getName());
//让消费者等待
this.wait();
}else{
//通知消费者消费
this.notifyAll();
}
//获得第一个包子,并删除
if(baozis.size() > 0){
Baozi baozi = baozis.remove(0);
System.out.println(Thread.currentThread().getName()+"吃了"+baozi);
}
}
public static void main(String[] args) {
BaoziShop baoziShop = new BaoziShop();
//一个生产者
Thread productor = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
baoziShop.makeBaozi();
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
productor.start();
//10个消费者吃10个包子
for (int i = 0; i < 10; i++) {
Thread consumer = new Thread(() ->{
try {
for (int j = 0; j < 10; j++) {
baoziShop.takeBaozi();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
}
}
}
阻塞队列
应用了生产者消费者模式的集合,能够根据满或者空的情况,自动对线程执行等待和通知
BlockingQueue接口:
put添加数据,达到上限会自动让线程等待
take取并删除数据,数据空了会自动让线程等待
实现类:
ArrayBlockingQueue 类 数据结构为数组
LinkedBlockingQueue类 链表结构
线程池:
作用:
线程是一种宝贵的系统资源,执行完任务后会死亡,如果有大量任务需要处理,需要频繁的创建和销毁线程,造成系统性能降低。
线程池会保存一定量的线程,线程执行完任务后,会回到线程池中,等待下一个任务,节省系统资源,提升性能。
线程池的使用: (顶层接口:Executor)
executor(Runnable) 启动线程执行一个任务
ExecutorService(用于创建线程池的工具类 )
主要的方法:
方法名 | 说明 |
---|---|
newCachedThreadPool() | 创建长度不限的线程池 |
newFixedThreadPool(int ) | 创建固定长度的线程池 |
newSingleThreadExecutor() | 创建单一个数的线程池 |
newScheduledThreadPool(int) | 创建可以调度的线程池 |
线程池的优化配置
线程池的实现类(ThreadPoolExecutor)
线程池的构造方法参数:
corePoolSize 核心线程数,创建线程池后自带线程,不会进行销毁
maximumPoolSize 最大线程数
keepAliveTime 存活时间,非核心线程能够闲置的时间,超过后被销毁
timeUnit 时间单位
blockingQueue 阻塞队列 存放任务(Runnable)的集合
优化配置
-
核心线程数 应该和CPU内核数量相关 CPU内核数 * N (N和任务执行需要时间和并发量相关)
Runtime.getRuntime().availableProcessors()
-
最大线程数可以和核心线程数一样,避免频繁创建和销毁线程
-
如果存在非核心线程,设置大一点,避免频繁创建和销毁线程
-
阻塞队列使用LinkedBlockingQueue,插入和删除任务效率更高
线程池的实现原理
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。