读书要趁黑发早,白首不悔少当时
文章目录
- 1. 什么是内存可见性问题
- 2. 避免内存可见性问题-----volatile(易变的)
- 3. 需要注意的点
- 4. wait()与notify()的使用
- 4.1 控制两个线程执行顺序
- 4.2 控制多个线程执行顺序
- 4.3 wait()与sleep()的区别
- 总结
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()可以控制线程之间的执行顺序.