【Java学习笔记】多线程

news2024/10/12 22:11:56

当我们在饭店聚餐时,多人同时吃一道菜的时候很容易发生争抢。例如,上了一道好菜,两个人同时夹这道菜,一人刚伸出筷子,结果伸到的时候菜已经被夹走了。为了避免这种现象,必须等一人 夹完一口后,另一人再夹菜。也就是说,资源共享就会发生争抢,这就是多线程争抢资源的问题。

线程第一个单独的程序流程,多线程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。如果程序被设置为多线程,则可以提高程序运行的效率和处理速度。可以通过控制线程来控制程序的运行,如操作线程的等待、休眠、唤醒等,

多线程的概念,可以类比在生活中当资源共享出现冲突的时候该怎么办,除了抢菜、还有公交车抢坐,多人雨中争抢出租车等,都是这样的例子。线程包括线程周期、线程调度、线程同步、线程通信和死锁等概念。

1 线程基础

多个线程可以同时在一个程序中运行,并且每个线程完成不同的任务。java中线程的实现通常有两种方法:派生Thread类实现Runnable接口

1.1 什么是线程

传统的程序设计语言同一时刻只能执行单线程任务,效率很低,如果网络程序在接受数据时发生阻塞,则只能等到程序接收完数据后才能继续运行。随着Internet快速发展,这种单任务运行的状况越来越不被接受。如果网络程序在接受数据时发生阻塞,那么后台服务程序就会一直处于等待状态而不继续接受任何操作;如果这种情况经常发生,那么CPU资源将完全处于闲置状态。

多线程实现后台服务程序,可以同时处理多个任务,并且不会发生阻塞现象。多线程是Java语言的一个很重要的特征。多线程程序设计的最大特点就是能够提高程序的执行效率和处理速度。Java程序可以同时运行多个相对独立的线程。例如,在开发一个E-mail邮件系统时,需要创建一个线程来接收数据,创建一个线程来发送数据。这样即使发送数据的线程出现阻塞,接收线程仍然可以运行。

线程(Thread)是控制线程(Thread of control)的缩写,是具有一定顺序的指令序列(所编写的程序代码)、存放方法中定义局部变量的栈和一些共享数据,线程是相互独立的,每个方法的局部变量和其他方法的局部变量是分开的。因此,任何线程都不能访问除自身之外其他线程的局部变量。如果两个线程同时访问同一个方法,那么每个线程将各自得到此方法的一个拷贝。

Java提供的多线程机制,使得一个程序可以执行多个任务,线程有时也被称为小进程,他是从一个大进程里分离 出来的小的独立的线程。多线程技术让java变的更健壮,同时带来更好的交互性能和实时控制性能。多线程是强大而灵巧的工具,但如果使用不当可能会造成一系列的错误,并且不容易排查。因此需要掌握其方法和特性后再进行使用。

在多线程编程中,每个线程都通过代码实现线程的行为。并将数据提供给代码操作。代码和数据有时是相对独立的,可以分别提供给线程。多个线程可以同时处理同一代码和同一数据,不同的线程也可以处理各自不同的代码和数据。

1.2 Thread类创建线程的方法

Java中有两种方式创建线程:

  • 对Thread类进行派生并覆盖run()方法;
  • 通过实现Runnable接口创建

继承Thread类并覆盖Thread类的run()方法完成线程类的声明,通过new关键字创建派生线程类的线程对象。run()方法中的代码实现了线程的行为。

前面的程序都是声明一个公共类,并在类内实现一个main()方法。实际上前面的这些程序就是一个单线程程序。当它执行完main()方法的程序后,线程正好退出,并且程序同时结束运行。
【例】创建单线程实例


public class OnlyThread {

	public static void main(String[] args) {
		run();				//调用run()方法
	}
	
	//实现run()方法
	public static void run() {
		for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
			for(int i = 0;i<count;i++) {
				System.out.print('*'); //输出符号
			}
			System.out.println();
		}
	} 
}

java.lang.Thread类是一个通用的线程类。由于默认情况下,run()方法是空的,直接通过Thread类实例化的线程对象不能完成任何事,所以可以通过派生Thread类,并用具体程序代码覆盖Thread类中的run()方法,来实现具有各种不同功能的类。

在程序中,创建新的线程的方法之一是继承Thread类,并通过Thread子类声明线程对象。

【例】通过Thread类创建线程实例

package example;

public class ThreadDemo extends Thread{
		//声明ThreadDemo构造方法
	ThreadDemo(){}
	//声明ThreadDemo()带参构造方法
	ThreadDemo(String szname){
		super(szname);//调用父类的构造方法
	}
	
	//重载run()方法
	public void run() {
		for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
			for(int i = 0;i<count;i++) {
				System.out.print('*'); //输出符号
			}
			System.out.println();
		}
	}
	public static void main(String[] args) {
		Thread demo = new ThreadDemo();
		demo.start();//调用start()方法执行一个新线程
	}
}

1.3 Thread类创建线程的步骤

(1)创建一个新的线程类,继承Thread类并覆盖Thread类的run()方法

class ThreadType extends Thread{
	public void run(){
	}
}

(2)创建一个线程类对象,创建方法与一般对象的创建方法相同,使用关键字new完成

ThreadType tt = new ThreadType();

(3) 启动新线程对象,调用start()方法

tt.start();

