在一个多线程程序中,多个线程通过共同协作来完成指定任务。在协作过程中,各个线程通过共享内存的方式来进行通信。一般CPU采用时间片轮转等不同算法来对线程进行调度,这个情况下,一个线程所做的操作对于其它线程并不一定可见,来看代码:
public class IdGenerator { private int value = 0; public int getNext() { return value++; } }
上面这一段代码对于一个单线程程序来说,每次对getNext方法的调用都可以保证得到不重复的值。而对于多线程程序来说,却不能保证每次取得的值都不一样。因为上面这一段代码中至少存在取值和赋值两个操作,再具体到cpu指令的话应该会有更多个指令序列,它并不是一个原子操作。
要解决这个问题,我们可以使用关键词synchronized对其添加线程锁:
public class IdGenerator { private int value = 0; public synchronized int getNext() { return value++; } public int getNext2() { synchronized(this) { return value++; } } }
synchronized关键词可以添加在方法或代码块之上,关键词内包含的代码块在同一时刻只允许有一个线程访问。对于声明为synchronized的方法,静态方法对应的监视器对象是所在Java类对应的Class类的对象所关联的监视器对象,而实例方法使用的是当前对象实例所关联的监视器对象。对于synchronized代码块,对应的监视器对象是synchronized代码块声明中的对象所关联的监视器对象。
在Java中,对于long型和double型64位的域的读取和写入操作也不是原子操作,Java一次只能操作32位的数据也就是说,在读取或者写入long和double型的时候也可能会被其它线程所打断。因此在多线程程序中使用long型和double型的共享变量时,需要把变量声明为volatile,以保证读取和写入操作的完整性。如:
volatile double d = 0;
将变量声明为volatile相当于为单个变量的读取和写入添加了同步操作。但是volatile在使用时不需要利用锁机制,因此性能上要优于synchronized关键词。但是在上面的IdGenerator类中,如果只是把value声明为volatile,这样是不够的,因为写入的value的正确值依赖于value的当前值。
Object类中有wait、notify和notifyAll方法:
package java.lang; public class Object { ... public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && timeout == 0)) { timeout++; } wait(timeout); } public final void wait() throws InterruptedException { wait(0); } ... }
在多线程程序中,单个线程可能会需要满足某些逻辑条件才能继续运行。当线程所要求的条件不满足时,线程进入等待状态,等待由于其他线程的运行而使条件得到满足;其他线程则负责在合适的时机发出通知来唤醒处于等待状态的线程。对于这种场景,可以使用while循环和volatile变量来处理。但是这种做法的本质是让线程处于忙等待的状态,并通过轮询的方式来判断条件是否满足。处于忙等待的线程仍然占用CPU时间,对性能造成影响。更好的做法是使用Object类提供的wait、notify和notifyAll方法:
package com.multithread; public class maintest { static private class Lock {} private static Lock lock = new Lock(); public static void main(String[] args) throws InterruptedException { Runnable r = new Runnable() { @Override public void run() { synchronized(lock) { while(true) { System.out.println("我是R"); try { lock.wait(); } catch (InterruptedException e) {} System.out.println("我是R wait之后的代码段"); } } } }; Thread t = new Thread(r); t.start(); Runnable r1 = new Runnable() { @Override public void run() { synchronized(lock) { while(true) { System.out.println("我是R1"); try { lock.wait(); } catch (InterruptedException e) {} System.out.println("我是R1 wait之后的代码段"); } } } }; Thread t1 = new Thread(r1); t1.start(); Thread.currentThread().sleep(1000); System.out.println("----------------------notify-------------------------"); synchronized(lock) { System.out.println("获得lock锁"); lock.notify(); } Thread.currentThread().sleep(1000); System.out.println("---------------------notifyAll-----------------------"); synchronized(lock) { System.out.println("获得lock锁"); lock.notifyAll(); } } }
运行该段代码,显示结果如下:
我是R 我是R1 ----------------------notify------------------------- 获得lock锁 我是R wait之后的代码段 我是R ---------------------notifyAll----------------------- 获得lock锁 我是R wait之后的代码段 我是R 我是R1 wait之后的代码段 我是R1
成功调用wait方法的先决条件是当前线程获取到监视器对象上的锁。如果没有锁,则抛出java.lang.IllegalMonitorStateException异常;如果有锁,那么当前线程被添加到对象所关联的等待集合中,并释放其持有的监视器对象上的锁。当前线程被阻塞,无法继续执行,直到被从对象所关联的等待集合中移除。
对应的notify和notifyAll方法用来通知线程离开等待状态。调用一个对象的notify方法会从该对象关联的等待集合中选择一个线程来唤醒。如果等待集合中有多个线程,具体选择哪个线程唤醒由虚拟机实现来决定,不能保证唤醒的顺序和开发者所预计的一样。而notifyAll则是唤醒等待集合中的所有线程,所以当等待集合中包含多个线程的话,使用notifyAll可以保证程序的正确性,代价是会造成一定的性能影响。
书上只说了wait方法的成功调用需要当前线程持有监视器对象上的锁,因此wait方法的调用需要放在使用synchronized关键词声明的方法或代码块中。但是博主实际测试的结果是notify和notifyAll也必须放到synchronized中。