【线程安全(二) Java EE】
作者: 小飞学编程…_CSDN博客-编程小白
专栏:JavaEE初阶
标题:线程安全(二) -Java EE
线程安全(二) -Java EE
- ==引言==
- ==线程安全问题产生的原因==
- `内存可见性问题`
- `代码案例`
- `代码分析`
- `执行结果`
- 执行结果
- `原因分析`
- ==内存可见性问题的解决方法==
- `方法一`:
- `代码实现`
- `执行结果`
- ==我们会发现这样结果就会正常运行并且结束了;==
- `方法二`
- `代码展示`
- `执行结果`
- ==协调线程之间执行的逻辑顺序的相关方法==
- `wait()和notify()方法的使用;`
- `生活案例`
- `wait方法的学习`
- `notify方法:`
- `代码案例`
- `代码解释`
- ==代码执行逻辑==
- -- ==重要代码图解;==
- 在这里插入图片描述
- `执行结果`
- ==关于wait和notify方法的补充==
- `一:` `wait与join方法的区别`;
- `二 `
- `wait方法和joi方法一样也提供了“死等”和“超时限制”方法;`
- `三,` `wait方法和sleep方法的区别;`
- ==单例模式==
- `设计模式`
- ==饿汉模式==
- `代码案例`
- `代码解释`
- ==懒汉模式==
- `代码解释`
- ==下面我么就进行考虑一下懒汉模式和饿汉模式是线程安全的吗????==
- 在这里插入图片描述 ==呢么,下面我们就进行探讨一下产生问题的另一种导致线程安全问题再次产生的编译器优化问题;==
- ==指令重排序==
- `图示的案例进行分析`;
- `生活案例:`
- 在生活中,我们肯定都要经历买房子的经历:
- `买房子的步骤:`
- ==第一步==:买房; ==第二步==:装修; ==第三步==:拿钥匙;
- `指令重排序后:`
- ==第一步==:买房子; ==第二步==:拿钥匙; ==第三步==:装修;
- 这样在多线程的情况下就,由于多线程的并发执行:就会导致;我们直接拿到了钥匙就会得到一间毛坯房(什么都没有装修)(没有电,没有水)的房子;这样肯定是不行的;这就是我们程序的bug;
- `指令重排序问题的解决方法:`
- 可以用volatile关键字进行修饰这个变量,用volatile修饰后的变量就不会使其触发指令重排序问题; 这样就解决了问题;
- `volatile的作用`
- ==1,确保每次读取被其修饰的变量,都是读内存操作;== ==2.确保被其修饰的变量在被读取和修改的时候,都不会触发指令重排序;==
引言
线程安全问题对我们写出一个正确的多线程相关的代码是十分关键的因素;
在我的上篇文章中,我们一起了解了造成线程安全问题的几个原因;以及关于解决线程安全问题是所需要我们要了解的相关知识;
这篇文章:我们再次进行学习造成线程安全问题的几个原因
线程安全问题产生的原因
内存可见性问题
这个问题直接上理论讲解还不容易进行理解,所以下面我就先写一个由内存可见性引起的相关的Java代码;然后进行分析什么是内存可见性问题,这样更有利于我们的理解与掌握;
代码案例
public class Demo21 {public static int flg=0;public static void main(String[] args){Thread t1=new Thread(()->{while(flg==0){}System.out.println("线程t1执行完毕"); }); Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in); System.out.println("请输入你想要输入的flg值:");flg=scanner.nextInt(); }); t1.start(); t2.start(); } }
代码分析
.此代码:我创建了两个线程,以及定义了一个flg变量。其中一个线程进行读操作(while循环只进行了读flg变量的操作)另一个线程是进行了写操作(用输入一个值进行修改flg变量的值);
执行结果
我们观察结果发现:程序无法正常运行完毕;也就是我们的写操作没有成功,导致while循环无法正常结束;这样就导致了线程无法正常结束;所以出现了这样的结果;
原因分析
1:首先我们要明白第一个线程在执行读操作的时候,速度是非常快的,我们在另一个线程进行输入数字的时候,这个线程以及执行了最少数千次,因为CPU的执行速度是相当快的,所以在这时就出现了问题;
2关于:读操作:我以详细的图示结果进行介绍;-------------------------------------------------------------------------------------------------------------------------------------------------------------
内存可见性问题的解决方法
方法一
:
进行添加适当的代码;
在进行线程一的读操作的时候进行一行让线程休眠的sleep()方法的代码;
这样就可以解决内存的可见性问题了;
为什么这么说呢????
一开始编译器的误判是因为想要进行时间的更好的优化,就导致了出现上述的情况,但是现在加上让线程休眠的方法,就可以让编译器来考虑这个方法的优化了,这是因为休眠所花费的时间是上述load,和cmp操作所消耗的时间的无数倍,所以根本就不会考虑到load和cmp操作的时间优化了,就会更加放在sleep方法的时间优化上了,这样不管对不对sleep方法进行时间优化,也解决了内存的可见性问题;
代码实现
public class Demo21 { public static int flg=0; public static void main(String[] args){Thread t1=new Thread(()->{while(flg==0){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e); } } System.out.println("线程t1执行完毕"); }); Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in); System.out.println("请输入你想要输入的flg值:"); flg=scanner.nextInt(); }); t1.start(); t2.start(); } }
执行结果
我们会发现这样结果就会正常运行并且结束了;
方法二
sleep()休眠方法的使用也不是长久之计,并且这样也会影响程序的执行效率,但是Java大佬们也早就想出2了一个应对之策;呢就是:Java大佬们就设计了一个新的语法:添加了一个关键字:volatile,通过这个关键字修饰的变量在进行被线程读取的相关操作是就不会让编译器进行优化;也即:不可异化
代码展示
public class Demo21 {public static volatile int flg=0;public static void main(String[] args){Thread t1=new Thread(()->{while(flg==0){} System.out.println("线程t1执行完毕"); }); Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in); System.out.println("请输入你想要输入的flg值:");flg=scanner.nextInt(); }); t1.start(); t2.start(); } }
执行结果
说明这个语法设定的关键字是可以解决内存可见性的问题了;
下面我们进行介绍关于协调线程执行的逻辑顺序的相关方法,方便为介绍下一个线程安全问题产生的原因进行铺垫;
协调线程之间执行的逻辑顺序的相关方法
wait()和notify()方法的使用;
在进行了解之前:我们要明确的是:1.这两个方法是搭配使用的,并且要求先执行wait方法后执行notify方法;这样才能使得我们的代码顺利执行; 2.wait方法是等待操作,notify是通知操作;即我们常说的等待通知;要想结束等待就需要通知,所以必须搭配使用;先有所谓的等待才能提出通知; 3.wait和notify都是Object提供的方法;Java中的任意对象都提供了这两方法;
在进行讲解之前:我们先进行讲解一个生活中的事情,方便在学习该知识的时候更好的理解这两个方法是什么,以及这两个方法的作用是如何体现的;
生活案例
作为学生的我们:每天都要去食堂吃饭,我们会选中我们今天想吃的饭的窗口进行点餐;当我们点餐的时候就会说明我们要吃什么,我们点餐完毕后就会拿到一个号牌,等待饭好了后,我们依据号牌进行拿到我们的饭,这等待饭好的这个时间段中,我们会离开窗口位置,在能听到叫号的位置进行等待,这样就可以让后面排队的学生,也可以进行点餐拿号牌,我们如果不离开就会影响到其它同学的报好,如果自己霸着窗口不让势必就会引起矛盾,这样我们点完餐离开就是很好的做法(生活美德,文明礼貌),在这个期间,我们收听我们的号数进行取餐,听到其它的号数也与我们无关;
这样一个生活案例就与我们的两个方法息息相关;
这个案例是理解我们两个方法的基础;
wait方法的学习
1.含义:阻塞等待;
2,应用场景: 当一个线程拿到锁对象后,发现要执行的任务时机不成熟,这样就会用到wait进行阻塞等待;
3,wait方法的执行:
①:这个方法是要进行正常执行就必须要进行释放锁;
②:而进行释放锁的前提是要求这个wait方法要进行加锁操作;
③:而要执行这个操作要进行解锁,这样就导致出现即要求要加锁,又要求要解锁的情况,这可怎么办;
对:就是:在加速与解锁的中间时段进行执行我们的wait操作;在这个中间时段是释放锁的状态;(也就是我们在食堂点餐之后,我们就不霸占窗口了,但是我们还要在饭做好之后进行取饭,拿完饭后才是真正的解锁,在等待取饭的时候,我们只是不霸占着锁对象,这期间由于线程的并发执行可以让其它线程上CPU上进行执行,也就是后面排队的同学也可以进行点餐,我们互不影响);方便大家理解,我进行稍微图示展示(画图能力有限);
notify方法:
Java大佬为了和wait方法的对应也要求notify必须也要在加锁操作下进行;
①:作用:让wait方法结束执行;
②:执行操作:相当于这个窗口做好你点的餐,这时进行通知你进行取餐操作;
③:注意事项:在进行加锁操作时要求:和wait方法加锁的对象一致,这样才能配合使用,就相当于:取餐号牌号数要一一对应;这样才能正确取饭;
代码案例
public class Demo24 {public static void main(String[] args){Object lock1=new Object();Object lock2=new Object(); Thread t1=new Thread(()->{try{System.out.println("wait方法执行之前");synchronized (lock1){lock1.wait(); } System.out.println("wait方法执行之后"); } catch (InterruptedException e) {throw new RuntimeException(e); } }); Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入一会你想输入的数字,执行t1线程的wait方法");scanner.next(); synchronized (lock1){lock1.notify(); } }); t1.start(); t2.start(); } }
代码解释
代码执行逻辑
我创建了两个线程,在t1线程中:我进行了使用wait方法;在t2线程中我使用了notify方法;这样由于线程的并发执行:线程t1在进行加锁操作中,正常情况下,执行完里面的逻辑才能进行解锁操作,但是由于wait方法执行期间会阻塞等待,并且在这期间会释放锁,这样就可以使得线程t2能够进行执行里面的逻辑代码,:我们输入完输入操作,在进行执行notify方法,只要一执行完就会唤醒wait方法,这样wait方法就执行完毕,就可以进行线程1的解锁操作,这样就能完成线程1的其他相关操作;这样就能使得程序顺利执行完毕;
–
重要代码图解;
执行结果
关于wait和notify方法的补充
一:
wait与join方法的区别
;wait方法是等待另一个线程执行完notify方法就开始继续执行;
joi方法是等到另一个线程彻底执行完毕后才开始继续执行;
二
wait方法和joi方法一样也提供了“死等”和“超时限制”方法;
“超时限制”:超过设置的规定时间,不管执行没有notify方法都会停止等待;
死等:只要另一个线程没有执行到notif方法就会一直等下去;
三,
wait方法和sleep方法的区别;
①:wait方法必须搭配锁进行使用,必须进行加锁,才能用wait,而sleep不用;
②:当两个方法都在锁的内部进行使用时:wait在执行过程中可以释放锁,而sleep不能释放锁;
单例模式
我们在进行了解单例模式的前提下,先进行了解一下什么的设计模式;因为单例模式就是一种设计模式;
设计模式
作为程序员的我们,由于书写代码的水平参差不齐,于是Java大佬们就将一些典型问题的场景整理了出来,进行了整理和总结,并且针对这些典型的场景进行分析具体代码怎么写,并对此提出了一些具体的方案的建议;这样设计出来的方案就叫做
设计模式
;
类似于我们在做数学题的时候:套用公式的方法,这个设计模式就类似于套用公式
来进行帮组我们进行更好的解决问题;
1.含义:
单例模式就是其中的一种设计模式
2.要求:
单例模式强制要求:在一个类中不能进行创建多个对象;只能进行创建一个;
3.分类:
懒汉模式和饿汉模式
饿汉模式
下面我以代码案例方式让大家更好的进行理解这种模式的使用;
代码案例
//用饿汉模式实现单例模式; class Singleton1{private static Singleton1 instance=new Singleton1();public static Singleton1 getInstance(){return instance;}private Singleton1(){} } public class Demo28_0 {public static void main(String[] args){Singleton1 singleton1=Singleton1.getInstance();Singleton1 singleton2=Singleton1.getInstance();System.out.println(singleton1==singleton2); } }
代码解释
1.代码实现逻辑:
我创建了一个Singlenton类实现饿汉模式的要求;并且在主函数中测试了使用这个类获取到的方法是否都是来自同一个类;
2.饿汉模式的要求:
①首先我在类进行加载的时候就进行了静态成员的初始化,即把实例对象赋值该静态成员(完成实例对象的创建);而类加载在程序一运行就会加载;类。所以这个实例对象的创建就是在程序一启动就完成;这样就像极了饿汉非常饿的情况;
②:其次饿汉模式会将构造方用private进行修饰这样就防止了类对象的被创建,就达到了单例模式的要求;
③:最后我们考虑到那怎么获取这个对象呢??这时我们也提供了静态成员的获取方法,获取到静态成员就相当于获取到了对象;
懒汉模式
我们同样以代码的案例来进行懒汉模式的学习
//通过懒汉模式创建单例模式; class SingletonLazy{private static SingletonLazy instance=null; public static SingletonLazy getInstance(){if(instance==null){instance=new SingletonLazy();} return instance; } } public class Demo28_1 {public static void main(String[] args){SingletonLazy s1=SingletonLazy.getInstance();SingletonLazy s2=SingletonLazy.getInstance(); System.out.println(s1==s2); } }
代码解释
1.
代码实现逻辑
创建了一个懒汉类,进行构建单例模式;并且在主函数中进行使用该类中的方法判断前后使用的对象是否是同一个对象,来验证是否完成了单例模式的要求;
2.
懒汉模式的要求:
不同于饿汉模式在类加载的时候就进行实例化对象,而是在我们什么时候用到对象的时候才进行创建,因此在类中定义了一个变量intace为空,接着我们使用到了获取这个对象的方法:
此方法就判断了该变量是否已经实例化了对象;
如果已经实例化了对象就不需要进行实例化这个对象了;
如果该变量没有实力化对象就进行实例化对象;
通过这个if调节的判断就使得这个方法只能进行一次实例化对象的判断了;
3.
懒汉模式的好处
:`
这样的延迟进行对象的创建,就可以提高程序执行效率;因为创建对象也会消耗一定的时间;当我们在用不同的时候就不会进行创建;这样在另一方面可以节省资源;
eg:当我们打开网页的时候,只会加载一页内容,当我们打开第二页的时候才进行加载;如果我们打开网页的时候就加载所以的页面,所有的页面资源相当多,这样一下就会使得网页出现卡顿;这样一页一页的进行加载就解决了一下子加载出来出现卡顿的影响;
我们既然已经学习了单例模式的懒汉模式和饿汉模式,我们肯定会想:这和我们所学的多线程安全问题产生的原因有什么关系呢??学这些干什么:其实单例模式的这两种模式还真和我们学习的线程安全有关系;
下面我么就进行考虑一下懒汉模式和饿汉模式是线程安全的吗????

呢么,下面我们就进行探讨一下产生问题的另一种导致线程安全问题再次产生的编译器优化问题;
指令重排序
指令重排序就是编程器的一种逻辑优化策略;
相当于把我们的一句代码执行的逻辑进行了调换,编译器感觉这就进行了很好的处理,其实在多线程的这种情况下就会出现一些细节上的错误;
下面我们就进行分析一下这个指令重排序后是如何对多线程情况下产生线程安全问题的?????;
图示的案例进行分析
;
下面一生活案例进行讲解,方便我们更好的理解
生活案例:
在生活中,我们肯定都要经历买房子的经历:
买房子的步骤:
第一步:买房;
第二步:装修;
第三步:拿钥匙;
指令重排序后:
第一步:买房子;
第二步:拿钥匙;
第三步:装修;这样在多线程的情况下就,由于多线程的并发执行:就会导致;我们直接拿到了钥匙就会得到一间毛坯房(什么都没有装修)(没有电,没有水)的房子;这样肯定是不行的;这就是我们程序的bug;
指令重排序问题的解决方法:
可以用volatile关键字进行修饰这个变量,用volatile修饰后的变量就不会使其触发指令重排序问题;
这样就解决了问题;
volatile的作用
1,确保每次读取被其修饰的变量,都是读内存操作;
2.确保被其修饰的变量在被读取和修改的时候,都不会触发指令重排序;