多线程(六)线程间通信机制

一、使用Object中的方法

  Object类中定义了wait/notify/notifyAll等线程间通信的方法。JDK官方文档中的解释:

	//唤醒在此对象监视器上等待的单个线程
	public final native void notify()
	//唤醒在此对象监视器上等待的所有线程
	public final native void notifyAll()
	//导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法
	public final void wait() throws InterruptedException
	//导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法,
	//或者指定的时间过完
	public final native void wait(long timeout) throws InterruptedException
	//导致当前的线程等待,直到其他线程调用此对象的notify() 方法或 notifyAll() 方法,
	//或者其他线程打断了当前线程,或者指定的时间过完
	public final void wait(long timeout, int nanos) throws InterruptedException

  用通俗一点的语言来解释wait()和notify():

  1. wait() ------> 我等会儿再用这把锁,CPU也让给你们,我先休息一会儿!
  2. notify() ------> 我用完了,你们谁用?

  wait:会让出对象锁,同时,当前线程休眠,等待被唤醒,如果不被唤醒,就一直等在那儿。
  notify:并不会让当前线程休眠,但会唤醒休眠的线程。

  简单总结:

  • 1、wait、notify、notifyAll都属于Object类,也就是每个对象都有wait、notify、notifyAll的功能,因为每个对象都有锁。
  • 2、调用使用wait、notify、notifyAll的时候,一定要对竞争资源进行加锁。如果不加锁的话,则会报 IllegalMonitorStateException 异常。
  • 3、调用wait方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  • 4、调用wait进行线程等待时,必须要取得这个锁,一般是放到synchronized(obj)代码中。
  • 5、在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
  • 6、notify或notifyAll方法调用后,等待线程依旧不会从wait返回,需要调用notify或notifAll的线程释放锁之后,等待线程才有机会从wait返回。
  • 7、notify方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。

  假设有三个线程执行了obj.wait,那么obj.notifyAll则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait的下一条语句,必须获得obj锁。因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
  当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。

  等待/通知机制,指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

1.1 wait、notify、notifyAll的使用

  一个线程因其执行目标动作所需的保护条件未满足而被暂停的过程称为等待,一个线程更新了系统的状态,使得其他线程所需的保护条件得以满足的时候唤醒哪些被暂停的线程的过程称为通知

  Object.wait()/Object.wait(long)以及Object.notify()/Object.notifyAll()可应用于实现等待和通知。

1.1.1 wait的作用

  Object.wait()的作用是使其执行线程被暂停,直到接到通知或被中断为止,该方法可用于实现等待
  在调用 wait方法之前,线程必须要获得锁,即只能在同步方法或同步块中调用wait方法。调用wait方法之后,当前线程会释放锁。如果再次获取到锁的话,当前线程才能从wait方法处成功返回。
  使用Object.wait()实现等待,示例:

	//调用wait方法之前获得相应对象的内部锁
	synchronized(someObject){
		while(保护条件不成立){
			//调用Object.wait()暂停当前线程
			someObject.wait();
		}
		//代码执行到这里说明保护条件已满足,执行目标动作
		doAction();
	}
1.1.2 notify和notifyAll的作用

  notify()方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得锁。
  notify()方法任意从处于WAITTING状态的线程中挑选一个进行唤醒,使得调用 wait方法的线程从等待队列移入到同步队列中,从而使得调用 wait()方法的线程能够从 wait()方法处退出。
  调用notify()后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁
  notifyAll()与notify()作用类似,不同的地方:notifyAll()的作用是唤醒正在等待对象监视器的所有线程。
  使用Object.notify()实现通知,示例:

	synchronized(someObject){
		//更新等待线程的保护条件涉及的共享变量
		updateSharedState();
		//唤醒其他线程
		someObject.notify();
	}

