Object源码阅读,顺带分析了一波notify和wait


不忘初心,方得始终

Object源码剖析(顺带分析了notify/wait)

package java.lang;

public class Object {   //所有的类都继承了Object,所有对象,包括数组都实现了这个类的方法

    private static native void registerNatives();
    static {
        registerNatives();      //静态代码块,初始化时调用,调用本地方法
    }

    public final native Class<?> getClass();
    //返回这个Object运行时的对象

    public native int hashCode();
    //返回这个对象的哈希值,默认返回该对象在内存中的地址,重写该方法可以提高hashmap的性能

    public boolean equals(Object obj) {
        return (this == obj);
    }
    //对象比较方法,默认比较对象的地址是否相等,重写equals一般也会重写hashcode

    protected native Object clone() throws CloneNotSupportedException;
    //对象克隆方法,有深克隆和浅克隆之分,支持复制的类必须要实现Cloneable接口,否则产生异常

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //toString方法,返回当前类的名字+@+默认hashcode方法16进制的值,重写可提高类的可读性
    
    public final native void notify();
    //对象唤醒方法,唤醒在对象监视器等待的单个线程,调用该方法后本线程进入对象锁定池,准备获取对象锁,进入运行状态

    public final native void notifyAll();
    //全部对象唤醒方法,唤醒在对象监视器等待的全部线程,具体唤醒哪个由操作系统决定

    public final native void wait(long timeout) throws InterruptedException;
    //线程等待方法,timeout表示要等待的最长时间(以毫秒为单位),调用该方法后,线程会放弃对象锁,进入等待此对象的等待锁定池

    public final void wait(long timeout, int nanos) throws InterruptedException {
        //加上超值时间,但是也难以达到,实际上是直接+1
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
            //等待的时间小于0,为负值抛出异常(以毫秒为单位)
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
            //等待时间小于0或者额外时间大于999999(以毫秒为单位)
        }

        if (nanos > 0) {
            timeout++;  //等待时间,毫秒+1
        }
        wait(timeout);
        //调用wait(timeout)方法
    }

    public final void wait() throws InterruptedException {
        wait(0);
    }
    //对象等待方法,等待时间为0,直接进入等待池里面

    protected void finalize() throws Throwable { }
    //不应该继续使用的方法
}

一. 等待/唤醒机制的常见用法

Java多线程开发中,我们常用到wait()和notify()方法来实现线程间的协作,简单的说步骤如下:

  1. A线程取得锁,执行wait(),释放锁;

  2. B线程取得锁,完成业务后执行notify(),再释放锁;

  3. B线程释放锁之后,A线程取得锁,继续执行wait()之后的代码;(请参考下面的代码一,线程相互通信)

二. Object里面的wait与notify方法


(1).监视器,对象锁,锁池与等待池

Java 虚拟机上运行的每个对象来说都一个内置的监视器(Monitor),Monitor里面又有一个内置的对象锁和两个池,锁(monitor)池和等待(wait)池(等待队列),而这两个池又与 Object 基类的 wait、notify、notifyAll 三个方法和 同步代码块 相关。

锁池的本质

就是假设线程 A 已经拥有了某个对象(不是类)的锁,而其它线程 B、C 想要调用这个对象的某个 synchronized 方法(或者块),由于这些 B、C 线程在进入对象的 synchronized 方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程 A 所拥有,所以这些 B、C 线程就进入了该对象的锁池,这就是锁池,也是之前存在的疑问之一,请见文末。
在这里插入图片描述
等待池的本质

就是假设线程 A 调用了某个对象的 wait() 方法,线程 A 就会释放该对象的锁(因为 wait() 方法必须在 synchronized中使用,所以执行 wait() 方法前线程 A 已经持有了该对象的锁),同时线程 A 就进入到了该对象的等待池中。

如果此时线程 B 调用了相同对象的 notifyAll() 方法,则处于该对象等待池中的线程就会全部进入该对象的锁池中去准备争夺一个锁的拥有权,没有获取到锁而已经呆在锁池的线程只能继续等待其他机会获取锁,而不能再主动回到等待池(除非该线程调用wait方法)。而如果此时线程 B 调用的是相同对象的 notify() 方法,则仅仅会有一个处于该对象等待池中的线程**(随机,具体唤醒哪一个,由cpu调度决定)**进入该对象的锁池中去准备争夺锁的拥有权(notify进入锁池之后是否还需要竞争???,目前是还没解决的疑问之一)。

请花点时间认真阅读下图(理解每一步执行顺序)
在这里插入图片描述


(2).注意事项

  • 当一个线程A调用了某个对象的 wait() 方法后就必须等其他线程B或者C调用这个对象的 notify/notifyall 方法**(意味着必须是调用同一个对象的wait和notify方法,但不是要同一个线程调用,因为调用wait的线程已经进入等待池里面了)**

  • notify方法只有一个线程在等待的时候它才有用武之地,但是还是不推荐使用notify,因为会容易造成死锁和不想关的线程的意外或者恶意的等待—来自Effective Java。请参考下面的代码二(notify发生死锁)

  • **wait与notify方法只能用在synchroized 同步代码块或者同步方法,并且在当前对象的同步块中。**如果在 A 对象的方法中调用 B 对象的 wait 或者 notify 方法,虚拟机会抛出 IllegalMonitorStateException,非法的监视器异常,因为你这个线程持有的监视器和你调用的监视器的不是一个对象,非法监视器异常的情况还有以下这些,还有实现中断和虚假唤醒是可能的,请参考下面的代码三(虚假唤醒)

    • 如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException
    • 如果调用 notify()时,没有持有适当的锁,也会抛出 IllegalMonitorStateException
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

  • 多线程由于synchronized抢占对象锁,被阻塞,进入Blocked状态,放入基于锁的等待队列(锁池),并不是在等待池中,也是疑问中的第三点。