(4)线程自己调用run()方法

void run();

【例】创建多线程的实例

package example;

public class CreateMoreThread extends Thread{
	//声明CreateMoreThread构造方法
	CreateMoreThread(){}
		//声明ThreadDemo()带参构造方法
	CreateMoreThread(String szname){
			super(szname);//调用父类的构造方法
		}
		
		//重载run()方法
		public void run() {
			for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
				for(int i = 0;i<count;i++) {
					System.out.print('*'); //输出符号
				}
				System.out.println();
			}
		}
		public static void main(String[] args) {
			CreateMoreThread demo1 = new CreateMoreThread();
			demo1.start();//调用start()方法执行一个新线程
			
			CreateMoreThread demo2 = new CreateMoreThread();
			demo2.start();//调用start()方法执行一个新线程
		
			CreateMoreThread demo3 = new CreateMoreThread();
			demo3.start();//调用start()方法执行一个新线程
		}
}

在这里插入图片描述
实际运行结果并不是直角三角形,这时因为线程并没有按照程序中调用的顺序来执行,而是产生了多线程赛跑现象。

1.4 Runnable接口创建线程的方法

Runnable接口可用于实现线程类,该接口只有一个run()方法。此方法必须由实现了此接口的类来实现。创建线程的第二种方法是实现Runnable接口。这种方法可以解决Java语言不支持多重继承问题。
Runnable接口提供了run()方法的原型,因此在创建新的线程时,只需要实现这个接口就可以完成新线程的运行。

package example;

public class CreThreadRunnable implements Runnable{

	@Override
	//重载run()方法
			public void run() {
				for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
					for(int i = 0;i<count;i++) {
						System.out.print('*'); //输出符号
					}
					System.out.println();
				}
			}
	
	public static void main(String[] args) {
		Runnable  demo1 = new CreThreadRunnable();
		Thread tb = new Thread(demo1);//通过Thread类创建线程
		tb.start();//调用start()方法执行一个新线程

	
}
	}

在这里插入图片描述

1.5 Runnable 接口创建线程的步骤

(1)创建一个实现Runnable接口的类,并在这个类中重写run()方法

class ThreadType implements Runnable{
	public void run(){
			......
	}
}

(2) 使用关键字new新建一个ThreadType对象的实例

Runnable rb = new ThreadType(); 

(3)通过Runnable接口的实例创建一个线程对象、在创建线程对象时,调用的构造函数式new Thread(ThreadType),它用ThreadType中实现的run()方法作为新线程对象的run()方法

Thread td = new Thread(rb);

(4)通过调用ThreadType对象的start()方法启动线程

td.start();

【例】通过Runnable接口创建多线程的实例

package example;

public class CreMorThreadRunnable implements Runnable{
	
	@Override
	//重载run()方法
			public void run() {
				for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
					for(int i = 0;i<count;i++) {
						System.out.print('*'); //输出符号
					}
					System.out.println();
				}
			}
	
	public static void main(String[] args) {
		Runnable  demo1 = new CreMorThreadRunnable();
		Thread tb1 = new Thread(demo1);//通过Thread类创建线程
		tb1.start();//调用start()方法执行一个新线程
		
		Runnable  demo2 = new CreMorThreadRunnable();
		Thread tb2 = new Thread(demo2);//通过Thread类创建线程
		tb2.start();//调用start()方法执行一个新线程

		Runnable  demo3 = new CreMorThreadRunnable();
		Thread tb3 = new Thread(demo3);//通过Thread类创建线程
		tb3.start();//调用start()方法执行一个新线程
	
}
	

}

在这里插入图片描述
同样由于两个线程不是一个线程结束之后再执行另一个线程的,所以出现了“线程赛跑”现象

2. 线程的生命周期

正如同人从出生到少年、青年、壮年,老年直至死亡是人的生命周期,线程也有它的生命周期。
线程的生命周期由线程创建、可运行状态、不可运行状态和退出等部分组成。这些状态之间的转换,是通过线程提供的一些方法来完成的。

2.1 线程的4种状态

任何一个线程都有4种状态,一个线程总会处于这4种状态的一种。

创建 New
可运行 Runnable
不可运行 non-Runnable
退出 Done

在这里插入图片描述

(1)通过new 关键字创建线程时,线程属于创建阶段。这时不能运行线程,需要等下一步的指令改变状态
(2)通过start()方法启动线程,并进入可运行状态。或者通过stop()方法进入退出状态。
(3)当线程进入退出状态时,线程已经结束执行。这是线程的最终状态。
(4)所有线程都处于退出状态时,程序会强制终止。
(5)当线程处于可运行状态时,在一个特定的时间点上每一个系统处理器只能运行一个线程。
如果线程被挂起,执行会被中断,线程将进入不可运行状态。进入不可运行状态后,可通过resume(),notify()等方法返回可运行状态。

2.2 线程创建和启动

