synchronized+wait+notify,方法比较低级
关键在于:put和get方法必须是synchronized修饰的,并且容器为空的时候不能get,容器满了的时候,不能put。
public class Container<T> {
List<T> list=new LinkedList<>();
int max=10;
synchronized void put(T t){
while(list.size()==max){
try {
this.wait();
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
}
list.add(t);
System.out.println("add object"+t.hashCode());
this.notifyAll();//通知所有等待者进入锁池争夺线程,即通知消费者进行消费
}
synchronized void get(){
while(list.size()==0){
try{
this.wait();//wait 99%的情况都是和while一起用。防止只判断一次,再获得锁的时候不继续判断就直接执行
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
}
T ans=list.remove(0);
System.out.println("Get this Object:"+ans.hashCode());
this.notifyAll();//通知生产者继续生产
//这里用notifyAll而不是notify的意义是:notify只能令一个处于等待池中的线程去锁池中争抢锁
//而notifyAll是让等待池的所有的线程都去锁池争抢资源
//如果用notify的话,很可能只选了一个同类的线程,也只能在while循环里一直等待,程序就不能正常运行
//effective java中说的:使用notifyAll,不要使用notify
}
public static void main(String[] args) {
Container<Integer> m=new Container<>();
//开启10个消费者线程
for(int i=0;i<30;i++){
new Thread(m::get).start();
}
//开启30个生产者线程
//所有的线程只有获取到m的锁,才能执行。
for (int c=0;c<30;c++){
int finalC = c;
new Thread(()->{
m.put(finalC);
}).start();
}
}
}
这里思考一个问题:notify和notifyAll的区别
上述代码中用notifyAll而不是notify的意义是:
notify只能令一个处于等待池中的线程去锁池中争抢锁,而notifyAll是让等待池的所有的线程都去锁池争抢资源;如果用notify的话,很可能只选了一个同类的线程,也只能在while循环里一直等待,程序就不能正常运行。
lock+condition实现生产者消费者模式,稍微高级一点
当容器为空时,不能get,说明消费者线程需要阻塞。
容器满了的时候不能put,说明生产者线程需要阻塞。
当put对象之后,可以唤醒消费者线程;当get对象之后,可以唤醒生产者线程。
创建两种条件,一个是生产者条件,一个是消费者条件,很有效。
public class Container2<T> {
List<T> list=new LinkedList<>();
int max=10;
ReentrantLock lock=new ReentrantLock();
Condition producer=lock.newCondition();//使用lock new出不同的condition,调用await和signal函数准确的控制线程
Condition consumer=lock.newCondition();//也不是任何时候都是用Reentrantlock,比较简单的用synchronized能实现的,也可以用synchronized和wait来实现
//抢购的时候用sychronized和reentrantlock都差不多,最好的是用原子性的一些方法。
void put(T t) {
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+"得到锁");
while(list.size()==max){
producer.await();//如果已经放满了,就会让当前线程阻塞,释放锁
}
System.out.println(Thread.currentThread().getName()+"得到锁");
list.add(t);
System.out.println("addObject:" +t.hashCode());
//把所有处于等待队列中的线程转移到SYNC 队列,让其尝试获取锁
consumer.signalAll();
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}finally {
lock.unlock();
}
}
void get(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"得到锁");
while (list.size() == 0) {
consumer.await();
//如果没有可以获取的对象,就在while循环里阻塞着
//当interrupted之后,如果没有拿到锁,继续阻塞REINTERRUPT
//只有拿到锁才能判断list.size()并向下执行
}
System.out.println(Thread.currentThread().getName()+"得到锁");
T ans=list.remove(0);
System.out.println("get Object:"+ans.hashCode());
producer.signalAll();
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}finally{
lock.unlock();
}
}
public static void main(String[] args) {
Container2<Integer> c=new Container2<>();
for(int i=0;i<30;i++){
new Thread(c::get,"消费者"+i).start();
}
try{
TimeUnit.SECONDS.sleep(2);
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
for(int j=0;j<30;j++){
int finalJ=j;
new Thread(()->{
c.put(finalJ);
},"生产者"+finalJ).start();
}
}
}
LinkedBlockingQueue实现生产者消费者模式
LinkedBlockQueue是单端的自动阻塞无限队列。
LinkedBlockDeque是双端的自动阻塞无限队列。
添加元素,有add、offer和put方法,只有put方法在容器满时会阻塞。底层是调用await方法,自动阻塞。
取出元素,有pool、peek、get、take方法,只有take方法在容器空时,会自动阻塞,底层也是调用await方法。
但是要注意takeFirst和putLast方法是lock原子的,但是如果和其他的代码一起执行比如System.out.println
等,是不能保持原子性的。
public class test {
static final LinkedBlockingDeque<Integer> que=new LinkedBlockingDeque<>();
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
try{
int ans=que.takeFirst();
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
}).start();
}
try{
TimeUnit.SECONDS.sleep(5);
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
for(int j=0;j<10;j++){
int finalJ1 = j;
new Thread(()->{
try{
que.putLast(finalJ1);
}catch(InterruptedException e){
System.out.println("InterruptedException!!");
}
}).start();
}
}
}
关于await方法如何实现阻塞的,可以看我的另一篇文章:
从源码分析await和signal怎样实现线程间通信