Java 并发之wait和notify

518 阅读8分钟

理论知识

理论知识来自Java文档

  • 尽量自己去看jdk源码中的注释

题外话

  • 并发离不开硬件的支持,首先cpu得多核
  • 线程由操作系统管理的
  • Thread和Runable大家都会创建和使用,不再赘述
  • 来扣细节

线程优先级相关

image.png

  • 每个线程都有优先级
  • 一个线程创建另外一个线程,被创建的线程优先级默认和创建者线程一样
  • 当且仅当一个线程是daemon时先才可以创建daemon线程
  • 就算你优先级高,操作系统也不一定先调用你

Thread类注意事项

image.png

  • 每个线程都有一个名称供识别。 一个以上的线程可能具有相同的名称。 如果在创建线程时未指定名称,则会为其生成一个新名称。

image.png

  • 除非另有说明,否则将null参数传递给此类(Thread)中的构造函数或方法将导致引发NullPointerException

image.png

  • 当Java虚拟机启动时,通常只有一个非守护程序线程(通常调用某些指定类的名为main的方法)。 Java虚拟机将继续执行线程,直到发生以下任何一种情况:
    • 调用了Runtime类的exit方法,并且安全管理器允许进行退出操作。
    • 非守程序线程的所有线程都已死,死的原因是:要么通过run方法的return语句,要么抛出异常传播到run方法之外。

image.png

  • thread 不可重复启动,即使它执行完了
  • 调用start方法的线程和执行run方法的线程会同时执行

image.png

  • 如果此线程是使用单独的Runnable对象构造的,则将调用该Runnable对象的run方法;否则,将调用Thread中的run方法。 否则,此方法不执行任何操作并返回。

wait()方法

  • wait方法在Object类中
  • 使当前线程等待,直到另一个线程为此对象调用notify()方法或notifyAll()方法。 换句话说,此方法的行为与仅执行调用wait(0)行为完全相同。

wait(long timeout)方法

image.png

  • 使当前线程等待,直到另一个线程为此对象调用notify()方法或notifyAll()方法,或者经过了指定的时间。
  • 当前线程必须拥有该对象的 monitor
  • 此方法使当前线程(称为T )将自己置于该对象的等待集中,然后放弃对该对象的任何和所有同步声明。 出于线程调度的目的,线程T被禁用,并且处于休眠状态,直到发生以下四种情况之一:
    • 其他一些线程为此对象调用notify()方法,并且线程T恰好被任意选择为要唤醒的线程。
    • 其他一些线程为此对象调用notifyAll()方法。
    • 其他一些线程中断线程T。
    • 指定的timeout或多或少已经过去。 但是,如果timeout为0,则不考虑指定的timeout,线程只是等待直到通知
  • 以上情况后将线程T从该对象的等待集中删除,并重新启用线程调度。 然后,它以通常的方式与其他线程竞争在对象上进行同步的权利。 一旦它获得了对象的控制权,它对对象的所有同步声明都将恢复到原样-即,恢复到调用wait方法时的情况。 然后,线程T从调用wait方法返回。 因此,从wait方法返回时,对象和线程T的同步状态与调用wait方法时的状态完全相同。
  • 一个线程在没有notify()、中断或者指定的时间已经过去也会被唤醒,即所谓的虚假唤醒。 尽管在实践中这种情况很少发生,但是应用程序必须通过测试应该导致线程唤醒的条件来防范它,并在条件不满足时继续等待。 换句话说,等待应该总是在循环中发生,就像这样:
      synchronized (obj) {
               while (<condition does not hold>)
                   obj.wait(timeout);
               ... // Perform action appropriate to condition
           }
  • 如果当前线程在等待前或等待时被其他线程中断,那么InterruptedException被抛出。 如上所述,直到该对象的锁定状态恢复之前,不会引发此异常。

经查阅, monitor就是实现(锁)lock的方式,可以近似将monitor理解为锁

notify()方法

