多线程--身兼数职

多线程

本章技能目标

  • 理解线程的概念
  • 掌握线程的创建和启动
  • 了解线程的状态
  • 掌握线程调度的常用方法
  • 掌握线程的同步
  • 理解线程安全的类型

多任务的定义是可并发运行一个或多个程序的能力。尽管所有程序好像在同时运行,但事实并非如此。操作系统通过调度程序来控制程序的运行方式。由于程序切换占用的时间极少,因此看上去所有的程序好像在同时工作。

在早期的计算机中时没有操作系统的,计算机开启后只能执行一个程序,直到结束。操作系统的出现使得计算机可以同时执行多个程序,操作系统为每个程序分配不同的进程,每个进程拥有独立的句柄、资源等,使得计算机可以同时执行多个程序。但是进程的创建和销毁耗费的代价太大,因此衍生出线程的概念。

允许在一个进程中创建多个线程,这些线程共享进程的资源,并且每个线程拥有自己独立的程序计数器、线程局部变量等资源。线程也被称为进程的轻量型运动实体。

1. 线程概述

1.1 进程

进程(Process)是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。

多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。

进程的特点是:

  • 进程是系统运行程序的基本单位
  • 每一个进程都有自己独立的一块内存空间、一组系统资源
  • 每一个进程的内部数据和状态都是完全独立的

1.2 线程

进程中要同时干几件事时,每一件事的执行路径成为线程。

多线程是指有多条线程并发的执行。

并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事

并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事

在这里插入图片描述

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核数越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1.3 多线程的好处

多线程作为一种多任务并发的工作方式,有着广泛的应用,合理地使用线程,将减少开发和维护地成本,甚至可以改善复杂应用程序的性能,使用多线程的优势如下:

  1. 充分利用CPU的资源:执行单线程程序时,若程序发生阻塞,CPU可能会处于空闲状态,这将造成计算机资源的浪费,而使用多线程可以在某个线程处理休眠或阻塞状态时运行其他线程,这样,大大提高了资源利用率。
  2. 简化编程模型:一个既长又复杂的进程可以考虑分为多个线程,成为几个独立的运行部分。如使用时、分、秒个使用一个线程控制,每个线程仅需要实现简单的流程,简化程序逻辑,这样更有助于开发人员对程序的理解和维护。
  3. 带来良好的用户体验,由于多个线程可以交替执行,减少或避免了因程序阻塞或意外情况造成的响应过慢现象,降低了用户等待的概率

多线程虽然带来了许多优势,但是使用不当也会引入许多问题。

  1. 安全性:多线程同时访问操作共享资源会造成不可预料的结果。
  2. 活跃性问题:由于多线程争夺资源导致无限循环,出现死锁、活锁、线程饥饿等问题。
  3. 性能问题:多线程的引入是为了提高处理器的性能,但是滥用线程不仅不能提高性能反而会导致性能下降。如创建过多的线程使CPU频繁地去执行线程切换导致性能降低。

2. 在Java中实现多线程

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

2.1 Thread类及其常用的方法

说 明
Thread()分配新的Thread对象
Thread(Runnable target)分配新的Thread对象,target为run()方法被调用的对象
Thread(Runnable target,String name)分配新的Thread对象,target为run()方法被调用的对象,name为新线程的名称
start()1.启动当前线程2.调用线程中的run方法
run()通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread()静态方法,返回执行当前代码的线程
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程
boolean isAlive()测试线程是否处于活动状态

2.2 主线程

当Java程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),因为它是程序开始时就执行的

主线程的重要性体现在两方面:

  • 它是产生其他子线程的线程
  • 通常它必须最后完成执行,因为它执行各种关闭动作。

尽管主线程在程序启动时自动创建,但它可以由一个Thread对象控制。为此,你必须调用方法currentThread()获得它的一个引用,currentThread()是Thread类的公有的静态成员。它的通常形式如下:

 static Thread currentThread( )

