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

本系列文章:
  多线程(一)线程与进程、Thread
  多线程(二)Java内存模型、同步关键字
  多线程(三)线程池
  多线程(四)显式锁、队列同步器
  多线程(五)可重入锁、读写锁
  多线程(六)线程间通信机制
  多线程(七)原子操作、阻塞队列
  多线程(八)并发容器
  多线程(九)并发工具类
  多线程(十)多线程编程示例

一、使用Object中的方法

  Object类中定义了wait/notify/notifyAll等线程间通信的方法。

	//唤醒在此对象监视器上等待的单个线程
	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:会让出对象锁,同时,当前线程休眠,等待被唤醒,如果不被唤醒,就一直等在那儿(我等会儿再用这把锁,CPU也让给你们,我先休息一会儿)。
  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有什么区别*

  相同点:

  1. 使当前线程暂停运行,把机会交给其他线程。
  2. 任何线程在等待期间被中断都会抛出InterruptedException。
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线程。
      sleep方法导致了程序暂停执行指定的时间,让出CPU该其他线程,当指定的时间到了又会自动恢复运行状态。
  • wait
      wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生。
      当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
1.3.2 如何调用wait方法的,使用if块还是循环*

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

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

  因为Java中,任何对象都可以作为锁,并且wait(),notify()等方法用于等待对象的锁或者唤醒线程。在Java的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
  JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象 。

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

  wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
  在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
  调用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则不会。
  wait()应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
  notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 Wait池中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到Wait池中。

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()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

1.3.10 上下文切换优化

  在并发程序中,并不是启动更多的线程就能让程序最大限度地并发执行。线程数量设置太小,会导致程序不能充分地利用系统资源;线程数量设置太大,又可能带来资源的过度竞争,导致上下文切换带来额外的系统开销。
  线程的生命周期:

  上下文切换可以分为两种:一种是程序本身触发的切换,称为自发性上下文切换;另一种是由系统或者虚拟机诱发的非自发性上下文切换。
  在多线程编程中,执行调用以下方法或关键字,常常就会引发自发性上下文切换:sleep()、wait()、yield()、join()、park()、synchronized、lock。
  非自发性上下文切换指线程由于调度器的原因被迫切出。常见的有:线程被分配的时间片用完,虚拟机垃圾回收导致或者执行优先级的问题导致。

  优化多线程上下文切换的几种方法:

  • 1、竞争锁优化
      在多线程编程中,锁其实不是性能开销的根源,竞争锁才是。锁的优化归根结底就是减少竞争。一些具体的方法:
  1. 减少锁的持有时间
  2. 降低锁的粒度
      可以考虑将锁粒度拆分得更小一些,以此避免所有线程对一个锁资源的竞争过于激烈。具体方式有以下两种:锁分离和锁分段。
      读写锁实现了锁分离,也就是说读写锁是由“读锁”和“写锁”两个锁实现的,其规则是可以共享读,但只有一个写。
      ConcurrentHashMap就使用了锁分段。
  3. 非阻塞乐观锁替代竞争锁
      volatile关键字的作用是保障可见性及有序性,volatile的读写操作不会导致上下文切换,因此开销比较小。
  • 2、wait/notify 优化
      Object.notify()能满足需求时,就用Object.notify()替代Object.notifyAll()。
      其次,在生产者执行完Object.notify() /notifyAll()唤醒其它线程之后,应该尽快地释放内部锁。
      建议使用Lock锁结合Condition接口替代Synchronized内部锁中的 wait /notify,实现等待/通知。这样做不仅可以解决上述的Object.wait(long)无法区分的问题,还可以解决线程被过早唤醒的问题。

  Condition接口定义的await方法 、signal方法和signalAll方法分别相当于Object.wait()、 Object.notify()和 Object.notifyAll()。

  • 3、合理地设置线程池大小,避免创建过多线程
      线程池的线程数量设置不宜过大,因为一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换。
  • 4、减少Java虚拟机的垃圾回收

二、使用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 Object的监视器方法与Condition接口的对比*

