学网站开发和游戏开发那个,php网站开发学习,有个性的个人网站,网站建设分销协议引言#xff1a; 如果多线程环境下代码运行的结果是符合我们预期的#xff0c;即在单线程环境应该的结果#xff0c;则说这个程序是线程安全的 线程安全问题的原因#xff1a; 一.操作系统的随机调度 #xff1a; 二.多个线程修改同一个变量#xff1a; 三.修改操作不是… 引言 如果多线程环境下代码运行的结果是符合我们预期的即在单线程环境应该的结果则说这个程序是线程安全的 线程安全问题的原因 一.操作系统的随机调度 二.多个线程修改同一个变量 三.修改操作不是原子的 四.内存可见性 五.指令重排序 解决上述的线程安全问题的措施 线程安全问题的原因 一.操作系统的随机调度 1. 这是线程安全问题的 罪魁祸首 随机调度使⼀个程序在多线程环境下, 执行顺序存在很多的变数. 例子这个代码返回结果就是随机调度的体现 现象 class MyThread extends Thread {Overridepublic 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 10wSystem.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.内存空间首地址赋值给引用变量 由于指令重排序可能会改变顺序,顺序可能从123一一132 在这个代码情况下就可能在类外调用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 10wSystem.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;}}