201709019工作日记--sleep、wait、notify的使用详解
1. sleep()和wait()的区分
(1)这两个方法来自不同的类分别是,sleep来自Thread类,wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep
(2)最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源,一直占用着锁;wait是进入阻塞状态,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
(3)使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){ x.notify() //或者wait() }
(4)sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
(5)Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。sleep(100)休眠一段时间后会自动唤醒,在休眠时间内处于“阻塞状态”,休眠时间结束后进入就绪状态,与其他的线程争夺CPU执行权,而sleep(0)就是重新让线程争夺CPU使用权。wait()进入阻塞状态,直到notify()唤醒,进入就绪态。
(6)新创建的但是没有执行(还没有调用start())的线程应该是处于“新建”状态,调用start()才进入“就绪”状态,在得到cpu资源以后才能开始执行run()进入“运行”状态。 注:线程在调用start()以后不会马上执行run()进入“运行”状态,而是要跟其他线程竞争cpu,在竞争中获得了cpu以后才能执行run()从而进入“运行”状态。
一张图搞定:
对图中其他函数进行解析:
(1)yield( ): 暂停线程的执行,给其它具有相同优先权的线程执行的机会,若此时没有其它线程执行,则此线程继续执行。这个函数并不会释放锁住的对象。我的理解类似于sleep(0),就是重新竞争获取CPU执行权。
(2)join( ): 等待加入的线程执行完毕才会执行下一个线程。加入的线程通过interrupt( )来唤醒。
(3)wait( ): 类似sleep( ), 不同的是,wait( )会先释放锁住的对象,然后再执行等待的动作。注意,这个函数属于Object类。另外,由于wait( )所等待的对象必须先锁住,因此,它只能用在同步化程序段或者同步化方法内,否则,会抛出异常IllegalMonitorStateException.
2.多线程中锁的理解
第二部分参考博文:http://blog.csdn.net/u013142781/article/details/51697672
所谓锁,就是指当前运行线程获取某个对象的同步监视器.如何锁,物理层面的话,不用知道了。软件层面,通俗的将,有个实例对象,该对象有个锁,某个线程先获取该对象的锁后,其他线程是不能再获取的。只有该线程主动释放锁,其他线程才可以公平的争夺这把锁。未获得锁的线程,执行到同步方法的时候,就得等着别人释放锁,然后去抢。没抢到,就继续等着被人主动释放。
1 一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限;
2 在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);
3 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。
4 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);
5 线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。
6 这样就保证了同步代码在统一时刻只有一个线程在执行。
多线程的线程同步机制实际上是靠锁的概念来控制的
在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调
1)保存在堆中的实例变量
2)保存在方法区(全局)中的类变量
这两类数据是被所有线程共享的,(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)
在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。
对于对象来说,相关联的监视器保护对象的实例变量。(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。) 为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。
但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)
在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。
3.针对锁的举例说明
在学习线程的时候,因为线程的调度具有不确定性,所以银行取钱问题、多个窗口售卖火车票问题都是反应多线程的优越性以及不确定性。当程序中有多个并发线程在进入一个代码块中并且修改其中参数时,就很有可能引发线程安全问题从而造成异常。
同步代码块:java在其中就引入了同步监视器来解决关于多线程的支持问题
1 synchronized(锁对象){ 2 需要被锁的代码 3 /**线程只有拿到了锁对象,才能执行这里的代码!!!换言之,这里的代码如果执行了,说明该线程拿到了锁对象,其他线程不能拿到该锁对象**/ 4 }
注意多个线程必须使用同一个锁对象,要不然锁无效
同步方法:其实这个还有一个同步方法,也就是用关键字synchronized来修饰一个方法,对于关键字synchronized修饰的方法,就不需要再指定同步监视器了,因为同步方法的同步监视器就是this,也就是调用了该方法的对象。
1 public synchronized void show(){} //普通方法的锁是this 2 public static synchronized void show(){} //静态方法的锁是当前类的字节码文件对象 类名.class
多个线程必须使用同一个锁对象,要不然锁无效
同步代码块锁可以是任意对象
同步方法的锁是this
静态方法的锁是当前类的字节码文件对象 类名.class
当线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,而且无论在什么时候,只能有一个线程可以获得对同步监视器的锁定,当同步代码执行完成之后,这个线程会释放该线程的同步监视器的锁定。 任何线程在修改指定的参数资源之前,都需要对该参数资源加锁,在加锁期间其他的线程是不能对这个参数资源进行修改的,只有在该线程完成修改并且释放对该参数的锁定之后其他线程才有机会对该参数进行修改。这样做也就是符合了“加锁–>修改–>释放”的逻辑顺序。
4.wait、notify的用法详解
wait(): Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.
notify(): Wakes up a single thread that is waiting on this object's monitor.
notifyAll(): Wakes up all threads that are waiting on this object's monitor.
5.为啥在用之前要获取锁?
这三个方法,都是Java语言提供的实现线程间阻塞(Blocking)和控制进程内调度(inter-process communication)的底层机制。在解释如何使用前,先说明一下两点:
1. 正如Java内任何对象都能成为锁(Lock)一样,任何对象也都能成为条件队列(Condition queue)。而这个对象里的wait(), notify()和notifyAll()则是这个条件队列的固有(intrinsic)的方法。
2. 一个对象的固有锁和它的固有条件队列是相关的,为了调用对象X内条件队列的方法,你必须获得对象X的锁。这是因为等待状态条件的机制和保证状态连续性的机制是紧密的结合在一起的。
An object's intrinsic lock and its intrinsic condition queue are related: in order to call any of the condition queue methods on object X, you must hold the lock on X. This is because the mechanism for waiting for state-based conditions is necessarily tightly bound to the mechanism fo preserving state consistency.
根据上述两点,在调用wait(), notify()或notifyAll()的时候,必须先获得锁,且状态变量须由该锁保护,而固有锁对象与固有条件队列对象又是同一个对象。也就是说,要在某个对象上执行wait,notify,先必须锁定该对象,而对应的状态变量也是由该对象锁保护的。
为什么在执行wait, notify时,必须获得该对象的锁? 一个简单的例子:这是因为,如果没有锁,wait和notify有可能会产生竞态条件(Race Condition)。考虑以下生产者和消费者的情景:
1.1生产者检查条件(如缓存满了)-> 1.2生产者必须等待 -> 2.1消费者消费了一个单位的缓存 -> 2.2重新设置了条件(如缓存没满) -> 2.3调用notifyAll()唤醒生产者 我们希望的顺序是: 1.1->1.2->2.1->2.2->2.3 但在多线程情况下,顺序有可能是 1.1->2.1->2.2->2.3->1.2。
也就是说,在生产者还没wait之前,消费者就已经notifyAll了,这样的话,生产者会一直等下去。
所以,要解决这个问题,必须在wait和notifyAll的时候,获得该对象的锁,以保证同步。
1 // main(主线程) 2 synchronized(t1) { 3 try { 4 t1.start(); 5 t1.wait(); 6 } catch(InterruptedException e) { 7 e.printStackTrace(); 8 } 9 } 10 // 在 t1 线程中唤醒主线程 11 synchronized (this) { //这里的 this 为 t1 12 this.notify(); 13 }
详细解释:
1.获取t1的监视器通过synchronized()方法
2.利用t1这个对象的锁,对多线程进入同步代码块进行限制,synchronized(t1)这里的锁定了t1,那么wait需用t1.wait()(释放掉t1)
3.因为wait需释放锁,所以必须在synchronized中使用(没有锁定则么可以释放?没有锁时使用会抛出IllegalMonitorStateException(正在等待的对象没有锁)
4.notify也要在synchronized使用,应该指定对象,t1. notify(),通知t1对象的等待池里的线程(阻塞状态)使一个线程进入锁定池,然后与锁定池中的线程争夺锁,通过竞争拿到需要的锁后线程进入就绪状态。那么为什么要在synchronized使用呢? t1. notify()需要通知一个等待池中的线程,那么这时我们必须得获得t1的监视器(需要使用synchronized),才能对其操作,t1. notify()程序只是知道要对t1操作,但是是否可以操作与是否可以获得t1锁关联的监视器有关。
参考:http://www.cnblogs.com/techyc/p/3272321.html
5.synchronized(),wait,notify() 对象一致性
在 Java 中可以用 wait、notify 和 notifyAll 来实现线程间的通信。线程在运行的时候,如果发现某些条件没有被满足,可以调用wait方法暂停自己的执行,并且放弃已经获得的锁,然后进入等待状态。当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是再次从同步代码块开始的地方开始执行。但是需要注意的一点是,对线程等待的条件的判断要使用while而不是if来进行判断。这样在线程被唤醒后,会再次判断条件是否正真满足。例如下面这个例子:
package 生产者消费者; class ThreadA extends Thread{ public ThreadA(String name) { super(name); } public void run() { synchronized (this) { try { Thread.sleep(10000); // 使当前线阻塞 1 s,确保主程序的 t1.wait(); 执行之后再执行 notify() } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" call notify()"); // 唤醒当前的wait线程 this.notify(); } } } public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); synchronized(t1) { try { // 启动“线程t1” System.out.println(Thread.currentThread().getName()+" start t1"); t1.start(); // 主线程等待t1通过notify()唤醒。 System.out.println(Thread.currentThread().getName()+" wait()"); t1.wait(); // 不是使t1线程等待,而是当前执行wait的线程等待 System.out.println(Thread.currentThread().getName()+" continue"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
显示结果如下:http://blog.csdn.net/superjunenaruto/article/details/58315357
下面列举一个生产者消费者的例子(注释已经非常清楚了):
package 生产者消费者; import java.util.LinkedList; import java.util.Queue; /** * 生产者消费者问题是一个很经典的问题,值得好好研究一下 * java的wait和notify方法在使用时也是要非常注意的 * * http://www.cnblogs.com/liujinhong/p/6661429.html */ public class ProducerConsumer { public static class Producer extends Thread { Queue<Integer> queue; int maxsize; Producer(Queue<Integer> queue, int maxsize, String name) { this.queue = queue; this.maxsize = maxsize; this.setName(name); } @Override public void run() { while (true) { synchronized (queue) { try{ Thread.sleep(500); } catch (Exception e) {} System.out.println(this.getName() + "获得队列的锁"); /** * 条件的判断一定要使用while而不是if, * 生产队列满的话就需要等待消费者消费掉之后,会有notify()进行通知唤醒生产者 */ while (queue.size() == maxsize) { System.out.println("队列已满,生产者" + this.getName() + "等待"); try { queue.wait(); } catch (Exception e) {} } //不满则往队列里面添加元素 int num = (int)(Math.random()*100); queue.offer(num); System.out.println(this.getName() + "生产一个元素:" + num); //因为生产者刚刚生产出来一个元素,队里里面一定有元素,将会通知消费者进行消费,调用notify(),唤醒消费者 queue.notifyAll(); System.out.println(this.getName() + "退出一次生产过程!"); } } } } public static class Consumer extends Thread { Queue<Integer> queue; int maxsize; Consumer(Queue<Integer> queue, int maxsize, String name) { this.queue = queue; this.maxsize = maxsize; this.setName(name); } @Override public void run() { while (true) { synchronized (queue) { try{ Thread.sleep(500); } catch (Exception e) {} System.out.println(this.getName() + "获得队列的锁"); /* * 条件的判断一定要使用while而不是if * 如果队列为空就等待生产者调用notify()唤醒自己 */ while (queue.isEmpty()) { System.out.println("队列为空,消费者" + this.getName() + "等待"); try{ queue.wait(); } catch (Exception e) {} } //有元素就从队列里面获取元素 int num = queue.poll(); System.out.println(this.getName() + "消费一个元素:"+num); //因为消费了一个元素,队列一定有剩余空间,此时可以调用notify(),通知生产者生产元素 queue.notifyAll(); System.out.println(this.getName() + "退出一次消费过程!"); } } } } public static void main(String[] args) { Queue<Integer> queue = new LinkedList<>(); int maxsize = 2; Producer producer = new Producer(queue, maxsize, "Producer"); Consumer consumer1 = new Consumer(queue, maxsize,"Consumer1"); Consumer consumer2 = new Consumer(queue, maxsize,"Consumer2"); Consumer consumer3 = new Consumer(queue, maxsize,"Consumer3"); producer.start(); consumer1.start(); consumer2.start(); consumer3.start(); } }