Java并发编程学习之路(五)wait/notify/notifyAll、await/signal/signalAll、生产者消费者问题

本系列文章:
  Java并发编程学习之路(一)并发编程三要素、Thread、Runnable、interrupted、join、sleep、yield
  Java并发编程学习之路(二)线程同步机制、synchronized、CAS、volatile、final、Lock、AQS
  Java并发编程学习之路(三)ReentrantLock、ReentrantReadWriteLock、死锁、原子类
  Java并发编程学习之路(四)线程池、FutureTask
  Java并发编程学习之路(五)线程协作、wait/notify/notifyAll、Condition、await/signal/signalAll、生产者–消费者
  Java并发编程学习之路(六)ThreadLocal、BlockingQueue、CopyOnWriteArrayList、ConcurrentHashmap
  Java并发编程学习之路(七)CountDownLatch、CyclicBarrier、Semaphore、Exchanger、Phaser
  Java并发编程学习之路(八)多线程编程例子

一、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、调用以上的方法的时候,一定要对竞争资源进行加锁,如果不加锁的话,则会报 IllegalMonitorStateException 异常。
  • 3、调用wait()进行线程等待时,必须要取得这个锁,一般是放到synchronized(obj)代码中。
  • 4、在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
  • 5、假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
  • 6、当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。

1.1 wait/notify/notifyAll作用

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

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

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

	//调用wait方法之前获得相应对象的内部锁
	synchronized(someObject){
    
    
		while(保护条件不成立){
    
    
			//调用Object.wait()暂停当前线程
			someObject.wait();
		}
		//代码执行到这里说明保护条件已满足,执行目标动作
		doAction();
	}
  • 2、notify和notifyAll作用
      notify()方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得锁。
      notify()方法任意从处于WAITTING状态的线程中挑选一个进行唤醒,使得调用 wait方法的线程从等待队列移入到同步队列中,从而使得调用 wait()方法的线程能够从 wait()方法处退出。
      调用notify()后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁
      notifyAll()与notify()作用类似,不同的地方:notifyAll()的作用是唤醒正在等待对象监视器的所有线程。
      使用Object.notify()实现通知,示例:
	synchronized(someObject){
    
    
		//更新等待线程的保护条件涉及的共享变量
		updateSharedState();
		//唤醒其他线程
		someObject.notify();
	}

1.2 wait/notify存在的问题

  • 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方法处。这样便保证了程序的正确性。

  • 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循环的条件时,跳出循环,执行后面的代码。

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

1.3 notify/notifyAll的选用

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

  1. 一次通知仅需要唤醒最多一个线程;
  2. 相应对象的等待集中仅包含同质等待线程。(同质等待线程是指这些线程用同一个保护条件,并且这些线程在Object.wait()调用返回后的处理逻辑一致。最经典的同质线程是使用同一个Runnable接口实例创建的不同线程或者从同一个Thread子类的new出来的多个线程)。

1.4 wait/notify/notifyAll的使用例子

  • 例子1
    	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
		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
		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.5 sleep() 和 wait() 有什么区别?

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

  • 1、方法所属类的不同
     sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 2、是否释放锁
    sleep() 不释放锁;wait() 释放锁
  • 3、用途不同
    sleep 通常被用于暂停执行,wait 通常被用于线程间交互/通信
  • 4、用法不同
     sleep() 方法执行完成后,线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。或者可以使用wait(long timeout)超时后线程会自动苏醒。

1.6 如何调用wait方法的?使用if块还是循环?

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

1.7 为什么wait(), notify()和 notifyAll()被定义在 Object 类里?

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

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

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

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

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

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

1.10 notify() 和 notifyAll() 有什么区别?

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

1.11 如何在两个线程间共享数据?

  线程之间的数据共享问题可以分为两类:一类是执行代码一致的的线程共享线程共享,另一类是执行代码不一致的线程共享问题。

  • 1、执行代码一致
      此种情况简单,比如读个线程使用一个Runnable即可。
  • 2、执行代码不一致
      该方法的实现:将共享数据写在内部类中,提供每个线程不同的方法(加锁),然后创建实例,作为参数传递给每个Runnable构造方法,通过构造方法传递的对象,进行数据数据共享和不同方法的调用。
      示例代码:
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Data data = new Data();
        Runnbale1 runnbale1 = new Runnbale1(data);
        Runnbale2 runnbale2 = new Runnbale2(data);
        new Thread(runnbale1).start();
        new Thread(runnbale2).start();
    }
 
    private static class Runnbale1 implements Runnable {
    
    
        private Data data;
 
        public Runnbale1(Data data) {
    
    
            this.data = data;
        }
 
        @Override
        public void run() {
    
    
            int plus = data.plus();
            System.out.println(Thread.currentThread().getName() + "---" + plus);
        }
    }
 
    private static class Runnbale2 implements Runnable {
    
    
        private Data data;
 
        public Runnbale2(Data data) {
    
    
            this.data = data;
        }
 
        @Override
        public void run() {
    
    
            int reduce = data.reduce();
            System.out.println(Thread.currentThread().getName() + "---" + reduce);
        }
    }
 
    /**
     * 共享数据内部类方式:加锁
     */
    private static class Data {
    
    
 
        private int i = 50;
 
        public synchronized int plus(){
    
    
           return i++;
        }
 
        public synchronized int reduce(){
    
    
            return i--;
        }
    }
}

  测试结果示例(这是其中一种测试结果,两个线程执行前后顺序不定,测试结果可能不同):