1.1.3 notify/notifyAll选择

  Object.notify()可能导致信号丢失这样的正确性问题,而Object.notifyAll()虽然效率不高,但在正确性方面有保障。
  一种较流行的保守方法:优先使用Object.notifyAll()保障正确性,只有在有证据表明使用Object.notify()足够的情况下才使用Object.notify()
  使用Object.notify()需要满足的两个条件:

  1. 一次通知仅需要唤醒最多一个线程;
  2. 相应对象的等待集中仅包含同质等待线程。(同质等待线程是指这些线程用同一个保护条件,并且这些线程在Object.wait()调用返回后的处理逻辑一致。最经典的同质线程是使用同一个Runnable接口实例创建的不同线程或者从同一个Thread子类的new出来的多个线程)。
1.1.4 wait/notify/notifyAll使用示例
  • 例子1:单个wait和notify
    	final Object object = new Object();
    	Thread t1 = new Thread() {
    		public void run(){
    			synchronized (object) {
    				System.out.println("T1 start!");
    	          	try {
    	          		object.wait();
    	           	} catch (InterruptedException e) {
    	           		e.printStackTrace();
    	           	}
    	           	System.out.println("T1 end!");
    	        }
    	    }
    	};
    	Thread t2 = new Thread() {
    		public void run(){
    			synchronized (object) {
    				System.out.println("T2 start!");
    	         	object.notify();
    	         	System.out.println("T2 end!");
    	       	}
    		}
    	};
    	t1.start();
    	t2.start(); 

  结果:

T1 start!
T2 start!
T2 end!
T1 end!

  这个例子中,两个线程用同一把锁,T1里面主要写了个wait(),而T2里面主要写了个notify()。
  代码执行流程:T1启动,让出锁,让出CPU;T2获得CPU,启动,唤醒使用了object的休眠的线程,T1被唤醒后等待启动;T2继续执行,T2执行完,T1获得CPU后继续执行。

  • 例子2:多个wait和notify
		final Object object = new Object();
		Thread t1 = new Thread() {
			public void run(){
				synchronized (object) {
					System.out.println("T1 start!");
		            try {
		            	object.wait();
		            } catch (InterruptedException e) {
		            	e.printStackTrace();
		            }
		            System.out.println("T1 end!");
				}
			}
		};
		Thread t2 = new Thread() {
			public void run(){
				synchronized (object) {
					System.out.println("T2 start!");
					object.notify();
					System.out.println("T2 end!");
				}
			}
		};
		Thread t3 = new Thread() {
			public void run(){
				synchronized (object) {
					System.out.println("T3 start!");
					object.notify();
					System.out.println("T3 end!");
				}
			}
		};
		Thread t4 = new Thread() {
			public void run(){
				synchronized (object) {
					System.out.println("T4 start!");
		            try {
		            	object.wait();
		            } catch (InterruptedException e) {
		            	e.printStackTrace();
		            }
		            System.out.println("T4 end!");
				}
			}
		};
		t1.start();
		t2.start();
		t3.start();
		t4.start();

  这个例子有两种结果:

  1. 刚好wait两次,notify两次,notify在wait之后执行,刚好执行完。
  2. 也是刚好wait两次,notify两次,但是,notify在wait之前执行,于是,至少会有一个线程由于后面没有线程将它notify而无休止地等待下去。

  结果示例1:

T1 start!
T2 start!
T2 end!
T1 end!
T4 start!
T3 start!
T3 end!
T4 end!

  这个结果对应的执行流程:执行流程是:T1启动后wait,T2获得锁和CPU,T2宣布锁用完了其它线程可以用了,然后继续执行;T2执行完,T1被刚才T2唤醒后,等待T2执行完后,抢到了CPU,T1执行。
  T1执行完,T4获得CPU,启动,wait,T3获得了锁和CPU执行,宣布锁用完其它线程可以用了,然后继续执行,T3执行完,已经被唤醒并且等待已久的T4得到CPU,执行。
  结果示例2:

T1 start!
T2 start!
T2 end!
T1 end!
T3 start!
T3 end!
T4 start!

  这个结果对应的执行流程:T1启动,wait,让出CPU和锁,T2得以启动。T2启动,并唤醒一个线程,自己继续执行。被唤醒的线程,也就是T1等待启动机会。
T2执行完,T1抢到了CPU,执行,并结束。
  这时,只剩下T3和T4。T3抢到了CPU,于是它执行了,而且唤醒了线程,虽然此时并没有线程休眠。说白了,它浪费了一次notify。T3顺利执行完。这时,终于轮到了T4,它启动了,wait了,但是,后面已经没有线程了,它的wait永远不会有线程帮它notify了。

  • 例子3:只使用notify
		final Object object = new Object();
	    Thread t1 = new Thread() {
	    	public void run(){
	    		synchronized (object) {
	    			System.out.println("T1 start!");
	                object.notify();
	                System.out.println("T1 end!");
	            }
	        }
	    };
	    t1.start();

  结果:

T1 start!
T1 end!

  这个例子说明:如果没有线程在wait,调用notify是不会有什么问题的。

1.2 wait、notify存在的问题

1.2.1 notify唤醒过早
  • 问题
      即threadA还没开始wait的时候,threadB已经notify了。因此,threadB的通知相当于没起到任何作用。当threadB退出synchronized代码块后,threadA再开始wait,便会一直阻塞等待,直到被别的线程打断。
      示例:
public class PCTest {
	private static String lockObject = "lockObject";

	public static void main(String[] args) {
	    WaitThread waitThread = new WaitThread(lockObject);
	    NotifyThread notifyThread = new NotifyThread(lockObject);
	    notifyThread.start();
	    try {
	        Thread.sleep(3000);
	    } catch (InterruptedException e) {
	        e.printStackTrace();
	    }
	    waitThread.start();
	}

	static class WaitThread extends Thread {
	    private String lock;

	    public WaitThread(String lock) {
	       this.lock = lock;
	    }

	    @Override
	    public void run() {
	        synchronized (lock) {
	        	try {
		            System.out.println(Thread.currentThread().getName() 
		           		+ "  进去代码块");
		            System.out.println(Thread.currentThread().getName() 
		            	+ "  开始wait");
	                lock.wait();
	                System.out.println(Thread.currentThread().getName() 
	                	+ "  结束wait");
	        	} catch (InterruptedException e) {
		           e.printStackTrace();
		        }
	        }
		}
	}

	static class NotifyThread extends Thread {
	    private String lock;

	    public NotifyThread(String lock) {
	    	this.lock = lock;
		}

	    @Override
	    public void run() {
	        synchronized (lock) {
	            System.out.println(Thread.currentThread().getName() 
	            	+ "  进去代码块");
	            System.out.println(Thread.currentThread().getName() 
	            	+ "  开始notify");
	            lock.notify();
	            System.out.println(Thread.currentThread().getName() 
	            	+ "  结束开始notify");
	        }
	    }
	}
}

  结果:

Thread-1 进去代码块
Thread-1 开始notify
Thread-1 结束开始notify
Thread-0 进去代码块
Thread-0 开始wait

  • 解决方法
      针对上述代码中的问题,解决方法一般是添加一个状态标志,让waitThread调用wait方法前先判断状态是否已经改变了没,如果通知早已发出的话,WaitThread就不再去wait。示例:
public class PCTest {
	private static String lockObject = "lockObject";
	private static boolean isWait = true;

	public static void main(String[] args) {
	    WaitThread waitThread = new WaitThread(lockObject);
	    NotifyThread notifyThread = new NotifyThread(lockObject);
	    notifyThread.start();
	    try {
	        Thread.sleep(3000);
	    } catch (InterruptedException e) {
	        e.printStackTrace();
	    }
	    waitThread.start();
	}

	static class WaitThread extends Thread {
	    private String lock;

	    public WaitThread(String lock) {
	        this.lock = lock;
	    }

