【Java基础教程】(四十三)多线程篇 · 下:深入剖析Java多线程编程:同步、死锁及经典案例——生产者与消费者,探究sleep()与wait()的差异

news2024/11/26 11:54:20

Java基础教程之多线程 · 下

  • 🔹本节学习目标
  • 1️⃣ 线程的同步与死锁
    • 1.1 同步问题的引出
    • 2.2 synchronized 同步操作
    • 2.3 死锁
  • 2️⃣ 多线程经典案例——生产者与消费者
    • 🔍分析sleep()和wait()的区别?
  • 🌾 总结

在这里插入图片描述

🔹本节学习目标

  • 理解多线程中的同步与死锁的概念;
  • 掌握Object 类中对于多线程的支持;

1️⃣ 线程的同步与死锁

程序利用线程可以进行更为高效的程序处理,如果在没有多线程的程序中, 一个程序在处理某些资源时会有主方法(主线程全部进行处理),但是这样的处理速度一定会比较慢,如下图 (a) 所示。而如果采用了多线程的处理机制,利用主线程创建出许多子线程(相当于多了许多帮手), 一起进行资源的操作,如下图 (b) 所示,那么执行效率一定会比只使用一个主线程更高。

图1 单线程与多线程的区别

在程序开发中,所有程序都是通过主方法执行的,而主方法本身就属于一个主线程, 所以通过主方法创建的新的线程对象都是子线程。在Android开发中,默认运行的 Activity 就可以理解为主线程,当移动设备需要读取网络信息时往往会启动新的子线程读取,而不会在主线程中操作。

利用子线程可以进行异步的操作处理,这样可以在不影响主线程运行的前提下进行其他操作,程序的执行速度不仅变快了,并且操作起来也不会产生太多的延迟。对于这部分知识,有些刚接触Java的朋友理解起来可能会有些困难,但随着开发经验提升,自己慢慢可以领会的更多。

虽然使用多线程同时处理资源效率要比单线程高许多,但是多个线程操作同一个资源时也一定会带来一些问题,如资源操作的完整性问题等等。

1.1 同步问题的引出

同步是多线程开发中的一个重要概念,既然有同步,就一定会存在不同步的操作。多个线程操作同一资源时就有可能出现不同步的问题,例如:现在产生 N 个线程对象进行卖票操作,为了更加明显地观察不同步所带来的问题,所以下面案例程序将使用线程的休眠操作。

//	范例 1: 观察非同步情况下的操作
package com.xiaoshan.demo;

class MyThread implements Runnable{
	private int ticket = 5;   	//一共有5张票
	
	@Override
	public void run(){
		for(int x=0; x<20; x++){
			if(this.ticket > 0){ 	//判断当前是否还有剩余票
				try{
					Thread.sleep(100);              //休眠1s, 模拟延迟
				}catch (InterruptedException e){
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "卖票,ticket="+ this.ticket--);
			}
		}
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt,"窗口A").start(); 	//启动多线程
		new Thread(mt,"窗口B").start(); 
		new Thread(mt,"窗口C").start(); 
		new Thread(mt,"窗口D").start();
	}
}

执行结果:

窗口A卖票,ticket=5
窗口B卖票,ticket=5 (错误的数据,因为不同步所引起)
窗口D卖票,ticket=4
窗口C卖票,ticket=3
窗口D卖票,ticket=2
窗口C卖票,ticket=0
窗口B卖票,ticket=-1 (错误的数据,因为不同步所引起)
窗口A卖票,ticket=1

此程序模拟了一个卖票程序的实现,其中将有4 个线程对象共同完成卖票的任务,为了保证每次在有剩余票数时实现卖票操作,在卖票前增加了一个判断条件 (if (this.ticket>0)), 满足此条件的线程对象才可以卖票,不过根据最终的结果却发现,这个判断条件的作用并不明显。

从上边范例的操作代码可以发现,对于票数的操作有如下步骤。
(1)判断票数是否大于0, 大于0 表示还有票可以卖;
(2)如果票数大于0, 则卖票出去。

但是,在上边范例的操作代码中,在第1步和第2步之间加入了延迟操作,那么一个线程就有可能在还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况,如下图所示。

