您的位置:首页 > 新闻 > 资讯 > 专业网站建设制_企业建站武汉_怎么免费建个人网站_sem什么意思

专业网站建设制_企业建站武汉_怎么免费建个人网站_sem什么意思

2025/5/24 5:39:46 来源:https://blog.csdn.net/weixin_74312694/article/details/147549417  浏览:    关键词:专业网站建设制_企业建站武汉_怎么免费建个人网站_sem什么意思
专业网站建设制_企业建站武汉_怎么免费建个人网站_sem什么意思

目录

volatile关键字

1.保证内存可见性:

2.保证有序性:

wait 和 notify

wait在执行的时候要做的三件事

使用wait 和 notify 可以避免线程饿死

volatile关键字

volatile的作用主要有如下两个

1.保证内存可见性:

说内存可见性之前,需要先谈谈关于编译器的优化:对于计算机运行程序/代码,经常需要访问数据,这些数据往往会村储在内存中(当我们定义一个变量,变量就会在内存中),当程序运行cpu需要使用这个变量的时候,就会吧这个内存中的数据先读出来,放到cpu的寄存器再参与运算。(cpu读取寄存器的速度,可能是读取内存的速度成千上万倍,,cpu的大部分操作都很快,但是一旦操作到读/写内存操作,此时速度一下就将了下来),为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器,尽量减少读取内存的此时,也就可以提高整体程序的效率了。

来看下面这个代码例子:

public class Demo1 {public static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()-> {while (isQuit == 0){//循环不做任何事//此时意味着这个循环一秒钟可以执行很多次}System.out.println("t1 退出");});t1.start();
​Thread t2 = new Thread(()->{System.out.println("请输入isQuit: ");Scanner sc = new Scanner(System.in);isQuit = sc.nextInt();});t2.start();}
}

当我们看完这个代码,心中预期的结果是,在t2线程中随便输入一个不为0的整数,t1线程会跳出循环,执行打印“t1 退出",然后整个线程结束释放,t2线程也会同样退出释放,整个程序会结束运行。

但当我们运行:

此时光标闪速,右侧红点没有熄灭,线程还没执行结束。实际效果与我们预期却有所不同,此时程序出现bug了,这里就是由多个线程引起的,也就是线程安全!!!

此处问题就是,“内存可见性”情况引起的,具体原因是因为: 1)load读取内存中的isQuit到寄存器中

2)通过cmp指令比较内存器的值是都是0,决定是否要继续循环。

由于这个循环,循环体中没有要执行的代码,这时的循环速度就飞快,短时间内就会进行大量的load 和 cmp 操作,此时编译器/JVM就的发现这个地方虽然进行这么多次load,但是load出来的结果都是一样,并且load操作又是对内存进行访问比较费时间效率低,所以编译器就决定,只是第一次循环的时候读取内存放到寄存器中,后续都不从内存中读了,而是直接从寄存器中,取出isQuit的值。

因此我们在t2线程中修改的isQuit的值是内存中的值,而t1却拿着寄存器中的值进行比较,所以导致出现这样的bug。

上述问题就称为“内存可见性“问题,其实就是编译器的bug(编译器优化出错了)。

volatile就是对应此问题的解决方案

在多线程环境下,编译器对于是否要进行这样的优化,判断不一定准。此时就需要我们手动通过volatile关键字,告诉编译器,不要对此进行优化(虽然优化过后是算的快,但是算的不准了)

