【并发编程】线程通信与TheadLocal类

线程通信

线程通信的目标是使线程之间能够互相发送信号。或者,线程通信使线程能够等待其它线程。

通过共享对象通信

线程间发送信号的一个简单方式是在共享对象的变量里设置信号值。线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess。这个简单的例子使用了一个持有信号的对象,并提供了set和check方法。

public class MySignal{
  protected boolean hasDataToProcess = false;
  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }
  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;
  }
}
线程A和B必须获得指向一个MySignal共享实例的引用,以便进行通信。如果它们持有的引用指向不同的MySingal实例,那么彼此将不能检测到对方的信号。需要处理的数据可以存放在一个共享缓存区里,它和MySignal实例是分开存放的。

忙等待(Busy Wait)

准备处理数据的线程B正在等待数据变为可用。换句话说,它在等待线程A的一个信号,这个信号使hasDataToProcess()返回true。线程B运行在一个循环里,以等待这个信号:

protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
	//do nothing... busy waiting
}

wait(),notify()和notifyAll()

忙等待没有对运行等待线程的CPU进行有效的利用,除非平均等待时间非常短,不然就最好让它进入休眠或者非运行状态,直到接收到激活信号。

Java内部有一套等待机制来完成这些工作,就是wait(),notify(),notifyAll()。

一个线程调用任意对象的wait(),这个线程就会进入非运行状态,直到另外一个线程调用了同一个对象的notify()方法,才会被重新激活运行。为了调用wait()和notify(),线程必须获得那个对象的锁,意思就是,wait()和notify()必须在同步块中调用:

public class MonitorObject{
}

public class MyWaitNotify{

	MonitorObject myMonitorObject = new MonitorObject();

	public void doWait(){
		synchronized(myMonitorObject){
			try{
				myMonitorObject.wait();
			} catch(InterruptedException e){...}
		}
	}

	public void doNotify(){
		synchronized(myMonitorObject){
			myMonitorObject.notify();
		}
	}
}
上例中,等待线程将调用doWait(),而唤醒线程将调用doNotify(),当一个线程调用一个对象的notify()方法,这个对象上的所有等待线程中,会有一个线程被唤醒并允许执行(这个唤醒是随机性的,任意性的)。

不管是线程等待还是唤醒线程,都是在同步块里调用wait()和notify(),一个线程如果没有持有对象锁,则不能调用wait(),notify()和notifyAll(),否则会抛出IllegalMonitorStateException异常。

一旦一个线程调用了wait()方法,就会释放所持有的监视器对象上的锁。这时其他的一个线程就能够得到这个锁,并允许其他线程也可以调用wait()或者notify()。

一旦一个线程调用了notify()方法被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。

丢失的信号

notify()和notifyAll()不会保存调用他们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态。通知信号过后便丢弃了。因此,如果一个线程在另外一个线程调用wait()之前调用了notify(),那么调用wait()的这个线程将错过唤醒信号,这可能使得其永远在等待,不再醒来。

为了信号的丢失,就要引入一个信号变量,并保存在信号类中:

public class MyWaitNotify2{
	MonitorObject myMonitorObject = new MonitorObject();
	boolean wasSignalled = false;

	public void doWait(){

		synchronized(myMonitorObject){
			if(!wasSignalled){
				try{
					myMonitorObject.wait();
				} catch(InterruptedException e){...}
			}
		//clear signal and continue running.
		wasSignalled = false;
		}
	}

	public void doNotify(){
		synchronized(myMonitorObject){
			wasSignalled = true;
			myMonitorObject.notify();
		}
	}
}
在notify()之前,设置自己已经被通知过了。在wait()后,设置自己没有被通知过,需要被验证。初始化时为未被通知过。

假唤醒

线程有可能在没有调用notify()和notifyAll()的情况下莫名其妙的醒来。这就是假唤醒。为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样,如果某个线程突然自己唤醒了,但是由于信号量未改变,线程会在while循环里重新进入睡眠状态。