图2 多线程操作同一资源未同步的问题

2.2 synchronized 同步操作

如果想解决上边范例程序的问题,就必须使用同步操作。所谓同步操作就是一个代码块中的多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成后才可以继续执行,如下图所示。

图3 多线程同步思想

在 Java 里面如果要想实现线程的同步,操作可以使用 synchronized 关键字。 synchronized 关键字可以通过以下两种方式进行使用。

  • 同步代码块:利用 synchronized 包装的代码块,但是需要指定同步对象,一般设置为 this
  • 同步方法:利用 synchronized 定义的方法。
//	范例 2: 观察同步块
package com.xiaoshan.demo;

class MyThread implements Runnable{
	private int ticket = 60;
	
	@Override
	public void run(){
		for (int x=0; x<20; x++){
			synchronized(this){		//定义同步代码块
				if(this.ticket>0){		//判断当前是否还有剩余票
					try{
						Thread.sleep(100);	//休眠1s, 模拟延迟
					}catch (InterruptedException e){
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--);
				}
			}
		}
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt,"窗口A").start(); 	//启动多线程
		new Thread(mt,"窗口B").start(); 
		new Thread(mt,"窗口C").start(); 
		new Thread(mt,"窗口D").start();
	}
}

程序执行结果:

窗口A卖票,ticket = 60
窗口A卖票,ticket = 59
窗口A卖票,ticket = 58
窗口A卖票,ticket = 57
窗口A卖票,ticket = 56
窗口A卖票,ticket = 55
窗口A卖票,ticket = 54
窗口A卖票,ticket = 53
窗口A卖票,ticket = 52
窗口A卖票,ticket = 51
窗口A卖票,ticket = 50
窗口A卖票,ticket = 49
窗口A卖票,ticket = 48
窗口A卖票,ticket = 47
窗口A卖票,ticket = 46
窗口A卖票,ticket = 45
窗口C卖票,ticket = 44
窗口C卖票,ticket = 43
窗口C卖票,ticket = 42
窗口C卖票,ticket = 41
窗口C卖票,ticket = 40
窗口C卖票,ticket = 39
窗口C卖票,ticket = 38
窗口C卖票,ticket = 37
窗口C卖票,ticket = 36
窗口C卖票,ticket = 35
窗口C卖票,ticket = 34
窗口C卖票,ticket = 33
窗口D卖票,ticket = 32
窗口D卖票,ticket = 31
窗口D卖票,ticket = 30
窗口D卖票,ticket = 29
窗口D卖票,ticket = 28
窗口D卖票,ticket = 27
窗口D卖票,ticket = 26
窗口D卖票,ticket = 25
窗口D卖票,ticket = 24
窗口D卖票,ticket = 23
窗口D卖票,ticket = 22
窗口D卖票,ticket = 21
窗口D卖票,ticket = 20
窗口B卖票,ticket = 19
窗口B卖票,ticket = 18
窗口B卖票,ticket = 17
窗口B卖票,ticket = 16
窗口B卖票,ticket = 15
窗口B卖票,ticket = 14
窗口B卖票,ticket = 13
窗口B卖票,ticket = 12
窗口B卖票,ticket = 11
窗口B卖票,ticket = 10
窗口B卖票,ticket = 9
窗口B卖票,ticket = 8
窗口B卖票,ticket = 7
窗口B卖票,ticket = 6
窗口B卖票,ticket = 5
窗口B卖票,ticket = 4
窗口B卖票,ticket = 3
窗口B卖票,ticket = 2
窗口B卖票,ticket = 1

此程序将判断是否有票以及卖票的两个操作都统一放到了同步代码块中,这样当某一个线程操作时,其他线程无法进入到方法中进行操作,从而实现了线程的同步操作。

可以从程序运行结果发现,卖票数量被大致平均到了各个线程,而且未出现错误数据的情况。

//	范例 3: 使用同步方法解决问题
package com.xiaoshan.demo;

class MyThread implements Runnable {
	private int ticket = 60;	//一共有60张票
	
