JavaEE初阶---多线程(三)---内存可见性/单例模式/wait,notify的利用解决

[复制链接]
发表于 2024-10-27 03:22:51 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1.volatile关键字

1.1保证内存的可见性–引入

什么叫做可见性,就是你原本应该可以瞥见的东西,但是如今你没有瞥见,这个时候,我们接纳这个volatile关键字的本领,保证这个可见性一定可以被瞥见,不可以出现你看不见的环境,就是让你必须瞥见—哈哈哈哈哈,这个是不是很希奇,通过下面的这个案例以及分析就可以明白上面的这段话的意思了;
下面的这个线程,我们利用一个全局变量控制一个死循环,这个全局变量的默认数值巨细就是0,这个时候我们的这个t1线程就会一直执行,但是我们的这个t2线程里面设置一个输入,我们期望输入一个不是0的数字,可以竣事这个死循环的过程,但是我们可以测试运行发现,这个是不成立的;
就是纵然我们输入这个1,这个时候的t1线程也不会停止下来

1.2保证内存的可见性–分析

这个时候上面的这个案例应该让你明白,就是我们的这个修改的过程应该是被瞥见,但是这个我们输入数据之后,这个线程并没有停止,就是“没瞥见”,由于如果瞥见的话,这个线程不应该继承执行的;
我们的volatile关键字就是解决这个问题的,就是我们的可见性的问题,我们先来分析一下这个为什么没有瞥见:
首先这个isQuit=0的处置惩罚涉及到两个步调,一个是我们的这个load操作,就是把这个数值从我们的内存读取到这个寄存器里面去,然后是这个cmp操作,就是把我们的这个值进行比较,决定是否须要继承这个循环的过程;
这个循环的过程很快,因此这个执行的过程中就会涉及到大量的这个load和cmp的操作,因此这个时候由于刚开始执行的时候我们没有进行修改这个时候每一次load的数值都是一样的,同时介于这个从内存里面进行数据的读取很浪费时间,因此这个时候编译器进行优化,就是我们的这个load不再执行,只进行这个cmp的操作;
因此这个时候的执行过程从本应该的load cmp load cmp load cmp load cmo变化成为了这个load cmp cmp cmp cmp…因此这个编译器的优化使得我们的这个修改无法被从内存里面进行读取,因此这个时候就会出现上面的这个不可见性的问题;
1.3保证内存的可见性–解决

我们的这个volatile关键字就是用来修饰这个变量的,这个时候我们输入1的时候,上面的这个线程就必须要竣事,也就是说我们的这个修改他必须瞥见!!!!
这个时候,你走该明白为什么叫做保证内存的可见性了吧~~

1.4内存可见性-JMM内存模子

JMM就是java memory model简称,全程就是我们的java内存模子–这个是java的官方的规范文档上面的叫法
还是上面的这个问题,我们的官方文档上面利用这个JMM进行解释:t1线程对应的这个isQuit变量,本身是存在于这个主内存上面的,由于这个编译器的优化,这个isQuit变量就会被放到这个工作内存里面,我们在这个t2线程上面修改这个变量的时候,不会影响这个工作内存里面的内容;
实在这个官方利用的JMM解释和我们上面的这个寄存器和内存的解释逻辑是一样的,就是换了一个方式罢了,这个里面的主内存相称于是我们上面介绍的这个内存,他们说的这个工作内存相称于我们的这个cpu寄存器,理解即可,就是这个相同的逻辑利用差别的方式表达罢了;
2.notify和wait介绍

上面的两个方法都是我们的object里面的方法;
2.1作用一:控制调度次序


wait执行的时候须要颠末三个步调:
1.释放当前的这个锁;
2.这个线程进入壅闭的状态;
3.等待线程被叫醒,叫醒的时候重新获取锁;
监督器实际上就是我们的这个synchronized关键字修饰对象,上面的这个缘故起因就是由于我们的这个对象还没有上锁就被解锁了,这个wait的第一步就是释放当前的这个锁,但是我们这个线程实在就没有加锁,因此这个就是监督器的状态非常,我们须要先加上这个锁,然后才可以对于这个锁进行释放(也就是利用我们的这个wait关键字);
加上这个synchronized关键字之后,,这个线程相称于就是被上锁了,这个时候我们就可以正常释放锁,并且这个线程会处于这个壅闭的状态,但是没有线程叫醒这个线程,因此这个线程就会一直处于壅闭的状态;