该方法返回一个调用它的线程的引用。一旦你获得主线程的引用,你就可以像控制其他线程那样控制主线程。

示例如下:

public static void main(String args[]) {
    Thread t= Thread.currentThread(); 
    System.out.println("当前线程是: "+t.getName()); 
    t.setName("MyJavaThread"); 
    System.out.println("当前线程名是: "+t.getName()); 
}

开发中,用户编写的线程一般都是除了主线程之外的其他线程。使用一个线程的过程,可以分为四个步骤:

  1. 定义一个线程,同时指明这个线程所要执行的代码,即期望完成的功能。
  2. 创建线程对象
  3. 启动线程
  4. 终止线程

创建线程有两种方式:一种是继承Thread类,另一种是实现Runnable接口。

2.3 继承Thread类创建线程

使用此方法创建线程类,步骤如下:

  • 定义MyThread类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

示例代码如下:

public class MyThread extends Thread{
    //重写run()方法
	public void run(){
		for(int i=1;i<100;i++){
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

编写测试类如下:

public class Test {
	public static void main(String[] args) {
		MyThread thread = new MyThread();
		thread.start(); //启动线程
	}
}

2.4 实现Runnable接口创建线程

使用Thread类的缺点是当定义的类继承了其他类,则无法再继承Thread类。使用Runnable接口创建线程的方式可以解决上述问题。

步骤如下:

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

演示代码如下:

class MyRunnable implements Runnable{
	//实现run()方法
	public void run(){
		for(int i=1;i<100;i++){	
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

测试类代码如下:

public class Test {
	public static void main(String[] args) {
		MyRunnable myRunnable = new MyRunnable();
		Thread myThread = new Thread(myRunnable);		
		myThread.start();
	}
}

使用匿名内部类简化线程的创建方式如下:

public class Test {
	public static void main(String[] args) {
		Thread myThread = new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i=1;i<100;i++){	
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});		
		myThread.start();
	}
}

使用Lambda表达式简化代码如下:

public class Test {
	public static void main(String[] args) {
		Thread myThread = new Thread(()-> {
				for(int i=1;i<100;i++){	
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		);		
		myThread.start();
	}
}

3. 线程的状态

任何一种线程一般都具有五种状态:创建、就绪、运行、阻塞、死亡状态。线程状态的转移与方法之间的关系如图所示:

在这里插入图片描述

1. 创建状态

创建一个线程对象后就处于创建状态,但还没有处于可运行状态,这时可以通过Thread类的方法来设置线程对象的属性,如设置线程名和优先级等

2. 就绪状态

线程创建之后,就可以调用start()方法启动线程,即进入就绪状态。此时线程将进入线程队列排队,等待CPU资源,这表明它已经具备了运行条件。

3. 运行状态

当就绪状态的线程获得CPU资源时,即可转入运行状态执行run方法。

4. 阻塞状态

阻塞:一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。

可能使线程暂停执行的条件如下:

  1. 线程优先级较低
  2. sleep()方法使线程休眠
  3. 调用wait()方法使线程等待
  4. 调用yield()方法,线程显示出让CPU控制权。
  5. 线程由于等待一个文件,I/O时间被阻塞

5. 死亡状态

一个线程的run()方法运行完毕则进入死亡状态。

使用代码演示线程可能具有的某些状态:

/**
 * 描述线程的状态
 */
public class Mythread implements Runnable{
	public void run() {
		try {
			System.out.println("线程t在运行!");
			Thread.sleep(500);
			System.out.println("线程t在短时间睡眠后重新运行!");
		} catch (InterruptedException IE) {
			System.out.println("线程被中断");
		}
	}
	public static void main(String args[]) {
		Thread t=new Thread(new Mythread());
		 System.out.println ("线程 t 为新建!");		
		 t.start();
		 System.out.println ("线程 t 为就绪!");
	}
}

4. 线程的调度

线程调度指按照特定机制为多个线程分配CPU的使用权

在Java实现多线程应用程序中,可以通过Thread类的方法实现对线程对象的调度。常用的方法如下:

说 明
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程
boolean isAlive()测试线程是否处于活动状态

下面重点介绍这些方法的使用:

4.1 线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。 Java 线程的优先级是一个整数,其取值范围是 1(Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

演示代码如下:

/**
 * 设置线程优先级
 *
 */
public class MyThread implements Runnable{
	public void run(){	
		for(int i=1;i<100;i++){			
			System.out.println(Thread.currentThread().getName()+"正在运行:"+i);
		}
	}
	public static void main(String[] args) {
		Thread t1 = new Thread(new MyThread(),"线程A");//通过构造方法指定线程名
		Thread t2 = new Thread(new MyThread(),"线程B");
		//设置线程的优先级(对比演示设置前后设置后的结果)
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.MIN_PRIORITY);
		System.out.println("****线程的优先级****");
		System.out.println("线程A的优先级:"+t1.getPriority());
		System.out.println("线程B的优先级:"+t2.getPriority());
		System.out.println("****************");
		t1.start();	
		t2.start();		
	}
}

4.2 线程的休眠

在程序中允许一个线程进行暂时的休眠,直接使用Thread.sleep()方法即可以实现线程的休眠。sleep() 方法定义语法如下:

public static void sleep(long millis)
  • 让线程暂时睡眠指定时长,线程进入阻塞状态

  • 睡眠时间过后线程会再进入可运行状态

  • millis为休眠时长,以毫秒为单位

  • 调用sleep()方法需处理InterruptedException异常

案例演示如下:

/**
 * 线程休眠
 */
public class ThreadSleepDemo {
	public static void main(String[] args) {
		System.out.println("Wait");
		Wait.bySec(5); // 让主线程等待5秒种再执行
		System.out.println("start");
	}
}

class Wait {
	public static void bySec(long s) {
		for (int i = 0; i < s; i++) {
			System.out.println(i + 1 + "秒");
			try {
				Thread.sleep(1000); // 睡眠1秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}
	}
}

4.3 线程的强制运行

join()方法使当前线程暂停执行,等待其他线程结束后再继续执行本线程。语法如下:

public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)
  • millis:以毫秒为单位的等待时长
  • nanos:要等待的附加纳秒时长
  • 需处理InterruptedException异常

演示示例如下:

class MyThread implements Runnable{
	public void run(){
		for(int i=0;i<10;i++){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			//输出当前线程的信息
			System.out.println(Thread.currentThread().getName()+"运行:"+i);
		}
	}	
}

/**
 * 线程强制执行
 */
public class ThreadJoinDemo {
	public static void main(String[] args) {
		System.out.println("*****线程强制执行******");
		//创建子线程并启动
		Thread temp = new Thread(new MyThread());
		temp.start();
		for(int i=0;i<20;i++){
			if(i==5){				
				try {
					//阻塞主线程,子线程强制执行
					temp.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) 
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"运行:"+i);
		}
	}	
}

运行结果如图所示:
在这里插入图片描述

从运行结果分析,当主线程运行到i的值为5时,子线程调用join()方法后,阻塞主线程,直到子线程运行完毕后,主线程才能继续执行。

4.4线程的礼让

yield()方法用于暂停当前线程,允许其他具有相同优先级的线程获得运行机会,该线程仍处于就绪状态,不转为阻塞状态

语法如下:

public static void yield()

注意:使用yield()只是提供一种可能,但是不能保证一定会实现礼让

演示代码如下:

class MyThread implements Runnable{
     public void run(){
    	 for(int i=0;i<5;i++){
    		 System.out.println(Thread.currentThread().getName()+"正在运行:"+i);
    		 if(i==3){
    			System.out.print("线程礼让:");
				Thread.yield();
    		 }
    	 }
     }
}
	
//测试代码如下
public class ThreadYieldDemo {
	public static void main(String[] args) {
		System.out.println("*****线程的礼让*****");
		MyThread my = new MyThread();
		Thread t1 = new Thread(my,"线程A");		
		Thread t2 = new Thread(my,"线程B");
		t1.start();
		t2.start();
	}
}

运行效果如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0sGqDEu-1607138266050)(JAVAOOP-CH07.assets/image-20200907145834838.png)]

5. 线程的同步

5.1 多线程共享数据引发的安全问题

使用多线程模拟网络购票,每个人可以看作是一个线程,购票过程是线程的执行体,而每售出一张票后,总票数就会减少。假定有3个人抢10张票,实现思路如下:

(1) 定义线程类Site模拟售票网站

(2) 实现run()方法

(3) 定义测试类模拟多人抢票。

在run()方法中模拟售票过程时,需要使用到循环语句,coun作为循环变量。购票过程分成以下两步:

  • 第一步:网站修改网站车票数据,修改当前售票序号(num)以及剩余的票数(count)

  • 第二步:显示出票反馈信息给用户

在两步之间,为模拟网络延时,使用sleep()方法设置线程休眠500毫秒。

示例代码如下:

/**
 * 线程不安全的网络抢票
 *
 */
public class Site implements Runnable{
	private int count=10;  //记录剩余票数	
	private int num = 0;   //记录买到第几张票
	public void  run(){
		while(true){
			try {
				Thread.sleep(500); //模拟网络延时
			} catch (InterruptedException e) {			
				e.printStackTrace();
			}
			//没有余票时,跳出循环
			if(count<=0){
				break;
			}
			//第一步:修改数据
			num++;
			count--;			
			//第二步:显示信息
			System.out.println(Thread.currentThread().getName()+"抢到第"+num+"张票,剩余"+count+"张票!");			
		}
	}
}

测试类代码如下:

public class Test {
	public static void main(String[] args) {
		Site site = new Site();
		Thread person1= new Thread(site,"桃跑跑");
		Thread person2= new Thread(site,"抢票代理");
		Thread person3= new Thread(site,"黄牛党");
		System.out.println("********开始抢票********");
		person1.start();
		person2.start();
		person3.start();
	}
}

运行效果如图所示:
在这里插入图片描述

最终显示效果存在以下问题:

  • 不是从第1张票开始

  • 存在多人抢到一张票的情况

  • 有些票号没有被抢到

多个线程操作同一共享资源时,将引发数据不安全问题。需要使用线程同步来解决此问题。

5.2 线程同步的实现

采用线程同步来控制线程的执行有两种方式,即同步代码方法和同步代码块。这两种方式都是用synchronized关键字实现。

1. 同步方法

使用synchronized修饰的方法控制对类成员变量的访问,语法如下:

访问修饰符 synchronized 返回类型 方法名(参数列表){……}
        或者
synchronized 访问修饰符 返回类型 方法名(参数列表){……}

synchronized就是为当前的线程声明一个锁,此时锁定的是当前类,可以控制类的所有实例的访问。

使用同步方法解决上述示例如下:

/**
 * 使用同步方法解决网络购票问题
 */
public class Site implements Runnable{
	private int count=10;  //记录剩余票数	
	private int num = 0;   //记录买到第几张票
	@Override
	public void run(){
		while(true){
			try {
				Thread.sleep(500); // 模拟网络延时
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if(sale()){
				break;
			}
		}
	}
    // 同步方法:卖票
	public synchronized boolean sale() {	
		if (count <= 0) {
			return true;
		}
		// 第一步:修改数据
		num++;
		count--;
		// 第二步:显示信息
		System.out.println(Thread.currentThread().getName() + "抢到第" + num
				+ "张票,剩余" + count + "张票!");
       if(Thread.currentThread().getName().equals("黄牛党")){
    	   return true;
       }
       return false;
        
	}
}

同步方法的缺陷是:

如果将一个运行时间较长的方法声明成synchronize将会影响效率。

2. 同步代码块

使用synchronized关键字修饰的代码块,称为同步代码块

语法如下:

synchronized(syncObject){
    //需要同步的代码
}

syncObject为需同步的对象,通常为this

效果与同步方法相同

示例代码如下:

/**
 * 同步后的网络购票
 */
public class Site implements Runnable {
	private int count = 10; // 记录剩余票数
	private int num = 0; // 记录买到第几张票

	public void run() {
		while (true) {
			try {
				Thread.sleep(500); // 模拟网络延时
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//同步代码块
			synchronized (this) {
				// 没有余票时,跳出循环
				if (count <= 0) {
					break;
				}
				// 第一步:修改数据
				num++;
				count--;
				// 第二步:显示信息
				System.out.println(Thread.currentThread().getName() + "抢到第"
						+ num + "张票,剩余" + count + "张票!");
			}
		}
	}
}
经验

使用同步方法或同步代码块实现线程同步时,能够保证在同一时刻最多只有一个线程执行该代码。使用时有以下几点需要注意:

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问也将被暂时阻塞。
  4. 当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
  5. 以上规则对其它对象锁同样适用。

6. 线程的死锁

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

下面我们通过一些实例来说明死锁现象。

先看生活中的一个实例,两个人面对面进行交易,甲和乙都想先看对方的东西,甲如果想看对方的钱的话,乙必须让其看钱,但是乙怕甲的货有问题,想先看甲的货,而甲又不愿意。于是就僵持不下,导致谁也交易不成,这就是死锁。

示例代码如下:

/**
 * 演示死锁
 */
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 DeadLock implements Runnable{
	static A a = new A();
	static B b = new B();
	public static void main(String[] args) {
		new DeadLock();
	}
	public DeadLock() {
		new Thread(this).start();
		a.say(b);
	}
	@Override
	public void run() {
		b.say(a);
	}
}

避免Java死锁情况

我们可以通过了解它的可能性来避免死锁状态。虽然这是一个非常复杂的过程,不易捕捉。但是,如果我们尝试,我们可以避免这种情况。有一些方法可以避免这种情况。我们不能完全消除它的可能性,但我们可以减少。

  • 避免嵌套锁:这是死锁的主要原因。Dead Lock主要发生在我们为多个线程提供锁定时。如果我们已经给了多个线程,请避免锁定多个线程。
  • 避免不必要的锁:我们应该只锁定那些必需的成员。不必要地锁定会导致死锁。
  • 使用线程连接:当一个线程等待其他线程完成时,将显示死锁条件。如果出现这种情况,我们可以使用Thread.join,您认为执行将花费最多时间。

重点:

  • 如果线程正在等待彼此完成,则该条件称为死锁。
  • 死锁条件是一种复杂的情况,仅在多线程的情况下才会发生。
  • 死锁条件可能会在运行时破坏我们的代码并可能破坏业务逻辑。
  • 我们应该尽可能地避免这种情况。

7. 生产者消费者模型

7.1 线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7qvqKHSd-1607138266056)(JAVAOOP-CH07.assets/image-20200910163732689.png)]

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

7.2 等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

7.3 生产与消费者问题

生产者和消费者指的是两个不同的线程类对象,操作同一资源的情况。

具体操作流程如下:

  • 生产者负责生产数据,消费者负责取走数据;

  • 生产者每生产完一组数据之后,消费者就要取走一组数据。

假设要生产的数据如下:

  • 第一组数据:name = Tom,content = 大学生
  • 第二组数据:name = Jack,content = 中学生

编写Info类,代码如下:

public class Info {
	private String name;
	private String content;
	//省略属性的getter以及setter	
}

编写生产者Producter类,代码如下:

/**
 * 生产者类
 */
public class Producter implements Runnable {
	private Info info;
	public Producter(Info info) {
		this.info = info;
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			if(i % 2 == 0) {//偶数
				this.info.setName("Tom");
                try {//为了直观看出效果 让线程休眠100毫秒
					Thread.sleep(200);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.info.setContent("大学生");
			}else {
				this.info.setName("Jack");
                try {//为了直观看出效果 让线程休眠100毫秒
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.info.setContent("中学生");
			}
		}
	}

}

编写消费者Customer类,代码如下:

/**
 * 消费者类
 */
public class Customer implements Runnable{
	private Info info;
	public Customer(Info info) {
		this.info = info;
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(this.info.getName() + "---" + this.info.getContent());
		}
	}

}

编写测试类,代码如下:

public class ProductCustomerDemo {
	public static void main(String[] args) {
		Info info = new Info();
		new Thread(new Producter(info)).start();
		new Thread(new Customer(info)).start();
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gEvywvN-1607138266058)(JAVAOOP-CH07.assets/image-20200907173326567.png)]

通过以上代码发现两个严重问题:

  • 数据错位:不再是所需要的完整数据;
  • 数据重复取出,数据重复设置

7.4 使用同步解决数据错位

数据的错位完全由非同步的操作所造成的,所以应该使用同步处理。

因为取和设置是两个不同的操作,要想进行同步设置,就需要将其定义在一个类里面。

修改代码如下:

public class Info {
	private String name;
	private String content;
	public synchronized void set(String name,String content) {
		this.name = name;
		try {//为了直观看出效果 让线程休眠200毫秒
			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.name + "---" + this.content);
	}	
	
}

修改生产者Producter类,代码如下:

/**
 * 生产者类
 *
 */
public class Producter implements Runnable {
	private Info info;
	public Producter(Info info) {
		this.info = info;
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			if(i % 2 == 0) {//偶数
				this.info.set("Tom","大学生");
			}else {
				this.info.set("Jack","中学生");
			}
		}
	}

}

修改消费者Customer类,代码如下:

public class Customer implements Runnable{
	private Info info;
	public Customer(Info info) {
		this.info = info;
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			this.info.get();
		}
	}
}

运行测试类,数据错位问题已经解决。

7.5 使用等待唤醒机制解决数据重复

若要想实现整个代码的操作,必须加入等待与唤醒机制。

在Objectl类里面提供有专门的处理方法。

  • 等待:

    public final void wait() throws InterruptedException
    

    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

  • 唤醒第一个等待线程:

    public final void notify()
    

    唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。

  • 唤醒全部等待线程:(哪个优先级高就先执行)

    public final void notifyAll()
    

    唤醒在此对象监视器上等待的所有线程。

修改Info类,代码如下:

public class Info {
	private String name;
	private String content;
	private boolean flag = true;
	 
	// flag为true表示可以生产,但是不能取走;false表示可以取走,但是不能生产
	public synchronized void set(String name,String content) {
		// 重复进入到set()方法里,发现不能生产,故需要等待
		if(!flag) {
			try {
				super.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		this.name = name;
		try {//为了直观看出效果 让线程休眠100毫秒
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content;
		this.flag = false; // 生产完成后修改生产标记为可以取走
		super.notify(); // 唤醒其他等待线程

	}
	
	public synchronized void get() {
		if(flag) {//还没生产
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(this.name + "---" + this.content);
		this.flag = true;//修改标记为可以生产
		super.notify();//唤醒其他等待线程
	}
	
	
}

运行测试类,效果如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XRas0ZcN-1607138266060)(JAVAOOP-CH07.assets/image-20200907180439666.png)]

面试题

请解释sleep()与wait()的区别?

  1. sleep()是Thread类定义的方法,wait()是Object类定义的方法。
  2. sleep()可以设置休眠时间,时间一到自动唤醒,而wait()需要等待notify()进行唤醒。

8. 线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。

官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

线程池测试类:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值