	@Override
	public void run(){
		for(int x=0; x<20; x++){ 
			this.sale();
		}
	}
	
	//卖票操作
	public synchronized void sale(){	//同步方法
		if(this.ticket>0){	//判断当前是否还有剩余票
			try{
				Thread.sleep(100);	//休眠1s, 模拟延迟
			} catch (InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+ "卖票,ticket=" + this.ticket--);
		}
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt,"窗口A").start(); 	//启动多线程
		new Thread(mt,"窗口B").start(); 
		new Thread(mt,"窗口C").start(); 
		new Thread(mt,"窗口D").start();
	}
}

程序执行结果:

窗口A卖票,ticket=60
窗口A卖票,ticket=59
窗口A卖票,ticket=58
窗口A卖票,ticket=57
窗口A卖票,ticket=56
窗口A卖票,ticket=55
窗口A卖票,ticket=54
窗口A卖票,ticket=53
窗口A卖票,ticket=52
窗口A卖票,ticket=51
窗口A卖票,ticket=50
窗口A卖票,ticket=49
窗口A卖票,ticket=48
窗口A卖票,ticket=47
窗口A卖票,ticket=46
窗口A卖票,ticket=45
窗口A卖票,ticket=44
窗口A卖票,ticket=43
窗口A卖票,ticket=42
窗口A卖票,ticket=41
窗口D卖票,ticket=40
窗口D卖票,ticket=39
窗口D卖票,ticket=38
窗口D卖票,ticket=37
窗口D卖票,ticket=36
窗口D卖票,ticket=35
窗口D卖票,ticket=34
窗口D卖票,ticket=33
窗口D卖票,ticket=32
窗口D卖票,ticket=31
窗口D卖票,ticket=30
窗口D卖票,ticket=29
窗口D卖票,ticket=28
窗口D卖票,ticket=27
窗口D卖票,ticket=26
窗口D卖票,ticket=25
窗口D卖票,ticket=24
窗口C卖票,ticket=23
窗口C卖票,ticket=22
窗口C卖票,ticket=21
窗口C卖票,ticket=20
窗口C卖票,ticket=19
窗口C卖票,ticket=18
窗口C卖票,ticket=17
窗口C卖票,ticket=16
窗口C卖票,ticket=15
窗口B卖票,ticket=14
窗口B卖票,ticket=13
窗口B卖票,ticket=12
窗口B卖票,ticket=11
窗口B卖票,ticket=10
窗口B卖票,ticket=9
窗口B卖票,ticket=8
窗口B卖票,ticket=7
窗口B卖票,ticket=6
窗口B卖票,ticket=5
窗口B卖票,ticket=4
窗口B卖票,ticket=3
窗口B卖票,ticket=2
窗口B卖票,ticket=1

此时利用同步方法同样解决了同步操作的问题。但是在此处需要说明一个问题:加入同步后明显比不加入同步慢许多,所以同步的代码性能会很低,但是数据的安全性会高,或者可以称为线程安全性高。

那么在了解了以上知识后,同步和异步有什么区别呢,在什么情况下分别使用它们呢?

如果一块数据要在多个线程间进行共享。例如,正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

2.3 死锁

同步就是指一个线程要等待另外一个线程执行完毕才会继续执行的一种操作形 式,虽然在一个程序中,使用同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。

例如:张三想要李四的画,李死想要张三的书,那么张三对李四说 了:“把你的画给我,我就给你书", 李四也对张三说了:“把你的书给我,我就给你画", 这时,张三在等着李四的答复,而李四也在等着张三的答复,这样下去最终结果可想而知,张三得不到李四的画,李四也得不到张三的书,这实际上就是死锁的概念,如下图所示。

图4 死锁的场景

所谓死锁就是指两个线程都在等待彼此先完成,造成了程序的停滞状态, 一般程序的死锁都是在程序运行时出现的,下面通过一个简单的范例来观察一下出现死锁的情况。

//	范例 4: 程序死锁操作
package com.xiaoshan.demo;

class A{
	public synchronized void say(B b){
		System.out.println("A先生:把你的本给我,我给你笔,否则不给!");
		b.get();
	}
	public synchronized void get(){
		System.out.println("A先生:得到了本,付出了笔,还是什么都干不了!");
	}
}

