Java 给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。 这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。…
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) 很相似:
- 都是静态方法
- 当前线程放弃线程锁,让出 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,当休眠时间到期,则返回可运行状态。
休眠状态
当线程进入休眠期后:
- 该线程不会释放线程锁
- 除非过了休眠期,否则是不会返回到可运行状态
- 休眠时间到期,返回可运行状态,而不是运行状态,因为其他原因,并不能保证线程苏醒后立即执行,它只是完全具备执行的可能性
- 该线程保持对象锁拥有
为什么要休眠
- 线程执行太快,需要减速慢行
- 得益于线程休眠状态中的前两点特点,在 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调用了对象O
的wait()
方法进入等待状态,另一个线程B调用了对象O
的notify()
或notifyAll()
方法,线程A收到通知后从wait()
方法返回。相关方法如下:
方法名称 | 描 述 |
---|---|
notify() |
通知一个在对象上等待的线程,使其从wait() 方法返回,返回的前提是该线程获取到了该对象的锁 |
notifyAll() |
通知所有等待在该线程的对象 |
wait() |
调用该方法的线程进入WAITING 状态,只有等待其他线程的通知或中断才会返回,调用wait() 方法后,会释放对象的锁 |
wait(long) |
等待一段时间(毫秒),若没有通知或中断,则超时后返回(TIMED_WAITING 状态) |
wait(long, int) |
纳秒级的超时返回(TIMED_WAITING 状态) |
wait/notify实现
运行过程
wait/notify
运行过程如下图:

WatiThread
首先获取到对象的锁,调用对象的wait()
方法,从而释放对象锁,进入等待队列WaitQueue
,进入等待状态WAITING
NotifyThread
获得同一对象的锁,调用notify()
方法,将WatiThread
从等待队列WaitQueue
移至同步队列SynchronizedQueue
,WaitThread
进入阻塞状态BLOCKED
NotifyThread
继续执行剩余代码,执行完后释放对象的锁WaitThread
再次获取对象的锁,并从wait()
方法返回继续执行
特别说明
- 使用
wait()
、notify()
、notifyAll()
时需要先对调用对象加锁 - 从
wait()
方法返回的前提是获得了调用对象的锁 notify()
或notifyAll()
方法调用后,等待线程不会立刻从wait()
返回,需要在调用notify()
或notifyAll()
的线程释放锁后,等待线程才有机会从wait()
返回notify()
方法是将等待队列中的一个等待线程从等待队列中移至同步队列;而notifyAll()
方法则是将等待队列中所有的线程全部移至同步队列。两种方法被移动的线程状态均由WAITING
变成BLOCKED
。
经典范式
wait/notify
机制的经典范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方(消费者)
- 获得对象的锁
- 如果条件不满足,那么调用对象的
wait()
方法,被通知后仍要检查条件是否满足 - 条件满足则执行对应的逻辑
伪代码如下:
synchronized(对象) { while (条件不满足) { 对象.wait(); } 处理逻辑; }
通知方(生产者)
- 获得对象的锁
- 执行逻辑,改变条件
- 通知所有等待在该对象的线程
伪代码如下:
synchronized(对象) { 改变条件; 对象.notifyAll(); }
实例:
方法简介
- wait()
synchronized (lockObjectA) { try { lockObjectA.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
此时,代码块中会释放对对象lockObjectA的锁定,同时使lockObjectA进行等待,直到有线程调用了它的notify()或notifyAll()方法,才继续将lockObjectA锁定,并继续执行下面程序。
即:调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。 - notify()
synchronized (lockObjectA) { lockObjectA.notify(); }
唤醒在等待该对象同步锁的线程(只唤醒一个),在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
即:调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 - 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 并发: 进程(Processes )和线程(Threads)
本文:Java并发程序(进程)设计——wait(),notify(),notifyAll(),interrupt(),yield(),sleep(),join(),synchronized同步