互联网面试总结第一课volatile相关内容
本章为听课笔记,主要课程为尚硅谷周阳老师的相关课程,里面的一些图为视频截图
volatile关键字相关,JMMJava内存模型
volatile关键字相关,JMM Java内存模型
并发和并行的区别
1、并发:多个线程访问同一资源(如秒杀系统)
2、并行:一边做XX,一边做XX同时进行(如一边看小说,一边听音乐)
volatile关键字
volatile是Java虚拟机提供的轻量级同步机制:(轻量级可以理解为乞丐版)
1、保证可见性
2、不保证原子性
3、禁止指令重排
1、保证可见性
这里涉及到JMM内存模型,这是一个抽象的不是真实存在的一组规则或规范(龙不存在,但我们都懂),在计算机当中,每个对象申请的变量存在于主内存当中(这里不一定对),而每个线程都有自己的工作内存,如果线程想对变量进行读写操作,不是直接对主内存中的变量操作,而是拷贝一份共享变量存在于自己的工作内存当中,线程A、B、C都拷贝了一份变量存在于自己的工作内存当中,当线程A操作了自己的工作内存变量并同步到主内存时,如果线程B、C没有得到通知,他们持有的仍是旧的变量,volatile关键字的可见性就是在更新主内存变量时,通知其他线程。
如果去掉volatile关键字,main线程将一直等待下去,可知,不加volatile,main线程无法“可见”主内存的改变。
import java.util.concurrent.TimeUnit;
public class volatile可见性 {
public static void main(String[] args) {
myData myData = new myData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t data="+myData.data);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
System.out.println(Thread.currentThread().getName()+"\t data="+myData.data);
},"AAA").start();
while (myData.data==0){ }
System.out.println(Thread.currentThread().getName()+"\t data="+myData.data);
}
}
class myData{
//去掉volatile关键字后,主线程一直等待
volatile int data=0;
public void add(){
data=60;
}
}
2、不保证原子性
从代码示例中我们可以看出,一共二十个线程每个线程都执行i++的操作1000次,最后结果并不是两万。主要原因是线程的执行速度太快,会发生不同的线程,来不及通知其他线程,就被其他线程覆盖写入主内存。从结果图中我们可以看到。volatile执行结果一定是小于两万的(有极小概率等于20000),正是这个原因。
解决办法也已经给出:
1、加synchronized关键字这个太重量级
2、使用JUC atomic包下的AtomicInteger具体使用已经写出,这里还涉及到JUC下的getAndIncrement()方法,类似于i++,具体情况等看到再深入分析。
public class volatile情况 {
public static void main(String[] args) {
myData myData = new myData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
// System.out.println(Thread.currentThread().getName()+"\t data="+myData.data);
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){//后台进程大于2(一个main,一个GC)
Thread.yield();//让出当前线程
}
System.out.println(Thread.currentThread().getName()+"\t data="+myData.data);
}
class myData{
//去掉volatile关键字后,主线程一直等待
volatile int data=0;
public void add(){
data=60;
}
/**
* 解决办法
* 1、加synchronized
* 2、使用java.util.concurrent.atomic中的AtomicInteger 保证原子性
*/
public void addPlusPlus(){
data++;
}
AtomicInteger atomicInteger=new AtomicInteger();
public void atomicAddPlusPlus(){
atomicInteger.getAndIncrement();
}
}
3、禁止指令重排
指令重排是计算机在编译过程对代码执行顺序的优化,但多线程操作时可能会导致数据不一致的结果。
public void example(){
/**
* 执行结果可能为 1234
* 1324
* 2134
* 2143
*/
int x=1;//1
int y=1;//2
x=x+1;//3
y=x*x;//4
}
多线程这里不一致性参杂在一起,比较难理解
应该是这种情况,但这种情况夹杂着多线程执行速度的问题,而且打印也会有快慢的问题,我暂时还没想明白,先放一放。
public class 指令重排1 {
int a=0,b=0,x=0,y=0;
public static void main(String[] args) {
指令重排1 demo = new 指令重排1();
for (int i = 0; i < 2; i++) {
int finalI = i;
new Thread(()->{
if (finalI % 2 == 0) {
demo.example1();
} else {
demo.example2();
}
},String.valueOf(i)).start();
}
}
public void example1(){
x=a;
b=1;
}
public void example2(){
y=b;
a=2;
}
}
第三个案例,也未实现,不知道是线程调度出现了问题还是概率太低,理论上可能a可能等于5;
public class 指令重排1 {
int a=0;
boolean flag=false;
public static void main(String[] args) {
指令重排1 demo = new 指令重排1();
new Thread(()->{
demo.example1();
},"A").start();
new Thread(()->{
demo.example2();
},"B").start();
}
public void example1(){
a=1;
flag=true;
}
public void example2(){
if (flag){
a=a+5;
System.out.println("result="+a);
}
}
}
上面的指令重排可在加入volatile后禁重排,主要原理是底层的理解 Memory barrier(内存屏障)
关于volatile的应用再写一章吧,涉及到单例,DCL相关知识