下面的这个环境,我们创建两个线程,t1线程利用这个wait进行这个加锁的操作,然后利用这个wait方法的时候就是出于壅闭的状态,我们的这个t2里面利用这个notify对于这个线程进行叫醒操作,就是让这个t1线程的壅闭状态执行;

2.2作用二:避免线程饿死

下面的这个就是线程的一个形象的图示,在这个图里面,我们的这个锁就是我们的线程里面的这个锁,我们的这个风趣1就是正在执行的线程,我们把这个调度执行的这个情形类比成为一个取款机的情形,就是我们想要取钱,但是这个风趣1进去之后把这个门锁上了之后,其他的四个风趣都是处于壅闭的状态,因此这个时候只能等这个风趣1出来,但是这个风趣1出来之后,想在进去看看这个时候有没有钱,这个时候本身又进去了,如许的话,大概我们的这个风趣1一直在进收支出,但是其他的四个风趣都是没有时机进入到这个里面去的。为什么会出现这个环境;
重要是我们的风趣1原来就是处于这个CPU上面执行的,这个时候他想要再次执行,就是很容易的,但是对于这个其他的四个线程风趣,如果他们想要执行,就须要被这个调度,这个调度的过程须要泯灭一定的时间,没有我们的这个风趣1来的方便,因此这个时候就是会出现这个风趣1一直进收支出,但是我们的其他的连进入这个取款机里面的这个时机都没有,这个环境是很常见的;
上面的这个一个线程一直在执行,但是其他的线程没有时机被调度就是属于我们的线程饿死的环境,想要解决这个线程饿死的环境,我们可以利用这个wait和notify进行处置惩罚;

利用这个wait之后,我们的这个线程就是按照上面说的三步操作,就是释放这个锁,然后处于壅闭的状态,具体到上面的这个例子里面,就是我们的线程1释放锁之后,处于壅闭的状态,这个时候我们其他的线程就有时机被调度,至于什么时候叫醒它,这个时候我们就可以控制了,风趣1想要进去查看这个环境,至少须要我们的其他的风趣都进去看了一遍之后,我们在叫醒它,这个时候就合理的解决了线程的饿死的环境,保证了线程都是会被调度的;
2.3notify和notifyAll区分

我们进行wait的线程大概是一个,实在可以是多个,这个时候,我们多个线程调用wait,都是处于壅闭的状态,这个时候,我们可以一次一次的进行叫醒,我们也可以利用这个notifyAll进行一次性全部叫醒;
3.单例模式–经典设计模式

单例:就是有的场景只须要每一个类只须要一个对象,不可以实例化多个对象;
这个环境下,大概有的人会说,我们设计程序的时候只new一次不就可以了吗,为什么会搞出来一个设计模式去处置惩罚这个问题,由于这个如果是程序员操控,大概会出现各种各样的问题,由于这个这个完全取决于我们程序员本身,有些时候如果哦我们忘记之类的,就会出现问题;
但是利用设计模式处置惩罚这个问题,就会交给这个编译器处置惩罚,如果一旦出现问题,这个机器肯定是会报错的,这个就是强制性的处置惩罚解决方案,因此这个时候就会变得更加的可靠,这个也是我们设计这个单例模式的一个缘故起因,总之,很多事变,交给机器处置惩罚就是比交给人处置惩罚更加靠谱,这个重要是由于我们的人处置惩罚问题带有一定的不可靠性,但是我们的机器处置惩罚就是强制性的,遇到问题就是报错对于程序员进行提示,这个处置惩罚的方法更加的安全和稳妥;
3.1饿汉模式

下面的这个就是两种设计模式:饿汉模式和懒汉模式,两个模式的区别实在是很显着的,但是只听这个名字大概不是很清晰,我们集合下面的这个代码进行说明:
下面的这个就是一个实例,我们对于这个实例只答应实在例化一个对象,想要利用这个实例就是调用这个里面的方法,直接返回这个实例即可;
由于这个单例模式的重要的特点就是这个只可以实例化一个对象,因此我们把这个类的构造方法设计成为一个私有的,如许的话,我们的这个类是被封装的,里面只有一个实例,类的外面无法利用这个私有的方法,因此也就是无法进行这个对象的实例化;
但是这个饿汉模式很显着嘛,就是饿,因此这个创建实例的时间就是我们的这个类进行加载的时候就会进行这个实例的创建,这个就是饿汉模式;

3.2懒汉模式