	    @Override
	    public void run() {
	        synchronized (lock) {
	            try {
	                while (isWait) {
		               System.out.println(Thread.currentThread().getName() 
		               		+ "  进去代码块");
	                   System.out.println(Thread.currentThread().getName() 
	                   		+ "  开始wait");
                       lock.wait();
		               System.out.println(Thread.currentThread().getName() 
		               		+ "  结束wait");
	                }
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    }
	}

	static class NotifyThread extends Thread {
	    private String lock;
	
	    public NotifyThread(String lock) {
	    	this.lock = lock;
		}

	    @Override
        public void run() {
	        synchronized (lock) {
	        	System.out.println(Thread.currentThread().getName() 
	        		+ "  进去代码块");
		        System.out.println(Thread.currentThread().getName() 
		        	+ "  开始notify");
		        lock.notifyAll();
		        isWait = false;
		        System.out.println(Thread.currentThread().getName() 
		        	+ "  结束开始notify");
		    }
	    }
	}
}

  结果:

Thread-1 进去代码块
Thread-1 开始notify
Thread-1 结束开始notify

  改进后的代码增加了一个isWait状态变量,NotifyThread调用notify方法后将状态变量置为false。在WaitThread中调用wait方法之前会先对状态变量进行判断,这样就可以根据状态变量的值,来决定是否调用wait方法,从而避免了 Notify 过早通知造成遗漏的情况。
  总结:在使用线程的等待/通知机制时,一般都要配合一个boolean变量,在notify之前改变该boolean变量的值,让wait返回后能够退出while循环(一般都要在wait方法外围加一层while循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。

1.2.2 等待wait的条件发生变化
  • 问题
      如果线程在等待时接受到了通知,但是之后等待的条件发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。
      示例:
public class PCTest {
	private static List<String> lockObject = new ArrayList();
	
	public static void main(String[] args) {
		Consumer consumer1 = new Consumer(lockObject);
		Consumer consumer2 = new Consumer(lockObject);
		Productor productor = new Productor(lockObject);
		consumer1.start();
		consumer2.start();
		productor.start();
	}
		
	static class Consumer extends Thread {
		private List<String> lock;
		public Consumer(List lock) {
		    this.lock = lock;
		}

		@Override
		public void run() {
		    synchronized (lock) {
		        try {
		            //这里使用if的话,就会存在wait条件变化造成程序错误的问题
		            if (lock.isEmpty()) {
		                System.out.println(Thread.currentThread().getName() 
		                	+ " list为空");
		                System.out.println(Thread.currentThread().getName() 
		                	+ " 调用wait方法");
		                lock.wait();
		                System.out.println(Thread.currentThread().getName() 
		                	+ " wait方法结束");
		            }
		            String element = lock.remove(0);
		            System.out.println(Thread.currentThread().getName() 
		            	+ " 取出第一个元素为:" + element);
		        } catch (InterruptedException e) {
		            e.printStackTrace();
		        }
		    }
		}
	}
		
	static class Productor extends Thread {
		private List<String> lock;
		public Productor(List lock) {
		    this.lock = lock;
		}

		@Override
		public void run() {
		    synchronized (lock) {
		        System.out.println(Thread.currentThread().getName() 
		        	+ " 开始添加元素");
		        lock.add(Thread.currentThread().getName());
		        lock.notifyAll();
		    }
		}
	}
}

  结果:

  分析:在这个例子中一共开启了3个线程,Consumer1、Consumer2和Productor。首先Consumer1调用了wait方法后,线程处于了WAITTING状态,并且将对象锁释放出来。Consumer2获取对象锁,从而进入到同步代块中,当执行到wait方法时,同样的也会释放对象锁。
  因此,productor能够获取到对象锁,进入到同步代码块中,向 list 中插入数据后,通过notifyAll方法通知处于WAITING状态的Consumer1和Consumer2线程。consumer1得到对象锁后,从wait方法出退出,删除了一个元素让List为空,方法执行结束,退出同步块,释放掉对象锁。这个时候Consumer2获取到对象锁后,从wait方法退出,继续往下执行,这个时候Consumer2再执行remove(0)操作就会出错,因为List由于Consumer1删除一个元素之后已经为空了。

  • 解决方案
      在wait退出之后再对条件进行判断即可。只将wait外围的if语句改为while循环即可,这样当List为空时,线程便会继续等待,而不会继续去执行删除list中元素的代码。
      示例:
	if (lock.isEmpty())
	//修改为下面的代码
	while (lock.isEmpty())

  总结:在使用线程的等待/通知机制时,一般都要在while循环中调用wait()方法,因此需要配合使用一个boolean变量(或其他能判断真假的条件),满足while循环的条件时,进入while循环,执行wait方法,不满足while循环的条件时,跳出循环,执行后面的代码。

1.2.3 “假死”状态
  • 问题
      如果是多消费者和多生产者情况,如果使用notify方法可能会出现“假死”的情况,即唤醒的是同类线程。
      分析:假设当前多个生产者线程会调用 wait 方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify通知处于WAITTING状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。
  • 解决办法
      将notify方法替换成 notifyAll 方法,如果使用的是 lock 的话,就将signal方法替换成signalAll方法。

1.3 wait、notify、notifyAll相关的问题

1.3.1 sleep和wait有什么区别

  两者都可以暂停线程的执行。

sleepwait
方法所属类ThreadObject
是否释放锁不释放释放
用途暂停执行线程间通信
用法sleep方法执行完成后,线程会自动苏醒wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。或者可以使用wait(long timeout)超时后线程会自动苏醒
是否捕获异常必须捕获异常不需要捕获异常
  • sleep
      sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行 catch 语句块(可能还有 finally 语句块)以及以后的代码。
      sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep而不是t线程。
  • wait
      wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
1.3.2 如何调用wait方法的,使用if块还是循环?

  处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
  wait()方法应该在循环中调用,因为当线程获取到CPU开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。
  wait和notify最常见的使用场景是生产者-消费者模式。

1.3.3 为什么wait、notify和notifyAll被定义在Object类里?

  因为Java中,任何对象都可以作为锁,并且wait(),notify()等方法用于等待对象的锁或者唤醒线程。在Java的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

1.3.4 为什么wait、notify和notifyAll必须在同步方法或者同步块中被调用?

  当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用

1.3.5 怎样唤醒一个阻塞的线程?

  可以使用以对象为目标的阻塞,即利用Object 类的 wait()和notify()方法实现线程阻塞。
  首先 ,wait()、notify()方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;
  其次,wait、notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放

wait会释放锁,notify仅仅只是通知,不释放锁。

  如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它。

1.3.6 notify和notifyAll有什么区别?

  如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁
  notifyAll() 会唤醒所有的线程,notify()只会唤醒一个线程。
  notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会(随机)唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
  综上所述,所谓唤醒线程,一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。
  notify可能会导致死锁,而notifyAll则不会。

1.3.7 Java 如何实现多线程之间的通讯和协作?

  可以通过中断共享变量的方式实现线程间的通讯和协作。线程通信协作的最常见的两种方式

  1. syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll();
  2. ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()。
1.3.8 线程B怎么知道线程A修改了变量?
  • 1、volatile修饰变量
      先看一个不使用volatile导致变量在线程之间不可见的例子:
public class ThreadTest {
    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        Thread thread = new Thread(td);
        thread.start();

        //main线程负责检查flag是否被变为了true
        while (true){
            if (td.isFlag()){
                // 如果完成了工作,那么就全部停止
                System.out.println("Thread td finished its work.");
                break;
            }
        }
    }

}

class ThreadDemo implements Runnable{

