提升--08---LockSupport、淘宝面试题


LockSupport

背景:

我们会以几个小程序为案例,展开对LockSupport的讲解,在以前我们要阻塞和唤醒某一个具体的线程有很多限制比如:

  1. ​因为wait()方法需要释放锁,所以必须在synchronized中使用,否则会抛出异常IllegalMonitorStateException
  2. notify()方法也必须在synchronized中使用,并且应该指定对象
  3. ​synchronized()、wait()、notify()对象必须一致,一个synchronized()代码块中只能有一个线程调用wait()或notify()

以上诸多限制,体现出了很多的不足,所以LockSupport的好处就体现出来了。

概念;

  • 在JDK1.6中的java.util.concurrent的子包locks中引了LockSupport这个API,LockSupport是一个比较底层的工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。
  • java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport
    .park()和 LockSupport .unpark()的方法,来实现线程的阻塞和唤醒的

案例解析1:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //使用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
         //启动当前线程t
        t.start();

      
        try {
         //主线程休眠8s
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程 8s later !!!");
        
        LockSupport.unpark(t);
    }

}

在这里插入图片描述

  1. 从以上的小程序中,我们不难看出LockSupport使用起来的是比较灵灵活的,没有了所谓的限制。我们来分析一下代码的执行过程,首先我们使用lombda表达式创建了线程对象 " t " ,然后通过 " t " 对象调用线程的启动方法start(),然后我们再看线程的内容,
  2. 在for循环中,当 i 的值等于5的时候,我们调用了LockSupport的.park()方法使当前线程阻塞,注意看方法并没有加锁,就默认使当前线程阻塞了,由此可以看出LockSupprt.park()方法并没有加锁的限制。
  3. 同时进行的main主线程在执行 t.start();后,主动休眠了8s,8s后,调用了LockSupport的unpark()方法来唤醒线程 " t "

案例解析2:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport2 {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //使用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动当前线程t
        t.start();
        //唤醒线程t
        LockSupport.unpark(t);
    }
}

在这里插入图片描述

  • 我们来分析一下以上小程序,我们只需要在第一个小程序的主线程中,调用LockSupport的unpark()方法,就可以唤醒某个具体的线程,这里我们指定了线程 " t " ,代码运行以后结果显而易见,线程并没有被阻塞,我们成功唤醒了线程 " t " ,在这里还有一点,需要我们来分析一下,在主线程中线程 " t " 调用了start()方法以后,因为紧接着执行了LockSupport的unpark()方法,所以也就是说,在线程 " t "还没有执行还没有被阻塞的时候,已经调用了LockSupport的unpark()方法来唤醒线程 " t " ,之后线程 " t "才启动调用了LockSupport的park()来使线程 " t " 阻塞,但是线程 " t " 并没有被阻塞,
由此可以看出,LockSupport的unpark()方法可以先于LockSupport的park()方法执行。

案例解析3:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport2 {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //调用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                if(i == 8){
                    //调用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }

                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动当前线程t
        t.start();
        //唤醒线程t
        LockSupport.unpark(t);
    }

}

在这里插入图片描述
我们来分析一下以上小程序,在第二个小程序的基础上又添加了一个if判断,在i等于8的时候再次调用LockSupport的park()方法来使线程 " t " 阻塞, 我们可以看到线程被阻塞了,原因是LockSupport的unpark()方法就像是获得了一个“令牌”,而LockSupport的park()方法就像是在识别“令牌”,当主线程调用了LockSupport.unpark(t)方法也就说明线程 " t " 已经获得了”令牌”,当线程 " t " 再调用LockSupport的park()方法时,线程 " t " 已经有令牌了,这样他就会马上再继续运行,也就不会被阻塞了,但是当i等于8的时候线程 " t " 再次调用了LockSupport的park()方法使线程再次进入阻塞状态,这个时候“令牌”已经被使用作废掉了,也就无法阻塞线程 " t " 了,而且如果主线程处于等待“令牌”状态时,线程 " t " 再次调用了LockSupport的park()方法,那么线程 " t "就会永远阻塞下去,即使调用unpark()方法也无法唤醒了