线程相关类Thread()中默认的run()方法没有任何可操作的代码,所以使用Thread类创建线程不能完成任何任务。
为了让创建的线程实现相应的功能,必须重新定义run()方法。

  • 派生线程类Thread的子类,并在子类中重写run()方法。
    Thread子类的实例对象有run()方法,启动线程后,执行重写的run()方法
  • 实现Runnable接口并重新定义run()方法
    先定义一个实现Runnable接口的类,在该类中定义run()方法,然后创建新的线程类对象,并以该对象作为Thread类构造方法的参数创建一个线程。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3 线程调度

  • 比如在火车站或长度汽车站等公交的车的经历,可能站点上有很多车,但是决定哪辆车先发出,如何控制发车的节奏和时间,需要公交调度员进行车辆调度。
  • 比如在公交车站乘坐公交车,如果同时有5个人上车,但只有1个座位,那么谁能抢到就是谁坐,但如果有老人或孕妇,需要让座。这就是优先级的概念。
  • 在多线程程序中,每一个线程的重要性和优先级可能不同,多个线程在等待获得CPU的时间片时,优先级高的就能抢占CPU并得以执行,并且占用时间较多。因此,高优先级的线程执行效率高一些,速度快一点。
  • Java中,CPU的使用通常采用抢占式调度的模式。意思是许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行到结束,或者进入不可运行状态&更高优先级的线程变为可运行状态,会让出CPU
public final void setPriority(int newPriority)

newPriority:线程优先级,1~10

【例】优先级举例说明

package priority;

import example.RunnableThread;
import example.SubThread;

class InheritThread extends Thread{
	//自定义线程的run()方法
	public void run() {
		System.out.println("InheritThread is running...");
		for(int i = 0 ;i<5;i++) {
			System.out.println("InheritThread:i="+i);
		}try {
			Thread.sleep((int)Math.random()*2000);//线程休眠时间
		}catch(InterruptedException e){
			
		}
		
	}

}
//通过Runnable接口创建另一个线程

class RunnableThread1 implements Runnable{

	public void run() {
		System.out.println("InheritThread is running...");
		for(int i = 0 ;i<5;i++) {
			System.out.println("InheritThread:i="+i);
		}try {
			Thread.sleep((int)Math.random()*2000);//线程休眠时间
		}catch(InterruptedException e){
			
		}
		
	}
	
}


public class ThreadPriority{
	public static void main(String[] args) {
		
		InheritThread itd =	new InheritThread();
		Thread rtd = new Thread(new RunnableThread1());
		
		itd.setPriority(5);
		rtd.setPriority(4);
		
		itd.start();
		rtd.start();	
	}
}

在这里插入图片描述
Inherit优先级高,会优先执行。

4.线程同步

生活中,经常会出现资源冲突的问题。例如,公交车上只有一个座位,但两个人看到都想坐,此时冲突就产生了,因为只有一个人能坐这个位置。
Java应用程序中,多线程可以共享资源,但当线程以并发模式访问共享数据时,共享数据可能会产生冲突。Java引入线程同步的概念,以实现共享数据的一致性。线程同步机制库让多个线程有序的访问共享资源,而不是同时操作(共享资源)

4.1 同步的概念

在线程的异步模式下,同一时刻有一个线程在修改共享数据,另一个线程在读取共享数据。当修改共享数据的线程没有处理完毕时,读取共享数据的线程肯定会得到错误的结果。
如果采用多线程的同步控制机制,那么只有当修改共享数据的线程完成数据共享时,读取线程才会读取共享数据。

例如:在武昌、汉口、武汉及市内车票代理点都可以出售武从武汉到北京的车票,将每一个站点看做一个线程。设置两个站点:线程Thread1和线程Thread2都可以出售车票。但在出售过程中,会出现数据与时间信息不一致的情况。即同一时刻两个线程同时查询数据库,同属出售此票,这时候就出现了一张车票出售两次的错误。这是一个典型的由于数据不同步导致的错误。


package Asynchronous;

//ThreadAsynchronous:线程异步模式访问数据实例

class ShareData1{
	public static String szData = "" ; //声明并初始化字符串数据域
}

class ThreadDemo2 extends Thread{	//定义类
	private ShareData1 oShare;		//声明并初始化oShare数据域
	ThreadDemo2(){}					//声明并实现ThreadDemo2的构造方法
	ThreadDemo2(String szName,ShareData1 oShare){
		super(szName);
		this.oShare = oShare;
	}
	public void run() {
		for(int i = 1;i<5;i++) {
			if(this.getName().equals("Thread1")) {
				oShare.szData= "这是第一个线程";
				try {
					Thread.sleep((int)Math.random()*100);//线程休眠
					
				}catch(InterruptedException e) { //捕获异常
				}
				//输出字符串信息
				System.out.println(this.getName()+":"+oShare.szData);
				
			}else if(this.getName().equals("Thread2")) {
				oShare.szData = "这是第二个线程";
				try {
					Thread.sleep((int)Math.random()*100);//线程休眠
					
				}catch(InterruptedException e) { //捕获异常
				}
				
				//输出字符串信息
				System.out.println(this.getName()+":"+oShare.szData);
			}
			
			
		}
	}
}

public class ThreadAsynchronous {
	
	public static void main(String[] args) {
		
		ShareData1 oShare = newShareData1();
		ThreadDemo2 th1 = new ThreadDemo2("Thread1",oShare);
		ThreadDemo2 th2 = new ThreadDemo2("Thread2",oShare);
		th1.start();
		th2.start();
	}

	private static ShareData1 newShareData1() {
		// TODO Auto-generated method stub
		return null;
	}

}

在这里插入图片描述
程序期待结果是 “Thread1:这是第二个线程 ” 或 “ Thread2:这是第二个线程 ”。但是线程对数据的异步操作导致运行结果出现了差错。