Object 监视器方法Condition
前置条件获取对象的锁调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象
调用方式直接调用,如:object.wait()直接调用,如:condition.await()
等待队列个数一个多个,使用多个condition实现
当前线程释放锁并进入等待状态支持支持
当前线程释放锁进入等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态到将来某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持

2.4 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来说条件满足,继续执行下去。

三、使用LockSupport中的方法

  LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。
  每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。

  1. 当调用unpark(thread)方法,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)。
  2. 当调用park()方法,如果当前线程的permit是1,那么将permit设置为0,并立即返回。如果当前线程的permit是0,那么当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0,并返回。

  因为permit默认是0,所以一开始调用park()方法,线程必定会被阻塞。调用unpark(thread)方法后,会自动唤醒thread线程,即park方法立即返回。

3.1 LockSupport中常用的方法

//阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
void park()
//阻塞当前线程,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
void park(Object blocker)
//阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性
void parkNanos(long nanos)
//功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
void parkNanos(Object blocker, long nanos)
//阻塞当前线程,直到deadline,deadline是一个绝对时间,表示某个时间的毫秒格式
void parkUntil(long deadline)
//功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题查
void parkUntil(Object blocker, long deadline)

//唤醒处于阻塞状态的指定线程
void unpark(Thread thread)

3.2 3种线程等待唤醒方式的对比

  • 1、使用Object类中的方法实现线程等待和唤醒
      示例:
public class Demo1 {
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
               System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
               try {
                   lock.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
           }
      });
      t1.setName("t1");
      t1.start();
      //休眠5秒
      TimeUnit.SECONDS.sleep(5);
      synchronized (lock) {
          lock.notify();
      } 
   }
}

  结果:

1563592938744,t1 start!
1563592943745,t1 被唤醒!

  t1线程中调用 lock.wait() 方法让t1线程等待,主线程中休眠5秒之后,调用 lock.notify() 方法唤醒了t1线程,输出的结果中,两行结果相差5秒左右,程序正常退出。
  唤醒方法在等待方法之前执行,线程能够被唤醒么?示例:

public class Demo2 {
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
           try {
              TimeUnit.SECONDS.sleep(5);
           } catch (InterruptedException e) {
              e.printStackTrace();
           }
           synchronized (lock) {
               System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
               try {
                   lock.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
           }
       });
       t1.setName("t1");
       t1.start();
       //休眠1秒之后唤醒lock对象上等待的线程
       TimeUnit.SECONDS.sleep(1);
       synchronized (lock) {
           lock.notify();
       }
       System.out.println("lock.notify()执行完毕");
    }
}

  结果:

lock.notify()执行完毕
1563593869797,t1 start!

  输出了上面2行之后,程序一直无法结束,t1线程调用wait()方法之后无法被唤醒了,从输出中可见,notify() 方法在 wait() 方法之前执行了,等待的线程无法被唤醒了。说明:唤醒方法在等待方法之前执行,线程无法被唤醒。
  关于Object类中的用户线程等待和唤醒的方法,其特点:

  1. wait()/notify()/notifyAll()方法都必须放在同步代码(必须在synchronized内部执行)中执行,需要先获取锁。
  2. 线程唤醒的方法(notify、notifyAll)需要在等待的方法(wait)之后执行,等待中的线程才可能会被唤醒,否则无法唤醒。
  • 2、使用Condition类中的方法实现线程等待和唤醒
      示例:
public class Demo3 {
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
           } finally {
               lock.unlock();
           }
       });
       t1.setName("t1");
       t1.start();
       //休眠5秒
       TimeUnit.SECONDS.sleep(5);
       lock.lock();
       try {
           condition.signal();
       } finally {
           lock.unlock();
       }
   }
}

  结果:

1563594349632,t1 start!
1563594354634,t1 被唤醒!

  t1线程启动之后调用 condition.await() 方法将线程处于等待中,主线程休眠5秒之后调用condition.signal() 方法将t1线程唤醒成功,输出结果中2个时间戳相差5秒。
  唤醒代码在等待之前执行,线程能够被唤醒么?示例:

public class Demo6 {
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
               TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            lock.lock();
            try {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
               System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
           } finally {
               lock.unlock();
           }
       });
       t1.setName("t1");
       t1.start();
       //休眠5秒
       TimeUnit.SECONDS.sleep(1);
       lock.lock();
       try {
           condition.signal();
       } finally {
           lock.unlock();
       }
       System.out.println(System.currentTimeMillis() + ",condition.signal();执行完毕");
    }
}

  结果:

1563594886532,condition.signal();执行完毕
1563594890532,t1 start!

  输出上面2行之后,程序无法结束,代码结合输出可以看出signal()方法在await()方法之前执行的,最终t1线程无法被唤醒,导致程序无法结束。
  关于Condition中方法使用总结:

  1. 使用Condtion中的线程等待和唤醒方法之前,需要先获取锁。否者会报IllegalMonitorStateException 异常。
  2. signal()方法先于await()方法之前调用,线程无法被唤醒。

  关于Object和Condtion中线程等待和唤醒的局限性,有以下几点:

  1. 2种方式中的让线程等待和唤醒的方法能够执行的先决条件是:线程需要先获取锁。
  2. 唤醒方法需要在等待方法之后调用,线程才能够被唤醒。

  关于这2点,LockSupport都不需要,就能实现线程的等待和唤醒。。

  • 3、使用Condition类中的方法实现线程等待和唤醒
      示例:
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            LockSupport.park();
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
       });
       t1.setName("t1");
       t1.start();
       //休眠5秒
       TimeUnit.SECONDS.sleep(5);
       LockSupport.unpark(t1);
       System.out.println(System.currentTimeMillis() + ",LockSupport.unpark();执行完毕");
   }
}

  结果:

1563597664321,t1 start!
1563597669323,LockSupport.unpark();执行完毕
1563597669323,t1 被唤醒!

  t1中调用 LockSupport.park(); 让当前线程t1等待,主线程休眠了5秒之后,调用LockSupport.unpark(t1); 将t1线程唤醒,输出结果中1、3行结果相差5秒左右,说明t1线程等待5秒之后,被唤醒了。
  LockSupport.park(); 无参数,内部直接会让当前线程处于等待中;unpark方法传递了一个线程对象作为参数,表示将对应的线程唤醒。
  唤醒方法放在等待方法之前执行,看一下线程是否能够被唤醒呢?示例:

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(() -> {
          try {
              TimeUnit.SECONDS.sleep(5);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
          LockSupport.park();
          System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被唤醒!");
      });
      t1.setName("t1");
      t1.start();
      //休眠1秒
      TimeUnit.SECONDS.sleep(1);
      LockSupport.unpark(t1);
      System.out.println(System.currentTimeMillis() + ",LockSupport.unpark();执行完毕");
   }
}

  结果:

1563597994295,LockSupport.unpark();执行完毕
1563597998296,t1 start!
1563597998296,t1 被唤醒!

  代码中启动t1线程,t1线程内部休眠了5秒,然后主线程休眠1秒之后,调用了LockSupport.unpark(t1); 唤醒线程t1,此时 LockSupport.park(); 方法还未执行,说明唤醒方法在等待方法之前执行的;输出结果中2、3行结果时间一样,表示 LockSupport.park(); 没有阻塞了,是立即返回的。
  说明:唤醒方法在等待方法之前执行,线程也能够被唤醒,这点是另外2中方法无法做到的。Object和Condition中的唤醒必须在等待之后调用,线程才能被唤醒。而LockSupport中,唤醒的方法不管是在等待之前还是在等待之后调用,线程都能够被唤醒。

  • 3种方式对比
ObjectCondtionLockSupport
前置条件需要在synchronized中运行需要先获取Lock的锁
无限等待支持支持支持
超时等待支持支持支持
等待到将来某个时间返回不支持支持支持
等待状态中释放锁会释放会释放不会释放
唤醒方法先于等待方法执行,能否唤醒线程可以
是否能响应线程中断
线程中断是否会清除中断标志
是否支持等待状态中不响应中断不支持支持不支持
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值