[JavaEE初阶] 内存可见性问题----volatile与wait(),notify()的使用

读书要趁黑发早,白首不悔少当时



1. 什么是内存可见性问题

线程A在读一个变量的时候,另一个线程B在修改这个变量,所以,线程A读到的值不是修改之后的,是一个未更新的值,读到的值是错误的.

如下代码,t1线程进行一个循环,循环条件是c.count = 0,线程2进行修改c.count值的操作.正常来说,t2线程修改了count的值,t1线程循环条件不满足,会跳出循环,打印,之后结束进程.但现实结果是,t1的循环一直没有结束,大家思考,这是为什么呢?

class Counter{
    public int count = 0;
}
public class Volatile {
    public static void main(String[] args) {
        Counter c = new Counter();
        Thread t1 = new Thread(()->{
            while(c.count == 0){

            }
            System.out.println("t1线程要结束啦~");
        });
        Thread t2 = new Thread(()->{
            Scanner in = new Scanner(System.in);
            int i = in.nextInt();
            c.count = i;
            System.out.println("t2线程要结束啦~");
        });
        t1.start();
        t2.start();
    }
}

执行结果如下,修改了count值后,线程t1一直没有结束
在这里插入图片描述

t1的循环条件,c.count = 0,这个比较操作需要两个具体操作才能完成.
1.每次将count的值读取到寄存器上,即load.
2.将寄存器中count的值与0进行比较,即cmp.

由于t1的循环执行速度非常快,1s能执行上百万次,并且比较值的操作cmp比读取值到寄存器的操作load要快得多,所以,这里编译器发现这里t1的循环读取的值貌似一直都是一个数,所以,这里编译器自作主张对程序做了个优化,只读一次count值,之后的循环都按第一次读到的值来进行比较.
正常时候,这个优化是没问题的,但这个是多线程程序,t2线程对count值进行了修改,t1没有察觉到,还是按第一次读取到的值0来进行比较,出现了线程安全问题----内存可见性问题,一个线程读,一个线程改,读到的数是修改之前的值,是错误的值.

2. 避免内存可见性问题-----volatile(易变的)

如下代码,用volatile修饰变量,这个操作是在告诉编译器,这个变量值有其他线程能修改,是能变化的值,防止编译器自作主张进行优化,避免只读取一次值的行为.t1线程每次循环都要重新读一次count值.

class Counter{
    volatile public int count = 0;
}

修改后,程序结果如下.
在这里插入图片描述

3. 需要注意的点

volatile不能修饰方法里的局部变量.由于不同线程调用方法时,都会开辟自己的栈空间,去单独使用变量,不同进程之间互不影响.(C++中volatile可以修饰局部变量,因为C++可以将线程A的局部变量给线程B使用)

4. wait()与notify()的使用

4.1 控制两个线程执行顺序

我们之前讲过,join()方法也能控制线程执行顺序,但join()方法是只能在一个线程执行完毕后才能执行另一个线程,控制的是进程结束的顺序.

线程A调用wait()方法,会释放锁,进入阻塞状态,让其他线程B先执行,直到线程B调用notify()方法,唤醒线程A.这里的notify()可以放在线程B的任意位置,可以使线程B执行一部分,就唤醒线程A,更为灵活.
wait()与notify()方法都属于object类中的方法.需要创建object对象来调用.

如下代码,线程t2调用wait()方法,t1与t2同样对对象o1加锁,线程t2就只能释放锁,进入阻塞状态,等到t1线程执行到notify(),通知t2,t2唤醒,进入执行状态.有效控制线程之间的执行顺序.

		Object o1 = new Object();
        Thread t1 = new Thread(() -> {
            System.out.print("A");
            synchronized (o1) {
                o1.notify();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                synchronized (o1) {
                    o1.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("B");
        });
        t1.start();
        t2.start();

如下图,需要注意,只有notify()和wait()的对象是同一个的时候,才会起效果.
在这里插入图片描述
wait()方法也可以带参数,表示最长等待时间.
如下代码,wait()方法参数3000ms,只过了3s之后,若还没有别的线程调用notify()去唤醒线程t,t会自动唤醒.

			Thread t = new Thread(()->{
            try {
                synchronized (o1) {
                    o1.wait(3000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("最长等待时间后,t执行");
        });

4.2 控制多个线程执行顺序

同样的方法,控制三个线程的执行顺序,方法很简单,大家可以独立思考以下.
定义两个object对象,o1和o2,由o1控制线程A和线程B的执行顺序,对象o2控制线程B与线程C的执行顺序.

		Thread t1 = new Thread(() -> {
            System.out.print("A");
            synchronized (o1) {
                o1.notify();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                synchronized (o1) {
                    o1.wait(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("B");
            synchronized (o2) {
                o2.notify();
            }
        });
        Thread t3 = new Thread(() -> {
            try {
                synchronized (o2) {
                    o2.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("C");
        });

这里需要注意一个问题,因为线程抢占式调度,t1的notify()方法有可能执行的比t2的wait()方法要早,这样,线程2就没有线程去唤醒它了,一直处在阻塞状态,出现了bug,t2与t3同理,所以,这里要控制,线程的开始顺序为t3,t2,t1,防止notify()比对应的wait()要早的情况.
如下代码,t3,t2之间加上sleep(),控制线程之间的开始顺序.

		t3.start();
        Thread.sleep(500);
        t2.start();
        Thread.sleep(500);
        //避免t2,t3的wait()比t1的notify要晚.t2,t3先执行,但都要释放锁.
        t1.start();

4.3 wait()与sleep()的区别

1.wait()方法需要搭配notify()使用.而sleep()可以单独使用
2.wait()是Object类中的方法,sleep()是Thread类中的静态方法.


总结

内存可见性问题出现在多线程中一线程读,一线程写造成的问题,由volatile修饰,防止编译器进行优化,每次重新读取值.
wait(),notify()可以控制线程之间的执行顺序.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值