Java并发程序(进程)设计——wait(),notify(),notifyAll(),interrupt(),yield(),sleep(),join(),synchronized同步

Thread类

  • sleep:暂停当前正在执行的线程;(类方法
  • yield:暂停当前正在执行的线程,并执行其他线程;(类方法
  • join:等待该线程终止;
  • interrupt:中断该线程,当线程调用wait(),sleep(),join()或I/O操作时,将收到InterruptedException或 ClosedByInterruptException;

Object类

  • wait:暂停当前正在执行的线程,直到调用notify()或notifyAll()方法或超时,退出等待状态;
  • notify:唤醒在该对象上等待的一个线程;
  • notifyAll:唤醒在该对象上等待的所有线程;

wait()

当在一个对象实例上调用 wait() 方法后,当前线程就会在这个对象上等待。

  • 在线程A中,调用了 obj.wait()方法后,那么线程A就会停止继续执行,而转为等待状态。
  • 线程A会一直等到其他线程调用了obj.notify() 方法为止(该obj与一条是同一个)。

    wait() 与 notify()

    当某个对象调用 wait() 之后,只有该对象再调用 notify() 后线程才能获得该对象锁,进入运行状态。

    工作原理

wait() 与 sleep()

常会将 wait() 同 sleep() 做比较。两者有相似点:从表面看起来,线程执行任务被中断。但是两者大有不同:

  • 方法属性
    wait() :Object 成员方法
    sleep() :Thread 静态方法
  • 对象锁
    wait() :线程释放对象锁
    sleep() :线程继续持有对象锁
  • 使用范围
    wait() :必须在 synchronized 同步块中使用,包括 notify(),notifyAll()
    sleep() :任何地方
  • 异常
    wait() :不需要捕获异常
    sleep() :需要捕获异常
  • 作用范围
    wait() :当前线程所持有的同步锁,不仅限于调用的对象
    sleep() :当前线程(因为是静态方法)

yield()

Thread.yield() 与 Thread.sleep(long millis) 很相似:

  1. 都是静态方法
  2. 当前线程放弃线程锁,让出 cpu
    2.1 Thread.yield():当前线程继续与其他线程都有机会再次获得线程锁
    2.2 Thread.sleep(long millis):当前线程在休眠期间不会获得线程锁

interrupt()

interrupt()方法的工作仅仅是改变中断状态,并不是直接中断正在运行的线程。中断的真正原理是当线程被Object.wait(),Thread.join()或sleep()方法阻塞时,调用interrupt()方法后改变中断状态,而wait/join/sleep这些方法内部会不断地检查线程的中断状态值,当发现中断状态值改变时则抛出InterruptedException异常;对于没有阻塞的线程,调用interrupt()方法是没有任何作用。

sleep()

sleep() 方法属于 Thread 类方法—— Thread.sleep(long millis)。表示强制当前线程(只能是当前线程)进入暂时休眠状态,休眠时间为参数:long millis,当休眠时间到期,则返回可运行状态。

休眠状态

当线程进入休眠期后:

  1. 该线程不会释放线程锁
  2. 除非过了休眠期,否则是不会返回到可运行状态
  3. 休眠时间到期,返回可运行状态,而不是运行状态,因为其他原因,并不能保证线程苏醒后立即执行,它只是完全具备执行的可能性
  4. 该线程保持对象锁拥有

为什么要休眠

  1. 线程执行太快,需要减速慢行
  2. 得益于线程休眠状态中的前两点特点,在 run() 方法中调用 Thread.sleep(),保证当前线程进入休眠状态,其他线程才有机会执行。这也是帮助所有线程获得运行机会的最好办法。

实例

线程进入休眠状态,线程将释放线程锁:

public class Synchronized implements Runnable
{
	
	public void run()
	{
		for (int i = 5; i < 10; i ++)
		{
			System.out.println(Thread.currentThread().getName() + "--" +i);
			try
			{
				Thread.sleep(1000);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args)
	{
		Synchronized s1 = new Synchronized();
		Thread t1 = new Thread(s1, "A");
		Thread t2 = new Thread(s1, "B");
		Thread t3 = new Thread(s1, "C");
		t1.start();
		t2.start();
		t3.start();
	}
}
/*	结果:
        C--5
	A--5
	B--5
	C--6
	A--6
	B--6
	C--7
	A--7
	B--7
	A--8
	C--8
	B--8
	C--9
	A--9
	B--9
*/

线程进入休眠状态,线程将保持对象锁拥有:

// 修改 main 方法
public static void main(String[] args)
{
	Synchronized s1 = new Synchronized();
	
	Thread t1 = new Thread(s1, "A");
	Thread t2 = new Thread(s1, "B");
	Thread t3 = new Thread(s1, "C");
	t1.start();
	t2.start();
	t3.start();
	
	// 新创建一个对象
	Synchronized s2 = new Synchronized();
	Thread t4 = new Thread(s2, "A2");
	Thread t5 = new Thread(s2, "B2");
	Thread t6 = new Thread(s2, "C2");
	t4.start();
	t5.start();
	t6.start();
}
/*  结果
	B--5
	A2--5
	C2--5
	C--5
	A--5
	B2--5
	B--6
	A--6
	B2--6
	C2--6
	A2--6
	C--6
	B--7
	B2--7
	A2--7
	A--7
	C--7
	C2--7
	B--8
	A2--8
	C2--8
	A--8
	B2--8
	C--8
	B--9
	C--9
	C2--9
	A--9
	B2--9
	A2--9
   */

join()

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

当 main 方法调用 (Thread)t.join() 时,main 线程会且必定能够获得线程对象的锁,直到该对象唤醒 main 线程(退出后也可)。

package com.zpq.thread;
public class ThreadJoin_1 implements Runnable
{
	public static int a = 0;
	
	public void run()
	{
		for (int i = 0; i < 100000; i ++)
		{
			a += 1;
		}
	}
	
	public static void main(String[] args)
	{
		Runnable run = new ThreadJoin_1();
		Thread t = new Thread(run, "A");
		t.start();
		/* 注释1
		try
		{
			t.join();
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		*/
		
		/* 注释2
		for (int i = 0; i < 2; i ++)
		{
			System.out.print(i);
		}
		*/
		
		System.out.println("\n"+a);
	}
}

每次执行,几乎a都为0,当然这并不是绝对的,与机器的性能也有关系。为什么会产生这样的结果呢?因为 t.start() 之后,还来不及执行t线程,就先执行mian线程了,main线程即输出a,此时的a的值极有可能还是:0。撤销 注释1,a的值输出为:100000。

通俗的理解就是,若t线程调用 join(),那么t就先执行,其他线程等待t线程的任务执行完毕,才有机会获得执行权。

为了证明 join() 的效果,反过来也就是证明如果没有 join(),main线程(System.out.println(“\n”+a);)将会抢在 t线程之前执行。保留 注释1,撤销 注释2,以 for循环 拖延 main线程执行的时间。循环两次的时候,a的结果为:4443(我的机器几乎不会达到100000);逐渐循环次数,当循环次数足够多的时候,a的结果会一直稳定在 100000。

join(arg0)

若 join(arg0) 添加一个参数,这个参数表示其他线程等待的毫秒数,如在 main线程中调用 t.join(1000),说明 main等待t线程1秒,一秒时候,t交出执行权。

 package com.zpq.thread;
public class ThreadJoin_2
{
	public static void main(String[] args)
	{
		Thread t = new Thread(new RunnableImpl(), "A");
		t.start();
		try
		{
			t.join(1000);
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		System.out.println("End join");
	}
}
class RunnableImpl implements Runnable
{
	public void run()
	{
		long timeCount = 500;
		System.out.println("Begin sleep " +timeCount + "ms");
		try
		{
			Thread.sleep(timeCount);
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		System.out.println("End sleep");
	}
}

结果为:

Begin sleep 500ms
End sleep
End join
mian 线程执行

main线程中启动 t线程,t线程调用 join(1000),意思是 main线程给 t线程 1秒的执行时间,t线程的任务是 sleep(500)。当sleep() 时间接近于join()的时间时,其实上述的结果不一定一直如此,偶尔会先 “End join”,但这不是关键。

为了证明 join(args) 中 args 的有效性,我们将 timeCount 的值增大至 2000,也就是让 t线程多“睡”1.5秒,即任务执行时长增加1.5秒,而join时间不变。结果为:

Begin sleep 2000ms
End join
mian 线程执行
End sleep

join 先结束,接着执行main线程任务,sleep后结束。

也就是说,若t线程调用join(arg0),main线程只等待 arg0毫秒,超过这个时间,main就会失去耐心,并且不用再继续等待t线程把任务执行完毕。

synchronized

多线程并发

单线程就像串行,一切按照先后顺序,多线程才存在并发的情况。在并发的情况下,该怎样控制多线程对共享资源的访问呢?同一个资源,多个线程之间要么是随机访问,还是有序访问,取决于线程是否同步,同步线程按照相对串行的方式访问资源,我理解的相对串行,是某个线程需要访问资源N次,其他线程必须等待上一个线程访问完N次后才开始访问。

synchronized 代码块

Java中的wait/notify相关方法是定义在超类java.lang.Object上,因此是任意对象都具备的。wati/nofity机制实际上理解为生产者-消费者模式,在功能方面实现了解耦,结构上有更好的灵活性。该机制依托于同步机制,目的是确保等待线程从wait()方法返回时能够感知到通知线程对变量的修改。

相关方法

wait/notify机制,是指一个线程A调用了对象Owait()方法进入等待状态,另一个线程B调用了对象Onotify()notifyAll()方法,线程A收到通知后从wait()方法返回。相关方法如下:

方法名称 描 述
notify() 通知一个在对象上等待的线程,使其从wait()方法返回,返回的前提是该线程获取到了该对象的锁
notifyAll() 通知所有等待在该线程的对象
wait() 调用该方法的线程进入WAITING状态,只有等待其他线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁
wait(long) 等待一段时间(毫秒),若没有通知或中断,则超时后返回(TIMED_WAITING状态)
wait(long, int) 纳秒级的超时返回(TIMED_WAITING状态)

wait/notify实现

运行过程

wait/notify运行过程如下图:

Java并发程序(进程)设计——wait(),notify(),notifyAll(),interrupt(),yield(),sleep(),join(),synchronized同步
Java并发程序(进程)设计——wait(),notify(),notifyAll(),interrupt(),yield(),sleep(),join(),synchronized同步
  1. WatiThread首先获取到对象的锁,调用对象的wait()方法,从而释放对象锁,进入等待队列WaitQueue,进入等待状态WAITING
  2. NotifyThread获得同一对象的锁,调用notify()方法,将WatiThread从等待队列WaitQueue移至同步队列SynchronizedQueueWaitThread进入阻塞状态BLOCKED
  3. NotifyThread继续执行剩余代码,执行完后释放对象的锁
  4. WaitThread再次获取对象的锁,并从wait()方法返回继续执行
特别说明
  • 使用wait()notify()notifyAll()时需要先对调用对象加锁
  • wait()方法返回的前提是获得了调用对象的锁
  • notify()notifyAll()方法调用后,等待线程不会立刻从wait()返回,需要在调用notify()notifyAll()的线程释放锁后,等待线程才有机会wait()返回
  • notify()方法是将等待队列中的一个等待线程从等待队列中移至同步队列;而notifyAll()方法则是将等待队列中所有的线程全部移至同步队列。两种方法被移动的线程状态均由WAITING变成BLOCKED

经典范式

wait/notify机制的经典范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方(消费者)
  1. 获得对象的锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件是否满足
  3. 条件满足则执行对应的逻辑

伪代码如下:

synchronized(对象) {
    while (条件不满足) {
        对象.wait();
    }
    处理逻辑;
}
通知方(生产者)
  1. 获得对象的锁
  2. 执行逻辑,改变条件
  3. 通知所有等待在该对象的线程

伪代码如下:

synchronized(对象) {
    改变条件;
    对象.notifyAll();
}

实例:

方法简介

  1. wait()
    synchronized (lockObjectA) {
    try {
        lockObjectA.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    此时,代码块中会释放对对象lockObjectA的锁定,同时使lockObjectA进行等待,直到有线程调用了它的notify()或notifyAll()方法,才继续将lockObjectA锁定,并继续执行下面程序。
    即:调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。

  2. notify()
    synchronized (lockObjectA) {
                lockObjectA.notify();
            }

    唤醒在等待该对象同步锁的线程(只唤醒一个),在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
    即:调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

  3. notifyAll()
    将所有等待该对象的线程全部唤起。

示例

package Test0315;
/**
 * Created by Cesar on 2016/3/15.
 */
public class TestWait extends Thread {
    private static Object lockObjectA = new Object();
    private int key;
    public TestWait(int key) {
        this.key = key;
    }
    @Override
    public void run() {
        if (key == 0) {
            synchronized (lockObjectA) {
                System.out.println(key + "开始等待");
                try {
                    lockObjectA.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(key + "等待结束");
                System.out.println(key + "成功锁定A");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(key + "释放A");
        } else if (key == 1) {
            synchronized (lockObjectA) {
                lockObjectA.notify();
            }
            System.out.println(key + "释放了A");
        } else {
            synchronized (lockObjectA){
                System.out.println(3+"锁定了A");
            }
        }
    }
    public static void main(String[] args) {
        TestWait wait = new TestWait(0);
        wait.setName("Test Wait");
        wait.start();
        System.out.println("主线程休眠开始");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程休眠结束,唤醒开始");
        TestWait wait1 = new TestWait(1);
        wait1.start();
        TestWait wait2 = new TestWait(2);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        wait2.start();
        try {
            wait2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部结束");
    }
}

结果

主线程休眠开始
0开始等待
主线程休眠结束,唤醒开始
1释放了A
0等待结束
0成功锁定A
0释放A
3锁定了A
全部结束

参考:

Java 多线程编程讲解

Java 程序中的多线程

JAVA 并发: 进程(Processes )和线程(Threads)

 

本文:Java并发程序(进程)设计——wait(),notify(),notifyAll(),interrupt(),yield(),sleep(),join(),synchronized同步

Leave a Reply