小结:

  • ​ LockSupport不需要synchornized加锁就可以实现线程的阻塞和唤醒
  • LockSupport.unpartk()可以先于LockSupport.park()执行,并且线程不会阻塞
  • 如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒

LockSupport中park()和unpark()方法的实现原理

  • park()和unpark()方法的实现是由Unsefa类提供的,而Unsefa类是由C和C++语言完成的,
  • 其实原理也是比较好理解的,它主要通过一个变量作为一个标识,变量值在0,1之间来回切换,当这个变量大于0的时候线程就获得了“令牌”,从这一点我们不难知道,其实park()和unpark()方法就是在改变这个变量的值,来达到线程的阻塞和唤醒的,具体实现不做赘述了。

淘宝面试题 1

需求:

实现一个容器,提供两个方法add、size,写两个线程:

  • 线程1,添加10个元素到容器中
  • 线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束

小程序1

public class T01_WithoutVolatile {
    List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T01_WithoutVolatile c = new T01_WithoutVolatile();
        new Thread(() -> {
            for(int i=0; i<10; i++) {
                c.add(new Object());
                System.out.println("add " + i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            while(true) {
                if(c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}


在这里插入图片描述

执行流程:

通过内部的list来new一个ArrayList,在自定义的add方法直接调用list的add方法,在自定义的size方法直接调用list的size方法,想法很简单,首先小程序化了这个容器,接下来启动了t1线程,t1线程中做了一个循环,每次循环就添加一个对象,加一个就打印显示一下到第几个了,然后给了1秒的间隔,在t2线程中写了了一个while循环,实时监控着集合中对象数量的变化,如果数量达到5就结束t2线程。

结果分析:

  • 方法并没有按预期的执行,我们注意看t2线程中c.size()这个方法,当对象添加以后,ArraylList的size()方肯定是要更新的,我们分析一下,当t1线程中的size()放法要更新的时候,还没有更新t2线程就读了,这个时候t2线程读到的值就与实际当中加入的值不一致了.
  • 所以得出两结论,第一这个方案没有加同步,第二while(true)中的c.size()方法永远没有检测到,没有检测到的原因是线程与线程之间是不可见的

小程序 2 -----volatile

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;


public class T02_WithVolatile {

    volatile List lists = Collections.synchronizedList(new LinkedList<>());
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T02_WithVolatile c = new T02_WithVolatile();
        new Thread(() -> {
            for(int i=0; i<10; i++) {
                c.add(new Object());
                System.out.println("add " + i);
				// try {
				// 	TimeUnit.SECONDS.sleep(1);
				// } catch (InterruptedException e) {
				// 	e.printStackTrace();
				// }
            }
        }, "t1").start();
        new Thread(() -> {
            while(true) {
                if(c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}

在这里插入图片描述

结果分析:

小程序2是在小程序1的基础上做了一些改动,用volatile修饰了一下List集合,实现线程间信息的传递,但是还是有不足之处,程序还是无法运行成功,而且我们还得出,volatile一定要尽量去修饰普通的值,不要去修饰引用值,因为volatile修饰引用类型,这个引用对象指向的是另外一个new出来的对象对象,如果这个对象里边的成员变量的值改变了,是无法观察到的,所以小程序2也是不理想的。

volatile修饰引用类型,是同步地址值,如果这个对象里边的成员变量的值改变了,是无法观察到的

另外:

  • 如果把上代码的注释的休眠语句打开,程序可以达到需求,线程t2能够监控到数据size的变化.
    在这里插入图片描述
    在这里插入图片描述

分析:

  • 操作系统在他线程t1休眠的时候,有主动跟新内存的值,并及时通知线程t2进行同步

小程序 3 -----wait notify

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class T03_NotifyHoldingLock { //wait notify
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
        final Object lock = new Object();
        //需要注意先启动t2再启动t1
        new Thread(() -> {
            synchronized(lock) {
                System.out.println("t2 启动");
                if(c.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");
            }
        }, "t2").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        new Thread(() -> {
            System.out.println("t1 启动");
            synchronized(lock) {
                for(int i=0; i<10; i++) {
                    c.add(new Object());
                    System.out.println("add " + i);

                    if(c.size() == 5) {
                        lock.notify();
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
    }
}


在这里插入图片描述

执行流程:

小程序3用了锁的方式(利用wait()和notify()),通过给object对象枷锁然后调用wait()和notify()实现这道面试题,我们从头到尾分析一下,首先List集合实现的add和size方法不多做解释,我们把重点放在mian方法上,main方法里我们创建了object对象,让后写了两个线程t1和t2,t1用来增加对象,t2用来监控list集合添加的对象个数,在t2线程我们给object对象加锁,然后判断list集合对象的个数为5的时候,就调用wait()方法阻塞t2线程,并给出相应提示,t1线程里我们给object对象加锁,通过for循环来给list集合添加对象,当对象添加到5个的时候,唤醒t2线程来完成对象个数的监控,这里我们需要保证先启动的是第二个线程,让它直接进入监控状态,以完成实时监控。

结果分析:

  • 当试过了小程序3,我们会发现,这种写法也是行不通的,原因是notify()方法不释放锁,当t1线程调用了notify()方法后,并没有释放当前的锁,所以t1还是会执行下去,待到t1执行完毕,t2线程才会被唤醒接着执行,这个时候对象已经不只有5个了,所以这个方案也是行不通的。
如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

改良版小程序 3-----wait notify

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class T03_NotifyHoldingLock2 {
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T03_NotifyHoldingLock2 c = new T03_NotifyHoldingLock2();
        final Object lock = new Object();
        //需要注意先启动t2再启动t1
        new Thread(() -> {
            synchronized(lock) {
                System.out.println("t2 启动");
                if(c.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");

                //通知t1继续执行
                lock.notify();
            }
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1 启动");
            synchronized(lock) {
                for(int i=0; i<10; i++) {
                    c.add(new Object());
                    System.out.println("add " + i);
                    if(c.size() == 5) {
                        lock.notify();
                        //释放锁,让t2得以执行
                        try{
                            lock.wait();
                        }catch(InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
    }

}

在这里插入图片描述

结果分析:

在小程序3的基础上做了一些小改动,我们来分析一下执行流程,

  1. 首先t2线程执行,判断到list集合里的对象数量没有5个,t2线程被阻塞了,接下来t1线程开始执行,
  2. 当循环添加了5个对象后,唤醒了t2线程,重点在于小程序3我们说过notify()方法是不会是释放锁的,所以在notify()以后,又紧接着调用了wait()方法阻塞了t1线程,wait()方法会释放锁.实现了t2线程的实时监控,t2线程执行结束,打印出相应提示,
  3. 最后调用notify()方法唤醒t1线程,让t1线程完成执行。看过执行结果,发现示例4完成了面试题的功能成功运行。

小程序4----CountDownLatch ( 门闩 )

public class T05_CountDownLatch1 {
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T05_CountDownLatch1 c = new T05_CountDownLatch1();
        //需要注意先启动t2再启动t1
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            System.out.println("t2 启动");
            if (c.size() != 5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2 结束");

        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add " + i);

                if (c.size() == 5) {
                    // 暂停t1线程
                    latch.countDown();
                }
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
            }
        }, "t1").start();
    }

}

在这里插入图片描述

结果分析:

我们用CountDownLatch ( 门闩 ) 来完成这一题的需求,我们来分析代码的执行流程,首先我们不难看出和小程序4的写法大同小异,同样是list集合实现add和size方法,两个线程t1和t2,t1线程里是循环添加对象,t2里是实时监控,不同点在于没有了锁,采用了await()方法替换了t2线程和t1线程中的wait()方法,执行流程是创建门闩对象latch,t2线程开始启动,判断到对象不等于5,调用await()方法阻塞t2线程,t1线程开始执行添加对象,当对象增加到5个时,打开门闩让t2继续执行。

隐患bug:

执行结果看似没什么大问题,但是当我们把休眠1秒这段带代码,从t1线程里注释掉以后,会发现出错了,原因是在t1线程里,对象增加到5个时,t2线程的门闩确实被打开了,但是t1线程马上又会接着执行,之前是t1会休眠1秒,给t2线程执行时间,但当注释掉休眠1秒这段带代码,t2就没有机会去实时监控了,所以这种方案来使用门闩是不可行的。但是如果我们非得使用门闩,还要求在对象数量为5的时候把t2线程打印出来,如何实现呢?
在这里插入图片描述

改良版小程序4----CountDownLatch ( 门闩 )

  • 我们只需要在t1线程打开t2线程门闩的时候,让他再给自己加一个门闩就可以了。new2把门闩
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class T05_CountDownLatch {
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T05_CountDownLatch c = new T05_CountDownLatch();
        CountDownLatch latch = new CountDownLatch(1);
        CountDownLatch latch1 = new CountDownLatch(1);
        //需要注意先启动t2再启动t1
        new Thread(() -> {
            System.out.println("t2 启动");
            if (c.size() != 5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2 结束");
            latch1.countDown();

        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add " + i);

                if (c.size() == 5) {
                    //打开门闩,让t2得以执行
                    latch.countDown();
                    //给t1上门闩,让t2有机会执行
                    try {
                        latch1.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }, "t1").start();
    }

}

在这里插入图片描述

小程序5----LockSupport

import java.util.ArrayList;
import java.util.List;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T06_LockSupport {
    // 添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    static Thread t1 = null, t2 = null;

    public static void main(String[] args) {
        T06_LockSupport c = new T06_LockSupport();

        t2 = new Thread(() -> {
            System.out.println("t2 启动");
                LockSupport.park();

            System.out.println("t2 结束");
            LockSupport.unpark(t1);
        }, "t2");

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        t1 =  new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add " + i);
                if (c.size() == 5) {
                    LockSupport.unpark(t2);
                    LockSupport.park();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        //需要注意先启动t2再启动t1
        t2.start();
        t1.start();
    }
}

在这里插入图片描述

结果分析:

在t1线程调用unpark()方法唤醒t2线程的时候,紧接着调用park()方法使t1线程阻塞,然后在t2线程打印信息结束后调用unpark()方法唤醒t1线程

小程序6----用join方法

import java.util.ArrayList;
import java.util.List;

public class T08_Join {
    // 添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    static Thread t1 = null, t2 = null;
    public static void main(String[] args) {
        T08_Join c = new T08_Join();

        t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    c.add(new Object());
                    System.out.println("add " + i);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                t2.start();
                t2.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                for (int i = 5; i < 10; i++) {
                    c.add(new Object());
                    System.out.println("add"+i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1");
        t2 = new Thread(() -> {

                System.out.println("t2 结束");


        }, "t2");
        t1.start();
    }
}

在这里插入图片描述

  • t1线程启动t2线程,调用join()把CPU的控制权交给t2线程,t2线程打印出提示信息,并继续输出后来的对象添加信息

面试题 2

需求:

  • 写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。

方法1

容器: MyContainer1


import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class MyContainer1<T> {

    final private LinkedList<T> lists = new LinkedList<>();
    final private int MAX = 10; //最多10个元素
    private int count = 0;


    //生产者
    public synchronized void put(T t) {
        while(lists.size() == MAX) { //想想为什么用while而不是if?
            try {
                this.wait(); //effective java
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        lists.add(t);
        ++count;
        System.out.println(t);
        this.notifyAll(); //通知消费者线程进行消费
    }

    //消费者
    public synchronized T get() {
        T t = null;
        while(lists.size() == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        t = lists.removeFirst();
        count --;
        System.out.println(Thread.currentThread().getName() + ":开始消费产品.....");
        this.notifyAll(); //通知生产者线程进行生产
        return t;
    }
   
}

测试----2个生产者线程以及10个消费者


import java.util.concurrent.TimeUnit;

public class Test01 {
    public static void main(String[] args) {
        MyContainer1<String> c = new MyContainer1<>();
        
        //启动消费者线程
        for(int i=0; i<10; i++) {
            new Thread(()->{
                while (true){
                    c.get();
                }
            }, "消费者c" + i).start();
        }
        
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //启动生产者线程
        for(int i=0; i<2; i++) {
            new Thread(()->{
                while (true){
                    c.put(Thread.currentThread().getName() + " 生成了一件产品" );
                }
            }, "生产者p" + i).start();
        }
    }
}

在这里插入图片描述

分析优化:

  • 注意看我们用的是notifyAll()来唤醒线程的,notifyAll()方法会叫醒等待队列的所有方法,那么我们都知道,用了锁以后就只有一个线程在运行,其他线程都得wait(),不管你有多少个线程,这个时候被叫醒的线程有消费者的线程和生产者的线程,所有的线程都会争抢这把锁,
  • 比如说我们是生产者线程,生产满了,满了以后我们叫醒消费者线程,可是很不幸的是,它同样的也会叫醒另外一个生产者线程,假如这个生产者线程难道了这把锁刚才第一个生产者释放的这把锁,拿到了以后,它又wait()一遍,wait()完以后,又叫醒全部的线程,然后又开始争抢这把锁,
  • 其实从这个意义上来讲,生产者的线程wait的你是没有必要去叫醒别的生产者的,我们能不能只叫醒消费者线程,就是生产者线程只叫醒消费者线程,消费者线程只负责叫醒生产者线程,

方法2: 优化版本-----lock.newCondition();

生产者线程只叫醒消费者线程,消费者线程只负责叫醒生产者线程
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();

容器: MyContainer2


import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyContainer2<T> {

    final private LinkedList<T> lists = new LinkedList<>();
    final private int MAX = 10; //最多10个元素
    private int count = 0;

    private Lock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();


    public void put(T t) {
        try {
            lock.lock();
            while(lists.size() == MAX) { //想想为什么用while而不是用if?
                producer.await();
            }
            lists.add(t);
            ++count;
            System.out.println(t);
            consumer.signalAll(); //通知消费者线程进行消费
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public T get() {
        T t = null;
        try {
            lock.lock();
            while(lists.size() == 0) {
                consumer.await();
            }
            t = lists.removeFirst();
            count --;
            System.out.println(Thread.currentThread().getName() + ":开始消费产品.....");
            producer.signalAll(); //通知生产者进行生产
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }


}

测试----2个生产者线程以及10个消费者

public class Test01 {
    public static void main(String[] args) {
        MyContainer2<String> c = new MyContainer2<>();
        //启动消费者线程
        for(int i=0; i<10; i++) {
            new Thread(()->{
                while (true){
                    c.get();
                }
            }, "消费者c" + i).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //启动生产者线程
        for(int i=0; i<2; i++) {
            new Thread(()->{
                while (true){
                    c.put(Thread.currentThread().getName() + " 生成了一件产品" );
                }
            }, "生产者p" + i).start();
        }
    }
}

在这里插入图片描述

Lock和Condition的本质是什么?

上面这个小程序用了ReentrantLock,它与synchronized最大区别其实在这个面试题里已经体现出来了,ReentrantLock它可以有两种Condition条件,在put()方法里是我们的生产者线程,生产者线程lock()最后unlock()不多说,一但MAX达到峰值的时候是producer.await(),最后是consumer.signalAll(),就是说我在producer的情况下阻塞的,我叫醒的时候只叫醒consumer在get()方法里是我们的消费者线程,一但集合的size空了,我是consumer.await(),然后我只叫醒producer,这就是ReentrantLock的含义,它能够精确的指定哪些线程被叫醒,注意是哪些不是哪个,我们来说一下Lock和Condition的本质是什么

  • 如果在synchronized里调用wait()和notify()的时候,它只有一个等待队列
  • 如果lock.newnewCondition()的时候,就变成了多个等待队列
Condition的本质就是等待队列个数

Condition的本质就是等待队列个数,以前只有一个等待队列,现在我new了两个Condition,一个叫producer一个等待队列出来了,另一个叫consumer第二个的等待队列出来了,当我们使用producer.await();的时候,指的是当前线程进入producer的等待队列,使用producer.signalAll()指的是唤醒producer这个等待队列的线程,consumner也是如此,所以上面的小程序就很容易理解了,我在生产者线程里叫醒consumer等待队列的线程也就是消费者线程,在消费者线程里叫醒producer待队列的线程也就是生产者线程,这个小程序的思路就是这样了。

方法 3

多线程–07–生产者 /消费者模型

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值