(3).工作流程

  1. wait的工作流程:调用任何对象的wait()方法会让当前线程进入等待池,并且用来释放对象锁

  2. notify的工作流程:对象调用notify方法,用来通知那些可能等待该对象的对象锁的其他线程(就是在等待池里面的线程们)

    如果调用notify时,有多个线程等待,则线程规划器任意从等待池挑选出其中一个的线程来发出通知,并使它等待获取该对象的对象锁(调用notify 后,当前线程不会马上释放该对象锁,此时内定的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,内定的线程才可以获取该对象锁),但不惊动其他同样在等待池的线程们。

    当第一个获得了该对象锁的线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他在锁池等待的线程由于没有得到该对象的通知,会继续阻塞在锁池,直到这个对象发出一个 notify 或 notifyAll

    这里需要注意:它们等待的是被 notify 或 notifyAll,而不是等待对象锁,等待对象锁应该时在notify或notifyAll方法之后。如果时调用了notify方法,则等待锁的过程应该是内定了对象锁的线程之后,等待调用wait方法之后或者说退出同步代码块之后。如果调用了notifyAll()方法执行后的情况不同,请看下面的notifyAll工作流程。

  3. notifyAll 的工作流程:notifyAll 使所有原来在该对象上的等待池的线程统统退出等待池(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(调用了 wait方法或者退出synchronized 代码块的时候)他们就会去竞争。

    如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

    言简意赅的小结:

    notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:

    notifyAll使在该对象上所有wait状态的线程变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

    notify则文明得多,他只是选择一个wait状态线程进行通知,并使它变成等待该对象上的锁,一旦该对象被解锁,它自己去执行。

    即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象再次发出一个notify或notifyAll。

    notify()方法只会唤起一个线程, 且无法指定唤醒哪一个线程,所以只有在多个执行相同任务的线程在并发运行时, 我们不关心哪一个线程被唤醒时,才会使用notify()

    必须从同步环境内调用wait()、notify()、notifyAll()方法,而且必须是先wait,后notify。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。

(4).什么时候释放对象锁?

由于等待一个锁定线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:

  1. 执行完同步代码块。
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止。
  3. 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进行对象的等待池。
    除了以上情况外,只要持有锁的此案吃还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁
  4. 在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
  5. 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
  6. 在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃。

(5).在synchronized中才能使用wait与notify方法?

假设有2个线程,分别是生产者和消费者,他们有各自的任务。

1.1生产者检查条件(如缓存满了)-> 1.2生产者必须等待
2.1消费者消费了一个单位的缓存 -> 2.2重新设置了条件(如缓存没满) -> 2.3调用notifyAll()唤醒生产者

我们希望的顺序是: 1.1->1.2->2.1->2.2->2.3
但是由于CPU执行是随机的,可能会导致 2.3 先执行,1.2 后执行,这样就会导致生产者永远也醒不过来了!

所以我们必须对流程进行管理,也就是同步,通过在同步块中并结合 wait 和 notify 方法,我们可以手动对线程的执行顺序进行调整。

synchronized 的含义:

  • Java中每一个对象都可以成为一个监视器(Monitor), 该Monitor由一个锁(lock), 一个等待队列(waiting queue ), 一个入口队列( entry queue).
  • 对于一个对象的方法, 如果没有synchronized关键字, 该方法可以被任意数量的线程,在任意时刻调用。
  • 对于添加了synchronized关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。
  • synchronized用于实现多线程的同步操作

wait()功用

  • wait(), notify(), notifyAll()synchonized 需要搭配使用, 用于线程同步

  • 从语义角度来讲, 一个线程调用了wait()之后, 必然需要由另外一个线程调用notify()来唤醒该线程, 所以本质上, wait()notify()的成对使用, 是一种线程间的通信手段。

  • 进一步分析, wait() 操作的调用必然是在等待某种条件的成立, 而条件的成立必然是由其他的线程来完成的。 所以实际上, 我们调用 wait() 的时候, 实际上希望达到如下的效果

// 线程A 的代码
while(!condition){ // 不能使用 if 存疑, 因为存在一些特殊情况, 使得线程没有收到 notify 时也能退出等待状态
    wait();
}
// do something
// 线程 B 的代码
if(!condition){ 
    // do something ...
    condition = true;
    notify();
}

现在考虑, 如果wait()notify() 的操作没有相应的同步机制, 则会发生如下情况

  1. 【线程A】 进入了 while 循环后突然被挂起
  2. 【线程B】 执行完毕了 condition = true; notify(); 的操作, 此时【线程A】的 wait() 操作尚未被执行, notify() 操作没有产生任何效果
  3. 【线程A】执行wait() 操作, 进入等待状态,如果没有额外的 notify() 操作, 该线程将持续在 condition = true 的情形下, 持续处于等待状态得不到执行。

那是否简单的将之前的代码包裹在一个 synchronized 代码块中就可以满足需求呢?

// 线程A 的代码
synchronized(obj_A)
{
    while(!condition){ 
        wait();
    }
    // do something 
}
// 线程 B 的代码
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        notify();
    }
}

乍一看, 上述的代码可以解决问题, 但是仔细分析一下, 由于wait() 操作会挂起当前线程, 那么必然需要在挂起前释放掉 obj_A 的锁, 但如果 obj_A 允许是任意一个指定的对象, wait() 操作内部必然无法得知应该释放哪个对象的锁, 除非将需要释放锁的对象作为参数传给 wait() 方法。那么很自然的, 语法就会被设计成 java 现在的样子。即基于对象的 wait()notify() 的调用, 必须先获得该对象的锁。

正确的用法示例如下

// 线程 A 的代码
synchronized(obj_A)
{
    while(!condition){ 
        obj_A.wait();
    }
    // do something 
}
// 线程 B 的代码
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        obj_A.notify();
    }
}

(6).如何正确地使用唤醒等待机制?—— effective java

始终优先使用并发工具,而不是使用wait和notify,但可能必须维护使用了wait和notify的遗留代码。wait方法被用来使线程等待某个条件。必须在同步区域内部被调用,这个同步区域将对象锁定在了wait方法上的对象。下面是使用wait方法的标准模式:

// The standard idiom for using the wait method,使用wait方法的标准习惯用法
synchronized(obj){
    while(<condition does not hold>){ //条件不成立
        obj.wait(); //(Release lock,and reacquire on wakeup,释放锁,重新获取的时候唤醒)
    }
}