class B{
	public synchronized void say(A a){
		System.out.println("B先生:把你的笔给我,我给你本,否则不给!");
		a.get();
	}
	public synchronized void get(){
		System.out.println("B先生:得到了笔,付出了本,还是什么都干不了!");
	}
}

public class TestDemo implements Runnable{
	private static A a= new A();                          //定义类对象
	private static B b= new B();                           //定义类对象
	
	public static void main(String[] args) throws Exception  {
		new TestDemo();                                                //实例化本类对象
	}
	
	public TestDemo(){	//构造方法
		new Thread(this).start();	//启动线程
		b.say(a);	//互相引用
	}
	
	@Override
	public void run(){
		a.say(b);	//互相引用
	}
}

程序执行结果:

B先生:把你的笔给我,我给你本,否则不给!
A先生:把你的本给我,我给你笔,否则不给!
(程序将不再向下执行,并且不会退出,此为死锁情况出现)

此程序由于两个类的都使用了同步方法定义,就会造成 a 对象等待 b 对象执行完毕,而 b 对象等待 a 对象执行完毕,这样就会出现死锁现象。

综上,多个线程访问同一资源时,考虑到数据操作的安全性问题, 一定要使用同步操作。同步有以下两种操作模式:

  • 同步代码块:synchronized(锁定对象){代码};
  • 同步方法:public synchronized 返回值 方法名称() {代码}

需要注意的是,过多的同步操作有可能会带来死锁问题,导致程序进入停滞状态。

2️⃣ 多线程经典案例——生产者与消费者

在开发中线程的运行状态并不固定,所以只能利用线程的名字以及当前执行的线程对象来进行区分。但是多个线程间也有可能会出现数据交互的情况。本节将利用一个线程的经典操作案例来分析线程的交互中存在问题以及问题的解决方案。

在生产者和消费者模型中,生产者不断生产,消费者不断取走生产者生产的产品,如下图所示。

图5 生产者与消费者案例

在图中非常清楚地表示出,生产者生产出信息后将其放到一个区域中,然后消费者从此区域里取出数据,但是在程序中因为牵涉线程运行的不确定性,所以会存在以下两点问题。
(1)假设生产者线程向数据存储空间添加信息的名称,还没有加入该信息的内容,程序就切换到了消费者线程,消费者线程将把该信息的名称和上一个信息的内容联系到一起。
(2)生产者放了若干次的数据,消费者才开始取数据,或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。

//	范例 5: 程序基本模型
package com.xiaoshan.demo;

class Message{
	private String title;	//保存信息的标题 
	private String content;	//保存信息的内容
	
	public void setTitle(String title){
		this.title = title;
	}
	public void setContent(String content){
		this.content = content;
	}
	public String getTitle(){
		return title;
	}
	public String getContent(){
		return content;
	}
}

class Producer implements Runnable {                       //定义生产者
	private Message msg = null;
	
	public Producer(Message msg){
		this.msg = msg;
	}
	
	@Override
	public void run(){
		for(int x=0; x<8; x++){                      //生产8次数据
			if(x%2 == 0){
				this.msg.setTitle("小山");     //设置 title属性
				try{
					Thread.sleep(100);                         //延迟操作
				} catch(InterruptedException  e){
					e.printStackTrace();
				}
				this.msg.setContent("Java专栏作者");	//设置content属性
			}else{
				this.msg.setTitle("xiaoshan");              //设置 title 属性
				try  {
					Thread.sleep(100);
				}catch (InterruptedException e){
					e.printStackTrace();
				}
				this.msg.setContent("www.xiaoshan.cn");// 设置content属性
			}
		}
	}
}

class Consumer implements Runnable {                          //定义消费者
	private Message msg = null;
	
	public Consumer (Message msg){
		this.msg = msg;
	}