这是由于线程不同步导致的错误。为了解决此类问题,java提供了锁机制来实现线程的同步。锁机制的原理是每个线程在机内共享代码之前获得锁,否则不能进入共享代码区。并且在推出共享代码之前释放锁。从而解决多个线程竞争共享代码的问题,达到了实现线程同步的目的。

Java中锁机制的实现方法时在共享代码之前加入synchronized关键字

Java专门提供了负责管理线程对象中方同步方法访问的工具——同步模型监视器。其原理是为每个具有同步代码的对象准备一把“锁”。通过wait(),notify(),notifyAll()方法可以完成线程间的消息传递。

4.2 同步的格式

只有把一个语句块声明为synchronized,在同一时间,它的访问线程之一才能执行该语句块。用关键字synchronized可将方法声明为同步

语法格式
class 类名{
		public synchronized 类型名称 方法名称(){
		......
	}
}
对于同步块,synchronized获取的是参数中的对象锁
synchornized(obj){ 

}

当线程执行到这里的同步块时,必须获取obj对象的锁才能控制同步块,否则线程只能等待获得锁。需要注意的是,obj对象的作用范围不同,控制情况也不尽相同。

- 创建不了锁

public void method(){
	Object obj =  new  Object();
	synchronized(obj){
		....
	}
}

因为创建了一个对象obj。每此new一个Object,都会产生一个obj对象。
每个obj对象都能得到锁。锁不起作用

- 可以创建锁

class method{
	{
		Object o = new Object();
		public void test(){
			synchronized(o){}
		}
	}
}

当一个线程访问某个对象的synchronized(this)同步块时,另一个线程必须等待该线程执行完此同步块,其他线程可以访问该对象中的非synchronized(this)同步块。
如果类中包含多个synchronized(this)同步块。而且同步线程中一个线程访问其中一个同步线程,其他线程不能访问该对象所有synchronized(this)同步块。

4.3 同步的应用

【实例】使用synchornized“线程赛跑”问题实例
该实例,使用synchronized"线程赛跑"问题的例子。线程首先创建一个共享数据域oShare,然后分别创建两个线程访问共享数据

package aaa;
//多线程不同步的解决方法-使用synchronized


class ShareData{
	public static String szData = "";//声明并初始化字符串数据域szData
}

class ThreadDemo extends Thread{
	private ShareData oShare;    //声明shareData数据域oShare
	ThreadDemo(){}		//声明并实现无参函数构造方法
	//声明并实现代餐函数构造方法
	ThreadDemo(String szName,ShareData oShare){
		super(szName); 	//调用父类的构造方法
		this.oShare = oShare;//初始化oShare域
	}
	public void run() {
		//同步块,并指出同步数据oShare
		synchronized(oShare) {	//指定同步块,给oShare加锁
			for(int i = 0;i<5;i++) { //循环执行
				if(this.getName().equals("Thread1")){//当前线程是Thread1
					oShare.szData = "这是第一个线程";
					//为了演示产生问题,设置一次休眠
					try {
						Thread.sleep((int)Math.random()*50);
					}catch(InterruptedException e) {//捕获异常
						}
					//输出字符串信息
					System.out.println(this.getName()+":"+oShare.szData);
				}else if(this.getName().equals("Thread2")){//当前线程是Thread2
					oShare.szData = "这是第二个线程";
					//为了演示产生问题,这里设置一次休眠
					try {
						Thread.sleep((int)Math.random()*50);
					}catch(InterruptedException e){
					}
					//输出字符串信息
					System.out.println(this.getName()+":"+oShare.szData);
				}
			}
		}
	}
}


public class ToSolveThreadRace {	
	public static void main(String[] args) {
		ShareData oShare = new ShareData();	//创建并初始化对象oShare
		ThreadDemo th1 = new ThreadDemo("Thread1",oShare); //创建线程th1
		ThreadDemo th2 = new ThreadDemo("Thread2",oShare);//创建线程th2
		th1.start();
		th2.start();
	}

}

在这里插入图片描述
和上个实例相比,区别在于利用同步块实现两个线程的同步问题,声明了两个线程th1和th2并启动线程,并在run()方法中包括同步块。最重要的就是使用synchornized定义同步块,当程序启动线程th1之后,th1获得对象的锁,直到th1结束之后,th2才获得对象的锁,并继续执行。

*两个线程都在等待对方释放各自拥有的锁的现象称为死锁。这种现象,往往是由于相互嵌套的synchronized代码块造成的,隐藏,在程序中应该尽量少用嵌套的synchronized代码块*

5 线程通信

我们在工作中遇到难题时,可以找朋友或者量领导沟通。通过他人的协作解决难题。我们自己、我们的朋友、我们的领导都可以看做独立的线程。平常自己做的事情,碰到难题时通过电话、QQ进行沟通,共同解决难题
多线程之前可以通过消息通信。以达到相互协作的目的。java中的线程之间的通信是通过Object类中的wait(),notify(),notifyAll()等方法实现的。java中,每个对象内部除了有一个对象锁,还有一个线程等待队列。这个队列用于存放所有等待对象锁的线程。

5.1 生产者/消费者

生产者/消费者模式是一个 很好的线程通信的例子。生产者在一个循环中不断生产共享数据,而消费者在不断地消费生产者产生的数据。
二者之间的关系可以很清楚的表明,需要现有生产者生产共享数据,才能有消费者消费共享数据。生产者与消费者模式结构图如下图所示。
在这里插入图片描述