始终应该使用wait循环模式来调用wait方法:永远不要在循环之外调用wait方法。因为循环会在等待之前和之后测试条件**(基于while来反复判断进入正常操作的临界条件是否满足)**。

在wait方法之前测试条件:当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程之前等待之前,notify/notifyAll 方法就已经被调用,则无法保证该线程将会从等待中苏醒过来。

在等待之后测试条件:如果条件不成立的话继续等待,这对于确保安全性是必要的。如果当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系,所以我们不要这样做。当条件不成立的时候,有下面的理由可使一个线程苏醒过来:

  • 另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。
  • 条件并不成立,但是另一个线程可能意外的或恶意的调用了notify。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的wait都会出现这样的问题。
  • 通知线程在唤醒等待线程时可能会过度“大方”。例如,即使只有某一些等待线程的条件已经被满足,但是通知线程可能仍然调用notifyAll。
  • 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒”。

一种常见的说法是,你总应该使用notifyAll,这是合理而保守的建议,它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程,你可能也会唤醒其他一些线程,但是这并不会影响程序的正确性。这些线程醒来的时候,会检查它们正在等待的条件,如果发现条件并不满足,就会继续等待。

从优化的角度,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你就应该选择调用notify,而不是notifyAll。但即使这些条件都是真的,也许还是有理由使用notifyAll而不是notify。就好像把wait调用放在一个循环里面,以避免在公有可访问对象上的意外或恶意的通知。与此类似,使用notifyAll代替notify可以避免来自不相关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使得真正接受的线程无限地等待下去。

请看下面的例子去理解while是如何在wait前后反复检查的

public class EarlyNotify extends Object {
    private List list;

    public EarlyNotify() {
        list = Collections.synchronizedList(new LinkedList());
    }

    public String removeItem() throws InterruptedException {
        print("in removeItem() - entering");

        synchronized ( list ) {
            if ( list.isEmpty() ) {  //这里用if语句会发生危险
                print("in removeItem() - about to wait()");
                list.wait();
                print("in removeItem() - done with wait()");
            }

            //删除元素
            String item = (String) list.remove(0);

            print("in removeItem() - leaving");
            return item;
        }
    }

    public void addItem(String item) {
        print("in addItem() - entering");
        synchronized ( list ) {
            //添加元素
            list.add(item);
            print("in addItem() - just added: '" + item + "'");

            //添加后,通知所有线程
            list.notifyAll();
            print("in addItem() - just notified");
        }
        print("in addItem() - leaving");
    }

    private static void print(String msg) {
        String name = Thread.currentThread().getName();
        System.out.println(name + ": " + msg);
    }

    public static void main(String[] args) {
        final EarlyNotify en = new EarlyNotify();

        Runnable runA = new Runnable() {
            public void run() {
                try {
                    String item = en.removeItem();
                    print("in run() - returned: '" +
                            item + "'");
                } catch ( InterruptedException ix ) {
                    print("interrupted!");
                } catch ( Exception x ) {
                    print("threw an Exception!!!\n" + x);
                }
            }
        };

        Runnable runB = new Runnable() {
            public void run() {
                en.addItem("Hello!");
            }
        };

        try {
            //启动第一个删除元素的线程
            Thread threadA1 = new Thread(runA, "threadA1");
            threadA1.start();

            Thread.sleep(500);

            //启动第二个删除元素的线程
            Thread threadA2 = new Thread(runA, "threadA2");
            threadA2.start();

            Thread.sleep(500);
            //启动增加元素的线程
            Thread threadB = new Thread(runB, "threadB");
            threadB.start();

            Thread.sleep(10000); // wait 10 seconds

            threadA1.interrupt();
            threadA2.interrupt();
        } catch ( InterruptedException x ) {}
    }
}

首先启动threadA1,threadA1在removeItem()中调用wait(),从而释放list上的对象锁。再过500ms,启动threadA2,threadA2调用removeItem(),获取list上的对象锁,也发现列表为空,从而在wait()方法处阻塞,释放list上的对象锁。再过500ms后,启动threadB,并调用addItem,获得list上的对象锁,并在list中添加一个元素,同时用notifyAll通知所有线程。

threadA1和threadA2都从wait()返回,等待获取list对象上的对象锁,并试图从列表中删除添加的元素,这就会产生麻烦,只有其中一个操作能成功。假设threadA1获取了list上的对象锁,并删除元素成功,在退出synchronized代码块时,它便会释放list上的对象锁,此时threadA2便会获取list上的对象锁,会继续删除list中的元素,但是list已经为空了,这便会抛出IndexOutOfBoundsException。

解决办法:

将if换为while。重新判断是否为空,为空时继续等待挂起。

放在while里面,是防止出于waiting的对象被别的原因调用了唤醒方法,但是while里面的条件并没有满足(也可能当时满足了,但是由于别的线程操作后,又不满足了),就需要再次调用wait将其挂起。

好笑的解释:

如果是两个狙击手,在同时等待一个人,第一个人先打死了(改变了某个状态,不满足了条件了),第二个人还要再开一枪?针对两个线程同时等待还是要加这个while的。

简而言之:

  1. 使用notify和wait就像使用“并发汇编语言”,应该在juc包下使用更高级的语言。
  2. 没有理由在新代码中使用wait和notify,即使有,也很少。
  3. 除非是维护旧的代码,那么应该利用标准的模式从while循环内部调用wait,优先使用notifyAll,如果使用notify,就一定要保证程序的活性。
  4. 因为wait()并不是Thread类下的函数,我们并不能使用Thread.call()。事实上很多Java程序员都喜欢这么写,因为习惯了使用Thread.sleep(),但很快他们就会发现这并不能顺利解决问题。正确的方法是对在多线程间共享的那个Object来使用wait。在生产者消费者问题中,这个共享的Object就是那个缓冲区队列。
  5. 既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。
  6. 永远在循环(loop)里调用 wait 和 notify,不是在 If 语句!!!因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者开始小号数据。
  7. 所以记住,永远在while循环而不是if语句中使用wait!我会推荐阅读《Effective Java》,这是关于如何正确使用wait和notify的最好的参考资料。在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。如果条件并未改变,wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。