	@Override
	public void run(){
		for(int x=0; x<8; x++){                                //取走8次数据
			try{
				Thread.sleep(100);                                      //延迟
			} catch(InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(this.msg.getTitle() + "-->" + this.msg.getContent());
		}
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Message msg = new Message();       	//定义Message 对象,用于保存和取出数据
		new Thread(new Producer(msg)).start();     // 启动生产者线程
		new Thread(new Consumer(msg)).start(); 		// 取得消费者线程
	}
}

程序执行结果:

xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->www.xiaoshan.cn

通过本程序的运行结果可以发现两个严重的问题:设置的数据错位;数据会重复设置及重复取出。

首先我们来解决数据错乱问题,数据错位完全是因为未同步的操作,所以应该使用同步处理。因为取出和设置是两个不同的操作,所以要想进行同步控制,就需要将其定义在一个类里面完成。

//	范例 6: 加入同步,解决数据错乱问题
package com.xiaoshan.demo;

class Message {
	private String title;                                         //保存信息的标题
	private String content;                                   //保存信息的内容
	
	public synchronized void set(String title, String content){
		this.title = title;
		try {
			Thread.sleep(200);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		this.content = content;
	}

	public synchronized void get(){
		try {
			Thread.sleep(100);
		} catch(InterruptedException  e){
			e.printStackTrace();
		}
		System.out.println(this.title + "-->" + this.content);
	}
	
	// setter、getter略
}

class Producer implements Runnable {                       //定义生产者
	private Message msg = null;
	
	public Producer(Message msg){
		this.msg = msg;
	}
	
	@Override
	public void run(){
		for(int x=0; x<8; x++){                      //生产8次数据
			if(x%2 == 0){
				this.msg.set("小山", "Java专栏作者");     //设置属性
			}else{
				this.msg.set("xiaoshan", "www.xiaoshan.cn");              //设置属性
			}
		}
	}
}

class Consumer implements Runnable {	//定义消费者
	private Message msg = null;
	
	public Consumer (Message msg){
		this.msg = msg;
	}

	@Override
	public void run(){
		for (int x=0; x<8; x++){	//取走8数据
			this.msg.get();	//取得属性
		}
	}
}

public class TestDemo  {
	public static void main(String[] args) throws Exception {
		Message msg = new Message();                                  //定义Message 对象,用于保存和取出数据
		new Thread(new Producer(msg)).start();	//启动生产者线程 
		new Thread(new Consumer(msg)).start();	//取得消费者线程
	}
}

程序执行结果:

小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
xiaoshan-->www.xiaoshan.cn
xiaoshan-->www.xiaoshan.cn

从运行结果可以发现,数据错位问题此时已经因为使用了同步处理而得到了解决。下面我们来解决数据重复问题,要想解决数据重复的问题,需要等待及唤醒机制,而这一机制的实现只能依靠 Object类完成,前面在《【Java基础教程】(十六)面向对象篇 · 第十讲:解读Object类——定义、操作方法、深拷贝和浅拷贝的差异、多线程编程支持及使用场景~》一文中介绍到了在 Object 类中定义了3个方法完成线程的操作,如下所示。

  • public final void wait(throws InterruptedException):线程的等待;
  • public final void notify():唤醒第一个等待线程;
  • public final void notifyAll():唤醒全部等待线程。

可以发现,一个线程可以为其设置等待状态,但是对于唤醒的操作却有两个: notify()notifyAll()。一般来说,所有等待的线程会按照顺序进行排列。如果使用了 notify()方法,则会唤醒第一个等待的线程执行;如果使用了notifyAll() 方法,则会唤醒所有的等待线程。哪个线程的优先级高,哪个线程就有可能先执行,如下图所示。

图6 notify()与notifyAll()的区别

清楚了Object 类中的3个方法作用后,下面就可以利用这些方法来解决程序中的问题。如果想让生产者不重复生产,消费者不重复取走,则可以增加一个标志位,假设标志位为 boolean 型变量。如果标志位的内容为 true, 则表示可以生产,但是不能取走,如果此时线程执行到了,消费者线程则应该等待;如果标志位的内容为 false, 则表示可以取走,但是不能生产,如果生产者线程运行,则应该等待。
操作流程如下图所示。

图7 操作流程

所以要想解决数据重复的问题,只需要直接修改 Message 类即可。在 Message 类中加入标志位,并通过判断标志位完成等待与唤醒的操作。

//	范例 7: 解决程序问题


class Message{
	private String title;
	private String content;
	private boolean flag = true; // flag == true: 表示可以生产,但是不能取走; flag == false:表示可以取走,但是不能生产

