我是阿福,公众号 JavaClub做者,一个在后端技术路上摸盘滚打的程序员,在进阶的路上,共勉!
文章已收录在 JavaSharing 中,包含Java技术文章,面试指南,资源分享。
掌握的技术点以下:java
什么是等待/通知机制git
等待/通知机制在咱们生活中比比皆是,好比在就餐时就会出现,以下图所示:程序员
这个过程就出现了“等待/通知”机制。github
使用专业术语讲:面试
等待/通知机制,是指线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操做。上述两个线程经过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号同样,用来完成等待方和通知方之间的交互工做。编程
等待/通知机制的实现后端
wait()方法的做用:多线程
是使当前线程进入阻塞状态,同时在调用wait()方法以前线程必须得到该对象的对象级别锁,即只能在同步方法或同步代码块中调用wait()方法。在执行wait()方法以后,当前线程释放锁。并发
notify() notifyAll()方法的做用:dom
就是用来通知那些等待该对象的对象锁的其余线程,若是有多个线程等待,则由线程规划器随机挑选其中一个呈wait()状态的线程,对其发起通知notify,并使它获取该对象的对象锁。
须要说明的是:在执行notify()方法以后,当前线程不会立刻释放该对象锁,呈wait()状态的线程也不能立刻获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized
代码块,当前线程才会释放锁,而呈wait()状态所在的线程才能够获取对象锁。
强调notify(),notifyAll()也是在同步方法或者是同步代码块中调用,即在调用以前必须得到该对象的对象级别锁。
用一句话总结一下wait和notify: wait使线程中止运行,而notify使中止的线程继续运行。
下面代码实现一个示例:
建立MyList.java,代码以下:
public class MyList { private static List list=new ArrayList(); public static void add(){ list.add("anyString"); } public static int size(){ return list.size(); } }
自定义线程类 MyThread1.java
, MyThread2.java
,MyThread3.java
代码以下:
public class MyThread1 extends Thread { private Object lock; public MyThread1(Object lock) { this.lock = lock; } @Override public void run() { try { synchronized (lock) { if (MyList.size() != 5) { System.out.println("开始 wait time=" + System.currentTimeMillis()); lock.wait(); System.out.println("结束 wait time=" + System.currentTimeMillis()); } } } catch (InterruptedException e) { e.printStackTrace(); } } } public class MyThread2 extends Thread { private Object lock; public MyThread2(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { for (int i = 0; i < 10; i++) { MyList.add(); if (MyList.size() == 5) { lock.notify(); System.out.println(" 已发出通知"); } System.out.println("添加了" + (i + 1) + " 个元素!!"); } } } }
建立测试类 Test.java
public class Test { public static void main(String[] args) { Object lock=new Object(); MyThread1 myThread1=new MyThread1(lock); myThread1.start(); MyThread2 myThread2=new MyThread2(lock); myThread2.start(); } }
程序代码运行结果以下:
开始 wait time=1618832467129
添加了1 个元素!!
添加了2 个元素!!
添加了3 个元素!!
添加了4 个元素!!
已发出通知
添加了5 个元素!!
添加了6 个元素!!
添加了7 个元素!!
添加了8 个元素!!
添加了9 个元素!!
添加了10 个元素!!
结束 wait time=1618832467130
从运行的结果来看,这也说明notify()方法执行后不是当即释放锁。
线程生命周期转换图
线程的状态
线程从建立,运行到结束老是处于五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
阻塞的状况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的全部资源,JVM会把该线程放入“等待池中”。进入这个状态后是不能自动唤醒的,必须依靠其余线程调用notify()或者notifyAll()方法才能被唤醒。
(2)、同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其余线程占用,则JVM会吧该线程放入“锁池”中。
(3)、其余阻塞:经过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程从新回到就绪状态。
阻塞线程方法的说明:
wait () , sleep()的区别:
一、 sleep()睡眠时,保持对象锁,仍然占有该锁,而wait()释放对象锁.
二、 wait只能在同步方法和同步代码块里面使用,而sleep能够在任何地方使用。
三、 sleep必须捕获异常,而wait不须要捕获异常
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成必定量的数据放到缓冲区中,而后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的状况,此时进程都在等待唤醒。
解决生产者/消费者问题的方法可分为两类
(1)采用某种机制保护生产者和消费者之间的同步;
(2)在生产者和消费者之间创建一个管道。第一种方式有较高的效率,而且易于实现,代码的可控制性较好,属于经常使用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。所以本文只介绍同步机制实现的生产者/消费者问题。
同步问题核心在于
如何保证同一资源被多个线程并发访问时的完整性。经常使用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了彻底对象化,提供了对同步机制的良好支持。在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。
(1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞队列方法
(4)PipedInputStream / PipedOutputStream
下面咱们经过 wait() / notify()方法实现生产者和消费者模式:
代码场景:
当缓冲区已满时,生产者线程中止执行,放弃锁,使本身处于等状态,让其余线程执行;
当缓冲区已空时,消费者线程中止执行,放弃锁,使本身处于等状态,让其余线程执行。
当生产者向缓冲区放入一个产品时,向其余等待的线程发出可执行的通知,同时放弃锁,使本身处于等待状态;
当消费者从缓冲区取出一个产品时,向其余等待的线程发出可执行的通知,同时放弃锁,使本身处于等待状态。
代码实现:
建立仓库Storage.java
代码:
public class Storage { // 仓库容量 private final int MAX_SIZE = 10; // 仓库存储的载体 private LinkedList<Object> list = new LinkedList<>(); public void produce() { synchronized (list) { while (list.size() + 1 > MAX_SIZE) { System.out.println("【生产者" + Thread.currentThread().getName() + "】仓库已满"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(new Object()); System.out.println("【生产者" + Thread.currentThread().getName() + "】生产一个产品,现库存" + list.size()); list.notifyAll(); } } public void consume() { synchronized (list) { while (list.size() == 0) { System.out.println("【消费者" + Thread.currentThread().getName() + "】仓库为空"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.remove(); System.out.println("【消费者" + Thread.currentThread().getName() + "】消费一个产品,现库存" + list.size()); list.notifyAll(); } } }
建立生产者线程Producer.java
,消费者线程Consumer.java
,代码以下:
public class Producer implements Runnable { private Storage storage; public Producer(){} public Producer(Storage storage){ this.storage = storage; } @Override public void run(){ while(true){ storage.produce(); } } }
public class Consumer implements Runnable{ private Storage storage; public Consumer(){} public Consumer(Storage storage){ this.storage = storage; } @Override public void run(){ while(true){ storage.consume(); } } }
建立测试类TestPc.java
public class TestPc { public static void main(String[] args) { Storage storage = new Storage(); Thread p1 = new Thread(new Producer(storage)); p1.setName("张三"); p1.start(); Thread c1=new Thread(new Consumer(storage)); c1.start(); c1.setName("李四"); } }
程序运行的部分结果:
【消费者李四】消费一个产品,现库存8
【消费者李四】消费一个产品,现库存7
【消费者李四】消费一个产品,现库存6
【消费者李四】消费一个产品,现库存5
【消费者李四】消费一个产品,现库存4
【生产者张三】生产一个产品,现库存5
【生产者张三】生产一个产品,现库存6
【生产者张三】生产一个产品,现库存7
【生产者张三】生产一个产品,现库存8
【生产者张三】生产一个产品,现库存9
【生产者张三】生产一个产品,现库存10
【生产者张三】仓库已满
【消费者李四】消费一个产品,现库存9
【消费者李四】消费一个产品,现库存8
【消费者李四】消费一个产品,现库存7
在不少状况下,主线程建立并启动子线程,若是子线程中进行大量的运算,主线程每每早于子线程结束。这时主线程要等待子线程完成以后再结束。好比子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法。
join()方法就是等待线程对象销毁。
建立测试MyJoinThread.java
代码:
public class MyJoinThread extends Thread{ @Override public void run() { int secondValue= (int) (Math.random() * 10000); System.out.println(secondValue); try { Thread.sleep(secondValue); } catch (InterruptedException e) { e.printStackTrace(); } } }
建立测试类TestJoin.java
代码:
public class TestJoin { public static void main(String[] args) throws InterruptedException { MyJoinThread myJoinThread=new MyJoinThread(); myJoinThread.start(); //myJoinThread.join(); System.out.println("我想当myJoinThread对象执行完毕我再执行,答案是不肯定的"); } }
代码的运行结果:
我想当myJoinThread对象执行完毕我再执行,答案是不肯定的
9618
把myJoinThread.join()代码注释去掉运行代码执行结果以下:
82
我想当myJoinThread对象执行完毕我再执行,答案是不肯定的
因此得出结论是:join()方法使所属线程对象myJoinThread正常执行run()方法中的任务,而使当前线程main进行无限的阻塞,等待myJoinThread销毁完再继续执行main线程后面的代码。
方法join()具备使线程排队运行的做用,有点相似同步运行的效果。
join()和synchronized的区别是:join()在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监听器”的原理作的同步。
方法join()与sleep(long)的区别
方法join(long)的功能在内部使用的是wait(long)方法实现的,所用join(long)方法具备释放锁的特色。
方法join(long)源代码以下:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
从源代码中能够了解到,当执行wait(long)方法后,当前线程的锁被释放,那么其余线程能够调用此线程中的同步方法了。
而Thread.sleep(long)方法不释放锁。
咱们知道变量值的共享可使用public static 变量的形式,若是想实现每个线程都有本身的共享变量该如何解决呢? JDK中提供ThreadLocal正是解决这样的问题。
类ThreadLocal主要解决的就是为每一个线程绑定本身的值,能够将ThreadLocal类比喻成全局存放数据的盒子,盒子中能够存储每个线程的私有数据。
建立run.java
类,代码以下:
public class run { private static ThreadLocal threadLocal=new ThreadLocal(); public static void main(String[] args) { if (threadLocal.get()==null){ System.out.println("从未放过值"); threadLocal.set("个人值"); } System.out.println(Thread.currentThread().getName()+"线程:"+threadLocal.get()); } }
代码的运行结果:
从未放过值
main线程:个人值
从图中运行结果来看,第一次调用threadLocal对象的get方法返回为null,经过调用set()赋值后值打印在控制台上,类ThreadLocal解决的是变量在不一样线程间的隔离性,也就是不一样的线程拥有本身的值,不一样线程的值能够存放在ThreadLocal类中进行保存的。
验证线程变量的隔离性
建立ThreadLocalTest项目,类 Tools.java
代码以下:
public class Tools { public static ThreadLocal local=new ThreadLocal(); }
建立线程类 MyThread1.java
,MyThread2.java
代码以下:
public class MyThread1 extends Thread { @Override public void run() { for (int j = 0; j < 5; j++) { Tools.local.set(j+1); System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { Tools.local.set(i+1); System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }
建立run.java
测试类
public class run { public static void main(String[] args) { MyThread1 myThread1=new MyThread1(); myThread1.setName("myThread1线程"); myThread1.start(); MyThread2 myThread2=new MyThread2(); myThread2.setName("myThread2线程"); myThread2.start(); } }
程序运行结果:
myThread1线程get value:1
myThread2线程get value:1
myThread2线程get value:2
myThread1线程get value:2
myThread1线程get value:3
myThread2线程get value:3
myThread2线程get value:4
myThread1线程get value:4
myThread1线程get value:5
myThread2线程get value:5
虽然2个线程都向local中set()数据值,但每一个线程仍是能取到本身的数据。
文章参考:
《Java多线程编程核心技术》
https://blog.csdn.net/ldx1998...
https://blog.csdn.net/MONKEY_...
看到这里今天的分享就结束了,若是以为这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~
欢迎关注我的公众号 「JavaClub」,按期为你分享一些技术干货。