【搞定Java并发编程】第4篇:多线程概述~下篇

上一篇:多线程上篇:https://blog.csdn.net/pcwl1206/article/details/84837530

目  录

1、等待/唤醒机制

2、线程中断

3、线程终止

4、线程休眠sleep

5、线程让步yield()

6、join()方法

7、sleep()、wait()、yield()和join()方法的区别

8、Daemon线程


1、等待/唤醒机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是“生产者”,后者是“消费者”,在功能层面实现了解耦。

1.1、等待/唤醒机制相关的方法

在Object.java中,定义了wait()、notify()notifyAll()等方法。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

Object类中关于等待/唤醒的API详细信息如下:

扫描二维码关注公众号,回复: 4449323 查看本文章
  • notify():唤醒在此对象监视器上等待的单个线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁;
  • notifyAll():唤醒在此对象监视器上等待的所有线程;
  • wait():让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。需要注意的是:调用wait()方法后,会释放对象的锁;
  • wait(long timeout) :让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
  • wait(long timeout, int nanos):让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。其实就是对于超时时间更细粒度的控制;
  • 等待/唤醒机制的含义:

等待/唤醒机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

上诉两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

1.2、等待/唤醒机制的案例

说明:该案例转发自:等待唤醒机制

任务:

  • 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
  • 当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。

这个问题的关键在于,如何实现交替进行。

public class Resource{
	
	private String name;
	private String sex;
	private boolean flag = false;
	