举例:一个通过寄信的粒子来说明这个模式

  • 把信写好:生产者制造数据
  • 把信放入邮筒:生产者把数据放入缓冲区
  • 邮递员把信从油桶中取出:消费者把数据从缓冲区中取出
  • 邮递员把信拿去邮局做相应的处理:消费者处理数据
    注意:程序必须保证在消费者消费之前有共享数据。如果没有,则消费者必须阻塞等待产生新的共享数据。

生产者和消费者之间的数据关系如下:

  • 生产者生产前,如果共享数据没有被消费,则生产者等待;生产者生产后,通知消费者消费
  • 消费者消费前,如果共享数据已经被消耗完,则消费者等待,消费者消费后,通知生产者生产

由此可见,生产者和消费者之间存在矛盾。为了解决两者间的矛盾,java引入了等待/通知(wait/notify机制)。等待使用wait()方法,通知生产者生产使用notifyAll()或者notify()方法

生产者/消费者实现代码

class Producer extends Thread{			//实现生产者线程
	Queue q;											//声明
	//生产者构造方法
	Producer(Queue q){
		this.q = q;
	}
	public void void run(){
		for(int i = 1;i<5;i++){//产生新元素
			q.put(i);//加入队列
		}
	}
}

class Consumer extends Thread{ //实现消费者线程
		Queue q; 	//声明队列Q
		Consumer(Queue q){//消费者构造方法
		this.q = q;	//队列初始化
	}
		public void void run(){
		while(true){//循环消费数据
			q.get(i);//获取队列中元素
		}
	}
}

Producer是从Thread派生的线程,是一个生产者类,提供一个以共享队列作为参数的构造方法,它的run()方法,循环产生新的元素,并将元素添加到共享队列。Consumer也是从Thread派生的线程,是一个消费者类。消费者类提供一个以共享队列作为参数的构造方法,他的run()方法循环消费元素,并获得队列中的元素

5.2 共享队列

  • 共享单车是目前很热门的一个项目。这些单车属于所有用户的共享资源,用户可以在任何地方通过分时租赁模式使用单车。正如共享单车的共享模式,在生产者与消费者模式中,共享队列类用于保存生产者生产、消费者消费等共享数据。
  • 共享队列有两个域:value(元素的数目) 和isEmpty(队列的状态)。共享队列提供了put()和get()两个方法。

运行生产者/消费者

【例】生产者/消费者实例
这个程序创建了一个共享队列,一个生产者线程,一个消费者线程,分别调用线程的start()方法启动这两个线程。

在这里插入图片描述

package example;
//功能:实现多线程间的通讯
public class ProducerAndConsumer {
	public static void main(String[] args) {
		Queue q = new Queue();//创建一个队列
		Producer p = new Producer(q);//创建一个生产者
		Consumer c = new Consumer(q);//创建一个消费者
		
		c.start(); //启动消费者线程
		p.start(); //启动生产者线程
	
	}

}

package example;

public class Queue { //共享队列的目的是用于保存生产者生产和消费者消费的共享数据

	int value = 0; //定义变量value并赋值
	boolean isEmpty = true;//定义变量isEmpty并赋值
	//value(元素的数目)、isEmpty(队列的状态) 
	//定义函数put()
	public synchronized void put(int v){//synchronized保证在同一时刻最多只有一个线程执行该代码
		if(!isEmpty) { //判断isEmpty状态
			try {
				System.out.println("生产者等待");
				wait();//等待
			}catch(Exception e) {
				e.printStackTrace();//捕捉异常
			}
		}
		value += v;//根据输入参数v的值改变value
		isEmpty = false;
		System.out.println("生产者生产总数量"+v);
		notify();
		
	}


	public synchronized int get(){//synchronized保证在同一时刻最多只有一个线程执行该代码
		if(isEmpty) { //判断isEmpty状态
			try {
				System.out.println("消费者等待");
				wait();//等待
			}catch(Exception e) {
				e.printStackTrace();//捕捉异常
			}
		}
		value --;//根据输入参数v的值改变value
		if(value<1) {
			isEmpty = true;
		}
		System.out.println("消费者消费一个,剩余"+value);//打印剩余
		notify();//调用通知函数notify()
		return value;
		
	}
	
	

}

package example;

class Producer extends Thread{			//实现生产者线程
	Queue q;											//声明
	//生产者构造方法
	Producer(Queue q){
		this.q = q;
	}
	public void run(){
		for(int i = 1;i<5;i++){//产生新元素
			q.put(i);//加入队列
		}
	}
}

package example;

class Consumer extends Thread{ //实现消费者线程
	Queue q; 	//声明队列Q
	Consumer(Queue q){//消费者构造方法
	this.q = q;	//队列初始化
}
	public void run(){
	while(true){//循环消费数据
		q.get();//获取队列中元素
	}
}
}

在这里插入图片描述
线程进入等待状态后,如果没有其他的线程被唤醒,除非强制退出JVM环境,否则他会一直等待。
考虑到程序的安全性,多数情况下使用notigyAll()方法,除非明确直到唤醒哪一个线程
wait()方法调用的前提条件式当前线程获得了这个对象的锁,也就是说wait()方法必须放在同步块或同步方法中。