(7).为什么一定要while判断,不能用if

synchronized (monitor) {
//  判断条件谓词是否得到满足*
if(!locked) {
//  等待唤醒*
monitor.wait();
}
//  处理其他的业务逻辑*
}

如果采用if判断,当线程从wait中唤醒时,那么将直接执行处理其他业务逻辑的代码,但这时候可能出现另外一种可能,条件谓词已经不满足处理业务逻辑的条件了,从而出现错误的结果,于是有必要进行再一次判断,如下

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    if(!locked) {
        //  等待唤醒
        monitor.wait();
        if(locked) {
            //  处理其他的业务逻辑
        } else {
            //  跳转到monitor.wait(); 
        }
    }
}

(8).notify 和 notifyAll 的一段代码分析

原文链接: stackoverflow 翻译: ImportNew.com - 踏雁寻花
译文链接: http://www.importnew.com/10173.html
[ 转载请保留原文出处、译者和译文链接。]

本文由 ImportNew - 踏雁寻花 翻译自 stackoverflow。欢迎加入翻译小组。转载请见文末要求。

【本文已经根据网友的意见,进行了修改,考虑到网友会看不懂评论内容,我将本文的第一版发布到了博客中,点击这里 踏雁寻花 可以阅读。 】

根据网友的意见,修改版如下:

当你 Google”notify() 和 notifyAll() 的区别” 时,会有大片的结果弹出来,(这里先把 jdk 的 javadoc 文档那一段撇开不说), 所有这些搜索结果归结为等待的线程被唤醒的数量:notify() 是唤醒一个, 而 notifyall() 是唤醒全部. 那他们的真正区别是什么呢?

让我们来看看个生产者 / 消费者的案例(假设生产者 / 消费者这个类中有两个方法 put 和 get),它是有问题的(因为这里用到了 notify 方法),是的,这段代码也许会执行,甚至大部分情况下能够正常运行,但是它也是有可能会出发生死锁,我们来看一看原因:

`public` `synchronized ``void` `put(``Object` `o) {``    ``while` `( buf.size() == MAX_SIZE) {``         ``wait(); ``// 如果buffer为full,就会执行wait方法等待(为了简单,我们省略try/catch语句块)``    ``}``    ``buf.add(o);``    ``notify(); ``// 通知所有正在等待对象锁的Producer和Consumer(译者注:包括被阻挡在方法外的Producer和Consumer)``}` `// Y:这里是C2试图获取锁的地方(原作者将这个方法放到了get方法里面,此处,我把它放在了方法的外面)   ``public` `synchronized ``Object` `get``() {``    ``while` `( buf.size() == ``0``) {``         ``wait(); ``// 如果buffer为Null,就会执行wait方法(为了简单,同样省略try/catch语句块)``          ``// X: 这里是C1试图重新获得锁的地方(看下面代码)``    ``}``    ``Object` `o = buf.remove(``0``);``    ``notify(); ``// 通知所有正在等待对象锁的Producer和Consumer(译者注:包括被阻挡在方法外的Producer和Consumer)``    ``return` `o;``}`

首先

我们为什么在 wait 方法外面加上 while 循环?

我们需要使用 while 循环来实现下面的情景,

情景分析:

消费者 1(C1)进入同步块中,此时 buf 是空的,所以 C1 被放入 wait 队列中(因为执行了 wait 方法,译者注:此时 C2 是恰好到方法处,而不是因为有线程在方法中运行才被阻挡在方法外的),当消费者 2(C2)正要进入同步方法的时候 (此时在 Y 的上面),生产者 P1 将一个对象放入到 buf 中,随后又调用 notify 方法。此时 wait 队列中唯一的线程是 C1(译者注:C2 不在 waiting 队列中,也不在 blocked 队列中),所以 C1 被唤醒,C1 被唤醒之后又开始试图重新获得对象锁,此时 C1 还在 X 的上面。

现在的情况是,**C1 和 C2 都在试图去获取同步锁,**这两个线程只能有一个被选择进入方法,另一个则会被堵塞(不是 waiting,而是 blocked 。译者注:虽然 C1 已经在方法中,不过还是会和 C2 竞争锁,如果 C2 获得锁,则 C2 进入方法执行接下来的操作,而 C1 还是继续等待锁(处于 blocked 状态);如果 C1 获得锁,则 C1 往下执行,而 C2 还是会被挡在方法外面(处于 blocked 状态))。假如 C2 先获得了对象锁,C1 仍然被阻挡着(此时 C1 还试图在 X 处获得锁),C2 完成了方法,并释放了锁。现在 C1 获得了锁。假设这里没有 while 循环,那么 C1 就会往下执行,从 buf 中删除一个对象,但是此时 buf 中已经没有对象了,因为刚刚 C2 已经取走了一个对象,如果此时 C1 执行 buf.remove(0),则会报 IndexArrayOutOfBoundsException 异常。为了防止这样的异常发生,我们在上面用到了 while 循环,在往下执行之前,判断此时 buf 的大小是否为 0,如果不是 0,则往下执行,如果是 0,则继续 wait()。

那么我们这里引出问题:为什么需要 notifyAll?

在上面生产者 - 消费者这个例子中,看起来我们用 notify 也能够侥幸成功,因为等待循环的哨兵对于消费者和生产者来说是互斥的。我们不能同时在 put 方法和 get 方法都有一个线程 wait,如果这种情况允许的话,那么下面的事情就会发生:

`buf.size() == ``0` `AND buf.size() == MAX_SIZE (假设MAX_SIZE不为``0``)`

然而,这样并不好,我们需要使用 notifyAll。让我们来看一看原因:

假设 buffer=1(为了更加容易理解),按照下面的步骤执行将会发生死锁。要注意的是:notify 可以唤醒任何一个线程,不过 JVM 不能确定是哪个线程被唤醒,所以,任何一个线程都有被唤醒的可能。另外要注意的是,当多个线程被阻塞在方法外的时候(在试图获得锁),获得锁的顺序也是不确定的。要记住,在任何时候,方法中只能有一个线程存在 - 在类中任何同步的方法只允许一个线程执行(这个线程要持有对象锁才可以执行)。如果下面的执行顺序发生了的话,就会导致死锁:

第一步:P1 放入一个对象到 buffer 中;
第二步:P2 试图 put 一个对象,此时 buf 中已经有一个了,所以 wait
第三步:P3 试图 put 一个对象,仍然 wait
第四步:

  • C1 试图从 buf 中获得一个对象;
  • C2 试图从 buf 中获得一个对象,但是挡在了 get 方法外面
  • C3 试图从 buf 中获得一个对象,同样挡在了 get 方法外面

第五步:

  • C1 执行完 get 方法,执行 notify,退出方法
  • notify 唤醒了 P2,
  • 但是 C2 在 P2 唤醒之前先进入了 get 方法,所以 P2 必须再次获得锁,P2 被挡在了 put 方法的外面,
  • C2 循环检查 buf 大小,在 buf 中没有对象,所以只能 wait;
  • C3 在 C2 之后,P2 之前进入了方法,由于 buf 中没有对象,所以也 wait;

第六步:

  • 现在,有 P3,C2,C3 在 waiting;
  • 最后 P2 获得了锁,在 buf 中放入了一个对象,执行 notify,退出 put 方法;

第七步:

  • notify 唤醒 P3;
  • P3 检查循环条件,在 buf 中已经有了一个对象,所以 wait;
  • 现在没有线程能够 notify 了,三个线程就会处于死锁状态。

下面是译者分析:

那么如果使用 notifyAll 方法唤醒线程,又会怎样呢?

在执行第五步时,即 C1 执行完 get 方法后,又执行了 notifyAll 方法,此时,notifyAll 方法会唤醒所有正在等待该锁的线程,那么所有的线程都会处于运行前的准备状态(此时不是 wait 状态),此时,即使 C2 在 P2(此时 P2 已经被唤醒,P3 也被唤醒,处于准备状态,而不是 wait 状态) 之前先进入了 get 方法,C2 循环检查 buf 大小,在 buf 中没有对象,所以进入 wait 状态;C3 在 C2 之后,P2 之前进入方法,由于 buf 中没有对象,所以也 wait;(这里重新分析了一下步骤五发生的情景)

第六步:现在,有 C2,C3 在 waiting,P3 在第五步已经被唤醒了,处于准备状态,此时,如果 P2 获得锁,在 buf 中放入一个对象,执行 notifyAll,又将 C2、C3 唤醒了;

第七步:此时,P3 检查循环条件,在 buf 中已经有了一个对象,所以 wait,不过此时并不会发生死锁,因为 C2 和 C3 还会继续执行。

总结:notify 方法很容易引起死锁,除非你根据自己的程序设计,确定不会发生死锁,notifyAll 方法则是线程的安全唤醒方法。

附:
notify 和 notifyAll 的区别:
notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
两者的最大区别在于:

notifyAll 使所有原来在该对象上等待被 notify 的线程统统退出 wait 的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify 他只是选择一个 wait 状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象 notify 的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用 notify 语句,即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,继续处在 wait 状态,直到这个对象发出一个 notify 或 notifyAll,它们等待的是被 notify 或 notifyAll,而不是锁。

以前大伙看到这两个区别的时候可能感觉到很懵,相信现在应该有些明白了吧,如果还没有搞清楚可以看一下我这里案例的分析:notify 发生死锁的情景

根据网友的评论补充:

解释下前三步:
synchronized 修饰的方法,同一时刻只能允许一个线程进入,所以第一步执行之后,P1 执行 notify,跳出方法,然后,P2 可以进入 synchronized 修饰的 put 方法,不过这一次是 wait,P2 线程会释放对象锁,此时 P3 就可以进入 put 方法,当然了,还是 wait(),释放了对象锁。
第四步:
get 方法也是用 synchronized 修饰的,所以同一时刻,只能有一个线程进入此方法,C1 进入之后,试图取走一个对象,但此时还没有取走,此时,C2 准备进入 get 方法,不过因为这个方法是用 synchronized 修饰的,所以 C2 被挡在了方法的外面,同理,C3 也被挡在了方法的外面。(注意:此时 C1 还没有取走对象)。

第五步:
当 C1 取走对象后,在执行 notify 方法之前,P2,P3 会继续 wait,继续等着 notify 的通知。
C1 执行 notify 方法,通知了 P2,此时 P2 可以获得对象锁了(这里的意思是说: P2 可以去抢对象锁了,但是能不能抢得到就看它的造化了)。(注意:此时的 C2 以及 C3 还在外面等着,不过它们不是因为执行了 wait 而等,所以它们不需要等 notify 的通知,只要有对象锁,它们两个就可以争抢,获得了对象锁的就可以进入方法,所以,虽然 notify 通知了 P2,但是 C2 和 P2 同属竞争关系,所以 C2 是可以在 P2 之前获得对象锁的)。

网友补充:

C2,C3 在 C1 进入 get 方法后会被 jvm 放入对象的锁池中,而 P2,P3 是被放入对象的等待池中,等待池的线程只有通过 notify、notifyAll 或者 interrupt 才能进入锁池中,而锁池的线程只有拿到锁标识后才进入 runnable 状态等待 cpu 时间片。

(9).延伸出来的生产者与消费者问题

生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。

其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。

public class ProducerConsumer { //生产者与消费者问题
    private LinkedList<Object> storeHouse = new LinkedList<Object>();
    private int MAX = 10;

    public ProducerConsumer() {
    }
    public void start() {
        new Producer().start();
        new Comsumer().start();
    }

    class Producer extends Thread {		//生产者
        public void run() {
            while (true) {
                synchronized (storeHouse) {	//对公共资源进行加锁
                    try {
                        while (storeHouse.size() == MAX) {
                            System.out.println("storeHouse is full , please wait");
                            storeHouse.wait();		//注意while判断的条件,必须使用while,不然会造成异常
                        }
                        Object newOb = new Object();
                        if (storeHouse.add(newOb)) {
                            System.out.println("Producer put a Object to storeHouse");
                            Thread.sleep((long) (Math.random() * 3000));
                            storeHouse.notify();
                        }
                    } catch (InterruptedException ie) {
                        System.out.println("producer is interrupted!");
                    }

                }
            }
        }
    }

