在介绍线程之前,我们需要先了解进程的概念:
线程和进程的概念:
进程:
CPU从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程
图中的QQ就是一个进程
线程:
线程:是进程中最小的执行单元
线程作用:负责当前进程中程序的运行.一个进程中至少有一个线程,一个进程还可以有多个线程,这样的应用程序就称之为多线程程序
将360比作进程,那360的杀毒,广告,解压等等功能就是线程,这么理解就差不多了
并发和并行:
并行:在同一个时刻,有多个执行在多个CPU上(同时)执行(好比是多个人做不同的事儿)
比如:多个厨师在炒多个菜
并发:在同一个时刻,有多个指令在单个CPU上(交替)执行
比如:一个厨师在炒多个菜
注意点:
1.之前CPU是单核,但是在执行多个程序的时候好像是在同时执行,原因是CPU在多个线程之间做高速切换
2.现在咱们的CPU都是多核多线程的了,比如2核4线程,那么CPU可以同时运行4个线程,此时不同切换,但是如果多了,CPU就要切换了,所以现在CPU在执行程序的时候并发和并行都存在
CPU调度
1.分时调度:值的是让所有的线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片
2.抢占式调度:多个线程轮流抢占CPU使用权,哪个线程先抢到了,哪个线程先执行,一般都是优先级高的先抢到CPU使用权的几率大,我们java程序就是抢占式调度
主线程:
主线程:CPU和内存之间开辟的转门为main方法服务的线程
当你创建了main方法,jvm会自动给这个main方法创建一个进程
创建线程的方式:
一:extends Thread
1.定义一个类,继承Thread (Thread实现了Runable接口,并重写了run方法)
2.重写run方法,在run方法中设置线程任务(所谓的线程任务指的是此线程要干的具体的事儿,具体执行的代码)
3.实例化对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
代码:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread...执行了"+i);
}
}
}
public class Test01 {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
//调用start方法,开启线程,jvm自动调用run方法
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程..........执行了"+i);
}
}
}
运行原理:
我们要先知道方法运行是在栈里面运行
方法从main方法中开始运行,main方法先进入栈内存,创建实例化对象
实例化对象再开启线程
实例化对象开启线程之后,内存会重新开一个栈空间取运行这个线程的方法(也就是run方法)
Thread类中的方法:
void start() -> 开启线程,jvm自动调用run方法
void run() -> 设置线程任务,这个run方法是Thread重写的接口Runnable中的run方法
String getName() -> 获取线程名字
void setName(String name) -> 给线程设置名字
static Thread currentThread() -> 获取正在执行的线程对象(此方法在哪个线程中使用,获取的就是哪个线程对象)
static void sleep(long millis)->线程睡眠,超时后自动醒来继续执行,传递的是毫秒值
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//线程睡眠
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"...执行了"+i);
}
}
}
public class Test01 {
public static void main(String[] args) throws InterruptedException {
//创建线程对象
MyThread t1 = new MyThread();
//给线程设置名字
t1.setName("金莲");
//调用start方法,开启线程,jvm自动调用run方法
t1.start();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName()+"线程..........执行了"+i);
}
}
}
这里说两个注意点就行了
1:Thread currentThread(),这个currentThread是Thread提供的静态方法,可以用来直接获取当前进程
2:是关于为啥在重写的run方法中有异常只能try,不能throws
其实这个和进程这一块没有什么关系,其实是异常的知识Java异常-优快云博客
原因:继承的Thread中的run方法没有抛异常,所以在子类中重写完run方法之后就不能抛,只能try
二:实现Runnable接口:
1.创建类,实现Runnable接口
2.重写run方法,设置线程任务
3.利用Thread类的构造方法:Thread(Runnable target),创建Thread对象(线程对象),将自定义的类当参数传递到Thread构造中 -> 这一步是让我们自己定义的类成为一个真正的线程类对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
具体代码:
public class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(MyThread.currentThread().getName()+"方法:"+i);
}
}
}
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread2 myThread2 = new MyThread2();
Thread myThread = new Thread(myThread2);
myThread.start();
}
}
通过匿名类的形式创建线程:
为什么把这个匿名类创建线程放在这个Runnable接口下呢
其实本质就是上面的步骤,知识用匿名类来简化创建:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(MyThread.currentThread().getName()+"方法:"+i);
}
}
},"用匿名类创建线程").start();
}
}
三:实现Callable接口:
Callable本质也就是一个接口,类似于Runnable
接口里面就一个方法:call() -> 设置线程任务的,类似于run方法
那call方法和run方法的区别:
我们先上一张图片:
从图片中也可得知:
- call方法有返回值,而且有异常可以throws
- run方法没有返回值,而且有异常不可以throws
使用方法:
获取call方法返回值: FutureTask<V>
a. FutureTask<V> 实现了一个接口: Future <V>
b. FutureTask<V>中有一个方法:
V get() -> 获取call方法的返回值
代码:
public class MyCallAble implements Callable<String> {
@Override
public String call() throws Exception {
return "啊吧啊吧";
}
}
public class Test01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallAble myCallAble = new MyCallAble();
FutureTask futureTask = new FutureTask(myCallAble);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
整体代码逻辑:
首先我们创建了一个类:MyCallable 实现了 Callable这个接口,重写了call方法
接着我们在测试类中创建了一个FutureTask的实例化对象,用来接收call方法的返回值
然后我们需要把这个对象变成线程对象,
这里稍微说一下,Thread的构造方法有Runnable的对象,我们点进去FutureTask的源码,发现FutureTask实现了一个RunnableFuture<V>,然后RunnableFuture<V>又实现了Runnable
所以这里就可以直接把FutureTask当成参数传给Thread
最后就是开启线程,接收返回值。
线程安全:
我们设想一个场景:
就是买票,10086上有一张票,A先看到了,下单了不过还没有付款,可能在转钱,这个时候B也看到了这张票,也下单了。
这下就出问题了,这个位置就会被两个人同时买。
原因就是因为,A在买票的时候没有给这个票上锁,导致B也可以访问。
不安全代码:
public class Test01 {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket, "赵四");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "广坤");
t1.start();
t2.start();
t3.start();
}
}
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
解决线程安全问题的方式(使用synchronized)
上面的案例中也分析过了,造成这种结果不唯一的问题是什么:就是A买票的时候没有锁,没有把那张票锁住,如果锁住了,B就看不到这张票了。
所以要想起到锁的功能:
synchronized:
第一种写法:同步代码块
1.格式:
synchronized(任意对象){
线程可能出现不安全的代码
}
2.任意对象:就是我们的锁对象
3.执行:
一个线程拿到锁之后,会进入到同步代码块中执行,在此期间,其他线程拿不到锁,就进不去同步代码块,需要在同步代码块外面等待排队,需要等着执行的线程执行完毕,出了同步代码块,相当于释放锁了,等待的线程才能抢到锁,才能进入到同步代码块中执行
下面看修改过后的代码:
package a_Thread;
public class MyTicket implements Runnable{
int ticket = 100;
Object object = new Object();
@Override
public void run() {
while (ticket>0){
synchronized (object){
System.out.println(Thread.currentThread().getName()+"买了"+ticket+"张票");
ticket --;
}
}
}
}
整体的代码逻辑就是:
创建一个锁对象object,然后将线程可能出现不安全的代码放到synchronized中
这样输出的结果:
a买了10张票
a买了9张票
a买了8张票
a买了7张票
a买了6张票
a买了5张票
a买了4张票
a买了3张票
a买了2张票
a买了1张票
c买了0张票
b买了-1张票
从这个结果中我就可以发现确实没有同一张票被两个人买走
不过我们仔细观察这个结果,会发现有的人竟然买到负数票和0号票
这显然也是不对的
问题原因:
这是因为在多线程环境下,即使你在 while
循环中检查了 ticket
变量的值,但是在线程切换的过程中,其他线程可能已经修改了 ticket
的值,导致当前线程在获取到锁后,实际上 ticket
的值已经不再满足 ticket>0
的条件。这种情况下,线程可能会继续执行,导致买到负数张票的情况。
解决办法:
public class MyTicket implements Runnable{
int ticket = 10;
Object object = new Object();
@Override
public void run() {
while (ticket > 0) {
synchronized (object) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买了" + ticket + "张票");
ticket--;
}
}
}
}
}
在while循环内部再加一层检测
最后还有一个注意点就是锁:那个object对象
我们也可以设想,就是一个票肯定只能有一把锁,如果有两把锁,那就和没锁是一样的了,其它线程还是能从另一个锁访问到
这里的object对象就是这个锁,保证这个对象唯一就行。
第二种写法:同步方法:
非静态方法:
1.格式:
修饰符 synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
2.默认锁:this(这里的例子就是下面的Runnable对象)
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//method01();
method02();
}
}
/* public synchronized void method01(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}*/
public void method02(){
synchronized(this){
System.out.println(this+"..........");
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
public class Test01 {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
System.out.println(myTicket);
Thread t1 = new Thread(myTicket, "赵四");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "广坤");
t1.start();
t2.start();
t3.start();
}
}
静态方法:
1.格式:
修饰符 static synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
2.默认锁:class对象
public class MyTicket implements Runnable{
//定义100张票
static int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//method01();
method02();
}
}
/*public static synchronized void method01(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}*/
public static void method02(){
synchronized(MyTicket.class){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
public class Test01 {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket, "赵四");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "广坤");
t1.start();
t2.start();
t3.start();
}
}
Lock锁:
Lock锁:顾名思义,就是一个锁
1.概述:Lock是一个接口
2.实现类:ReentrantLock
3.方法:
lock() 获取锁
unlock() 释放锁
我们对上面这个买票案例进行一下改造:
测试类不变:
public class MyTicket implements Runnable {
//定义100张票
int ticket = 100;
//创建Lock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
//获取锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
ticket--;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放锁
lock.unlock();
}
}
}
}
整体得代码逻辑:之前是通过 synchronized关键字来解决线程安全
现在换成了lock锁,首先lock是一个接口,接口时不能直接创建对象得,我们需要去new它得实现类ReentrantLock,调用方法获取一个锁,然后{同步代码块},最后用一个fianlly来释放锁,不过这个代码是否抛了异常,都把这个锁释放掉。
死锁:
什么是死锁:
死锁是指在多线程或多进程的程序中,各个线程或进程因竞争资源而陷入相互等待的状态,导致它们无法继续执行下去,这种情况被称为死锁。在死锁状态下,没有任何一个线程或进程能够继续执行,它们都在等待其他线程或进程释放资源,而形成了一个闭环。
再举两个例子把
第一个例子很简单
有两个房间,A房间需要先拿到锁1,再拿到锁2才能进去,B房间相反,然后有两个人,a要去A房间,b要去B房间
这个时候就会出现一个现象,就是a拿到锁1,在等b给我锁2,然后b拿到了锁2,在等a给锁1
这样就造成无限等待状态
第二个例子:
第二个例子纯属自己想写
就是操作系统书上的哲学家吃空心面的例子
这个是什么呢?
就是五个哲学家坐在一个圆桌,中间摆着一盘面,然后每个人的左手边(右手边也行)都有一个叉子,规定:哲学家必须拿到两个叉子才能开始吃(这规定也蛮离谱)
然后和死锁有什么关系呢?
就是设想一个场景,每个人都同时拿起自己左手边的叉子,然后每个人都在等另一边的叉子,这样每个人都在等,就造成了死锁。
在程序中的死锁将出现在同步代码块的嵌套中
来一段代码展示一下把:
public class LockA {
public static LockA lockA = new LockA();
}
public class LockA {
public static LockA lockA = new LockA();
}
public class DieLock implements Runnable{
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag){
synchronized (LockA.lockA){
System.out.println("if...lockA");
synchronized (LockB.lockB){
System.out.println("if...lockB");
}
}
}else{
synchronized (LockB.lockB){
System.out.println("else...lockB");
synchronized (LockA.lockA){
System.out.println("else...lockA");
}
}
}
}
}
public class Test01 {
public static void main(String[] args) {
DieLock dieLock1 = new DieLock(true);
DieLock dieLock2 = new DieLock(false);
new Thread(dieLock1).start();
new Thread(dieLock2).start();
}
}
上面的代码逻辑就是:
先定义了两把锁:A和B
然后定义了一个死锁类实现Runnable,并且重写了run方法
在run方法中定义了两段代码块(就是上面说的两个房间),并且呢用了一个flag,来控制
为什么要控制呢,很好理解,如果不控制,这个线程直接顺序执行下来,就不好卡住
控制一下,在主函数中创建两个死锁类,一个值设置为true,另一个false,这样两个线程就分别执行两个代码段(相当于有两个人分别进两个房间)
结果:
大概率都是这种情况,也可以出现特殊情况,就是有个人拿得很快,两把锁都拿到了,那这样就锁不住了。
线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或者调用过时方法stop() |
注意点:
1:sleep(time)和wait(time)有什么区别:
sleep(time)
方法可以直接在任何线程中调用,而wait(time)
方法需要在同步块中调用,sleep(time)
方法会使当前线程暂停执行指定的时间(以毫秒为单位),在这段时间内线程不会释放锁。而wait(time)
方法会使当前线程进入等待状态,并释放对象的锁
2:wait和notify:
notify()
方法用于唤醒在对象上等待的单个线程(随机唤醒) notifyall方法叫醒所有等等线程- 两个方法都只能在同步代码块中实现
- 都必须被锁对象调用
举个例子:
public class WaitNotifyExample {
public static void main(String[] args) {
final Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1: Waiting for lock...");
try {
lock.wait(); // 等待lock对象
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Resumed.");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2: Acquired lock.");
System.out.println("Thread 2: Calling notify.");
lock.notify(); // 唤醒在lock对象上等待的一个线程
System.out.println("Thread 2: Completed notify.");
}
});
thread1.start();
thread2.start();
}
}
这里得锁对象就是那个lock
创建了两个线程 thread1
和 thread2
,它们共享同一个对象 lock
。thread1
在同步块中调用了 lock.wait()
方法进入等待状态,而 thread2
在同步块中先获取了 lock
对象的锁,然后调用了 lock.notify()
方法来唤醒正在等待的 thread1
。当 thread1
被唤醒后,会继续执行等待状态之后的代码
生产者消费者案例分析:
这个等待唤醒案例就以操作系统中最经典的生产者消费者问题来模拟:
要求:一个线程生产,一个线程消费,不能连续生产,不能连续消费 -> 等待唤醒机制(生产者,消费者)(线程之间的通信)
下面直接上代码:
得提前解释一下,下面这段代码的要求是不能连续生产,不能连续消费
包子铺:(也可以单纯的理解为缓冲区)
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
/*
getCount 改造成消费包子方法
直接输出count
*/
public void getCount() {
System.out.println("消费了..............第"+count+"个包子");
}
/*
setCount 改造成生产包子
count++
*/
public void setCount() {
count++;
System.out.println("生产了...第"+count+"个包子");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
生产者线程:
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
//1.判断flag是否为true,如果是true,证明有包子,生产线程等待
if (baoZiPu.isFlag()==true){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为false,证明没有包子,开始生产
baoZiPu.setCount();
//3.改变flag状态,为true,证明生产完了,有包子了
baoZiPu.setFlag(true);
//4.唤醒消费线程
baoZiPu.notify();
}
}
}
}
消费者线程:
public class Consumer implements Runnable{
private BaoZiPu baoZiPu;
public Consumer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
//1.判断flag是否为false,如果是false,证明没有包子,消费线程等待
if (baoZiPu.isFlag()==false){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为true,证明有包子,开始消费
baoZiPu.getCount();
//3.改变flag状态,为false,证明消费完了,没 有包子了
baoZiPu.setFlag(false);
//4.唤醒生产线程
baoZiPu.notify();
}
}
}
}
测试类:
主要用来开启线程和统一锁对象
public class Test01 {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
Thread t1 = new Thread(product);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
整体的代码逻辑:
首先我们有一个包子铺(缓冲区),生产者可以往里面放东西,消费者可以往里面取东西
缓冲区的两个变量:count:用来输出包子数量 flag:判断生产者是否可以放/消费者是否可以取
因为我们代码要求:不能连续放,也不能连续取,所以根据这个逻辑,我们其实很好理解
我们每次放的时候,把这个flag开一下(flag设置为true)
每次取得时候,把这个flag关一下(flag设置为false)
每次放包子得时候,看一下flag是否可以放,可以放,我们生产一个放进去,然后count++,设置flag,并且叫醒消费者线程
每次取包子得逻辑和放包子相同。
注意点:
我们需要在测试类中统一锁对象,这句话什么意思呢
我们知道synchronized后面+锁对象
如果我们得锁对象不同,那这段代码就无意义
所以我们可以在测试类中创建一个锁对象,并且利用消费者和生产者得构造方法,把这个锁对象传进去。
下面这个代码是用信号量与PV操作所改造得代码:
import java.util.concurrent.Semaphore;
class Buffer {
Semaphore empty = new Semaphore(1);
Semaphore full = new Semaphore(0);
Semaphore mutex = new Semaphore(1);
int product;
void produce() {
while (true) {
try {
empty.acquire();
mutex.acquire();
product++;
System.out.println("Producing product: " + product);
mutex.release();
full.release();
Thread.sleep(1000); // 生产一个产品需要的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void consume() {
while (true) {
try {
full.acquire();
mutex.acquire();
System.out.println("Consuming product: " + product);
product--;
mutex.release();
empty.release();
Thread.sleep(1500); // 消费一个产品需要的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producerThread = new Thread(() -> {
buffer.produce();
});
Thread consumerThread = new Thread(() -> {
buffer.consume();
});
producerThread.start();
consumerThread.start();
}
}
在上面的代码中,我们定义了一个Buffer
类来管理缓冲区,使用Semaphore
来控制空闲缓冲区和产品数量,并使用互斥信号量mutex
来对共享资源进行操作。
acquire()相当于P操作
release()相当于V操作
多个生产者和多个消费者案例分析:
public class Test01 {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
new Thread(product).start();
new Thread(product).start();
new Thread(product).start();
new Thread(consumer).start();
new Thread(consumer).start();
new Thread(consumer).start();
}
}
如果当我们得代码改成这样,有多个生产和消费线程
就会发生连续生产,连续消费得情况
原因:
多消费者和多生产者为什么原来得代码不行:
因为notify是随机唤醒得,不一定唤醒得是哪一个(有可以是消费者线程又唤醒了一个消费者线程)
解决办法:
1(第一步):将:if (this.flag==true) 替换成: while (this.flag == false)
解释:首先一开始三个生产者线程同时进入if后面得{}代码块中,三个一起进去,肯定只有一个抢到了锁,另外两个都wait了
这个时候,抢到锁得哪一个,就取生产了,并且叫醒了一个线程,假如叫醒得线程是生产者线程(因为如果是消费者线程,那就不会出问题),另外两个线程都在wait,突然有一个被唤醒了,
那这个被唤醒得线程会怎么样?会跳出if判断,
然后继续生产,这样就违背了连续生产,连续消费得规则,
所以,我们可以将if换成while,这样就算你被唤醒了,你得flag还是false,这样你就出不去,还得等
2(第二步):将:this.notify(); 替换成:this.notifyAll();
为什么呢,这一步又是为什么呢
我们设想,刚刚那个生产者进程是出不去了,不过整个程序也卡住了,没有人再去唤醒其它进程了,整个程序就陷入了一个死锁问题
所以,第一个抢到锁得生产者可以唤醒所有得进程,这样就算第一个抢到锁得还是生产者,它还在循环了,消费者也被唤醒,这样消费者也能抢锁。
线程池:
线程池是一种管理和复用线程的机制,它可以在程序启动时创建一定数量的线程,并将它们保存在池中等待使用。当有任务需要执行时,可以从线程池中获取一个空闲的线程来执行任务,避免频繁创建和销毁线程,提高了线程的复用性和效率。
线程池的概念应该蛮好理解,就是线程池中有很多空闲的线程,当我们创建一个线程的时候,我们去调用一个空闲的线程,执行完任务之后归还即可,如果所有空闲线程都用完了,就等待。
主要介绍三个线程池的方法:
1:创建线程对象:工具类-> Executors
2:执行线程任务: ExecutorService中的submit方法
submit方法有返回值:Future接口:用于接收run方法或者call方法返回值的,但是run方法没有返回值,所以可以不用Future接收,执行call方法需要用Future接收
Future中有一个方法:V get() 用于获取call方法返回值
3:关闭线程:ExecutorService中的void shutdown() 方法
代码实例:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "abab";
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行任务");
}
}
public class Test01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//1:Runnable方式:
executorService.submit(new MyRunnable());
//2:Callable方式:
Future<String> future = executorService.submit(new MyCallable());
System.out.println(future.get());
// executorService.submit(new MyCallable());
// executorService.submit(new MyCallable());
executorService.shutdown();
}
}
这一块整体的逻辑很简单,就是两个类分别实现Runnable和Callable,然后通过executorService.submit方法来执行任务。