    // 内置一个标志变量
    private boolean flag = false;

    // 工作任务是将自己线程内部的标志变量变为true
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("Changed flag from false to " + isFlag() + ".");
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

  结果:

Changed flag from false to true.

  分析:在ThreadDemo对象的setFlag中,已经将flag变量的值修改成了true,但是main线程并未“看到”,所以main线程会一直在while循环中检测flag值,不能停下来。
  如果将上面代码中的flag变量用volatile修饰,即:

	private volatile boolean flag = false;

  此时的结果为:

Thread td finished its work.
Changed flag from false to true.

  分析:此时的main线程就能“看到”flag的值变化了,进而退出while循环。

  • 2、synchronized修饰修改变量的方法
      先看反例:
public class SynchronizedTest {

    //共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;
    
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        //启动线程执行写操作
        test.new WriteReadThread(true).start();
        //启动线程执行读操作
        test.new WriteReadThread(false).start();
    }

    //写操作
    public void write() {
        ready = true; //1.1
        number = 2;   //1.2
    }

    //读操作
    public void read() {
        if (ready) {              //2.1
            result = number * 3;  //2.2
        }
        System.out.println("result:" + result);
    }

    //内部类
    private class WriteReadThread extends Thread {
        
        private  boolean flag = false;
        
        public WriteReadThread(boolean flag){
            this.flag = flag;
        }
        