image.png

  • 唤醒正在此对象的monitor上等待的单个线程。 如果有多个线程在此对象上等待,则选择其中一个唤醒。 该选择是随机的,并且可以根据实现情况进行选择。 线程通过调用其中一个wait方法来等待对象的monitor
  • 在当前线程放弃对该对象的之前,唤醒的线程将无法继续运行。 唤醒的线程将正常的与其他想在此对象上进行同步的其他线程竞争。 例如,被唤醒的线程在作为获得该对象锁的下一个线程时没有任何可靠的优势或劣势。
  • 此方法只能由作为该对象的monitor的所有者的线程调用。
  • 线程通过以下三种方式之一成为对象monitor的所有者:
    • 通过执行该对象的同步实例方法。
    public class Test{
       public synchronized void t(){....}//同步实例方法
       public static void main(String[] args)  {
           Test test = new Test();
           //test是实例对象,线程1 执行test.t()时就获得了对象的锁,其他任何线程都不得访问
           Test test2 = new Test();
           //这里有两个实例,当线程1在执行test.t()时,其他线程可以执行test2.t().
           //因为两个实列对象,就有两把锁
       }
    }
    
    • 通过执行在对象上synchronized语句的主体。
    public class Test{
      public void t(){
           synchronized(object){
              .....语句块//synchronized语句的主体
           }
      }
      public static void main(String[] args)  {
          Test test = new Test();
          //当线程1 执行到test.t()中的synchronized语句块时就获得了对象的锁,其他任何线程都不得访问该对象
      }
     }
    
    • 对于Class类型的对象,通过执行该类的同步(synchronized)静态方法。
    public class Test{
       public synchronized void t(){....}//同步实例方法
       public static synchronized void t2(){....}//属于Class对象的静态同步方法
       public static void main(String[] args)  {
           Test test = new Test();
           //test是实例对象,线程1 执行test.t()时就获得了对象的锁,其他任何线程都不得访问该对象
           //但是,当线程1 执行test.t(),其他的任意一个一个线程可以执行Test.t2()
           //因为有两把锁,一个是实例对象的,一个是Class对象的,毕竟static方法属于类的
       }
    }
    
  • 一次只能有一个线程拥有对象的monitor

总之

1、waitnotify要对同一个对象使用,调用wait方法后将对象加入等待集(wait set)

2、objectmonitor很关键 。调用wait方法之后会释放monitor,然后进入到等待状态

3、object类的wait方法和Thread类的sleep方法的区别,后者不会释放monitor

4、在调用wait方法时,线程必须要持有被调用对象的monitor,当调用wait方法后,线程就会释放掉该对象的锁(monitor)。在调用Thread类的sleep方法时,线程是不会释放掉对象的锁(monitor)的

5、线程被notify唤醒后将与其他线程公平的共同竞争对象的锁

6、在某一时刻,只有唯一一个线程拥有对象的锁

7、notify是从等待集(wait set) 中随机唤醒一个,notifyAll是将其全部唤醒,唤醒后将与其他线程公平的共同竞争对象的锁

8、注意:static属于Class对象,Class有它自己的锁,要和实例对象区分

如何让对象获得monitor

用synchronized

  • 当对象没有获得monitor时

image.png

  • 让对象获得monitor
public class WaitTest {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){//通过synchronized让对象获得monitor
            object.wait();
        }
    }
}
  • 通过javap -c反编译验证对象是否获得monitor

image.png

多说无益,上代码

请听题

编写一个多线程程序,实现这样一个目标:
1.存在一个对象,该对象有一个int类型的成员变量count,该成员变量的初始值为0
2.创建两个线程,其中一个线程对该对象的成员变量counter增1,另一个线程对该对象的成员变量减1
3.输出该对象成员变量counter每次变化后的值
4.最终输出的结果应为:1010101010101。。 。 . 。

代码

  • 先用两个线程试一试