Thread-0—50
Thread-1—51

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

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

  1. syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll();
  2. ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()。

  最经典的生产者-消费者模型:

  • 当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。
  • 一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。
  • 当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

1.12 在 Java 程序中怎么保证多线程的运行安全?

  • 1、使用线程安全类
      如JUC包下的原子类AtomicInteger。
  • 2、使用隐式锁synchronized
  • 3、使用显式锁Lock

1.13 线程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的使用参考之前小节的例子。

二、Condition的await和signal

  在上个小节中,介绍了wait、notify和notifyAll方法实现的等待/通知机制。在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 同步队列与等待队列

  在单纯地使用显式锁,比如ReentrantLock的时候,这个锁组件内部有一个继承同步器AQS的类,实现了其抽象方法,加锁、释放锁会涉及到AQS中的同步队列。
  当使用Condition的时候,就需要用到等待队列了。Condition的获取一般都要与一个锁Lock相关,一个锁上面可以生产多个Condition。Condition接口的主要实现类是AQS的内部类ConditionObject,每个Condition对象都包含一个等待队列,该队列是Condition对象实现等待/通知的关键。
  AQS中同步队列与等待队列的关系(上面是同步队列,下面是等待队列):

  在Object的监视器模型上,一个对象拥有一个同步队列与一个等待队列,而AQS拥有一个同步队列和多个等待队列

  • 从同步队列和阻塞队列的角度看,调用condition的await方法时,相当于同步队列的首节点移到condition的等待队列中,同时线程状态转为等待状态。
  • 调用condition的signal方法时,将会把等待队列的首节点移到同步队列的尾部,然后唤醒该节点。被唤醒,并不代表就会从await方法返回,也不代表该节点的线程能获取到锁。它一样需要加入到锁的竞争中去,只有成功竞争到锁,才能从await方法返回。

2.2 等待队列

  创建一个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中有两个成员变量:

	/** First node of condition queue. */
	private transient Node firstWaiter;
	/** Last node of condition queue. */
	private transient Node lastWaiter;

  可以看出ConditionObject通过持有等待队列的头尾指针来管理等待队列,此处的复用了在AQS中的Node类。Node类还有这样一个属性:

	//后继节点
	Node nextWaiter;

  说明:等待队列是一个单向队列

2.3 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.4 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.5 await与signal/signalAll的关系


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

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

三、生产者–消费者问题

3.1 生产者–消费者问题介绍

  在生产者—消费者问题中,生产者的主要职责是生产产品,产品既可以是数据,也可以是任务。消费者的主要职责是消费生产者生产的产品,这里的消费包括对产品所代表的数据进行加工处理或执行产品所代表的任务
  生产者–消费者问题中,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据。
  为了解耦生产者和消费者的关系,通常会采用共享数据区的方式,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中去获取数据,不再需要关心生产者的行为。这个共享数据区域中应该具备线程间并发协作的功能:

  1. 如果共享数据区已满的话,阻塞生产者继续放入数据;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据。

  在实现生产者消费者问题时,可以采用三种方式:

  1. 使用Object的wait/notify的消息通知机制;
  2. 使用Condition的await/signal的消息通知机制;
  3. 使用BlockingQueue实现(BlockingQueue在后续文章详细介绍)。

  通常,生产者和消费者的处理能力是不同的,即生产者生产产品的速率和消费者消费产品的速率是不同的,较为常见的情形是生产者的处理能力比消费者的处理能力大。这种情况下,传输通道所起的作用不仅仅作为生产者和消费者之间传递数据的中介,它在一定程度上还起到一个平衡生产者和消费者处理能力的作用。
  按照生产者数量和消费者数量的组合来划分,可以分为以下几种:

类别 生产者线程数量 消费者线程数量
单生产者-单消费者 1 1
单生产者-多消费者 1 N(N>=2)
多生产者-多消费者 N(N>=2) N(N>=2)
多生产者-单消费者 N(N>=2) 1