public class Demo1 {public static volatile int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()-> {while (isQuit == 0){//循环不做任何事//此时意味着这个循环一秒钟可以执行很多次}System.out.println("t1 退出");});t1.start();
​Thread t2 = new Thread(()->{System.out.println("请输入isQuit: ");Scanner sc = new Scanner(System.in);isQuit = sc.nextInt();});t2.start();}
}

我们对要判断的变量isQuit加入关键字后,程序的运行结果就能和预期一样了

还有另一个解决办法:在循环中加入些其他代码,例如sleep一会

public class Demo1 {public static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()-> {while (isQuit == 0){//循环不做任何事//此时意味着这个循环一秒钟可以执行很多次try {Thread.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1 退出");});t1.start();
​Thread t2 = new Thread(()->{System.out.println("请输入isQuit: ");Scanner sc = new Scanner(System.in);isQuit = sc.nextInt();});t2.start();}
}

这同样程序也能正常结束。

所以到底编译器到底啥时候进行代码优化,其实是说不清楚的,所以还是使用volatile关键字更靠谱

总结 :“保证内存可见性”是 基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

关于内存可见性,还涉及到一个关键的概念,JMM(Java Memory Model,Java内存模型)

2.保证有序性:

同样是编译器为了提升执行效率,可能会调整原有代码的执执行顺序,调整的前天是保证逻辑不变。

例如我们去菜市场买菜需要买:西红柿,鸡蛋,黄瓜,香菜

可能我们的顺序不是最佳的,不能保证每一此买菜的顺序都是最佳的路线,此时编译器在保证我们要买的菜不变的情况下,会修改我们的买菜顺序:

此时编译器就优化了我们的买菜顺序,达到比较高效的状态。

通常情况下,指令重排序,能够保证逻辑不变的前提下,把执行的效率大幅度提高,在单线程的情况下可能没什么问题,但在多线程下,可能会出现误判。

class Instance{private static Instance instance = null;public Instance getInstance(){if(instance == null){synchronized (Instance.class){if(instance  == null){instance = new Instance();}}}return instance;}private Instance(){}
}

对于上面代码,new操作,是很可能触发指令重排序的。

new操作可以拆分为三步: 1.申请内存空间

2.把内存空间上构造对象(构造方法)

3.把内存的地址赋值给instance引用

这个执行顺序可以是1,2,3 也可以是1,3,2来执(1一定是先执行),不管什么顺序,单线程下都是无所谓的,但是在多线程下,可能就会出问题。

情况如下:

假设这里是按照1,3,2来执行,当第一个线程(t1)调用此方法执行完1和3时,instance已经非空了,此t1还没执行完,cpu调度器就调度到第二个线程(t2),t2就开始执行,此时t判断instance为非空,就直接return instance了,进一步t2就有可能访问到instance里的属性和方法了,这时就容易出现bug了,访问的instance引用是还没进行初始化执行构造方法的引用

解决办法添加关键字volatile

class Instance{private static volatile Instance instance = null;public Instance getInstance(){if(instance == null){synchronized (Instance.class){if(instance  == null){instance = new Instance();}}}return instance;}private Instance(){}
}

对Instance添加关键字后,就能预防编译器的不合理优化。

结论:保证有序性是禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

wait 和 notify

wait和notify都是Object的方法,所以随便定义一个对象都可以使用该方法,(需要借助同一个对象来实现),都需要搭配synchronized来使用。

wait在执行的时候要做的三件事

1.释放当前锁

2.让线程进入阻塞

3.当线程被唤醒的时候,重新获取锁

wait() 不带参数的方法会持续的阻塞等待下去,直到其他线程调用notify唤醒。

wait() 带参数的方法会指定一个超时时间,避免wait无休止的等待下去。

使用wait 和 notify 可以避免线程饿死

在当前场景下,1号火柴人进入ATM需要取钱,并且1号使用ATM时会”上锁“此时其他人无法进入,但

1号发现ATM里里没钱了,取不了,1号就会释放锁,此时其他火柴人会尝试去竞争这个锁(此处是非公平竞争,线程竞争锁的概率是相等的,不是先来先到),但是刚才的1号也能参与竞争

这些线程等待锁的时候都是阻塞等待,没在cpu上执行,当1号释放锁后,其他人想去cpu,还需要有一个系统调度的过程,而1号自身,已经在cpu上执行,没有这个调度的过程,更容易拿到这个锁,这种情况就会导致出现”线程饿死“。

针对上面的情况就可以使用wait 和 notify来解决

让1号,在发现没钱的时候就会进行wait(wait本身会释放锁,并且进入阻塞),1号就不会参与后续锁竞争了,也就能把锁释放出来让别人获,就给其他人提供机会了。

唤醒的两种不同的用法:

notify():一次唤醒一个线程

notifyAll:一次唤醒全部线程

调用wait不一定就只有一个线程调用,N个线程都可以调用wait,此时当有个多个线程调用的时候,这些线程都会进入阻塞状态。

在唤醒的时候,wait要设计到一个重新获取锁的过程,也是需要串行执行的。

虽然提供了唤醒的两个方式,但还是notify更可控一些,用的更多一些。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com