6.死锁

  • 生活中,如果两个小朋友打架,厮打在一起互不想让,就会发生死锁的情况
  • 在线程进入不可运行的状态时,其他线程无法访问那个加锁的对象,所以一个线程会一直处于等待另一个线程的状态,而另一个线程与会处于等待下一个线程的状态,此时所有的线程都陷入无休止的等待状态中,无法继续运行下去,这种情况被叫做死锁。
    死锁一般不会发生,但是一旦出现,就会调试困难,且不容易查错。
package aaa;

public class ThreadDeadLock implements Runnable {
	public static boolean flag = true; //定义标志变量flag
	private static Object A = new Object(); //声明并初始化静态Object数据域A
	private static Object B = new Object();	//声明并初始化静态Object数据域B
	
	public static void main(String[] args) throws InterruptedException{
		Runnable r1 = new ThreadDeadLock();	//创建并初始化ThreadLocked对象r1
		Thread t1 = new Thread(r1);	//创建线程t1
		Runnable r2 = new ThreadDeadLock();	//创建并初始化ThreadLocked对象r2
		Thread t2 = new Thread(r1);	//创建线程t2
		t1.start();
		t2.start();
	}
	
	public void AccessA() {
		flag = false;//初始化域flag
		//同步块
		synchronized(A) {//声明同步块,给对象A加锁
			System.out.println("线程t1:得到了A的锁");
			try {
				//当前线程休眠,另一个线程可以先得到对象B的锁
				Thread.sleep(1000);
				
			}catch(InterruptedException e) {//捕获异常
				e.printStackTrace();//输出异常信息
			}
			//在得到A的锁之后,又想得到B的锁
			System.out.println("线程t1:还想要得到B的锁");
			//在同步块内部嵌套同步块
			synchronized(B) { //声明内部嵌套同步块,指定对象B的锁
				System.out.println("线程t1:我得到了B的锁");
				
			}
			
		}
	}
	
	
	public void AccessB() {
		flag = false;//修改flag的值
		//同步块
		synchronized(A) {//指定同步块,给对象B加锁
			System.out.println("线程t2:得到了B的锁");
			try {
				//当前线程休眠,另一个线程可以先得到对象A的锁
				Thread.sleep(1000);
				
			}catch(InterruptedException e) {//捕获异常
				e.printStackTrace();//输出异常信息
			}
			//在得到B的锁之后,又想得到A的锁
			System.out.println("线程t2:还想要得到A的锁");
			//在同步块内部嵌套同步块
			synchronized(A) { //声明内部嵌套同步块,指定对象B的锁
				System.out.println("线程t2:我得到了A的锁");
				
			}
			
		}
	}
	public void run() {
		if(flag) {	//当flag为true时,执行下面的语句
			AccessA(); //调用AccessA()方法
		}else {
			AccessB();//调用AccessB()方法
		}
	}


	
	
}

在这里插入图片描述
在运行过程中,线程t1先得到A的锁,然后要求获得B的锁。而线程t2先获得B的锁,再要求去获得A的锁。这时他们两个就会进入无休止的等待状态,即死锁状态。
java语言本身并没有提供防止死锁的具体办法,在具体的程序设计中要注意避免出现死锁,通常不要出现stop()、suspend()、resume()、destroy()方法

  • stop()方法不安全,因此它会解除由该线程获得的所有对象锁,使对象处于不连贯状态,如果其他线程此时访问该对象,那么由此造成的错误很难被检查出来。
  • suspend()/resume()方法不安全。调用suspend()时,线程会停下来,但是该线程没有并没有放弃对象的锁。从而导致其他线程不能获得对象锁。
  • 调用destroy()方法会强制终止线程,但是这个线程也不会释放对象锁。

7.拓展训练

【拓展要点:线程休眠和唤醒】
在NBA赛场上,我们经常为高水平的篮球运动员呐喊助威。但是在高手如林的篮球比赛中,主教练如何进行比赛队员的安排,才能获取胜利,是一个需要考虑的问题。

使用线程的休眠和唤醒方法,来模拟篮球运动员在比赛中的比赛安排。线程的休眠是指让正在运行的线程暂停一段时间,进入不可运行的状态。当线程进入不可运行的状态后,在其休眠的时间内,该线程不会执行。在Java中,通过调用Thread类的静态方法sleep()来实现线程的休眠。

package example;
import java.text.*;
import java.util.*;

public class ThreadSleepWake extends Thread{

	private DateFormat dateformat = new SimpleDateFormat("ss:SS");
	public static void main(String[] args) {
		ThreadSleepWake mythread = new ThreadSleepWake();//线程实例化对象
		mythread.start();//线程启动
		try {
			mythread.join();//等待线程运行结束
		}catch(InterruptedException e) {
			System.out.println("收到主教练命令,准备上场"+e.getMessage());
		}
		mythread.incident();//调用方法来判断是否唤醒
		
	}
	
