java多线程编程(二)一一>线程安全问题, 单例模式, 解决程线程安全问题的措施
引言:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的
线程安全问题的原因:
一.操作系统的随机调度 :
二.多个线程修改同一个变量:
三.修改操作不是原子的 :
四.内存可见性 :
五.指令重排序:
解决上述的线程安全问题的措施:
线程安全问题的原因:
一.操作系统的随机调度 :
1. 这是线程安全问题的 罪魁祸首 随机调度使⼀个程序在多线程环境下, 执行顺序存在很多的变数.例子:这个代码返回结果就是随机调度的体现
现象:
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("这⾥是t线程运⾏的代码"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Demo1 { public static void main(String[] args) throws InterruptedException { MyThread t = new MyThread(); t.start(); while (true) { System.out.println("这里是主线程"); Thread.sleep(1000); } } }
现象:
二.多个线程修改同一个变量:
上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进行修改.此时这个 count 是⼀个多个线程都能访问到的 "共享数据"
例子:下面这个代码应该预期应该自增10w次,但是由于线程安全问题,达不到预期public class Demo11 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1 结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2 结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); // 一个线程自增 5w 次, 两个线程, 总共自增 10w 次. 预期结果, count = 10w System.out.println(count); } }
三.修改操作不是原子的 :
这里我们count++,时候站在操作系统层面,我们要进行大致三步:
load:把count的值读到寄存器里
add: 把寄存器中的内容加1
save: 把寄存器写回内存
进行以上以上操作时候由于操作系统随机调度多个线程之间,可能出现数据被覆盖的情况,这就是操作不原子的体现:
四.内存可见性 :
这个问题可以引入Java内存模型说明:
线程之间的共享变量存在 主内存 (Main Memory).(主内存就是泛指的内存)每⼀个线程都有自己的 "工作内存" (Working Memory) .(工作内存指的是CPU 的寄存器和高速缓存L1,L2,L3)当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存(CPU 的寄存器), 再从工作内存(CPU 的寄存器),读取数据。其实是通过jvm和编译器来实现优化,把读内存优化为读寄存器了,有时候这个优化逻辑不符合我们的预期的逻辑出现细节上的偏差,就导致内存可见性问题
代码实例:一个线程读取一个线程修改
这个循环条件的flag判断是条件跳转指令cmp是寄存器操作会很快,while会循环很多次,jvm觉得每次觉得读到的都是0,直接就把 读内存优化为读寄存器了, 此时寄存器的值为0,此时用户输入 1 想结束线程时,t1线程读不到这个在内存中(主内存)的值,所以这个t1线程结束不了public static void main(String[] args) { Thread t1 = new Thread(()->{ while (flag == 0){ } System.out.println("t1线程结束"); }); Thread t2 = new Thread(()->{ Scanner in = new Scanner(System.in); System.out.println("请输入flag的值"); flag = in.nextInt(); }); t1.start(); t2.start(); }
五.指令重排序:
要说清楚这个问题就要引入一种设计模式:单例模式
单例模式:
单例模式能保证某个类在程序中只存在唯⼀⼀份实例, 而不会创建出多个实例,不能创建多个对象,这里有两种写法:饿汉模式和懒汉模式:
1.饿汉模式: 类加载的同时, 创建实例:类加载时就new对象所以成为饿汉模式,注意构造方法私有化,防止类外多次实例化。 class Singleton { private static Singleton instance = new Singleton();//类加载时就new对象 //构造方法私有化,防止类外被实例化多个对象 private Singleton() { } public static Singleton getInstance() { return instance; } }
2.懒汉模式-单线程版:
懒汉模式,能不实例化就不实例化所以的懒汉, 第⼀次使用的时候才创建实例
class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
但是这个在多线程下还是存在线程安全问题的,而且还有一个指令重排序问题 。
就算加锁(结合下面看看),解决了线程安全问题,但是instance = new Singleton();
new对象操作,细分为这三步:
1 .申请内存空间
2.构造对象(初始化)
3.内存空间首地址,赋值给引用变量由于指令重排序,可能会改变顺序,顺序可能从1,2,3一一>1,3,2在这个代码情况下就可能,在类外调用,getInstance方法拿到未初始化的对象导致线程安全问题。class Singleton { private static Singleton instance = null; private static Object locker = new Object(); private Singleton() { } public synchronized static Singleton getInstance() { if(instance == null) {//进一步优化效率,减少锁的阻塞状态,(instance == null才加锁才new对象) synchronized(locker){ if (instance == null) { instance = new Singleton(); } } } return instance; } }
解决上述的线程安全问题的措施:
操作系统的随机调度 是操作系统,计算机一脉传承,不能解决,接下来我们围绕三~四~五~展开
三.修改操作不是原子的 :
这里我们可以把相关的操作打包起来,就是引入锁:
synchronized 关键字 - 监视器锁 monitor lock :
synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执行 到同⼀个对象 synchronized 就会阻塞等待.进⼊ synchronized 修饰的代码块, 相当于 加锁退出 synchronized 修饰的代码块, 相当于 解锁就和上厕所一样:
理解 "阻塞等待":针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试 进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁。注意:这里还有一种应用程序级别的忙等,不涉及操作系统
三.的解决代码:
这里注意两个线程要加他一把锁,才有互斥的效果。
private static int count = 0; public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { synchronized (locker) { for (int i = 0; i < 50000; i++) { count++; } } System.out.println("t1 结束"); }); Thread t2 = new Thread(() -> { synchronized (locker) { for (int i = 0; i < 50000; i++) { count++; } } System.out.println("t2 结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); // 一个线程自增 5w 次, 两个线程, 总共自增 10w 次. 预期结果, count = 10w System.out.println(count); }
四.内存可见性解决:
这里引入volatile关键字:
1.volatile 能保证每次读取操作都是读内存2.volatile 能保证变量的读取和修改不会出发指令重排序
public class{ public volitile static int flag = 0 //这样修饰变量编译器就不会优化了 public static void main(String[] args) { Thread t1 = new Thread(()->{ while (flag == 0){ } System.out.println("t1线程结束"); }); Thread t2 = new Thread(()->{ Scanner in = new Scanner(System.in); System.out.println("请输入flag的值"); flag = in.nextInt(); }); t1.start(); t2.start(); } }
五.指令重排序:
饿汉模式是没有线程安全问题和指令重排序的,因为都是读操作
懒汉模式下就会有:
我们也加上volatile关键字:
class Singleton { private static volatile Singleton instance = null;//加上volatile private static Object locker = new Object(); private Singleton() { } public synchronized static Singleton getInstance() { if(instance == null) {//进一步优化效率,减少锁的阻塞状态,(instance == null才加锁才new对象) synchronized(locker){ if (instance == null) { instance = new Singleton(); } } } return instance; } }