Java之synchronized、wait、notify和notifyAll

总结:

// 小王排队办理证件
synchronized(窗口)  {
        System.out.println("1111111")
        window.wait();
        window.notify();  
        System.out.println("222222")
}

// 路人甲排队办理证件
synchronized(窗口)  {
        window.wait();
        window.notify();  
}

小王预约了一早去办证件,办证窗口排起了长长的队伍,在等待过程中,发现漏了身份证没带,于是他打电话给家人送过来,自己一边排队一边等,眼看还有几个人就轮到他了,这时有两种情况:
1)轮到他但身份证没送到,小王退到窗口(对象)的一旁,对身后排队的人大声说(wait):“先让你们办理,等身份证到了我要立刻把位置要回来“,后面的人就一直在小王面前办证,直到看到小王的身份证送过来,这时下一位办理的人对小王同时也对后面的人说(notify):“既然你东西送过来了,你先办理吧”,然后小王重新回到窗口办理证件。
// 释放窗口占有权(对象锁),让其他人可以办理(竞争拥有锁);其他人主动礼让(其他线程notify),让小王重新占据窗口(执行wait后面的代码)

2)还没轮到他,前面还有人排队,身份证已经送到,小王对后面的人宣布(notify):“我办理完就走了”,一切还是按照原来的顺序。对于这一条队伍来说,

 

 

背景

几个线程同时执行对某一个对象读写,就会产生原子性需求,即任一时刻,最多只允许一个线程读写这个对象。

 

synchronized有什么用?

保证在同一时刻最多只有一个线程执行该段代码,可用来修饰方法、代码块、类。

  • 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  • 当然,其他线程仍然可以访问非synchronized(this)的代码块。
  • synchronized是一个非公平的锁,如果竞争激烈的话,可能导致某些线程一直得不到执行

 

wait、notify、notifyAll 有什么用?

注意:它们都是Object类而非Thread类的方法,都是在synchronized代码块中使用。