    class Comsumer extends Thread {
        public void run() {
            while (true) {
                synchronized (storeHouse) {	//同样也是对公共资源进行加锁
                    try {
                        while (storeHouse.size() == 0) {	//while循环条件
                            System.out.println("storeHouse is empty , please wait");
                            storeHouse.wait();
                        }
                        storeHouse.removeLast();	
                        System.out.println("Comsumer get  a Object from storeHouse");
                        Thread.sleep((long) (Math.random() * 3000));
                        storeHouse.notify();
                    } catch (InterruptedException ie) {
                        System.out.println("Consumer is interrupted");
                    }

                }
            }

        }
    }

    public static void main(String[] args) throws Exception {
        Produc	erConsumer pc = new ProducerConsumer();
        pc.start();
    }
}

原文链接: javarevisited 翻译: ImportNew.com - 伯乐在线翻译组
译文链接: http://www.importnew.com/16453.html
[ 转载请保留原文出处、译者和译文链接。]

//第二份生产者消费者代码 http://www.importnew.com/16453.html
import java.util.LinkedList; 
import java.util.Queue; 
import java.util.Random; 
/** 
* Simple Java program to demonstrate How to use wait, notify and notifyAll() 
* method in Java by solving producer consumer problem.
* 
* @author Javin Paul 
*/
public class ProducerConsumerInJava { 
    public static void main(String args[]) { 
        System.out.println("How to use wait and notify method in Java"); 
        System.out.println("Solving Producer Consumper Problem"); 
        Queue&lt;Integer&gt; buffer = new LinkedList&lt;&gt;(); 
        int maxSize = 10; 
        Thread producer = new Producer(buffer, maxSize, "PRODUCER"); 
        Thread consumer = new Consumer(buffer, maxSize, "CONSUMER"); 
        producer.start(); consumer.start(); } 
    } 
    /** 
    * Producer Thread will keep producing values for Consumer 
    * to consumer. It will use wait() method when Queue is full 
    * and use notify() method to send notification to Consumer 
    * Thread. 
    * 
    * @author WINDOWS 8 
    * 
    */
    class Producer extends Thread 
    { private Queue&lt;Integer&gt; queue; 
        private int maxSize; 
        public Producer(Queue&lt;Integer&gt; queue, int maxSize, String name){ 
            super(name); this.queue = queue; this.maxSize = maxSize; 
        } 
        @Override public void run() 
        { 
            while (true) 
                { 
                    synchronized (queue) { 
                        while (queue.size() == maxSize) { 
                            try { 
                                System.out .println("Queue is full, " + "Producer thread waiting for " + "consumer to take something from queue"); 
                                queue.wait(); 
                            } catch (Exception ex) { 
                                ex.printStackTrace(); } 
                            } 
                            Random random = new Random(); 
                            int i = random.nextInt(); 
                            System.out.println("Producing value : " + i); queue.add(i); queue.notifyAll(); 
                        } 
                    } 
                } 
            } 
    /** 
    * Consumer Thread will consumer values form shared queue. 
    * It will also use wait() method to wait if queue is 
    * empty. It will also use notify method to send 
    * notification to producer thread after consuming values 
    * from queue. 
    * 
    * @author WINDOWS 8 
    * 
    */
    class Consumer extends Thread { 
        private Queue&lt;Integer&gt; queue; 
        private int maxSize; 
        public Consumer(Queue&lt;Integer&gt; queue, int maxSize, String name){ 
            super(name); 
            this.queue = queue; 
            this.maxSize = maxSize; 
        } 
        @Override public void run() { 
            while (true) { 
                synchronized (queue) { 
                    while (queue.isEmpty()) { 
                        System.out.println("Queue is empty," + "Consumer thread is waiting" + " for producer thread to put something in queue"); 
                        try { 
                            queue.wait(); 
                        } catch (Exception ex) { 
                            ex.printStackTrace(); 
                        } 
                    } 
                    System.out.println("Consuming value : " + queue.remove()); queue.notifyAll(); 
                } 
            } 
        } 
    }
//为了更好地理解这个程序,我建议你在debug模式里跑这个程序。一旦你在debug模式下启动程序,它会停止在PRODUCER或者CONSUMER线程上,取决于哪个线程占据了CPU。因为两个线程都有wait()的条件,它们一定会停止,然后你就可以跑这个程序然后看发生什么了(很有可能它就会输出我们以上展示的内容)。你也可以使用Eclipse里的Step into和Step over按钮来更好地理解多线程间发生的事情。

这是关于Java里如何使用wait, notify和notifyAll的所有重点啦。你应该只在你知道自己要做什么的情况下使用这些函数,不然Java里还有很多其它的用来解决同步问题的方案。例如,如果你想使用生产者消费者模型的话,你也可以使用BlockingQueue,它会帮你处理所有的线程安全问题和流程控制。如果你想要某一个线程等待另一个线程做出反馈再继续运行,你也可以使用CycliBarrier或者CountDownLatch。如果你只是想保护某一个资源的话,你也可以使用Semaphore。

三. Object里面的finalize方法

尚未补充完全,请参考Effective Java 第7条

四. 参考代码

(1)线程相互通信,这是一个有缺点的案例,使用wait前,最好需要加上while循环

public class WaitNotifyDemo {   //线程间相互通信的例子

    final static Object lock = new Object();   //一个lock的对象

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程A等待拿锁");
                synchronized (lock){
                    try {
                        System.out.println("线程A拿到锁了");
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("线程A开始等待并且放弃锁了");
                        lock.wait();
                        System.out.println("被通知可以继续执行,则运行到结束");
                    }catch (InterruptedException e){

                    }
                }
            }
        },"线程A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程B等待拿锁");
                synchronized (lock){
                    try {
                        System.out.println("线程B拿到锁了");
                        TimeUnit.SECONDS.sleep(5);
                    }catch (InterruptedException e){
                    }
                    lock.notify();
                    System.out.println("线程B随机通知某个线程");
                }
            }
        },"线程B").start();
    }
}