public class MyWaitNotify3{

	MonitorObject myMonitorObject = new MonitorObject();
	boolean wasSignalled = false;

	public void doWait(){
		synchronized(myMonitorObject){
			while(!wasSignalled){
				try{
					myMonitorObject.wait();
				} catch(InterruptedException e){...}
			}
			//clear signal and continue running.
			wasSignalled = false;
		}
	}

	public void doNotify(){
		synchronized(myMonitorObject){
			wasSignalled = true;
			myMonitorObject.notify();

		}
	}
}

多个线程等待相同的信号

如果有多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行,使用while循环也是个好办法。

每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出wait()调用并且清除信号变量(设置为false)。一旦这个线程退出doWait()的同步块,其他线程想要退出wait()调用时,会在while循环里检查信号变量值,但是这个变量值已经被第一个唤醒的线程设置为false了,所以其余的线程会在while中重新归于等待状态。

不要在字符串常量或者全局对象中调用wait()
先看如下的例子:

public class MyWaitNotify{

	String myMonitorObject = "";
	boolean wasSignalled = false;

	public void doWait(){
		synchronized(myMonitorObject){
			while(!wasSignalled){
				try{
					myMonitorObject.wait();
				} catch(InterruptedException e){...}
			}
			//clear signal and continue running.
			wasSignalled = false;
		}
	}

	public void doNotify(){
		synchronized(myMonitorObject){
			wasSignalled = true;
			myMonitorObject.notify();
		}
	}
}
将空字符串(或者其他字符串常量)作为锁的同步块里调用wait()和notify()产生的问题是,JVM/编译器会将常量字符串转为化同一个对象,这意味着,你是你有两个不同的MyWaitNotify实例,但是他们都用了相同的常量字符串实例,这会导致一个问题,在第一个MyWaitNotify实例上调用doWait(),可能被第二个MyWaitNotify实例上调用doNotify()的线程唤醒,这种情况类似于引发了一次假唤醒。


但是由于两个MyWaitNotify中各自的信号变量互不干扰,所以被唤醒的线程会在while循环检查中再次进入非运行状态。

表面上看没有什么问题,实际上会造成信号丢失的问题。由于doNotify()调用的是notify()而非notifyAll(),即使有多个线程在相同的字符串实例上等待,也只能有一个线程被唤醒。假设线程A被线程C的唤醒信号唤醒,虽然A会在检查自己的信号变量值后归于等待,但是属于C的唤醒信号就被丢失掉了,线程C有可能一直处于等待状态。

解决这个问题的办法可以将notify()改为notifyAll(),这样在常量字符串上等待的所有线程都会被唤醒,然后依次检查信号量,A或B会重新归于等待,而C和D中的一个会退出wait(),另一个会再次归于等待。这样看确实是解决了问题,但是这种办法在性能上是个坏办法。

在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象。

ThreadLocal类

ThreadLocal类可以让你创建的变量只能被同一个线程进行读和写操作。

因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量域,具有私有性。

创建一个ThreadLocal对象

private ThreadLocal myThreadLocal = new ThreadLocal();

访问ThreadLocal对象

//设置值
myThreadLocal.set("A thread local value");
//读取值
String threadLocalValue = (String) myThreadLocal.get();

ThreadLocal泛型

为了使get()方法返回值不用做强制类型转换,通常可以创建一个泛型化的ThreadLocal对象。

private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

初始化ThreadLocal

由于ThreadLocal对象的set()方法设置的值只对当前线程可见,我们可以通过实现ThreadLocal的子类,重写initialValue()方法,就可以为ThreadLocal对象指定一个初始化值。

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
	@Override protected String initialValue() {
		return "This is the initial value";
	}
};
此时,在set()方法调用前,当调用get()方法的时候,所有线程都可以看到同一个初始化值。

InheritableThreadLocal

InheritableThreadLocal类是ThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有值,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值。






猜你喜欢

转载自blog.csdn.net/East_MrChiu/article/details/72232606