	public synchronized void set(String name, String sex){	
		// 如果flag为true,证明Resource还没有输出,则进入等待状态
		if(flag){
			try {
				wait();   // 等待消费者消费
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// 设置成员变量
		this.name = name;
		this.sex = sex;
		System.out.println("输入的是:The name is : " + name + "&& The sex is :" + sex);
		// 设置之后,Resource中有值,相当于生产出了新的Resource对象,将flag设置为true
		flag = true;
		// 唤醒output线程,进行数据的写出,即消费
		this.notify();
	}
	
	public synchronized void get(){
		// 如果没有了Resource对象,则进入等待状态
		if(!flag){
			try {
				wait();   // 等待生产者生产
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// out线程将数据输出
		System.out.println("输出的是:The name is : " + name + "&& The sex is :" + sex);
		// 改变标记,以便input线程输入数据
		flag = false;
		// 唤醒input线程,进行数据的写入,即生产
		this.notify();
	}
}

public class Input implements Runnable {

	private Resource res;
	
	public Input(Resource res){
		this.res = res;
	}
	
	@Override
	public void run() {
		int count = 0;
		while(true){
			if(count == 0){
				res.set("Tom", "man");   // 生产数据
			}else{
				res.set("Lily", "woman");
			}
			// 在两个数据之间进行切换
			count = (count + 1) % 2;
		}
	}
}

public class Output implements Runnable {

	private Resource res;
	
	public Output(Resource res){
		this.res = res;
	}
	
	@Override
	public void run() {
		while(true){
			res.get();  // 消费数据
		}
	}
}

public class ResourceTest {

	public static void main(String[] args) {
		
		// 资源对象
		Resource res = new Resource();
		
		// 任务对象,同一个res
		Input in = new Input(res);
		Output out = new Output(res);
		
		// 线程对象
		Thread t1 = new Thread(in);   // 输入线程
		Thread t2 = new Thread(out);  // 输出线程
			
		// 开启线程
		t1.start();
		t2.start();
	}
}

运行结果:

上面的这个例子是典型的生产者和消费者案例。只不过是单生产者和单消费者案例。

  • 几点细节:

1、调用wait()、notify()和notifyAll()时需要先对调用对象加锁;

2、调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列;

3、notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回;

4、notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态由WAITING变为BLOCKED;

5、从wait()方法返回的前提是获得了调用对象的锁。

6、等待/唤醒机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。

  • 为什么notify()、wait()等方法定义在了Object类中,而不是Thread类中呢?

Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!

线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”

负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。

总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有的,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。


2、线程中断

2.1、什么是线程中断?

线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程调用了该线程的 interrupt() 方法,使得该线程中断标志位属性改变。

深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性,到底该线程生命周期是去终止,还是继续运行,由线程根据标志位属性自行处理。

2.2、线程中断操作

调用线程的 interrupt() 方法,根据线程不同的状态会有不同的结果。

下面新建 InterruptedThread 对象,代码如下:

public class InterruptThread implements Runnable {

	@Override
	public void run() {
		// 一直run
		while(true){
			// ...
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		Thread interruptedThread = new Thread(new InterruptThread(), "InterruptedThread");
		interruptedThread.start();
		
		TimeUnit.SECONDS.sleep(2);
		
		interruptedThread.interrupt();
		System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());
		
		TimeUnit.SECONDS.sleep(2);
	}
}

运行结果:

代码详解:

  • 线程一直在运行状态,没有停止或者阻塞等
  • 调用了interrupt()方法,中断状态置为 true,但不会影响线程的继续运行

另一种情况,新建 InterruptedException 对象,代码如下:

public class InterruptException implements Runnable {

	@Override
	public void run() {
		// 一直sleep
		try {
			TimeUnit.SECONDS.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		Thread interruptedThread = new Thread(new InterruptException(), "InterruptedThread");
		interruptedThread.start();
		
		TimeUnit.SECONDS.sleep(2);
		
		// 中断被阻塞状态(sleep、wait、join 等状态)的线程,会抛出异常 InterruptedException
                // 在抛出异常 InterruptedException 前,JVM 会先将中断状态重置为默认状态 false
		interruptedThread.interrupt();
		System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());
		
		TimeUnit.SECONDS.sleep(2);
	}
}

运行结果:

代码详解:

  • 中断被阻塞状态(sleep、wait、join 等状态)的线程,会抛出异常 InterruptedException
  • 抛出异常 InterruptedException 前,JVM 会先将中断状态重置为默认状态 false

线程中断总结:

  • 线程中断,不是停止线程,只是改变一个线程的标志位属性;
  • 如果线程状态为被阻塞状态(sleep、wait、join 等状态),线程状态退出被阻塞状态,抛出异常 InterruptedException,并重置中断状态为默认状态 false;
  • 如果线程状态为运行状态,线程状态不变,继续运行,中断状态置为 true。

3、线程终止

比如在 IDEA 中强制关闭程序,立即停止程序,不给程序释放资源等操作,肯定是不正确的。线程终止也存在类似的问题,所以需要考虑如何终止线程?

上面提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

案例如下代码所示:

public class ThreadSafeStop {

	public static void main(String[] args) throws Exception {
		
		Runner run1 = new Runner();
		Thread countThread = new Thread(run1, "CountThread");
		countThread.start();
		// 睡眠1秒后,通知CountThread中断,并终止线程
		TimeUnit.SECONDS.sleep(1);
		countThread.interrupt();
		
		Runner run2 = new Runner();
		countThread = new Thread(run2,"CountThread");
        countThread.start();
        // 睡眠 1 秒,然后设置线程停止状态,并终止线程
        TimeUnit.SECONDS.sleep(1);
		run2.stopSafely();
	}
	
	// Runner:静态内部类
	private static class Runner implements Runnable{

		private long i;
		
		// 线程状态变量
		private volatile boolean on = true;
		
		@Override
		public void run() {
			while(on && !Thread.currentThread().isInterrupted()){
				// 线程执行的具体逻辑
				i++;
			}
			System.out.println("Count i = " + i);
		}
		
		public void stopSafely(){
			on = false;
		}
	}
}

从上面代码可以看出,通过while(on && !Thread.currentThread().isInterrupted())代码来实现线程是否跳出执行逻辑,并终止。但是疑问点就来了,为啥需要on和isInterrupted()两项一起呢?用其中一个方式不就行了吗?答案是:

  1. 线程成员变量on通过 volatile 关键字修饰,达到线程之间可见,从而实现线程的终止。但当线程状态为被阻塞状态(sleep、wait、join 等状态)时,对成员变量操作也阻塞,进而无法执行安全终止线程;
  2. 为了处理上面的问题,引入了isInterrupted(); 只去解决阻塞状态下的线程安全终止;
  3. 两者结合是真的没问题了吗?不是的,如果是网络 io 阻塞,比如一个 websocket 一直再等待响应,那么直接使用底层的 close 。

4、线程休眠sleep

sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

public class ThreadA extends Thread {

	public ThreadA(String name) {
		super(name);
	}

	public synchronized void run() {
		try {
			for (int i = 0; i < 6; i++) {
				System.out.printf("%s: %d\n", this.getName(), i);
				// i能被4整除时,休眠1000毫秒
				if (i % 4 == 0)
					Thread.sleep(1000);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		ThreadA t1 = new ThreadA("t1");
		t1.start();
	}
}

运行结果:

结果说明

程序比较简单,在主线程main中启动线程t1。t1启动之后,当t1中的计算i能被4整除时,t1会通过Thread.sleep(1000)休眠1000毫秒。


5、线程让步yield()

yield()的作用是让步它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

public class Thread_Yield extends Thread {

	public Thread_Yield(String name){
		super(name);
	}
	
	public synchronized void run(){
		for(int i = 0; i < 10; i++){
			System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
			// i整除4时,调用yield()
			if(i % 4 == 0){
				Thread.yield();
			}
		}
	}
	
	public static void main(String[] args) {
		
		Thread_Yield t1 = new Thread_Yield("t1");
		Thread_Yield t2 = new Thread_Yield("t2");
		t1.start();
		t2.start();
	}
}

某一次的运行结果:

结果说明

“线程t1”在能被4整数的时候,并没有切换到“线程t2”。这表明,yield()虽然可以让线程由“运行状态”进入到“就绪状态”;但是,它不一定会让其它线程获取CPU执行权(即,其它线程进入到“运行状态”),即使这个“其它线程”与当前调用yield()的线程具有相同的优先级。


6、join()方法

join() 的作用是:让“主线程”等待“子线程”结束之后才能继续运行

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

说明

上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。

在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

  • join()的源码(JDK1.7)
public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    // 当millis等于0的时候,判断子线程是否是活的
    if (millis == 0) {
        while (isAlive()) {
            wait(0);  // 如果是活的,就无限等待下去
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;   
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

7、sleep()、wait()、yield()和join()方法的区别

1.sleep()方法

1、在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。

2、sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

2.wait()方法

1、在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。

2、当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。

3、唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。

4、wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

3.yield方法

1、暂停当前正在执行的线程对象。

2、yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

3、yield()只能使同优先级或更高优先级的线程有执行的机会。 

4.join方法

1、让“主线程”等待“子线程”结束之后才能继续运行,即等待调用join方法的线程结束。


8、Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作,被称为守护线程

当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。但是需要说明的是虚拟机退出时Daemon线程中的finally代码块并不一定会执行。

可以通过调用Thread.setDemon(true)将线程设置为Daemon线程,但是必须在线程启动前设置。


转载声明:

本文内容主要出自于以下文章:

1、Java多线程系列:https://www.cnblogs.com/skywang12345/p/java_threads_category.html

2、并发基础与多线程基础:https://blog.csdn.net/a724888/article/details/60867044

3、《Java并发编程的艺术》书中内容

猜你喜欢

转载自blog.csdn.net/pcwl1206/article/details/84843170