运行结果分析:

多次执行代码存在两种情况

1.顺利成功执行代码,其中sleep的过程,为其中红色划线处,A线程等待拿锁,B线程也等待拿锁,且线程A拿到了锁。开始执行线程A中的代码,然后sleep,让出cpu,但是线程B也无法继续执行,因为它缺少对象锁,sleep到了之后,线程A继续执行,调用wait方法,放弃对象锁,进入等待池,此时B拿到了锁,进入sleep,然后调用notify方法(注意此时还没有释放锁),通知A,此时等待池只有A线程在里面,输出"线程B随机通知某个线程",然后退出同步代码块,释放对象锁。线程A又拿到了对象锁,继续执行。

1553866873107

2.不顺利地执行代码,B拿到锁了,进入sleep方法,让出cpu执行权,但是线程A没有对象锁,还是无法执行,线程B调用notify方法,但此时等待池没有等待的线程(存在疑问),输出"线程B随机通知某个线程",退出同步代码块,释放对象锁,此时A得到了对象锁,进入sleep方法,让出cpu,sleep方法结束之后,线程A开始等待并且放弃锁了,调用wait方法,但是没有人通知它,所以一直在等待池等待。

1553867566020

(2)notify使用不当造成死锁

public class WaitNotifyDemo2 {  	//notify使用不当导致的死锁问题
    public static void main(String[] args) {
        // System.out.println("lock");

        final OutTurn ot = new OutTurn();

        for (int j = 0; j < 100; j++) {

            new Thread(new Runnable() {

                public void run() {
                    // try {
                    // Thread.sleep(10);
                    // } catch (InterruptedException e) {
                    // e.printStackTrace();
                    // }
                    for (int i = 0; i < 5; i++) {
                        ot.sub();
                    }
                }
            }).start();

            new Thread(new Runnable() {

                public void run() {
                    // try {
                    // Thread.sleep(10);
                    // } catch (InterruptedException e) {
                    // e.printStackTrace();
                    // }
                    for (int i = 0; i < 5; i++) {
                        ot.main();
                    }
                }
            }).start();
        }

    }

}
class OutTurn {
    private boolean isSub = true;
    private int count = 0;

    public synchronized void sub() {
        try {
            while (!isSub ) {
                this.wait();
            }
            System. out.println("sub ---- " + count);
            isSub = false ;
            this.notify();
        } catch (Exception e) {
            e.printStackTrace();
        }
        count++;

    }

    public synchronized void main() {
        try {
            while (isSub ) {
                this.wait();
            }
            System. out.println("main (((((((((((( " + count);
            isSub = true ;
            this.notify();
        } catch (Exception e) {
            e.printStackTrace();
        }
        count++;
    }
}

解释一下原因:

​ OutTurn类中的sub和main方法都是同步方法,所以多个调用sub和main方法的线程都会处于阻塞状态,等待一个正在运行的线程来唤醒它们。下面分别分析一下使用notify和notifyAll方法唤醒线程的不同之处:

​ 上面的代码使用了notify方法进行唤醒,而notify方法只能唤醒一个线程,其它等待的线程仍然处于wait状态,假设调用sub方法的线程执行完后(即System. out .println("sub ---- " + count )执行完之后),所有的线程都处于等待状态,此时在sub方法中的线程执行了isSub=false语句后又执行了notify方法,这时如果唤醒的是一个sub方法的调度线程,那么while循环等于true,则此唤醒的线程也会处于等待状态,此时所有的线程都处于等待状态,那么也就没有了运行的线程来唤醒它们,这就发生了死锁。

​ 如果使用notifyAll方法来唤醒所有正在等待该锁的线程,那么所有的线程都会处于运行前的准备状态(就是sub方法执行完后,唤醒了所有等待该锁的状态,注:不是wait状态),那么此时,即使再次唤醒一个sub方法调度线程,while循环等于true,唤醒的线程再次处于等待状态,那么还会有其它的线程可以获得锁,进入运行状态。

​ 总结:notify方法很容易引起死锁,除非你根据自己的程序设计,确定不会发生死锁,notifyAll方法则是线程的安全唤醒方法,代码造成死锁的原因就是notify是随意唤醒一个线程,唤醒的可能是功能相同的线程,而锁并不在唤醒的线程手里,就造成了死锁的产生。

(3)虚假唤醒(原文章已经过修改,参考链接见下方)

public class MyStack {  
    private List<String> list = new ArrayList<String>();  
  
    public synchronized void push(String value) {  
        synchronized (this) {  
            list.add(value);  
            notify();  
        }  
    }  
  
    public synchronized String pop() throws InterruptedException {  
        synchronized (this) {  
            if (list.size() <= 0) {  
                wait();  
            }  
            return list.remove(list.size() - 1);  
        }  
    }  
}

这段代码大多数情况下运行正常,但是某些情况下会出问题

代码分析:

从整体上,在并发状态下,push和pop都使用了synchronized的锁,来实现同步,同步的数据对象是基于List的数据;大部分情况下是可以正常工作的。

问题描述:

状况1:

  1. 假设有三个线程: A,B,C. A 负责放入数据到list,就是调用push操作, B,C分别执行Pop操作,移除数据。

  2. 首先B先执行,于pop中的wait()方法处,进入waiting状态,进入等待队列,释放锁。

  3. A首先执行放入数据push操作到List,在调用notify()之前; 同时C执行pop(),由于synchronized,被阻塞,进入Blocked状态,放入基于锁的等待队列。注意,这里的队列和2中的waiting等待队列是两个不同的队列。

  4. A线程调用notify(),唤醒等待中的线程B。

  5. 如果此时, C获取到基于对象的锁,则优先执行,执行pop方法,获取数据,从list移除一个元素。

  6. 然后,B获取到竞争锁,B中调用list.remove(list.size() - 1),则会报数据越界exception。