        @Override
        public void run() {
            if (flag)
                write();
            else
                read();
        }
    }
}

  上述代码理论上可能出现的执行顺序:

  • 1)1.1 --> 1.2 --> 2.1–> 2.2 result的值为6 (正常情况)
  • 2) 1.1 --> 2.1 --> 2.2 --> 1.2 result的值为3 (当写线程执行完1.1之后,读线程开始)
  • 3) 1.2 --> 2.1 --> 2.2 --> 1.1 result的值为0 (1.1跟1.2重排序)

  除了上面的结果,由于重排序和线程的交叉执行,还可能出现很多种执行顺序。
  导致共享变量在线程间不可见的原因:

  1. 线程的异步执行;
  2. 重排序结合线程交叉执行;
  3. 共享变量更新后的值没有在工作内存与主内存间及时更新。

  要想让共享变量在不同线程间变得可见,使用synchronized修改两个方法即可:

    public synchronized void write() {
        ready = true; //1.1
        number = 2;   //1.2
    }
    
    public synchronized void read() {
        if (ready) {             //2.1
            result = number * 3; //2.2
        }
        System.out.println("result:" + result);
    }

  • 3、wait/notify
      其实就是利用同一个对象锁实现线程间的协作,wait/notify的使用参考之前小节的例子。
1.3.9 wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

  wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

二、使用Condition中的方法

  在Lock体系中,在有相似的方法实现等待/通知机制。
  Object的wait、notify和notifyAll是与对象监视器配合完成线程间的等待/通知机制。Condition与Lock配合完成等待通知机制,前者是Java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者还有别的差别:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持。

  await和signal这些用于线程间协作的方法,都是在Condition接口中定义的:

	//当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常
	void await() throws InterruptedException;
	//当前线程进入等待状态直到被通知,中断或者超时
	long awaitNanos(long nanosTimeout) throws InterruptedException
	//与awaitNanos方法作用类似,不过可以自定义超时时间单位
	boolean await(long time, TimeUnit unit) throws InterruptedException
	//当前线程进入等待状态直到被通知,中断或者到了某个时间
	boolean awaitUntil(Date deadline) throws InterruptedException
	//唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,
	//如果在同步队列中能够竞争到Lock则可以从等待方法中返回
	void signal()
	//与signal的区别在于能够唤醒所有等待在condition上的线程
	void signalAll()

2.1 Lock中的同步队列与等待队列

  当使用Condition的时候,就会用到等待队列。Condition的获取一般都要与一个锁Lock相关,一个锁上面可以生产多个Condition。Condition接口的主要实现类是AQS的内部类ConditionObject,每个Condition对象都包含一个等待队列,该队列是Condition对象实现等待/通知的关键。也就是说,在Object的监视器模型上,一个对象拥有一个同步队列与一个等待队列,而AQS拥有一个同步队列和多个等待队列

