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

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篇 · 上》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ARM练习

通过汇编语言完成LED1-3循环点亮练习 .text .global _start _start: /**********LED1点灯**************/ /*初始化RCC*/ RCC_INIT:LDR R0,0X50000A28LDR R1,[R0]ORR R1,R1,#(0X1<<4)ORR R2,R1,#(0x1<<5)STR R1,[R0]STR R2,[R0]LED1_INIT:设置输出模式LDR R0,0X5…

企业网络安全合规框架体系

云安全联盟大中华区发布报告《企业网络安全合规框架体系》&#xff08;以下简称报告&#xff09;&#xff0c;该报告对典型业务场景给出了参考实例&#xff0c;供广大甲方单位、集成商、咨询机构参考。 近些年&#xff0c;随着国内网络安全领域相关法律、法规、政策文件、标准规…

数据结构--图的遍历 BFS

数据结构–图的遍历 BFS 树的广度优先遍历 从 1 结点进行 b f s bfs bfs的顺序&#xff1a; 【1】 【2】【3】【4】 【4】【6】【7】【8】 图的广度优先遍历 从 2 号点开始 b f s bfs bfs的顺序&#xff1a; 【2】 【1】【6】 【5】【3】【7】 【4】【8】 树 vs 图 不存在“回…

ChatGPT:人工智能语言模型的革命性进步

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

PHP后台登录功能单账号登录限制

PHP后台登录功能单账号登录限制 单账号登陆是什么第一步创建数据表第二步创建登录页面test2.html第三步创建登录提交test2.php第四步访问后台首页第五步演示 单账号登陆是什么 一个用户只能登录一个账号通常被称为单账号登录限制或单用户单账号限制。这意味着每个用户只能使用…

微服务一 实用篇 - 5.分布式搜索引擎(ElasticSearch基础)

《微服务一 实用篇 - 5.分布式搜索引擎&#xff08;ElasticSearch基础&#xff09;》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《微服务一 实用篇 - 5.分布式搜索引擎&#xff08;ElasticSearch基础&#xff09;》 《微服务一 实用篇 - 5.分布式搜索…

vue实现excel数据下载,后端提供的list由前端转excel并下载

前言,因为项目需求需要,我们需要把后端传来的list转成excel模板,并且下载下来) 之前有用的插件,但是会有少0的情况,如下 所以采用另一个项目用过的方法,最终完美实现效果,如下: 1,首先我们来看下后端提供的数据结构 2,具体前端代码如下 封装的组件,需要的同学直接copy就行(这…

ORACLE实时SQL监控视图

引言 实时的SQL监控&#xff08;Real Time SQL Monitoring&#xff09;是Oracle 11g的一个新特性&#xff0c;它是一项强大的工具&#xff0c;用于监视和分析正在执行的SQL语句的性能和执行计划。该功能允许我们实时地跟踪SQL查询的执行过程&#xff0c;以及了解其资源消耗、等…

PHP登陆/php登录--【强撸项目】

强撸项目系列总目录在000集 PHP要怎么学–【思维导图知识范围】 文章目录 本系列校训本项目使用技术 上效果图phpStudy 设置导数据库 项目目录如图&#xff1a;页面代码后台代码 这么丑的界面能忍&#xff1f;配套资源作业&#xff1a; 本系列校训 用免费公开视频&#xff0…

macOS 源码编译 Percona XtraBackup

percona-xtrabackup-2.4.28.tar.gz安装依赖 ╰─➤ brew install cmake ╰─➤ cmake --version cmake version 3.27.0brew 安装 ╰─➤ brew update╰─➤ brew search xtrabackup > Formulae percona-xtrabackup╰─➤ brew install percona-xtrabackup╰─➤ xtr…

投个 3D 冰壶,上班玩一玩

本篇文章将介绍如何使用物理引擎和图扑 3D 可视化技术来呈现冰壶运动的模拟。 Oimo.js 物理引擎 Oimo.js 是一个轻量级的物理引擎&#xff0c;它使用 JavaScript 语言编写&#xff0c;并且基于 OimoPhysics 引擎进行了改进和优化。Oimo.js 核心库只有 150K &#xff0c;专门用…

基于扩展(EKF)和无迹卡尔曼滤波(UKF)的电力系统动态状态估计

1 主要内容 该程序对应文章《Power System Dynamic State Estimation Using Extended and Unscented Kalman Filters》&#xff0c;电力系统状态的准确估计对于提高电力系统的可靠性、弹性、安全性和稳定性具有重要意义&#xff0c;虽然近年来测量设备和传输技术的发展大大降低…

2816. 判断子序列

题目链接&#xff1a; 自己的做法&#xff1a; #include <bits/stdc.h>using namespace std;const int N 1e5 10; int a[N], b[N]; int main() {int n, m;bool flag true;scanf("%d%d", &n, &m);for (int i 0; i < n; i) scanf("%d"…

【并发专题】阻塞队列BlockingQueue实战及其原理分析

目录 前置知识队列有界队列、无界队列Queue——队列在JAVA里面的接口 阻塞队列介绍BlockingQueue——阻塞队列在JAVA里面的接口阻塞队列的应用场景JUC包下的阻塞队列 课程内容*一、ArrayBlockingQueue基本介绍应用场景使用示例基本原理数据结构核心源码解析双指针与环形数组 *二…

内存的五大分区(自用水文)

1、堆区&#xff08;heap&#xff09;——由程序员分配和释放&#xff0c; 若程序员不释放&#xff0c;程序结束时一般由操作系统回收。注意它与数据结构中的堆是两回事 2、栈区&#xff08;stack&#xff09;——由编译器自动分配释放 &#xff0c;存放函数的参数值&#xff0…

Android图形系统之SurfaceFlinger/OpenGL/HWC/Gralloc/FrameBufer/ION/GPU等关系(十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

【C++】多态案例— —计算器类

author&#xff1a;&Calton tag&#xff1a;C topic&#xff1a;【C】多态案例— —计算器类 website&#xff1a;黑马程序员C date&#xff1a;2023年7月23日 目录 多态概要 案例实现 原理剖析 多态概要 多态是C三大特性之一&#xff08;封装、继承、多态&#xff…

【SpringBoot项目】Tomcat started on port(s): 8080 (http) with context path ‘‘

运行程序后出现下面的错误&#xff0c;并且在postman中无法获取到数据 在idea中的错误显示的如下 本人的原因是忘记在Controller中忘记写&#xff01;&#xff01;&#xff01;&#xff01; RestController 如果你不是以下原因可以参考下面的文章&#xff1a; Initializing S…

在VSCode中实现Rust编程调试指南

在 VS Code 中调试 Rust&#xff1a;终极指南 在本教程中&#xff0c;您将学习如何使用 VS Code 调试 Rust。可用于使用 VS Code 调试 Rust 的操作。设置 VS Code 来调试 Rust Rust因其易用性、安全性和高性能而继续保持其作为最受欢迎的编程语言的地位。随着 Rust 的流行&…

MySQL 的 crash-safe浅谈

MySql执行流程 MySQL作为当下最流行的开源关系型数据库&#xff0c;有一个很关键和基本的能力&#xff0c;就是必须能够保证数据不会丢。那么在这个能力背后&#xff0c;MySQL是如何设计才能保证不管在什么时间崩溃&#xff0c;恢复后都能保证数据不会丢呢&#xff1f;有哪些…