3.2 使用wait/notify实现生产者–消费者

  此种方式指的是:通过配合调用Object对象的wait方法和notify方法或notifyAll方法来实现线程间的通信,简单来说可以分为两个步骤:

  • 在线程中调用wait方法,将阻塞当前线程;
  • 其他线程调用了调用notify方法或notifyAll方法进行通知之后,当前线程才能从wait方法返回,继续执行下面的操作。

  假设有一个箱子,最多只能装10个苹果,箱子里没苹果时不能消费(简单理解为从箱子里往外取苹果),箱子里苹果的数量为10时不能生产(简单理解为向箱子里放苹果)。要实现这一效果,至少有三个点需要关注:

  1. 要达到阻塞生产者和消费者两种效果,需要用不同的条件判断+wait实现
  2. 要达到通知生产者和消费者正常运行下去的效果,需要用notify/notifyAll实现(具体用哪种通知方法看需求,notify是随机通知一个,notifyAll是通知所有);
  3. 存放共享数据,需要选择合适的集合类(该例子比较简单,并没有用集合类,而是用了一个变量来实现)。

  示例:

//箱子,存量苹果的容器
public class Box {
    
    
	//箱子中目前苹果的数量
    int num;

    public synchronized void put() {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        
        //10是箱子中能存放苹果的最大数量
        while (num == 10) {
    
                 
            try {
    
    
                System.out.println("箱子满了,生产者暂停");
                //等待消费者消费一个才能继续生产,所以要让出锁
                this.wait();                       
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        
        num++;
        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
        //唤醒可能因为没苹果而等待的消费者
        this.notify();     
    }
    
    public synchronized void take() {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        
        while (num == 0) {
    
              
            try {
    
    
                System.out.println("箱子空了,消费者暂停");
                //等待生产者生产一个才能继续消费,所以要让出锁
                this.wait();                      
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        
        num--;
        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
        //唤醒可能因为苹果满了而等待的生产者
        this.notify();                    
    }
}

//消费者
public class Consumer implements Runnable {
    
    
	 
    private Box box;
 
    public Consumer(Box box) {
    
    
        this.box= box;
    }
 
    @Override
    public void run() {
    
    
        while (true){
    
    
            box.take();
        }
    }
}

//生产者
public class Producer implements Runnable {
    
    
	 
    private Box box;
 
    public Producer(Box box) {
    
    
        this.box= box;
    }
 
    @Override
    public void run() {
    
    
        while (true){
    
    
            box.put();
        }
    }
}

public class ConsumerAndProducer {
    
    
	 
    public static void main(String[] args) {
    
    
        Box box = new Box();
        //生产者线程
        Producer p1 = new Producer(box);  
        //消费者线程
        Consumer c1 = new Consumer(box);    
 
        new Thread(p1).start();
        new Thread(c1).start();
    }
}

  结果示例:

3.3 使用await/signal实现生产者–消费者

  该种方式实现生产者消费者,和之前一种方式原理是一样的,不过是将隐式锁换成了显式锁而已。以上个小节的功能为例,修改Box.java代码即可,示例:

//箱子,存量苹果的容器
public class Box {
    
    
	//箱子中目前苹果的数量
    int num;
    Lock lock = new ReentrantLock();
    Condition full = lock.newCondition();
    Condition empty = lock.newCondition();

    public void put() {
    
    
    	lock.lock();
    	try {
    
    
	        try {
    
    
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
    
    
	            e.printStackTrace();
	        }
	        
	        //10是箱子中能存放苹果的最大数量
	        while (num == 10) {
    
                 
	            try {
    
    
	                System.out.println("箱子满了,生产者暂停");
	                //等待消费者消费一个才能继续生产,所以要让出锁
	                full.await();                       
	            } catch (InterruptedException e) {
    
    
	                e.printStackTrace();
	            }
	        }
	        
	        num++;
	        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
	        //唤醒可能因为没苹果而等待的消费者
	        empty.signal(); 
    	}finally {
    
    
    		lock.unlock();
		}
    }
    
    public synchronized void take() {
    
    
    	lock.lock();
    	try {
    
    
	        try {
    
    
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
    
    
	            e.printStackTrace();
	        }
	        
	        while (num == 0) {
    
              
	            try {
    
    
	                System.out.println("箱子空了,消费者暂停");
	                //等待生产者生产一个才能继续消费,所以要让出锁
	                empty.await();                      
	            } catch (InterruptedException e) {
    
    
	                e.printStackTrace();
	            }
	        }
	        
	        num--;
	        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
	        //唤醒可能因为苹果满了而等待的生产者
	        full.signal();    
    	}finally {
    
    
    		lock.unlock();
		}
    }
}

结果示例:

3.4 使用阻塞队列实现生产者–消费者

  由于阻塞队列内部已经实现了两个阻塞操作:

  1. 即当队列已满时,阻塞向队列中插入数据的线程,直至队列中未满;
  2. 当队列为空时,阻塞从队列中获取数据的线程,直至队列非空时为止。

  因此可以利用阻塞队列实现生产者-消费者为题,阻塞队列完全可以充当共享数据区域,就可以很好的完成生产者和消费者线程之间的协作。
  仍以上面的向箱子里装苹果为例,由于阻塞队列本身具有阻塞功能,因此阻塞功能不再需要关注,但还是有通知部分的代码需要我们实现,此处写的比较简单,当箱子满了或空了时,对应的线程直接释放对应的锁。仍然只修改Box.java代码即可,示例:

//箱子,存量苹果的容器
public class Box {
    
    
    //存放苹果的队列
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(10);
    

    public synchronized void put() {
    
    	        	        
	    try {
    
    
	    	Thread.sleep(1000);
	    	if(blockingQueue.size() == 10) {
    
    
	    		System.out.println("箱子已装满");
	    		return;
	    	}
			blockingQueue.put(1);
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}

	    System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+blockingQueue.size());
    }
    
    public synchronized void take() {
    
    
	    try {
    
    
	    	if(blockingQueue.size() == 0) {
    
    
	    		System.out.println("箱子已空");
	    		return;
	    	}
	    	Thread.sleep(1000);
			blockingQueue.take();
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}
	        
	    System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+blockingQueue.size());
    }
}

  结果示例:

  当消费者的处理能力低于生产者的处理能力时,产品的生产速率大于消费速率,这会导致队列中的产品积压,即队列中存储的产品会越来越多,这些对象所占的内存空间及其他资源越来越多。此时,可以使用有界阻塞队列作为传输通道。
  使用有界队列作为传输通道时,可能会产生一种现象:当消费者的能力跟不上生产者的处理能力时,队列中的产品会逐渐积压到队列满。此时生产者会被暂停,直到消费者消费了部分产品使队列非满,这相当于生产者暂停其产品生产而给消费者一个跟上步伐的机会,此处的代价可能是增加的上下文切换。
  有界队列可以使用ArrayBlockingQueue或LinkedBlockingQueue来实现。ArrayBlockingQueue内部使用一个数组作为其存储空间,而数组的存储空间是预先分配的,因此ArrayBlockingQueue的put、take操作本身不会增加垃圾回收的负担。  ArrayBlockingQueue的缺点是内部在实现put、take操作的时候使用的是同一个锁(显式锁),从而可能导致锁的高争用,导致较多的上下文切换
  LinkedBlockingQueue既能实现无界队列,也能实现有界队列,LinkedBlockingQueue实例化时可以指定队列容量。LinkedBlockingQueue的优点是其内部在实现put、take操作的时候使用了两个显式锁,这降低了锁争用的可能性。

  LinkedBlockingQueue的内部存储空间是一个链表,链表的节点所需的存储空间是动态分配的,put、take操作都会导致链表节点的动态创建和移除,因此LinkedBlockingQueue的缺点是会增加垃圾回收的负担。此外,由于LinkedBlockingQueue的put、take操作使用的是两个锁,因此LinkedBlockingQueue维护其队列的当前长度(size)无法通过一个普通的int型变量而是使用的原子变量(AtomicInteger),这个原子变量可能会被生产者线程和消费者线程争用,因此可能导致额外的开销。
  SynchronousQueue可以被看作一个特殊的有界队列,SynchronousQueue内部并不维护用于存储队列元素的存储空间。SynchronousQueue适合于在消费者处理能力与生产者处理能力相差不大的情况下使用
  ArrayBlockingQueue和SynchronousQueue都既支持公平调度也支持非公平调度,而LinkedBlockingQueue仅支持非公平调度
  如果生产者线程和消费者线程之间的并发程度比较大,那么这些线程对传输通道内部锁使用的锁的争用可能性也随之增加。这时,有界队列的实现适合用LinkedBlockingQueue,否则可以考虑ArrayBlockingQueue。
  阻塞队列也支持非阻塞式操作(即不会导致线程被暂停)。比如,BlockingQueue接口定义的offer(E)和poll()分别相当于put(E)和take()的非阻塞版。非阻塞式方法通常用特殊的返回值作为结果。offer(E)的返回值false表示入队失败(队列已满),poll返回null表示队列为空。
  生产者-消费者使用规则:

  1. LinkedBlockingQueue适合在生产者线程和消费者线程之间的并发程度比较大情况下使用。
  2. ArrayBlockingQueue适合在生产者线程和消费者线程之间的并发程度较低的情况下使用。
  3. SynchronousQueue适合在消费者处理能力与生产者处理能力相差不大的情况下使用。

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/120916675
今日推荐