	public void incident() {
		Thread.currentThread().interrupt();//唤醒当前线程
		while(true) {
			if(Thread.currentThread().isInterrupted()) {//判断当前线程是否被唤醒
				System.out.print(dateformat.format(new Date())+"比赛开始,现在是否正在准备上场?");
				System.out.println(Thread.currentThread().isInterrupted() ? "是":"没有");
				try {
					Thread.currentThread();
					Thread.sleep(5000);//线程休眠5s
				}catch(InterruptedException e) {//捕获唤醒异常
					System.out.println(dateformat.format(new Date())+"收到主教练命令,停止休息:"+e.getMessage());
					
				}
				System.out.print(dateformat.format(new Date())+"该比赛结束后是否参加下一轮比赛?");
				System.out.println(Thread.currentThread().isInterrupted() ? "是":"不参加");
			}
		}
	}
	
	public void run() {
		System.out.println("第一场比赛结束的时间为:"+dateformat.format(new Date()));
		System.out.println("休息4小时");
		try {
			sleep(2000);//线程休眠2秒,假设1s代表1h
		}catch(InterruptedException e){
			System.out.println(dateformat.format(new Date())+"收到主教练命令,准备上场"+e.getMessage());
		}
		System.out.println(dateformat.format(new Date())+"在休息的过程中,是否又参加了其他的比赛?");
		try {
			sleep(2000);//线程休眠2秒,假设1s代表1h
		}catch(InterruptedException e){
			System.out.println(dateformat.format(new Date())+"收到主教练命令,准备上场"+e.getMessage());
		}
		//线程是否激活?false表示没有激活
		System.out.println(!isAlive()?"参加比赛":"没有参加比赛");
		interrupt();//唤醒线程
		System.out.println(dateformat.format(new Date())+"休息中,替补队员受伤,是否参加比赛?");
		
		System.out.println(isAlive()?"参加比赛":"不参加比赛");
	}

}

在这里插入图片描述
使用sleep()方法根据需要暂停某个线程的运行,在适当的时候再恢复其运行。
Thread类的sleep()方法可以让当前线程休眠一定的时间,这时线程由可运行状态变成不可运行状态;等待停止执行时间结束后,线程再重新进入可运行状态。
sleep()方法的两种语法格式:


【拓展要点:线程同步】
线程同步是指多个线程同时访问某一个共享资源时,要求在同一时刻最多只能有一个线程访问资源。不允许出现不同线程在同一时刻对同一个数据进行操作的情况。
例如,在ATM机上,同一时刻只能有一个用户在使用。其他用户只能排队等候。
这个拓展训练时使用ATM采用线程同步与否来展示synchronized关键字的用法。

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

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

相关文章

【大数据技术基础 | 实验一】配置SSH免密登录

文章目录 一、实验目的二、实验要求三、实验原理&#xff08;一&#xff09;大数据实验一体机&#xff08;二&#xff09;SSH免密认证 四、实验环境五、实验内容和步骤&#xff08;一&#xff09;搭建集群服务器&#xff08;二&#xff09;添加域名映射&#xff08;三&#xff…

工业物联网关-ModbusTCP

Modbus-TCP模式把网关视作Modbus从端设备&#xff0c;主端设备可以通过Modbus-TCP协议访问网关上所有终端设备。用户可以自定义多条通道&#xff0c;每条通道可以配置为TCP Server或者TCP Slave。注意&#xff0c;该模式需要指定采集通道&#xff0c;采集通道可以是串口和网口通…

51WORLD携手浙江科技大学,打造智慧校园新标杆

当前&#xff0c;国家教育数字化战略行动扎实推进&#xff0c;高等教育数字化转型步伐加快。 紧抓数字教育发展战略机遇&#xff0c;浙江科技大学联合51WORLD、正方软件股份有限公司&#xff08;简称&#xff1a;正方软件&#xff09;&#xff0c;共同研发打造浙科大孪生数智校…

为什么很多人宁愿加钱买港版,也不愿买国行 iPhone 16

最近的 iPhone 16 市场&#xff0c;真的是倒反天罡&#xff0c;攻守异形啊。 过去&#xff0c;港版 iPhone 都是性价比的次选&#xff0c;便宜个 10% 都得考虑考虑。但今年&#xff0c;港版 iPhone 16 的价格&#xff0c;反而比国行还贵。 比如&#xff0c;闲鱼上某个卖家&am…

Java消息摘要:MD5验证数据完整性、密码的加密与校验

MD5&#xff08;Message-Digest Algorithm 5&#xff09;是一种被广泛使用的密码散列函数&#xff0c;是一种不可逆的加密算法&#xff0c;该算法可以产生出一个128位&#xff08;16字节&#xff09;的散列值&#xff08;hash value&#xff09;&#xff0c;用于确保信息传输完…

【工具变量】知识产权示范城市DID(2000-2023年)

数据简介&#xff1a;为深入贯彻落实《国家知识产权战略纲要》&#xff0c;强化知识产权治理效能&#xff0c;国家知识产权局制定了《国家知识产权试点、示范城市&#xff08;城区&#xff09;评定和管理办法》&#xff0c;知识产权示范城市成为知识产权强国战略落地的城市支撑…

echarts 入门

工作中第一次碰到echarts&#xff0c;当时有大哥。二进宫没办法&#xff0c;只能搞定它。 感觉生活就是这样&#xff0c;不能解决的问题总是会反复出现。通过看视频、查资料&#xff0c;完成了工作要求。写一篇Hello World&#xff0c;进行备查。 基本使用 快速上手 <!DO…

QNAP新手必看!轻松搞定反向代理设置