public class WaitTest {
    int count;
    public WaitTest() {
        count = 0;//构造方法,初始化count =0;
    }
    public static void main(String[] args) {
        WaitTest test = new WaitTest();
        Runnable inCrease = () -> {//Lambda表达式
            for (int i = 0; i <100000 ; i++) {
                synchronized (test) {//让线程获得对象锁
                    try {//Thread.sleep((long) (Math.random() * 100));
                        if (test.count != 0) {//如果count不等于0,说明count等于1,不需要加1
                            test.wait();//加入等待集(wait set),后面的语句不会执行,释放锁
                        }
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.count++;//如果没有被加入等待集,就加1
                    System.out.print(test.count);//输出
                    test.notify();//加1完毕,通知等待集中的任意一个线程,将其唤醒,公平竞争对象锁
                }
            }
        };
        Runnable deCrease =() -> {//Lambda表达式
            for (int i = 0; i < 100000; i++) {
                synchronized (test) {//让线程获得对象锁
                    try {
                        //Thread.sleep((long) (Math.random() * 100));
                        if (test.count == 0) {//如果count等于0,不需要减1
                            test.wait();//加入等待集(wait set),后面的语句不会执行,释放锁
                        }
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.count--;//如果没有被加入等待集,就减1
                    System.out.print(test.count);
                    test.notify();//减1完毕,通知等待集中的任意一个线程,将其唤醒,公平竞争对象
                }
            }
        };
        new Thread(inCrease).start();
        new Thread(deCrease).start();
    }
}
  • 结果正确 image.png
  • 4个线程试一试

image.png

为什么4个线程就错了?

  • 在原来的基础上加一些print
public class WaitTest {
    int count;

    public WaitTest() {
        count = 0;
    }
    public static void main(String[] args) {
        WaitTest test = new WaitTest();
        Runnable inCrease = () -> {
            for (int i = 0; i <100 ; i++) {
                synchronized (test) {//线程获取对象锁
                    Thread t = Thread.currentThread();
                    try {
                        //Thread.sleep((long) (Math.random() * 100));
                        if (test.count != 0) {
                            System.out.println(t.getName()+"---wait.......");
                            test.wait();//加入等待集,后面语句不会执行,释放锁
                            Thread.sleep((long) (Math.random() * 100));
                            System.out.println("在"+t.getName()+"   if 中wait后");
                        }
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("count++之前");
                    test.count++;
                    System.out.println(t.getName()+"---"+t.getState()+"   "+test.count);
                    //System.out.print(test.count);
                    test.notify();
                }
            }
        };
        Runnable deCrease =() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (test) {
                    Thread t = Thread.currentThread();
                    try {

                        if (test.count == 0) {
                            System.out.println(t.getName()+"---wait.......");
                            test.wait();
                            Thread.sleep((long) (Math.random() * 100));
                            System.out.println("在"+t.getName()+"   if 中wait后");
                        }
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("count--之前");
                    test.count--;
                    System.out.println(t.getName()+"---"+t.getState()+"   "+test.count);
                    //System.out.print(test.count);
                    test.notify();
                }
            }
        };
        //把for循环调大就会死锁
        new Thread(inCrease,"Thread-0").start();
        new Thread(inCrease,"Thread-1").start();
        new Thread(deCrease,"Thread-2").start();
        new Thread(deCrease,"Thread-3").start();
    }
}

image.png

如何改进

  • 关键点:当线程被唤醒时,不走if语句进行判断。当线程被唤醒时,只要能对count进行判断就可解决问题
  • wait()放进while循环即可,即将if该为while
public class WaitTest {
    int count;

    public WaitTest() {
        count = 0;
    }
    public static void main(String[] args) {
        WaitTest test = new WaitTest();
        Runnable inCrease = () -> {
            synchronized (test) {//线程获取对象锁
                for (int i = 0; i < 10000; i++) {
                    while (test.count != 0) {
                        try {
                      //当wait线程被唤醒时,肯定进行会进行一次判断,为了防止所有线程wait,要notifyAll()
                     // 如果其他线程都在wait,且该线程的while条件为真,该线程就会wait,导致所有线程wait,
                            test.notifyAll();
                            test.wait();
                            //Thread.sleep((long) (Math.random() * 100));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    test.count++;
                    System.out.print(test.count);
                    test.notify();//执行完要唤醒其他线程
                }
            }
        };
        Runnable deCrease = () -> {
            synchronized (test) {
                for (int i = 0; i < 10000; i++) {
                    while (test.count == 0) {
                        try {
                     //当wait线程被唤醒时,肯定进行会进行一次判断,为了防止所有线程wait,要notifyAll()
                    // 如果其他线程都在wait,且该线程的while条件为真,该线程就会wait,导致所有线程wait,
                            test.notifyAll();
                            test.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    test.count--;
                    System.out.print(test.count);
                    test.notify();//执行完要唤醒其他线程
                }
            }
        };
        new Thread(inCrease, "Thread-0").start();
        new Thread(inCrease, "Thread-1").start();
        new Thread(deCrease, "Thread-2").start();
        new Thread(deCrease, "Thread-3").start();
    }
}

总结

1、将wait线程唤醒时,将执行wait后面的语句,前面的语句并不会执行

2、如果wait前面有判断语句,在wait语句后应该要再次判断,一般while就很不错

3、用while时要时刻防止全部线程wait

什么是中断?一个线程如何中断另一个线程?

  • 可以使用Thread类提供的interrupt()方法中断一个线程。当一个线程调用另一个线程的interrupt()方法时,被中断线程会收到一个中断信号,但不是立即中断,而是等待被中断线程进入到可中断状态后再中断。