常见用法:
private Object obj = new Object();
Runnable r = new Runnable()  {
    public void run () {
          synchronized(obj) { // 保持访问obj对象的原子性
                System.out.println("11111");
                obj.notify();  // 要跳出synchronized块才起作用,但有例外:若后面有obj.wait;执行完wait后也会通知唤醒
                      或
                obj.wait();     // 调用后该线程不再活动,处于阻塞状态,JVM会切换运行其他线程

                System.out.println("22222"); 
          }

    }
);
// 一般有多个线程竞争
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();

  • Object.wait():”我困到睁不开眼了,需要马上睡觉,你们玩“,说完睡着了!
    只能出现在synchronized代码块,作用是暂停当前线程,立刻释放当前锁,让当前线程进入阻塞队列(等待状态);
    线程A 调用 obj.wait() 后,会立刻破坏synchronized代码块对 obj 的加持,CPU 会立刻失去线程A 的synchronized代码块的控制权,它后面的代码就会暂停执行,直到被另外一个线程B 调用 obj.notify() 后才能恢复继续执行,恢复执行时,synchronized代码块不是重新运行一次,而是从wait后面第一句开始执行。
     
  • Object.notify()我玩完这盘游戏就睡觉!言下之意:先声明我接下来要做什么,但现在不做,先忙完手头上的事!
    执行完synchronized方法后,才释放锁,通知JVM 随机唤醒一个 需要相同锁的、处于等待状态的线程B、C、D、E、F......(我用完了,谁要拿去用!言下之意:我还要用一会obj,直到synchronized 模块失效,之后随便你们怎么抢

     
  • Object.notifyAll()
    用法同notify,不同的是:唤醒所有  需要相同锁的、处于等待状态的线程;

 

锁是什么梗?

我们先要了解几个知识点

  • 每一个对象都有一个与之对应的监视器(Monitor)
  • 每一个监视器里面都有一个该对象的锁、一个等待队列、一个同步队列。
  • wait()而导致阻塞的线程是放在阻塞队列中的,
    竞争失败导致的阻塞是放在同步队列中的,
    notify()/notifyAll()实质上是把阻塞队列中的线程放到同步队列中

    每个对象都有一个monitor的锁,初始状态是0,执行完synchronized 值变成1.
    这时候,其他线程访问这个代码时,先检测这个对象的monitor是否是0:
    1、如果是0,就直接占用,并且把monitor改成1
    2、如果这是这个值是1,线程等待,直到其他占用者释放这个锁。
    3、synchronized code block 同理。
    4、static synchronized method 由于是静态方法,锁住的是这个类。所有对于这个类的静态同步方法,都会进行上面的竞争机制不在局限于同一个对象。
     

什么是监视器

  • 监视器可以看做是经过特殊布置的建筑,这个建筑有一个特殊的房间,该房间通常包含一些数据和代码,但是一次只能一个消费者(thread)使用此房间。 
  • 当一个消费者(线程)使用了这个房间,首先他必须到一个大厅(Entry Set)等待,调度程序将基于某些标准(e.g.FIFO)将从大厅中选择一个消费者(线程),进入特殊房间,如果这个线程因为某些原因被“挂起”,它将被调度程序安排到“等待房间”,并且一段时间之后会被重新分配到特殊房间,按照上面的线路,这个建筑物包含三个房间,分别是“特殊房间”、“大厅”以及“等待房间”。
  • 简单来说,监视器用来监视线程进入这个特别房间,他确保同一时间只能有一个线程可以访问特殊房间中的数据和代码。

 

JAVA中监视器的实现

  • 在JAVA虚拟机中,每个对象(Object和class)通过某种逻辑关联监视器,为了实现监视器的互斥功能,每个对象(Object和class)都关联着一个锁(有时也叫“互斥量”),这个锁在操作系统书籍中称为“信号量”,互斥(“mutex”)是一个二进制的信号量。
  • 如果一个线程拥有了某些数据的锁,其他的线程则无法获得锁,直到这个线程释放了这个锁。在多线程中,如果任何时候都是我们自己来写这个信号量,显然不是很方便,幸运的是,JVM为我们自动实现了这些。
  • 为了使数据不被多个线程访问,java 提供了同步块 以及同步方法两种实现,一旦一段代码被嵌入到一个synchronized关键字中,意味着放入了监视区域,JVM在后台会自动为这段代码实现锁的功能。

JAVA的同步代码中,哪一部分是监视器?

  • 我们知道JAVA每个对象(Object/class)都关联一个监视器,更好的说法应该是每个对象(Object/class)都有一个监视器,对象可以有它自己的临界区,并且能够监视线程序列为了使线程协作,JAVA为提供了wait()和notifyAll以及notify()实现挂起线程,并且唤醒另外一个等待的线程,此外这些方法有三种不同版本:
wait(long timeout, int nanos)
wait(long timeout) notified by other threads or notified by timeout.
notify(all)

这些方法只能在一个同步块或同步方法中被调用,原因是,如果一个方法不需要相互排斥,不需要监测或线程之间协作,每一个线程可以自由访问此方法,那就不需要协作。

  • 在监视器(Monitor)内部,是如何做线程同步的?

    • 监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
  •  

示例分析

class NumberPrint implements Runnable{
    private int number;
    public byte res[];
    public static int count = 5;
    public NumberPrint(int number, byte a[]){
        this.number = number;
        res = a;
    }
    public void run(){
        synchronized (res){
            while(count-- > 0){
                try {
                    res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁)
                    System.out.println(" "+number);
                    res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。
                    System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }//end of while
            return;
        }//synchronized

    }
}
public class WaitNotify {
    public static void main(String args[]){
        final byte a[] = {0};//以该对象为共享资源
        new Thread(new NumberPrint((1),a),"1").start();
        new Thread(new NumberPrint((2),a),"2").start();
    }
}




输出结果:
 1  
 2  
——线程1获得锁,wait()后的代码继续运行:1  
 1  
——线程2获得锁,wait()后的代码继续运行:2  
 2  
——线程1获得锁,wait()后的代码继续运行:1  
 1  
——线程2获得锁,wait()后的代码继续运行:2  

下面解释为什么会出现这样的结果:

首先1、2号线程启动,这里假设1号线程先运行run方法获得资源(实际上是不确定的),获得对象a的锁,进入while循环(用于控制输出几轮):

1、此时对象调用它的唤醒方法notify(),意思是这个同步块执行完后它要释放锁,把锁交给等待a资源的线程;
2、输出1;
3、该对象执行等待方法,意思是此时此刻起拥有这个对象锁的线程(也就是这里的1号线程)释放CPU控制权,释放锁,并且线程进入阻塞状态,后面的代码暂时不执行,因未执行完同步块,所以1也没起作用;
4、在这之前的某时刻线程2运行run方法,但苦于没有获得a对象的锁,所以无法继续运行,但3步骤之后,它获得了a的锁,此时执行a的唤醒方法notify(),同理,意思是这个同步块执行完后它要释放锁,把锁交给等待a资源的线程;
5、输出2;
6、执行a的等待方法,意思是此时此刻起拥有这个对象锁的线程(也就是这里的2号线程)释放CPU控制权,释放锁,并且线程进入阻塞状态,后面的代码暂时不执行,因未执行完同步块,所以2号线程的4步骤的唤醒方法也没起作用;
7、
此时1号线程执行到3步骤,发现对象锁没有被使用,所以继续执行3步骤中wait方法后面的代码,于是输出:——线程1获得锁,wait()后的代码继续运行:1;
8、此时while循环满足条件,继续执行,所以,再执行1号线程的唤醒方法,意思是这个同步块执行完后它要释放锁;
9、输出1;
10、执行等待方法,线程1阻塞,释放资源锁;
11、此时线程2又获得了锁,执行到步骤6,继续执行wait方法后面的代码,所以输出:——线程2获得锁,wait()后的代码继续运行:2;
12、继续执行while循环,输出2;
··· ···

通过上述步骤,相信大家已经明白这两个方法的使用了,但该程序还存在一个问题,当while循环不满足条件时,肯定会有线程还在等待资源,所以主线程一直不会终止。当然这个程序的目的仅仅为了给大家演示这两个方法怎么用。

 

 总结:

    wait()方法与notify()必须要与synchronized(resource)一起使用。也就是wait与notify针对已经获取了resource锁的线程进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait()线程在获取对象锁后,主动释放CPU控制权,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的释放操作。【因此,我们可以发现,wait和notify方法均可释放对象的锁,但wait同时释放CPU控制权,即它后面的代码停止执行,线程进入阻塞状态,而notify方法不立刻释放CPU控制权,而是在相应的synchronized(){}语句块执行结束,再自动释放锁。】释放锁后,JVM会在等待resoure的线程中选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制,而在同步块中的Thread.sleep()方法并不释放锁,仅释放CPU控制权。

 

其他扩展---线程顺序执行

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。

核心:
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。 
想要更深入了解,建议看一下join的源码,也很简单的,使用wait方法实现的。

t.join(); //调用join方法,等待线程t执行完毕 
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
代码实现:

public static void main(String[] args) {
        method01();
        method02();
    }
 
    /**
     * 第一种实现方式,顺序写死在线程代码的内部了,有时候不方便
     */
    private static void method01() {
        Thread t1 = new Thread(new Runnable() {
            @Override public void run() {
                System.out.println("t1 is finished");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override public void run() {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 is finished");
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t3 is finished");
            }
        });
 
        t3.start();
        t2.start();
        t1.start();
    }
 
 
    /**
     * 第二种实现方式,线程执行顺序可以在方法中调换
     */
    private static void method02(){
        Runnable runnable = new Runnable() {
            @Override public void run() {
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        };
        Thread t1 = new Thread(runnable, "t1");
        Thread t2 = new Thread(runnable, "t2");
        Thread t3 = new Thread(runnable, "t3");
        try {
            t1.start();
            t1.join();
            t2.start();
            t2.join();
            t3.start();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
  }




 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值