反向代理是一种服务器配置&#xff0c;允许你通过一个域名或者IP地址来访问不同的内部应用服务。在QNAP NAS上配置反向代理可以提升应用程序的安全性和可访问性。 准备工作 确保QNAP NAS已连接网络并有公网IPv4/IPv6。 确认已启用Web服务 步骤 1&#xff1a;启用Web服务 登…

相机光学(三十九)——光学暗角与机械暗角

1.什么是暗角 在玩摄影一段时间,拍摄一定数量的照片之后,每个人都会不可避免地遇上一个新问题,那就是暗角现象。所谓暗角,是指在拍摄亮度均匀的场景时,画面的四角却出现与实际景物不符的、亮度降低的现象,又被称为“失光“。 2.暗角的成因 (1)边角的成像光线与镜头光轴…

【智能控制】第2章 专家系统,专家控制,模糊关系,模糊推理,专家PID控制

目录 2.1 专家系统 2.1.1 专家系统概述 2.1.2 专家系统构成 2.1.3 专家系统的建立 1&#xff0e;知识库 2&#xff0e;推理机 3&#xff0e;知识的表示 4&#xff0e;专家系统开发语言 5&#xff0e;专家系统建立步骤 第二节 专家控制 2&#xff0e;功能 3 与专家…

三、账号密码存储

使用Playfers存储 Unity本地持久化类Playerprefs使用详解 - PlaneZhong - 博客园 (cnblogs.com) 一、登陆界面切换 1、登陆界面的脚本&#xff08;机制类脚本&#xff09; 在这个UI上挂载一个脚本LoginWnd 先声明一下这个脚本&#xff0c;拖拽 2、在登录模块中调用 这里的l…

华为全联接大会2024 | 聚焦运维智能化,麒麟信安分享“基于大模型的新一代智能运维平台”

2024年9月19日至21日&#xff0c;以“共赢行业智能化”为主题的华为全联接大会2024在上海世博中心盛大召开。麒麟信安受邀出席大会&#xff0c;与全球的思想领袖、商业精英、技术专家和合作伙伴&#xff0c;共同探讨智能化、数字化技术赋能千行万业&#xff0c;把握新机遇&…

第十五章 Java多线程--线程池

目录 一、线程池基础概念 常见的线程池类型&#xff1a; 创建线程池的例子&#xff1a; 注意事项&#xff1a; 二、线程池使用场景 三、JDK自带的构建线程池的方式 1 newFixedThreadPool 2 newSingleThreadExecutor 3 newCachedThreadPool 4 newScheduleThreadPool …

震撼!一句话就让 AI 帮你做 UI 测试,多模态测试智能体 AUITestAgent 横空出世!

美团到店研发平台携手复旦大学周扬帆教授团队&#xff0c;共同开发了智能化终端测试工具AUITestAgent。该工具是第一个能够基于自然语言测试用例&#xff0c;自动化完成终端UI测试驱动、校验全流程的智能化测试工具。仅需输入自然语言形式的测试需求&#xff0c;AUITestAgent通…

福建谷器参加泉州市中小企业数字化转型试点工作启动会

为进一步加快推动试点城市工作,10月9日,泉州市产业数字化转型工作现场会暨2024年中小企业数字化转型试点工作启动会成功举办。出席本次会议的有福建省工业和信息化厅副厅长许永西、泉州市人民政府副市长雷连鸣等领导,及来自国家工业信息安全发展研究中心、中国工业互联网研究院…

newlibc memcpy 存在问题

背景 sdk 中发现 memcpy 函数没有达到预期&#xff0c;执行后&#xff0c;目的地址与源地址中的内容不一致。 复现方法 通过单步调试 memcpy 汇编代码&#xff0c;发现使用了 ldrh 指令&#xff0c;该指令在对 uncacheable memory 同时该 memory 非对齐的情况下&#xff0c;…

高性能计算平台(HPC)如何选型

选型高性能计算平台&#xff08;HPC&#xff09;非常复杂&#xff0c;需要考针对行业的痛点等多个因素进行考虑&#xff0c;来确保平台系统能满足特定行业和应用的需求。下面为大家列举了几个方面&#xff0c;大家可以参考。 1.计算需求 首先需要了解你需要处理的数据类型、计算…

如何给照片加文字?几个方法帮助你给照片轻松填字

如何给照片加文字&#xff1f;几个方法帮助你给照片轻松填字 给照片添加文字是常见的图片处理需求&#xff0c;尤其是在社交媒体、海报设计和个人相册制作中。以下是5款常用软件&#xff0c;它们能够帮助你轻松在照片上添加文字&#xff0c;满足不同层次的用户需求&#xff0c…

os镜像包一键安装

5、解压后点击运行旗胜PE维护系统。 6、插入U盘识别到U盘后点那个蓝色的制作启动U盘就可以了&#xff0c;设置用默认的就行不用改。 7、正在制作启动U盘中大约5分钟左右。 8、出现这个页面就制作完成了&#xff0c;可以关掉这个软件了 9、打开启动U盘就可以看到里面有os文件夹跟…

Python酷库之旅-第三方库Pandas(145)

目录 一、用法精讲 656、pandas.Timestamp.resolution属性 656-1、语法 656-2、参数 656-3、功能 656-4、返回值 656-5、说明 656-6、用法 656-6-1、数据准备 656-6-2、代码示例 656-6-3、结果输出 657、pandas.Timestamp.second属性 657-1、语法 657-2、参数 6…