目录
一、等待唤醒机制
1.生产者消费者
等待唤醒机制可以简单的理解为下图。厨师相当于生产者,吃货相当于消费者。当桌子(缓冲区)上没有食物时,吃货等待,厨师抢夺到CPU使用权生产食物,然后将食物放到桌子上,再唤醒吃货。因为此时桌子上有食物,所以厨师等待,吃货拿到CPU使用权开始吃,吃完之后唤醒厨师继续做,自己等待。
代码实现如下:
java
public class Desk {
/*
控制生产者和消费者的执行
*/
//判断桌子上是否有食物
public static int foodFlag = 0;
//消费者能吃的食物总数
public static int count = 10;
//生产这能做的面条总数
public static int noodle = 10;
//锁对象
public static final Object obj = new Object();
}
java
public class Foodie extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.obj) {
if (Desk.count == 0) {
break;
}else {
//先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
//如果没有就等待
try {
Desk.obj.wait(); //当前线程跟锁进行绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Desk.count--;
System.out.println("吃货在吃面条, 还能再吃" + Desk.count + "碗面条");
Desk.obj.notifyAll();//唤醒所有绑定在这个锁对象上的线程
Desk.foodFlag = 0;
}
}
}
}
}
java
public class Cook extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.obj) {
if (Desk.noodle == 0) {
break;
}else {
if (Desk.foodFlag == 1) {
try {
Desk.obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Desk.noodle--;
System.out.println("厨师正在做面条,还能再做" + Desk.noodle +"碗面条");
Desk.obj.notifyAll();
Desk.foodFlag = 1;
}
}
}
}
}
}
java
public class WNDemo1 {
public static void main(String[] args) {
/*
* 需求:完成生产者和消费者(等待唤醒机制)的代码
* 完成线程轮流交替执行的效果
*/
Foodie f = new Foodie();
Cook c = new Cook();
f.start();
c.start();
}
}
运行结果的部分截图:

2.阻塞队列
阻塞队列可以理解为消费者和生产者之间的管道,生产者往队列中放数据,消费者从队列中拿数据。