2.1.1 ConditionObject

  创建一个condition对象是通过lock.newCondition(),这个方法实际上是会new出一个ConditionObject对象。以ReentrantLock为例:

	public Condition newCondition() {
        return sync.newCondition();
    }

  Condition是一个接口,ConditionObject是Condition的实现类,这个关系可以在AQS中看到:

	public class ConditionObject implements Condition, java.io.Serializable

  至此可以看出:ConditionObject是AQS的一个内部类。

  在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。同样的,Condition内部也是使用同样的方式,内部维护了一个等待队列,所有调用Condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。
  ConditionObject中有两个成员变量:

	private transient Node firstWaiter;
	private transient Node lastWaiter;

  可以看出ConditionObject通过持有等待队列的头尾指针来管理等待队列,此处复用了在AQS中的Node类。
  Condition内部的等待队列是一个单向队列

2.1.2 等待队列

  等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
  一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构:

  Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
  在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系:

  Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。

2.1.3 等待

  调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
  如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。Condition的await()方法:

public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
	//当前线程加入等待队列
	Node node = addConditionWaiter();
	//释放同步状态,也就是释放锁
	int savedState = fullyRelease(node);
	int interruptMode = 0;
	while (!isOnSyncQueue(node)) {
		LockSupport.park(this);
		if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
			break;
		}
		if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
			interruptMode = REINTERRUPT;
		if (node.nextWaiter != null)
			unlinkCancelledWaiters();
		if (interruptMode != 0)
			reportInterruptAfterWait(interruptMode);
	}
}

  调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
  当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
  如果从队列的角度去看,当前线程加入Condition的等待队列,该过程:

  同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。

2.1.4 通知

  调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition的signal()方法:

public final void signal() {
	if (!isHeldExclusively())
		throw new IllegalMonitorStateException();
	Node first = firstWaiter;
	if (first != null)
		doSignal(first);
}

  调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
  节点从等待队列移动到同步队列的过程:

  通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
  被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
  成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
  Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

2.2 await、signal和signalAll

2.2.1 await实现原理

  当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,该方法的源码在AQS的内部类ConditionObject中:

	public final void await() throws InterruptedException {
	    if (Thread.interrupted())
	        throw new InterruptedException();
		//1. 将当前线程包装成Node,尾插入到等待队列中
	    Node node = addConditionWaiter();
		//2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
	    int savedState = fullyRelease(node);
	    int interruptMode = 0;
	    while (!isOnSyncQueue(node)) {
			//3. 当前线程进入到等待状态
	        LockSupport.park(this);
	        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
	            break;
	    }
		//4. 自旋等待获取到同步状态(即获取到lock)
	    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
	        interruptMode = REINTERRUPT;
	    if (node.nextWaiter != null) // clean up if cancelled
	        unlinkCancelledWaiters();
		//5. 处理被中断的情况
	    if (interruptMode != 0)
	        reportInterruptAfterWait(interruptMode);
	}

  该方法的大致逻辑:当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。

  • 1、将当前线程添加到等待队列
	private Node addConditionWaiter() {
	    Node t = lastWaiter;
	    // If lastWaiter is cancelled, clean out.
	    if (t != null && t.waitStatus != Node.CONDITION) {
	        unlinkCancelledWaiters();
	        t = lastWaiter;
	    }
		//将当前线程包装成Node
	    Node node = new Node(Thread.currentThread(), Node.CONDITION);
	    if (t == null)
	        firstWaiter = node;
	    else
			//尾插入
	        t.nextWaiter = node;
		//更新lastWaiter
	    lastWaiter = node;
	    return node;
	}

  addConditionWaiter方法逻辑:将当前节点包装成Node,如果等待队列为空队列,则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可,即:通过尾插入的方式将当前线程封装的Node插入到等待队列中。
  此处可以看出等待队列是一个不带头结点的链式队列,同步队列是一个带头结点的链式队列

  • 2、释放锁
      将当前节点插入到等待对列之后,会使当前线程释放锁,由fullyRelease方法实现,fullyRelease源码:
	final int fullyRelease(Node node) {
	    boolean failed = true;
	    try {
	        int savedState = getState();
	        if (release(savedState)) {
				//成功释放同步状态
	            failed = false;
	            return savedState;
	        } else {
				//不成功释放同步状态抛出异常
	            throw new IllegalMonitorStateException();
	        }
	    } finally {
	        if (failed)
	            node.waitStatus = Node.CANCELLED;
	    }
	}

  调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

  • 3、从await方法退出
      await方法有这样一段逻辑:

      当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态。
      要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,两种方式:
  1. 逻辑走到break退出while循环;
  2. while循环中的逻辑判断为false。

  出现第1种情况的条件是当前等待的线程被中断后,代码会走到break退出。第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。