	public synchronized void set(String title, String content){
		if (this.flag == false) {                                //已经生产过了,不能生产
			try {
				super.wait();                            //等待
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.title = title;
		try {
			Thread.sleep(200);
		} catch (InterruptedException e){
			e.printStackTrace();
		}
		this.content = content;
		this.flag = false;	//已经生产完成,修改标志位
		super.notify();	//唤醒等待线程

	}

	public synchronized void get(){
		if (this.flag == true){                                    //未生产,不能取走
			try{
				super.wait();                                    //等待
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		try{
			Thread.sleep(100);
		}catch(InterruptedException	e){
			e.printStackTrace();
		}
		System.out.println(this.title + "-->" + this.content);
		this.flag = true;	//已经取走了,可以继续生产
		super.notify();		//唤醒等待线程
	}

	// setter、getter略
}

class Producer implements Runnable {                       //定义生产者
	private Message msg = null;

	public Producer(Message msg){
		this.msg = msg;
	}

	@Override
	public void run(){
		for(int x=0; x<8; x++){                      //生产8次数据
			if(x%2 == 0){
				this.msg.set("小山", "Java专栏作者");     //设置属性
			}else{
				this.msg.set("xiaoshan", "www.xiaoshan.cn");              //设置属性
			}
		}
	}
}

class Consumer implements Runnable {	//定义消费者
	private Message msg = null;

	public Consumer (Message msg){
		this.msg = msg;
	}

	@Override
	public void run(){
		for (int x=0; x<8; x++){	//取走8数据
			this.msg.get();	//取得属性
		}
	}
}

public class TestDemo  {
	public static void main(String[] args) throws Exception {
		Message msg = new Message();                                  //定义Message 对象,用于保存和取出数据
		new Thread(new Producer(msg)).start();	//启动生产者线程
		new Thread(new Consumer(msg)).start();	//取得消费者线程
	}
}

程序的运行结果:

小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn

从程序的运行结果中可以清楚地发现,生产者每生产一个信息就要等待消费者取走,消费者每取走一个信息就要等待生产者生产,这样就避免了重复生产和重复取走的问题。

🔍分析sleep()和wait()的区别?

  • sleep()Thread类定义的 static方法,表示线程休眠,将执行机会给其他线程,但是监控状态依然保持,休眠时间到了会自动恢复;
  • wait()Obiect类定义的方法,表示线程等待,一直到执行了 notify()notifyAll() 后才会被唤醒,结束等待。

🌾 总结

本文主要介绍了多线程编程中的同步与死锁问题,以及经典的生产者与消费者案例,并分析了sleep()wait()方法的区别。

我们首先引出了线程同步问题,解释了多个线程同时访问共享资源时可能导致的数据不一致性和并发安全性问题。为了解决这些问题,我们介绍了synchronized关键字,说明了如何使用它来实现线程的同步操作,以确保只有一个线程可以访问共享资源,从而避免数据的争用和冲突。

接下来,我们讨论了死锁问题,详细说明了死锁是由于多个线程相互等待对方释放资源而无法继续执行的情况。死锁的出现是由于资源竞争和线程之间的依赖所导致的,后面的文章中将会为大家介绍一些避免死锁的常见方法,如避免嵌套锁、按顺序获取资源等,敬请期待。

随后,我们介绍了经典的生产者与消费者案例,展示了多线程协作的实践应用。通过使用wait()notify()notifyAll()方法,我们演示了如何实现生产者与消费者之间的有效通信和资源共享。

最后,我们对比了sleep()wait()方法的区别。sleep()方法是让线程暂停一段指定的时间,不释放锁资源;而wait()方法是让线程进入等待状态,同时释放锁资源,直到被其他线程唤醒并重新获得锁资源。我们强调了在使用wait()方法时需要注意与notify()notifyAll()方法配合使用,以免出现线程无法被唤醒或永久等待的情况。


温习回顾上一篇(点击跳转)
《【Java基础教程】(四十二)多线程篇 · 上:多进程与多线程、并发与并行的关系,多线程的实现方式、线程流转状态、常用操作方法解析~》

继续阅读下一篇(点击跳转)
《【Java基础教程】(四十四)IO篇 · 上:解析Java文件操作——File类、字节流与字符流,分析字节输出流、字节输入流、字符输出流和字符输入流的区别》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/782217.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot Redis 配置多数据源

Redis 从入门到精通【应用篇】之SpringBoot Redis 配置多数据源 文章目录 Redis 从入门到精通【应用篇】之SpringBoot Redis 配置多数据源1.教程0. 添加依赖1. 配置多个 Redis 连接信息我们将上面的配置改造一下&#xff0c;支持Redis多数据源 2. 配置3. 创建 RedisTemplate 实…

Compose中常用的一些Modifier的扩展ui方法记录

Compose中常用的一些Modifier的扩展ui方法记录 关于防快速点击虚实分割线虚线边框阴影 关于 本篇主要记录一些开发中可能用到的常用方法的扩展记录&#xff0c;包括防快速带点击&#xff0c;画虚实线divider&#xff0c;画虚线边框&#xff0c;绘制阴影等。 防快速点击 inlin…

每天五分钟机器学习:线性回归和非线性回归之间的区别?

本文重点 在前面的课程中,我们学习了单变量线性回归模型以及多变量的线性回归模型,无论是单变量线性回归还是多变量线性回归,这二者都是一样的,都是线性的。本文我们将学习一下线性回归模型和非线性回归之间的区别和联系。 关于模型的基本区别 线性回归:线性回归就是每…

第三天 运维高级 MySQL主从复制

1.理解MySQL主从复制原理 1、master&#xff08;binlog dump thread&#xff09;主要负责Master库中有数据更新的时候&#xff0c;会按照binlog格式&#xff0c;将更新的事件类型写入到主库的binlog文件中。 2、I/O thread线程在Slave中创建&#xff0c;该线程用于请求Master&…

YApi 服务端测试新增 globalCookie ,兼容自动化触发服务端测试功能

YApi是一个开源的接口管理平台&#xff0c;它提供了丰富的接口管理和测试功能。其中&#xff0c;服务端测试是YApi的一个重要特性&#xff0c;可以帮助开发人员自动化执行接口测试。 在YApi的服务端测试中&#xff0c;新增globalCookie是一个很有用的功能。通过设置globalCook…

2023/7/23周报

目录 摘要 论文阅读 1、题目和现存问题 2、问题阐述及相关定义 3、LGDL模型框架 4、实验准备 5、实验过程 深度学习 1、GCN简单分类任务 2、文献引用数据分类案例 3、将时序型数据构建为图数据格式 总结 摘要 本周在论文阅读上&#xff0c;对基于图神经网络与深度…

LabVIEW使用支持向量机对脑磁共振成像进行图像分类

LabVIEW使用支持向量机对脑磁共振成像进行图像分类 医学成像是用于创建人体解剖学图像以进行临床研究、诊断和治疗的技术和过程。它现在是医疗技术发展最快的领域之一。通常用于获得医学图像的方式是X射线&#xff0c;计算机断层扫描&#xff08;CT&#xff09;&#xff0c;磁…

pnpm 与monorepo架构

软链接与硬链接 创建方式&#xff1a; mklink &#xff08;windows&#xff09; 软链接 &#xff1a; a、b指向同一个文件 b相当于一个快捷方式 硬链接&#xff1a; a、b指向同一个内存地址 某一文件修改&#xff0c;其他文件跟这变化 上图所示&#xff1a;安装某依赖&…

LabVIEW - DAQmx 数据采集

1. 题目 基于NI MAX创建模拟仿真设备&#xff0c;然后基于DAQmx编写模拟量数据采集程序&#xff0c;实现按照1s时间间隔&#xff0c;采集制定模拟输入端口一个数据的功能&#xff0c;并能够将采集的数据、数据采集的时间等参数写入文本文件保存。 2. 过程 通过在NI max的设备与…

虚拟人直播怎么做?3d虚拟主播全栈技术方案来了

元宇宙浪潮来袭后&#xff0c;虚拟人直播的应用场景得到进一步拓宽&#xff0c;大量的3d虚拟主播出现在品牌直播间、娱乐节目、发布会等应用中&#xff0c;那想要让3d虚拟主播“活得即时”&#xff0c;开启虚拟人直播要怎么做&#xff1f;本文将基于广州虚拟动力的3d虚拟主播全…

AWS IAM介绍

前言 AWS是世界上最大的云服务提供商&#xff0c;它提供了很多组件供消费者使用&#xff0c;其中进行访问控制的组件叫做IAM(Identity and Access Management)&#xff0c; 用来进行身份验证和对AWS资源的访问控制。 功能 IAM的功能总结来看&#xff0c;主要分两种&#xff1…

3、线性数据结构

线性数据结构&#xff0c;从名字可以看出&#xff0c;和“线”脱离不了关系。 那么从“线”联想&#xff0c;水平的&#xff0c;我们可以想到食堂打饭排的队伍&#xff0c;垂直的&#xff0c;我们可以联想到书桌上层叠摆放的书籍。 打饭的队伍一般遵循“先来先服务”的原则&a…

力扣热门100题之移动0【中等】

题目描述 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0] 示例 2: 输入: nums […

利用Graphics的CopyFromScreen实现简陋版的打印(C#)

前段时间&#xff0c;在做一个打印的需求&#xff0c;需要把Winform界面的控件及内容全部打印出来&#xff0c;但有一个比较坑的地方是&#xff0c;公司提供的打印API打印单选框&#xff0c;打印单选框时发现选框和内容总是有那么一点点不对齐&#xff0c;看着很别扭。不过客户…

【Linux】文件操作(一)

目录 预备知识 复习C语言文件接口 fopen() 写入类&#xff1a;fwrite()、fprintf()、fputs() 读取类&#xff1a;fgets() 系统接口 open() 一个参数如何传递多个选项&#xff1f; close() write() read() 预备知识 在正式讲解文件之前&#xff0c;我们需要有一些预…

C#中小数保留固定位数

我们写程序的时候&#xff0c;有时候数据想要对齐一点&#xff0c;如果小数位数不一样&#xff0c;自然就对不齐了。这里提供一个方法. 1.这里举例保留小数点后4位(不足4位后面补0)。 String result String.Format("{0:F4}", 123.456); 输出结果为result123.4560&a…

Linux离线安装mysql8.0+

文章目录 1.查看是否安装过MySQL2.MySQL卸载3.下载mysql4.上传mysql到指定目录5.解压MySQL安装包6.安装1.准备工作2.开始安装3.查看MySQL版本 7.修改my.cnf配置文件8.授权给mysql用户8.服务初始化10.启动MySQL11.登录12.修改密码13.设置远程登录1连接超时问题2确认网络3查看端口…

STL vector 详解

STL vector 详解 STL vector 详解 STL vector 详解一级目录二级目录三级目录 1. vector容器2. vector 容器的初始化函数1.初始化2.案例 3. vector的访问函数1. 成员函数访问2. 操作符[ ]访问3.案例 4. vector的插入函数1. 语法2. 案例 5. vector的删除函数1.语法2. 案例 6. vec…

Vue el-table 多表格联合显示、合并单元格

原型图 分析 先看内容是三个表&#xff0c;每个表的合并单元格都有点不同。 按照原型图给的内容&#xff0c;第一个是两列&#xff0c;有行合并和列合并&#xff0c;还有表头行合并。 现根据图造出mock数据&#xff0c;然后再写对应的代码。 export const columnVarsData {s…

数据结构day7(2023.7.21)

一、Xmind整理&#xff1a; 二、课上练习&#xff1a; 练习1&#xff1a;折半查找/二分查找 1-------100 key8850---10075-100int arr[]{12,23,33,45,66,78,99};key7912,23,33,45,66,78,990 6low mid high66, 78, 99mid1 mid highlow99lowhigh…