和上面的这个饿汉模式不一样的就是我们的这个懒汉模式,就是懒汉,因此这个时候就就不会在很早的时候进行这个类的实例化的工作,由于上面的这个饿汉模式就是在类加载的时候进行这个类的实例化;
由于上面的这个在类加载的时候就创建这个实例大概我们临时用不到,但是这个懒汉模式就是基于这个环境,我们的懒汉模式是在利用这个实例的时候进行这个实例的创建,如许的话我们一开始的这个实例就是空的,我们用到的时候,再次利用这个new进行实例的创建;

3.3设计模式和线程安全

上面的两个设计模式,各自都是有本身的特点的,但是两个设计模式哪一个会保证线程安全呢,这个懒汉模式实在线程就是不安全的,由于我们之前说过线程不安全的一个重要的缘故起因就是对于这个变量进行修改;
在下面的这个饿汉模式的设计代码里面,只会涉及到去读操作,根本就谈不上修改的操作,因此这个就不会有这个线程的安全问题;

但是在我们的这个懒汉模式里面,由于这个判定之后进行实例,这个实际上就是一个先读取,然后就会进行修改,由于原来是空的,如今是一个新的实例,这个难道不是修改吗;
但是我们的这个懒汉就是初始化,因此这个没有涉及到这个修改的内容;
下面的这个形象的展示了我们的懒汉模式出现的这个线程安全问题的环境,我们的第一个线程进行读取判定的时候,发现是空的,这个时候就会准备进行修改,但是这个时候我们的t2线程开始执行,这个是穿插执行的,因此这个时候还没有等到这个t1线程进行修改,我们的这个t2线程就会再次进行判定,因此这个时候就会t2线程先辈行修改,然后这个修改之后,轮到我们的这个t1线程执行,这个时候t1线程再次修改,这个实在就是线程不安全缘故起因

3.4解决饿汉模式的安全问题

想要解决这个线程的不安全的问题,我们的解决方案和之前一样,就是加锁,我们须要进行这个对象的加锁,这个加锁想要解决这个问题,重要是解决这个交叉执行的问题,因此我们的这个加锁的范围就是我们的这个循环分支的范围;
如许的话,两个线程就不会出现上面的这个交叉执行的环境了;

如许写固然可以解决这个线程安全的问题,但是一旦加上之后,我们每一次调用这个getInstance方法的时候,都须要先加上锁,但是这个安全问题只会出如今这个最开始的时候,一旦创建出来之后,我们的这个就不存在线程安全了,由于第一次是没有创建对象,但是一旦创建对象,我们的线程安全不会存在问题了,但是我们如许写就会每一次调用这个getinstance方法的时候都会加锁,这个降低了我们的程序的效率,有些多此一举了;
3.5解决方案的优化

下面的这个就是在原来的基础上面加上我们的这个外层的if判定,判定我们的这个实例是不是被创建,如许的话,这个只会在第一次的时候去加锁,解决了我们上面说的这个每一次都须要加锁的环境;
上面的这个加锁,实在如果我们的这个实例已经存在,就不存在线程的安全问题了,这个时候加锁就没有须要了,因此我们判定这个时候是不是进行实例的创建,如果是已经创建,我们就不会加锁了;
我们的两个if内容同等,但是意义差别,第一个是判定是否须要加锁,第二个是判定是不是须要进行这个实例的创建,由于如果没有创建实例的话,我们须要本身去创建(这个是最开始的版本就存在的);

3.6指令重排序的解决

指令重排序也是我们的编译器优化的一个体现,这个也会对于我们的代码执行环境产生影响;
什么是指令的重排序,就是这个执行的先后次序发生变化,这个变化也是编译器的优化导致的;
例如上面的这个new实际上就是三个步调:
1.申请内存空间;
2.在内存空间上面利用构造方法创建对象;
3.把内存的地点,赋值给我们的instance引用;
上面的这个执行的次序大概是这个123,但是如果出现了这个指令重排序的环境,这个的执行次序就是132
执行13的时候,我们的这个instance已经不是一个null了,只不外这个对象没有创建,指向的是这个非法的内存区域,这个时候我们的这个t2线程进行判定,发现这个instance==null不成立,这个时候就会直接返回我们的instance实例,然后就可以对于这个实例进行操作;---------这个时候极容易出现bug!!!

解决上面的这个问题,就是利用我们的volatile关键字修饰,由于我们之前总结过但是没有介绍过的这个volatile就有这个解决指令重排序的特性;
因此颠末上面的分析,下面的这个才是我们的单例模式(懒汉式)的最终代码,这个涉及到三次调解和改进,请仔细琢磨~~

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表