2.2.2 signal/signalAll实现原理

  调用signal/signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得锁。等待队列是先进先出(FIFO)的,所以头节点必然会是等待时间最长的节点。因此,每次调用condition的signal方法是将头节点移动到同步队列中。
  signal方法源码:

	public final void signal() {
	    //1. 先检测当前线程是否已经获取lock
	    if (!isHeldExclusively())
	        throw new IllegalMonitorStateException();
	    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
		Node first = firstWaiter;
	    if (first != null)
	        doSignal(first);
	}

  doSignal方法源码:

	private void doSignal(Node first) {
	    do {
	        if ( (firstWaiter = first.nextWaiter) == null)
	            lastWaiter = null;
			//1. 将头结点从等待队列中移除
	        first.nextWaiter = null;
			//2. while中transferForSignal方法对头结点做真正的处理
	    } while (!transferForSignal(first) &&
	             (first = firstWaiter) != null);
	}

  真正对头节点做处理的逻辑在transferForSignal方法中,该方法源码:

	final boolean transferForSignal(Node node) {
		//1. 更新状态为0
	    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
	        return false;
	
		//2.将该节点移入到同步队列中去
	    Node p = enq(node);
	    int ws = p.waitStatus;
	    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
	        LockSupport.unpark(node.thread);
	    return true;
	}

  可以看出:调用condition的signal的前提条件是当前线程已经获取了锁,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。
  sigllAll与sigal方法的区别体现在doSignalAll方法上。doSignal方法只会对等待队列的头节点进行操作,而doSignalAll不过将等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。

2.2.3 await与signal/signalAll的关系


  线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

2.3 Condition使用示例

  示例:

public class ConditionTest {
	private static ReentrantLock lock = new ReentrantLock();
	private static Condition condition = lock.newCondition();
	private static volatile boolean flag = false;

	public static void main(String[] args) {
	    Thread waiter = new Thread(new waiter());
	    waiter.start();
	    Thread signaler = new Thread(new signaler());
	    signaler.start();
	}

	static class waiter implements Runnable {
	    @Override
	    public void run() {
	        lock.lock();
	        try {
	            while (!flag) {
	                System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
	                try {
	                    condition.await();
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	            System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
	        } finally {
	            lock.unlock();
	        }
	    }
	}
	
	static class signaler implements Runnable {
	    @Override
	    public void run() {
	        lock.lock();
	        try {
	            flag = true;
	            condition.signalAll();
	        } finally {
	            lock.unlock();
	        }
	    }
	}
} 

  结果:

Thread-0当前条件不满足等待
Thread-0接收到通知条件满足

  分析:这段代码里开启了两个线程waiter和signaler,waiter线程开始执行的时候由于条件不满足,执行condition.await方法使该线程进入等待状态同时释放锁。signaler线程获取到锁之后更改条件,并通知所有的等待线程后释放锁。这时,waiter线程获取到锁,并由于signaler线程更改了条件此时相对于waiter来说条件满足,继续执行下去。

转载:https://blog.csdn.net/m0_37741420/article/details/120916675

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值