🔥个人主页:寻星探路

🎬作者简介:Java研发方向学习者

📖个人专栏: 、《

⭐️人生格言:没有人生来就会编程,但我生来倔强!!!



目录

 一、volatile 关键字

1、volatile 能保证内存可见性

2、volatile 不保证原子性

二、wait 和 notify

1、wait()方法

2、notify()方法

3、notifyAll()方法

4、wait 和 sleep的对比(面试题)


 一、volatile 关键字

1、volatile 能保证内存可见性

volatile 修饰的变量,能够保证“内存可见性”

代码在写入volatile修饰的变量的时候:

        改变线程工作内存中volatile变量副本的值

        将改变后的副本的值从工作内存刷新到主内存

代码在读取volatile修饰的变量的时候:

        从主内存中读取volatile变量的最新值到线程的工作内存中

        从工作内存中读取volatile变量的副本

        前面我们讨论内存可见性时说了,直接访问工作内存(实际是CPU的寄存器或者CPU的缓存),速度非常快,但是可能出现数据不一致的情况

        加上volatile,强制读写内存,速度是慢了,但是数据变的更准确了。

代码示例:

在这个代码中

        创建两个线程t1和t2

        t1中包含一个循环,这个循环以flag==0为循环条件

        t2中从键盘读入一个整数,并把这个整数赋值给flag

        预期当用户输入非0的值的时候,t1线程结束

 static class Counter {
     public int flag = 0;
 }

 public static void main(String[] args) {
     Counter counter = new Counter();
     Thread t1 = new Thread(() -> {
         while (counter.flag == 0) {
             // do nothing
         }
         System.out.println("循环结束!");
     });

     Thread t2 = new Thread(() -> {
         Scanner scanner = new Scanner(System.in);
         System.out.println("输⼊⼀个整数:");
         counter.flag = scanner.nextInt();
     });

     t1.start();
     t2.start();
 }

 // 执⾏效果 
 // 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug) 

t1 读的是自己工作内存中的内容

当t2对flag变量进行修改,此时t1感知不到flag的变化

如果给flag加上volatile

 static class Counter {
     public volatile int flag = 0;
 }

 // 执⾏效果 
 // 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束

2、volatile 不保证原子性

        volatile 和 synchronized 有着本质的区别:synchronized能够保证原子性,volatile保证的是内存可见性

代码示例:

这个是最初的演示线程安全的代码

        给increase方法去掉synchronized

        给count加上volatile关键字

 static class Counter {
     volatile public int count = 0;

     void increase() {
         count++;
     }
 }

 public static void main(String[] args) throws InterruptedException {
     final Counter counter = new Counter();

     Thread t1 = new Thread(() -> {
         for (int i = 0; i < 50000; i++) {
             counter.increase();
         }
     });
     Thread t2 = new Thread(() -> {
         for (int i = 0; i < 50000; i++) {
             counter.increase();
         }
     });
     t1.start();
     t2.start();
     t1.join();
     t2.join();

     System.out.println(counter.count);
 }

此时可以看到,最终count的值仍然无法保证是100000

二、wait 和 notify

        由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知

        但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

        球场上的每个运动员都是独立的“执行流”,可以认为是一个“线程”

        而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程 1 先“传球”,线程2才能“扣篮”

        球场上的每个运动员都是独立的“执行流”,可以认为是一个“线程”

        而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1 先“传球”,线程2才能“扣篮”

完成这个协调工作,主要涉及到三个方法:

        wait()/wait(long timeout): 让当前线程进入等待状态

        notify() / notifyAll(): 唤醒在当前对象上等待的线程

注意:wait,notify,notifyAll 都是 Object 类的方法

1、wait()方法

wait 做的事情:

        使当前执行代码的线程进行等待(把线程放到等待队列中)

        释放当前的锁

        满足一定条件时被唤醒,重新尝试获取这个锁

wait 要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常

wait 结束等待的条件:

        其他线程调用该对象的notify方法

        wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)

        其他线程调用该等待线程的interrupted方法,导致wait抛出 InterruptedException 异常

代码示例:观察wait()方法使用

 public static void main(String[] args) throws InterruptedException {
     Object object = new Object();
     synchronized (object) {
         System.out.println("等待中");
         object.wait();
         System.out.println("等待结束");
     }
 }

        这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。

2、notify()方法

notify 方法是唤醒等待的线程

        方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

        如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程。(并没有"先来后到")

        在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()⽅法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

代码示例:使用notify()方法唤醒线程

        创建WaitTask类,对应一个线程,run内部循环调用wait

        创建NotifyTask类,对应另一个线程,在run内部调用一次notify

        注意,WaitTask和NotifyTask内部持有同一个Objectlocker.WaitTask 和 NotifyTask要想配合就 需要搭配同一个Object

 static class WaitTask implements Runnable {
     private Object locker;

     public WaitTask(Object locker) {
         this.locker = locker;
 }

     @Override
     public void run() {
         synchronized (locker) {
             while (true) {
               try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 }

 static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
        }
    }
 }

 public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
 }

3、notifyAll()方法

        notify方法只是唤醒某⼀个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程

范例:使用notifyAll()方法唤醒所有等待线程,在上面的代码基础上做出修改

        创建3个WaitTask实例,1个NotifyTask实例

 static class WaitTask implements Runnable {
     // 代码不变 
 }

 static class NotifyTask implements Runnable {
     // 代码不变 
 }

 public static void main(String[] args) throws InterruptedException {
     Object locker = new Object();
     Thread t1 = new Thread(new WaitTask(locker));
     Thread t3 = new Thread(new WaitTask(locker));
     Thread t4 = new Thread(new WaitTask(locker));
     Thread t2 = new Thread(new NotifyTask(locker));
     t1.start();
     t3.start();
     t4.start();
     Thread.sleep(1000);
     t2.start();
 }

此时可以看到,调用 notify 只能唤醒一个线程

        修改NotifyTask中的run方法,把notify替换成notifyAll

 public void run() {
     synchronized (locker) {
         System.out.println("notify 开始");
         locker.notifyAll();
         System.out.println("notify 结束");
     }
 }

此时可以看到,调用 notifyAll 能同时唤醒3个wait中的线程

#注:虽然是同时唤醒3个线程,但是这3个线程需要竞争锁,所以并不是同时执行,而仍然是有先有后的执行

理解notify和notifyAll

notify 只唤醒等待队列中的一个线程.其他线程还是乖乖等着

4、wait 和 sleep的对比(面试题)

        其实理论上wait和sleep完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间

当然为了面试的目的,我们还是总结下:

1)wait需要搭配synchronized使用.sleep不需要

2)wait是Object的方法sleep是Thread的静态方法

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