多线程
1.线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行.
一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), - Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。
2.同步与异步
- 同步:排队执行 , 效率低但是安全.
- 异步:同时执行 , 效率高但是数据不安全
3.并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
4.继承Thread类
-
Thread是Java提供的用于实现线程的类
-
有一个继承的方法run,run方法中的代码就是一条新的执行路径,路径的触发方式不是调用run方法,而是通过Thread对象的start来启动任务
- 常用方法
- 字段
- 常用方法
-
daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了
-
用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡
面试题:
如何将一个线程停止?
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过变量做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
4.1 Thread在程序中的使用
- 常规用法
新建一个类继承Thread
代码示例:
- 编写一个线程
class MyThread extends Thread{ //Thread,JAVA提供的用于实现线程的类
/**
* 线程线程要执行的任务方法
* 每个线程都有自己的栈空间,共用一份堆内存
*/
@Override
public void run() {
//这里的代码,就是一条新的执行路径
//这个执行路径的触发方式,不是调用run方法,而是通过Thread对象的start来启动任务
for(int i = 0;i < 5;i++){
System.out.println( i + "这是另一个线程");
}
}
- 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); //时间分配是抢占式分配
for(int i = 0;i < 5;i++){
System.out.println( i + "这是main中的线程");
}
}
- 由于Java的时间分配是抢占式时间分配,谁抢占到谁就先执行,因此得到如下输出结果
0这是另一个线程
0这是main中的线程
1这是另一个线程
1这是main中的线程
2这是另一个线程
2这是main中的线程
3这是另一个线程
4这是另一个线程
3这是main中的线程
4这是main中的线程
- 图解
使用匿名内部类
- 代码展示
public static void main(String[] args) {
//new Thread(){}.start();
new Thread(){ //匿名内部类,仅几行代码即可实现一个线程
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(i + "hahaha");
}
}
}.start();
for (int i = 0;i < 5;i++){
System.out.println(i + "heiheihei");
}
}//end main
- 运行结果
0hahaha
0heiheihei
1heiheihei
1hahaha
2heiheihei
3heiheihei
2hahaha
4heiheihei
3hahaha
4hahaha
5.Runnable接口
- 用于给线程执行的任务,但是还是要借助Thread
- 【创建一个任务对象,里面包含了任务→再创建一个线程,为其分配这个任务→start执行】
与前面继承Thread类相比,优势在于:
-
通过 创建任务→给线程分配的方式 来实现多线程,更适合多个线程同时执行相同任务的情况
-
可以避免单继承所带来的的局限性(java单继承,但是可以多实现)
-
任务与线程本身分离,提高了程序的健壮性
-
线程池技术,接收Runnable类型的任务,不接收Thread类型的线程
- 代码示例
/**
* 第二种实现多线程技术
* 实现Runnable接口
* 用于给线程执行的任务,但是还是要借助Thread
*/
class MyRunnable implements Runnable{
@Override
public void run() {
//线程任务
for(int i = 0;i < 5;i ++){
System.out.println( i + "这是另一个线程");
}
}
}
public static void main(String[] args) {
/**
* 第二种实现多线程技术
* 实现Runnable接口(用的更多)
*/
MyRunnable r = new MyRunnable();//创建一个任务对象,里面包含了任务
Thread t = new Thread(r);//创建一个线程,为其分配一个任务
t.start();//执行这个线程
for(int i = 0;i < 5;i++){
System.out.println( i + "这是main中的线程");
}
}
输出结果:
0这是另一个线程
1这是另一个线程
2这是另一个线程
3这是另一个线程
0这是main中的线程
4这是另一个线程
1这是main中的线程
2这是main中的线程
3这是main中的线程
4这是main中的线程
6.Callable接口
用的少 我不想写 用到再补
7.线程有关操作
7.1 设置和获取线程名称
- 代码示例
static class MyRunnable implements Runnable{
@Override
public void run() {
//currentThread():获取当前正在执行的对象
//getName获取线程名称
System.out.println(Thread.currentThread().getName());
}
}
//currentThread():获取当前正在执行的对象
System.out.println(Thread.currentThread().getName());//main线程
Thread t = new Thread(new MyRunnable());
t.setName("第0个线程");//使用setName设置线程名称
t.start();
new Thread(new MyRunnable(),"第1个线程").start();
new Thread(new MyRunnable(),"第2个线程").start();
new Thread(new MyRunnable(),"第3个线程").start();
new Thread(new MyRunnable()).start();//没有给线程setName,则系统会自动命名
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
main
第0个线程
第1个线程
第3个线程
Thread-1
第2个线程
Thread-3
Thread-2
7.2 线程休眠
- 常用方法
sleep为Thread的静态方法,因此可以用Thread直接调用:Thread.sleep()
1秒 = 1000毫秒
1毫秒 = 1000微妙 = 1000000纳秒
代码示例:
for (int i = 0;i < 5;i++){
System.out.println(i);
Thread.sleep(1000); //每次循环暂停1000毫秒后再继续执行
}
0
1
2
3
4
7.3 线程中断
-
一个线程是一个独立的执行路径,是否应该结束,应该由其自身决定
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时我们可以通过给对象做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
(代码里用interrupt打断 但是打断之后只是提示程序员是否终止 因为可以选择不中止 起到一个提示作用 如果要终止 依靠方法的 异常 方法终止 可以添加return)
代码示例: -
首先新建类实现Runnable接口,并继承run方法 新建线程
static class MyRunnable implements Runnable{
@Override
public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
for (int i =0;i < 10;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 编写main
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); //新建线程
t1.start();
for (int i =0;i < 5;i ++){//main线程
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//打标记之后程序进去catch
e.printStackTrace();
}
}
//main线程打印5次,t1线程打印10次,因此main线程先中断,此时对t1线程打标记
//给线程t1添加中断标记,但是只是告诉线程它可以死亡,但是未必死亡
t1.interrupt();
}
- 打标记处理:
对线程对象打标记,触发异常,使程序进入catch,后续的处理依旧由程序员决定
① 修改MyRunnable中的try-catch语句
发现中断标记后进入catch,但是程序可以选择不死亡,继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("发现了中断标记,但是不死亡");
}
- 输出结果
每个线程都隔1秒打印一个数,由于线程不死亡,因此发现标记之后继续执行
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,但是不死亡
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
- ② 修改MyRunnable中的try-catch语句
发现中断标记后进入catch,程序死亡,中断程序、释放资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;//表示线程结束,资源释放
}
- 输出结果:
每个线程都隔1秒打印一个数,由于发现中断标记后线程自杀死亡,因此发现标记之后结束程序
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,线程自杀
7.4 设置守护线程
线程分为守护线程和用户线程
用户线程:当一个进程不包含任何存活的用户线程时,进行结束(我们直接创建的线程都是用户线程)
守护线程:用于守护用户线程,当最后一个用户线程结束时,守护线程自动死亡
- 代码示例:
新建类实现Runnable接口,并继承run方法 新建线程
static class MyRunnable implements Runnable{
@Override
public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
for (int i =0;i < 10;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 编写main
在线程启动前,用setDaemon()来标记守护线程
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());//t1为子线程
t1.setDaemon(true);//设置t1为守护线程,在t1启动前设置
t1.start();//启动守护线程
//main主线程,当主线程结束时,守护线程也会结束
for (int i =0;i < 5;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
main:4
Thread-0:4
Thread-0:5
7.5 停止线程
以下文多线程通信问题中的生产者与消费者问题中的程序为例,我们让线程运行,但是我们只是打开了线程,并没有关闭线程,到最后程序运行完只能手动停止线程
Thread.currentThread().interrupt();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
8.线程安全
代码示例:
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(count > 0){
//卖票
System.out.println("正在准备卖票");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("出票成功!余票:" + count);
}
}//end
}
public static void main(String[] args) {
//线程不安全
Runnable runnable = new Ticket();
//启动三个线程
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1
通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了
出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同
这就是多线程完成统一任务时出现的线程不安全问题
8.1显式锁与隐式锁
- 所谓的显式和隐式,就是在使用的时候使用者是否需要手动写代码去获取锁和释放锁
- 隐式锁:隐式锁使用synchronized修饰符。在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁
- 显式锁:显式锁使用Lock关键字。在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
- 添加synchronized关键字的同步代码块和同步方法属于隐式锁
8.2隐式锁同步代码块
线程同步,使线程排队执行
实现思路:每个线程在执行时看同一把锁,谁抢到了锁,谁就执行
线程同步实现:synchronized
格式:
synchronized(锁对象){
// 同步代码块
}
锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
- 代码示例:
对原有的线程不安全的卖票示例进行修改
在while循环中加锁
同步代码块为:当余票大于0时,进行卖票操作
因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();//创建对象
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
}
- 由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁
而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,谁抢到锁谁就执行,排队执行
Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个
//启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个o
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
//如果上述写法写成如下,则依旧为不安全线程
//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张
//错误写法!!!!注意
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
- 加了锁之后的输出结果:
正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0
如果将创建锁的对象写在任务的代码块中,如下所示
此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!
错误写法:
public void run() {//每次被触发就进卖买票操作
Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
8.3 同步方法
-
与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符
-
同步方法的锁为this
同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class
代码示例:
/**
* 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
boolean flag = sale();//sale()为加了锁的方法
if(!flag){
break;
}
}//end while
}//end run
//添加synchronized修饰符,给方法加锁
public synchronized boolean sale(){
//this,同步的方法的锁
//Ticket.class,如果方法为静态方法,则同步方法的锁为类.class
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
return true;
}
return false;
}
}
- 如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行
如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行
public void run() {
synchronized (this){//再加一把锁
}
while(true){
boolean flag = sale();
if(!flag){
break;
}
}//end while
}//end run
- 如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行
8.4 显式锁
显式锁使用Lock关键字
在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制
显式锁Lock的子类:ReentrantLock
代码示例:
- 创建隐式锁
Lock l = new ReentrantLock();
- 在进行代码块前锁住
l.lock();
- 在代码块结束后开锁
l.unlock();//代码执行完毕,开锁
- 完整代码
public static void main(String[] args) {
//线程不安全
//解决方案3:显式锁Lock
//java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
Runnable runnable = new Ticket();//只有一个任务
//启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁l
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//创建显式锁l
private Lock l = new ReentrantLock();
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
l.lock();//进入if之前,锁住
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
l.unlock();//代码执行完毕,开锁
}//end while
}//end run
}
不论是显式锁还是隐式锁,都可以有效地控制多线程获取资源、解决所出现的线程不安全问题
8.5公平锁与非公平锁
-
公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁
-
非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁
实现公平锁: -
显式锁Lock的构造方法中,参数为True则表示公平锁
9. 线程死锁
- 死锁:多个线程线程互相持有对方所需要的资源,多个线程因竞争资源而造成的一种僵局(互相等待)
死锁举例
拿生活中的场景并结合代码,举一个简单的栗子:
- 挟持着人质的罪犯与警察两人僵持不下(警察抓着罪犯,而罪犯手上有人质)
罪犯对警察说:“你放了我,我放人质!”
然而警察听到后内心想:“我救人质,但是罪犯跑了”
警察对罪犯说:“你放了人质,我放过你!”
然而罪犯听到后内心想:“警察放过我,但是人质跑了”
根据这个场景,来进行代码的实现
- 罪犯Culprit
/**
* 罪犯
*/
static class Culprit{
//罪犯对警察说
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放人质!");
p.fun();
}
//听了警察的话,内心回应
public synchronized void fun(){
System.out.println("罪犯内心:警察放过我,但是人质跑了");
}
}
- 警察Police
/**
* 警察
*/
static class Police{
//警察对罪犯说
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放过你!");
c.fun();
}
//听了罪犯的话,警察回应
public synchronized void fun(){
System.out.println("警察内心:我救人质,但是罪犯跑了");
}
- 新建线程MyThread,警察对罪犯说
static class MyThread extends Thread{
private Culprit c;
private Police p;
//构造方法
public MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
/**
* 警察say方法执行完之后,调用罪犯的fun方法,等待罪犯回应
*/
p.say(c);//警察说话,让罪犯回应
}
}
- 新建主线程,罪犯对警察说
public static void main(String[] args) throws InterruptedException {
Culprit c = new Culprit();//新建一个罪犯对象
Police p = new Police();//新建一个警察对象
new MyThread(c,p).start();//新建线程:警察说话,让罪犯回应
/**
* 罪犯的say方法调用执行完后,调用警察的fun方法,等待警察回应
*/
c.say(p);//主线程:罪犯说话,让警察回应
}
此时,有两个线程,而这两个线程中,警察和罪犯都说完了自己的话(执行say),等待对方回应(执行fun),然而等待对方回应前先必须等待对方把话说完(执行say),但是不知道对方有没有先说完(有没有执行完say),因此卡住了,造成了死锁
死锁的结果输出:
罪犯和警察说完之后都在等待对方回应,从而造成了死锁,程序卡在那无法继续进行,只能手动结束程序
罪犯:你放了我,我放人质!
警察:你放了人质,我放过你!
罪犯:你放了我,我放人质!
警察内心:我救人质,但是罪犯跑了
警察:你放了人质,我放过你!
罪犯内心:警察释放我,但是人质跑了
死锁避免
- 线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
根源上解决: 在任何有可能产生锁的方法中,不调用另一个有可能产生锁的方法
10.多线程通信问题
- 多线程通信问题,也就是生产者与消费者问题
- 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全
以下为百度百科对于该问题的解释:
- 生产者与消费者问题:
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
解决办法:
- 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
-
厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。
厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)… -
在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全
-
定义厨师线程
/**
* 厨师,是一个线程
*/
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
//运行的线程,生成100道菜
@Override
public void run() {
for (int i = 0 ; i < 100; i ++){
if(i % 2 == 0){
f.setNameAneTaste("小米粥","没味道,不好吃");
}else{
f.setNameAneTaste("老北京鸡肉卷","甜辣味");
}
}
}
}
- 定义服务员线程
/**
* 服务员,是一个线程
*/
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i =0 ; i < 100;i ++){
//等待
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}//end run
}//end waiter
- 新建食物类
/**
* 食物,对象
*/
static class Food{
private String name;
private String taste;
public void setNameAneTaste(String name,String taste){
this.name = name;
//加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = taste
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}//end set
public void get(){
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
}
}//end food
- main方法中去调用两个线程
public static void main(String[] args) {
Food f = new Food();
Cook c = new Cook(f);
Waiter w = new Waiter(f);
c.start();//厨师线程
w.start();//服务生线程
}
运行结果:
只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系
...
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
name和taste对应错乱的原因:
当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。
两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象
以上运行结果解释如图:
加入线程安全
针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行
代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全
/**
* 食物,对象
*/
static class Food{
private String name;
private String taste;
public synchronized void setNameAneTaste(String name,String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}//end set
public synchronized void get(){
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
}
}//end food
输出结果:
由输出可见,又出现了新的问题:
虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)
...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
实现生产者与消费者问题
由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序
生产者与消费者问题:
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全
-
厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…
-
在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全
- 首先在Food类中加一个标记flag:
True表示厨师生产,服务员休眠
False表示服务员端菜,厨师休眠
private boolean flag = true;
- 对set方法进行修改
当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作
做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠
public synchronized void setNameAneTaste(String name,String taste){
if(flag){//当标记为true时,表示厨师可以生产,该方法才执行
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
this.notifyAll();//唤醒在当前this下休眠的所有进程
try {
this.wait();//此时厨师线程进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}//end set
- 对get方法进行修改
当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作
端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况
然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠
public synchronized void get(){
if(!flag){//厨师休眠的时候,服务员开始端菜
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况
this.notifyAll();//唤醒在当前this下休眠的所有进程
try {
this.wait();//此时服务员线程进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}// end if
}//end get
作了以上调整之后的程序输出:
我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的
...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
11.线程池Executors
- 池:容器的意思
使用一个线程通常要经过创建线程、创建任务、执行任务、关闭线程,在这个过程中,创建任务和执行任务的时间很少
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,浪费的时间多,因此o频繁创建线程o会大大降低系统的效率(频繁创建线程和销毁线程需要时间)
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源
- 作用
降低资源消耗
提高响应速度
提高线程的可管理性
分类
不论是哪一类,获取线程池的对象都是ExecutorService
1 缓存线程池
长度没有限制
- 创建缓存线程池:
.newCachedThreadPool()
//创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
- 向线程池中加入新的任务,指挥线程池执行新的任务(run):
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
//向线程池中加入新的任务,执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
- 输出结果:
由输出可知,三个线程名称为1,2,3
pool-1-thread-3任务执行
pool-1-thread-1任务执行
pool-1-thread-2任务执行
添加休眠时间,使程序休眠一段时间
Thread.sleep(1000);//停一秒之后,再去执行线程,此时缓存线程池中已有内容,执行缓存池中的内容
- 指挥线程池执行任务
此时缓存池中已有内容,再去执行任务时,执行缓存池中空闲的任务
//向线程池中加入任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
- 输出结果:
由输出结果可知,线程实现了重复使用,休眠后执行的任务是缓存池中已有的空闲任务3
pool-1-thread-1任务执行
pool-1-thread-3任务执行
pool-1-thread-2任务执行
pool-1-thread-3任务执行
2 定长线程池
相对于缓存线程池,长度有限制,线程池中的当前线程数目不会超过给定的长度
当该值为0的时候,意味着没有任何线程,线程池会终止
代码示例:
创建定长线程池,这里指定线程池大小为2
.newFixedThreadPool(参数),参数为线程池的长度
//创建定长线程池,指定了线程池的大小为2
ExecutorService service = Executors.newFixedThreadPool(2);
向线程池中加入新的任务,指挥线程池执行任务
如下面代码所示,添加3个任务
//向线程池中加入任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
输出结果:
由于线程池长度为2,因此最多两个任务,线程池中的当前线程数目不会超过2
pool-1-thread-2任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
3 单线程线程池
与定长线程池中传入参数为1的作用相同,即线程池中只有一个线程
- 创建单线程线程池
.newSingleThreadExecutor()
ExecutorService service = Executors.newSingleThreadExecutor();
- 向线程池中加入新的任务,指挥线程池执行任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
- 输出结果:
由输出可知,线程池中只有一个线程
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
4 周期性任务定长线程池
为定长线程池
把一个任务定时在某个时期执行,或者是周期性执行
- 任务在某个时期执行
创建单线程线程池
.newScheduledThreadPool(参数),参数为线程池的长度
//创建 周期性任务定长线程池
//任务创建出来的结果不一样
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
- 向线程池中加入新的任务,指挥线程池执行任务
.schedule(参数1,参数2,参数3)
参数1:定时执行的任务
参数2:表示时长的数字x(每隔x运行一次任务)
参数3:时长数字的时间单位,由TimeUnit的常量制定
/**
* 定时执行一次
* 参数1:定时执行的任务
* 参数2:表示时长的数字x(每隔x运行一次任务)
* 参数3:时长数字的时间单位,由TimeUnit的常量制定
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
},5, TimeUnit.SECONDS);//任务在5秒钟后执行
- 输出结果:
5秒钟后输出
pool-1-thread-1任务执行
周期性执行
- 创建单线程线程池
.newScheduledThreadPool(参数),参数为线程池的长度
//创建 周期性任务定长线程池
//任务创建出来的结果不一样
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
- 向线程池中加入新的任务,指挥线程池执行任务
.schedule(参数1,参数2,参数3,参数4)
参数1:定时执行的任务
参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
参数3:表示时长的数字x(每隔x运行一次任务)
参数4:时长数字的时间单位,由TimeUnit的常量制定
/**
* 周期性执行
* 参数1:任务
* 参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
* 参数3:表示时长的数字x(每隔x运行一次任务)
* 参数4:时长数字的时间单位,由TimeUnit的常量制定
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
},5,1,TimeUnit.SECONDS);//5秒后执行,每隔1秒执行一次
- 输出结果:
5秒钟后开始输出,之后每隔1秒输出一次,直到停止程序
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
- 无论是哪种线程池,使用完毕后必须手动关闭线程池,否则会一直在内存中存在