状况2:

  1. 相同于状况1

  2. B、C都处于等待waiting状态,释放锁。等待notify()、notifyAll()操作的唤醒。

  3. 存在被虚假唤醒的可能。

何为虚假唤醒?

虚假唤醒就是一些obj.wait()会在除了obj.notify()和obj.notifyAll()的其他情况被唤醒,而此时是不应该唤醒的。

解决的办法是基于while来反复判断进入正常操作的临界条件是否满足:

​ synchronized (obj) {
​ while ()
​ obj.wait();
​ … // Perform action appropriate to condition
​ }

如何修复问题?

  1. 使用可同步的数据结构来存放数据,比如LinkedBlockingQueue之类。由这些同步的数据结构来完成繁琐的同步操作。

  2. 双层的synchronized使用没有意义,保留外层即可。

  3. 将if替换为while,解决虚假唤醒的问题

(4) 三个线程轮流打印1-100(一道常见的面试题)

public class WaitNoitfyDemo4 extends Thread {  //三个线程轮流打印1-100

    int i = 0;

    public static void main(String[] args) {
        WaitNoitfyDemo4 waitNoitfyDemo4 = new WaitNoitfyDemo4();
        Thread thread1 = new Thread(waitNoitfyDemo4);
        Thread thread2 = new Thread(waitNoitfyDemo4);
        Thread thread3 = new Thread(waitNoitfyDemo4);
        thread1.setName("线程A");
        thread2.setName("线程B");
        thread3.setName("线程C");
        thread1.start();
        thread2.start();
        thread3.start();

    }
    @Override
    public void run() {
        while (true){
            synchronized (this){
                notify();       //第一次的通知是无效的,因为此时等待池中没有线程
                try {
                    Thread.sleep(3000); //等待更能看出效果
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                if (i<=100){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    i++;
                    try {
                        wait(); //放弃锁,让给别人
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

(5) 按照顺序去打印数字

public class WaitNotifyDemo5 {  //如果我们让三个线程按顺序去打印改怎么实现呢?比如线程1打印1,2,3线程2打印4,5,6依次类推。
    static class Printer implements Runnable{
        static int num = 1; //开始数字
        static final int END = 75;
        int id;

        public Printer(int id) {
            this.id = id;
        }

        @Override
        public void run(){
            synchronized (Printer.class) {
	//注意第synchronized (Printer.class) ,为什么是Printer.class,而不是this呢?
	//是因为Print.class也是一个对象,在当前JVM中是唯一的类对象,它相当于一个“公证人”,三个线程竞争资源的时候都是从唯一的这个“公证人”手里拿到许可,才能进入synchronized体。
	//而如果是synchronized (this)的话,this也相当于一个“公证人”,那么三个线程各自有一个“公证人”,相当于各干各的,三个中间没有竞争关系,构不成同步。
                while(num <= END){
                    if(num / 5 % 3 == id){ //如果是属于自己的数,依次打印出来五个
                        System.out.print(id + ":");
                        for(int i = 0; i < 5; i++){
                            System.out.print(num++ + ", ");
                        }
                        System.out.println();
                        Printer.class.notifyAll();//放弃CPU使用权,唤醒等待在Print.class队列上的的打印线程
                    }else{
                        try {
                            Printer.class.wait();//如果不属于自己的数,把当前线程挂在Printer.class这个对象的等待队列上(也是放弃CPU使用权),等待唤醒
                        } catch (InterruptedException e) {
                            System.out.println("id" + "被打断了");
                        }
                    }
                }
            }
        }
    }


    public static void main(String[] args) {
        //下面可以不按0,1,2的顺序来,而且在两两中间随便sleep(),都会正确打印出来
        new Thread( new Printer(0)).start();
        new Thread( new Printer(1)).start();
        new Thread( new Printer(2)).start();
    }
}

(6)一些值得去看的代码,下面给出一些链接

http://cmsblogs.com/?p=1280

https://blog.csdn.net/u010002184/article/details/72600694

https://blog.csdn.net/superit401/article/details/52254087

五.存在的疑问与不足

  • notify通知之后的线程还需要在锁池里面竞争吗?还是说之前内定好了?

  • 三个线程轮流打印1-100的代码中,第一个红线的上面三处,为什么线程A会切换到线程B,这三个输出是无序的嘛?

  • B拿到锁了,进入sleep方法,让出cpu执行权,但是线程A没有对象锁,还是无法执行,线程B调用notify方法,但此时等待池没有等待的线程?——是的,等待池中没有,因为A和B在竞争的时候,B拿到了锁,A就会进入阻塞状态也就是被放进了锁池,并不在等待池。

  • 释放锁的时机:wait之后就释放锁了还是等待sychronized执行完之后?——请看上面第二大点的第4小点

  • wait 不能使用 if 为什么???存疑,到底会有什么问题?——看上面

  • 补充 wait与notify源码

    https://blog.csdn.net/lsgqjh/article/details/61915074>

    https://blog.csdn.net/boling_cavalry/article/details/77793224

  • 三. Object里面的finalize方法补充完全——只需要参考

参考资料

如下:
Effective Java
[我就是小在]: https://blog.csdn.net/u014561933/article/details/58639411 “sleep()和wait()方法与对象锁、锁池”
[stateiso]: https://blog.csdn.net/qq_38182963/article/details/78948024 “并发编程之 wait notify 方法剖析”
[世界之大追梦者]: https://www.cnblogs.com/hy928302776/p/3255641.html “什么时候释放锁”
[萧萧冷]: https://blog.csdn.net/lengxiao1993/article/details/52296220 " 搭配synchonized关键字使用"
[zhao_perry]: https://blog.csdn.net/perrywork/article/details/16819153 “3个线程依次轮流打印出75个数”
[blueheart20]: https://www.linuxidc.com/Linux/2014-03/98715.htm “一道阿里面试题的分析与应对”
[蚁方阵]: https://blog.csdn.net/yiifaa/article/details/76341707 “为什么wait()一定要放在循环中”
[菜鸡小王子]: https://blog.csdn.net/qq_35181209/article/details/77362297 “wait放在while循环里面的原因探析”

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值