代码如下:
java
public class Cook extends Thread{
private ArrayBlockingQueue<String> queue;
//测试类中定义阻塞队列,然后传给生产者和消费者进程,保证两个进程用的是同一个队列。
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//不断的往阻塞队列中华放面条
try {
//put方法的底层就已经给线程上锁
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
java
public class Foodie extends Thread{
private ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//从阻塞队列中取出数据
try {
queue.take();
System.out.println("吃货吃了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
java
public class WNDemo2 {
public static void main(String[] args) throws InterruptedException {
/*
利用阻塞队列完成生产者和消费者
细节:
生产者和消费者必须使用同一个阻塞队列
*/
//创建阻塞队列的对象
//参数传递的是阻塞队列的大小
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
c.start();
f.start();
}
}
可以发现运行结果中,吃货和厨师的打印语句重复打印了。是因为take和put方法底层就已经给线程上锁了,但是打印语句并没有在锁的内部,所以导致打印语句重复打印。
3.线程的状态
线程有七种状态,如下图所示:

但是要注意在JVM(Java虚拟机)中是没有定义运行态的,因为当一个线程抢到CPU的使用权之后,JVM就会把当前的线程交给操作系统去管理,JVM就不管了,所以就没有定义运行态。
所以Java中的线程就只有以下六种状态:

二、线程池
1.理解与使用
这里先来讲个小故事,假设A同学中午要吃饭,但是他没有碗,然后他就去买了一个碗,吃完饭之后就把碗给砸了。到了晚上,又要吃饭,但是发现又没有碗,然后又去买了一个碗,吃完饭后又把碗给砸了。是不是既浪费时间又浪费资源,上述创建线程跟这个是差不多的,当我们用到线程的时候就创建,用完之后线程就消失,浪费了操作系统的资源。
因此,为了解决这个问题,就需要准备了容器用来存放线程,这个容器就叫线程池 。刚开始线程池里面是空的,当我们给线程池去提交一个任务的时候,线程池本身就会自动地去创建一个线程,拿着这个线程就去执行任务,执行完了就把线程还回线程池中。当第二次再提交一个任务的时候,就不需要创建新的线程了,拿着已经存在的线程去执行任务,执行完后再还回去。但是还有一种特殊情况,当线程1还在执行第一个任务的时候,提交了第二个任务,此时线程池里会再新建一个线程去执行任务2。
当线程池有容量限制时,情况就如下图所示,后面的任务只能等线程执行完前面的任务之后再去执行它们。

主要核心原理:

线程池代码实现:
利用Executors类创建一个线程池对象。
当向线程池中提交5个任务时,结果是什么样呢?
java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "-----" + i);
}
}
}
java
public class ThreadPool {
public static void main(String[] args) {
//1.获取线程池的对象
ExecutorService pool1 = Executors.newCachedThreadPool();
MyRunnable mr = new MyRunnable();
//2.提交线程池的任务
pool1.submit(mr);
pool1.submit(mr);
pool1.submit(mr);
pool1.submit(mr);
pool1.submit(mr);
}
}
我们会发现线程池中创建了5个线程,
线程复用演示如下:
java
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-----" + 1);
}
}
java
public class ThreadPool {
public static void main(String[] args) throws InterruptedException {
//1.获取线程池的对象
ExecutorService pool1 = Executors.newCachedThreadPool();
MyRunnable mr = new MyRunnable();
//2.提交线程池的任务
pool1.submit(mr);
//让main线程睡1s是要让任务执行完毕赶紧把线程还回去
Thread.sleep(1000);
pool1.submit(mr);
Thread.sleep(1000);
pool1.submit(mr);
Thread.sleep(1000);
pool1.submit(mr);
Thread.sleep(1000);
pool1.submit(mr);
}
}
发现5个任务都是线程1来执行的。
2.自定义线程池
这里还是要先讲一个小故事,A同学开了一家饭店,这个饭店服务员对顾客是一对一服务的,当顾客走了,这个服务员才能去服务别人。这个饭店里有6名服务员,3位正式员工,3位临时员工。当临时员工一段时间空闲下来,为了解决成本,老板就需要把临时员工给辞退,但是正式员工是不能辞退的。如果生意特别好,有许多人在门外排队,但是老板最多只允许10名顾客排队,其他的等明天再来。
上述的故事就跟自定义线程池中的参数是相似的。

当自定义线程池中有3条核心线程、3条临时线程时,有以下几种情况。
情况一(提交三个任务):
线程池就会创建三条线程去处理这三个任务。

情况二(提交五个任务):
线程池会先创建三个线程来处理任务,剩下的两个线程排队等待,直到有了空闲的线程才去执行剩下的任务。

情况三(提交8个任务):
线程池会先创建三个线程来处理任务,剩余的任务就会在后面排队,现在定义队伍的长度为3,所以只有三个任务(任务4 5 6)在队列中排队等待。这个时候,线程池才会创建临时线程去处理任务7和任务8。由此可见,先提交的任务不一定会先执行。
即核心线程没有空闲且队伍已经排满了,才会去创建临时线程。

情况四(提交10个任务):
此时,提交的任务数量太多,已经超过了核心线程数量 + 临时线程数量 + 队伍长度。线程池会先创建三个核心线程来执行任务1 2 3, 然后任务4 5 6去队列中等待,线程池再创建三个临时线程去执行任务7 8 9,任务10就会触发任务拒绝策略,直接舍弃不要。

任务拒绝策略有以下的几种,常用的是第一个:
代码实现如下:
java
public class ThreadPool2 {
public static void main(String[] args) {
/*
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:核心线程数量 不能小于0
参数二:最大线程数 不能小于等于0,最大数量>=核心线程数量
参数三:空闲线程最大存活时间 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务的拒绝策略 不能为null
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,
6,
60,
TimeUnit.SECONDS, //秒
new ArrayBlockingQueue<>(3), //任务队列就使用阻塞队列
Executors.defaultThreadFactory(), //线程池获取线程,这个方法在底层也是new了一个Thread
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略(AbortPolicy是一个静态内部类)
);
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
}
}

三、线程池额外知识
线程池设置多大合适呢??
首先要看电脑的最大并行数,有的电脑是4核8线程,有的电脑是8核16线程等等。线程数就是最大并行数。
CPU密集型指的是程序中运算步骤比较多,IO密集型指的是文件操作多的程序
