当前位置: 首页 > article >正文

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;
 }

}

​


http://www.kler.cn/a/378143.html

相关文章:

  • 从零开始在本地服务器上安装OnlyOffice并进行跨地域协同编辑文件
  • 如何学习Python编程?
  • 【IEEE出版 | EI稳定检索】2024智能机器人与自动控制国际学术会议 (IRAC 2024,11月29-12月1日)
  • 【C++、数据结构】哈希表——散列表(一)(概念/总结)
  • 【Nginx】前端项目开启 Gzip 压缩大幅提高页面加载速度
  • 【初阶数据结构与算法】复杂度分析练习之轮转数组(多种方法)
  • FRAMES数据集:由谷歌和哈佛大学 联合创建一个综合评估数据集,目的测试检索增强生成系统在事实性、检索准确性和推理方面的能力
  • .card ~ img { width: 100%; height: 100%; object-fit: cover; }
  • git入门教程12:git命令与技巧
  • 论 ONLYOFFICE:开源办公套件的深度探索
  • PyTorch实战-手写数字识别-CNN模型
  • 【已解决,含泪总结】Ubuntu18.04下非root用户Anaconda3卸载重装,conda install终于不再报错
  • 可编辑31页PPT | 智慧业务中台规划建设与应用总体方案
  • 大厂面试真题-MVCC有哪些不好
  • 小白从零开始配置pytorch环境
  • Apache 负载均衡详细配置步骤
  • StringTable
  • 利用ExcelJS封装一个excel表格的导出
  • git 入门作业
  • 学习记录:基于Z-Stack 3.0.1的Zigbee智能插座实现
  • Django-分页
  • 构建后端为etcd的CoreDNS的容器集群(七)、编写适合阅读的域名管理脚本
  • Vue2.0 通过vue-pdf-signature@4.2.7和pdfjs-dist@2.5.207实现PDF预览
  • 目前最新最好用 NET 混淆工具 .NET Reactor V6.9.8
  • Claude 3.5 新功能 支持对 100 页的PDF 图像、图表和图形进行可视化分析
  • diffusion model 学习笔记