【五】线程安全VS线程不安全

1. Java内存模型的特征

  Java内存模型是围绕着在并发过程中如何处理原子性可见性有序性这三个特征来建立。下面逐个看下哪些操作实现这三个特性:

1.1 原子性(Atomicity)

  由Java内存模型来直接保证的原子性变量操作包括 readloadassignusestorewrite 这六个,我们可以大致认为,基本数据类型的访问、读写都是具备原子性(例外就是 longdouble 的非原子性协定),当然如果应用场景需要更大的范围来保证原子性,可以使用 synchronized 关键字,在 synchronized 块之间的操作也具备原子性。

1.2 可见性(Visibility)

  所谓的可见性就是指当一个线程修改了共享变量的值时,其他线程能够立马知道这个修改。Java内存模型是通过在变量修改后将新值台同步回主内存,在变量读取之前从主内存刷新变量值这种依赖主内存作为传递中介的方式来实现可见性!不论是普通变量还是 volatile 变量都是一样。然后 volatile 和普通变量的区别在于,volatile 变量可以保证新值能立刻同步主内存,每次使用都是拿到最新的值。
  另外 synchronizedfinal 也可以实现可见性。

1.3 有序性(Ordering)

  如果在同一个线程内,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指:线程内表现为串行的语义;后半句:是指 指令重排序工作内存与主内存同步延迟现象。

2. 线程安全、线程不安全

  • 线程安全:就是说多线程访问同一代码,不会产生不确定的结果。
  • 线程不安全:就是多线程访问同一代码,会产生不确定的结果,造成线程不安全有5个原因:
    • 抢占式执行
    • 多个线程修改同一个变量
    • 修改操作,不是原子的
    • 内存可见性,引起的线程不安全
    • 指令重排序,引起的线程不安全

3. 如何解决线程不安全

3.1 使用synchronized 关键字

先来看一段线程不安全的代码

class Counters{
    public int count = 0;

    public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo22 {

    public static void main(String[] args) throws InterruptedException {
        Counters counters = new Counters();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counters.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counters.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counters.getCount());
    }
}

在这里插入图片描述
可以看出结果并不是我们想的 20000. 是因为当我们在开启t1线程和t2线程操作同一个变量count,但是由于 count++ 这个操作并不具备原子性,该操作是从主内存中读取值到工作内存中,然后+1,在写回到主内存中,如果在t1读取旧值和写回新值的中间,t2线程也读取了值,这个值和t1读取的是一样的,如果t2读完旧值以后进行+1,并写回主内存,此时t2再写回主内存,在这种情况下,就少了一次+1的操作!此时就造成了线程不安全!因此需要 synchronized 关键字修饰,来保证线程的安全!下面来看synchronized的使用方法。


3.1.1 修饰实例方法

作用于当前实例加锁,进入同步代码前要获得当前实例的锁

class Counter{
    public int count = 0;
    synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }

}

此时输出结果为20000,此时保证了,当t1线程在访问synchronzed方法时,t2线程并不能访问。这就保证了操作的原子性! 另外要注意,当一个线程正在访问一个对象的synchronized实例方法,那么其他线程并不能访问该对象的其他synchronized方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,但是其他线程可以访问该实例对象的非synchronized方法。


3.1.2 修饰静态方法的时候

相当于给类加锁,会作用于这个类的所有对象实例
当一个线程A调用实例对象的非静态 synchronized方法,而线程B调用实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥对象!
因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用锁是当前实例对象的锁

class Counter {
    public static int count = 0;

    synchronized public static void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Counter counter1 = new Couter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
                System.out.println(counter.count);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
            	couter1.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

在这里插入图片描述
上述代码中,虽然创建了两个实例,但是由于count是被static修饰的,所以是类成员,所以当t1和t2线程进行操作时,操作的是同一个变量!所以当synchronized修饰静态方法的时候,锁就是当前class对象锁,就是 Counter.class。所以当两个线程进行访问时,竞争的是同一把锁,会产生互斥现象!所以保证了线程的安全性!另外如果线程t1访问的是实例对象的非静态synchronized方法时,另一个线程t2需要访问实例对象所属类的静态synchronized方法时,是不会发生互斥的,因为锁对象不同!


3.1.3 修饰代码块

指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

class Counter {
    Object locker = new Object();
    public int count = 0;

    public void add() {
        synchronized (locker) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }
}

修饰代码块时候,可以在()里面填写任意对象!由于在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,但是需要同步的代码块又是一小部分,所以此时就可以用修改代码块的方式加锁!


3.1.4 synchronized关键字总结

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的代码块或者方法在任意时刻都只能有一个线程执行。

4. 使用volatile关键字

4.1 内存可见性

先看一段代码:

public class Demo23 {
    public 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 scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

在这里插入图片描述
此处可以看出程序并没有停止!利用下图来解释!
在这里插入图片描述
所以当你输入1的时候,由于优化,所以寄存器中的值仍为0


public class Demo23 {
    volatile public 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 scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

在这里插入图片描述
当我们在用 volatile修饰 flag之后,可以看出程序最终结果和我们预想的一样!因为 volatile 保证了内存可见性,当变量修改时,可以立即同步回内存!

总结:volatile 不保证原子性! 适用的场景,一个线程读,一个线程写!

4.2 指令重排序

可以看一段伪代码

Map configOptions;
char[] configText;
// 此变量必须定义为 volatile
volatile boolean initialized = fasle;

// 假设以下代码在线程A中执行
// 模拟读取配置信息,当读取完成后
// 将initialized设置为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized  = true;

// 假设以下代码在线程B执行
// 等待initialized为true,代表线程A已经把配置信息初始化
while(!initialized){
	sleep();
}

// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

可以试想一下,如果定义的 initialized 没有被 volatile修饰,就可能会因为指令重排序的优化,导致线程A中最后一行代码被提前执行,这样在线程B中使用配置信息就会可能出现错误,而volatile关键字就可以避免此类情况发生!


如果有错误,请留言指正~~

本文参考资料:
《深入理解Java虚拟机》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/5321.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【机器学习】线性回归

文章目录前言一、单变量线性回归1.导入必要的库2.读取数据3.绘制散点图4.划分数据5.定义模型函数6.定义损失函数7.求权重向量w7.1 梯度下降函数7.2 最小二乘法8.训练模型9.绘制预测曲线10.试试正则化11.绘制预测曲线12.试试sklearn库二、多变量线性回归1.导入库2.读取数据3.划分…

Linux--抓包-连接状态

目录 一、TCP&#xff1a; 1.抓包&#xff1a; 2.工具&#xff1a; 3.状态&#xff1a; 4.命令&#xff1a; 三次握手&#xff1a; 应答确认&#xff1a; 四次挥手 一、TCP&#xff1a; 面向连接、可靠的、流式服务 1.抓包&#xff1a; 三次握手、四次挥手 2.工具&…

数据库:Redis数据库

目录 一、数据库类型 1、关系型数据库 2、非关系型数据库 3、关系型非关系型区别 二、Redis数据库 1、什么是Redis 3、Redis特点 4、Redis为什么读写快 5、部署Redis数据库 6、redis管理 7、Redis数据库五大类型 8、Redis数据库基础使用 9、redis五大类型增删查 …

数据库管理-第六十三期 烦(20230327)

数据库管理 2023-03-27第六十三期 烦1 跨版本PDB迁移补遗2 BUGs3 就低不就高总结第六十三期 烦 上个周末呢&#xff0c;因为一些客户的事情整的一个周末都在干活&#xff0c;其中两天还搞到的晚上12点&#xff0c;几乎没咋休息&#xff0c;现在感觉贼累&#xff0c;继续写文章…

为什么我们认为GPT是一个技术爆炸

从23年初&#xff0c;ChatGPT火遍全球&#xff0c;通过其高拟人化的回答模式&#xff0c;大幅提升了人机对话的体验和效率&#xff0c;让用户拥有了一个拥有海量知识的虚拟助手&#xff0c;根据UBS发布的研究报告显示&#xff0c;ChatGPT在1月份的月活跃用户数已达1亿&#xff…

Java实习生------Redis哨兵机制详解⭐⭐⭐

“无数的我们被世界碾压成一缩黑团&#xff0c;无数的我们试图与世界抗争到底”&#x1f339; 参考资料&#xff1a;图解redis 目录 什么是哨兵机制&#xff1f; 哨兵机制主要干了哪三件事&#xff1f; 哨兵监控主节点的过程是怎样的&#xff1f; 判断主节点故障之后&…

Servlet---服务端小应用程序(服务器端的小组件)

零.前置知识 1.tomcat—服务器容器 tomcat就是一个服务器容器&#xff0c;通常说的将项目部署到服务器&#xff0c;就是将项目部署到tomcat中&#xff08;将项目放到tomcat容器中&#xff09;。 浏览器向服务器发送一个HTTP请求&#xff0c;请求访问demo09.html页面&#xf…

【Linux】进程相关笔记

文章目录查看进程方式批量化注释fork进程状态R状态S状态D状态T状态t状态退出码问题X&&Z状态僵尸进程的危害makefile 新知识孤儿进程查看进程方式 ls /proc ls /proc/13045 (可以查看到之情进程的属性) ps axj | head -1 && ps ajx | grep myprocess(文件名) |…

垃圾回收之CMS、G1、ZGC对比

ZGC&#xff08;The Z Garbage Collector&#xff09;是JDK 11中推出的一款低延迟垃圾回收器&#xff0c;它的设计目标包括&#xff1a; 停顿时间不超过10ms&#xff1b;停顿时间不会随着堆的大小&#xff0c;或者活跃对象的大小而增加&#xff1b;支持8MB~4TB级别的堆&#x…

【C++】string类的模拟实现

目录 一、前言 二、模拟实现 1、构造函数 2、拷贝构造函数 3、operator 4、operator[] 5、迭代器 6、string类的比较 7、string类的扩容 7.1、reserve 7.2、resize 8、string类的尾插 8.1、push_back 与 append 8.2、operator 9、string类的insert 9.1、插入字符…

deepin15.11无法正常输入汉字问题的解决

1,起因 本来是sougou输入法 但是由于自己突发奇想 在那瞎折腾 一不小心把配置给弄坏了 就再也回不到之前可以正常打印汉字的状态 历经两个小时的折腾 总算是又能输入汉字啦 耗费两个多小时 对当下的我来说时间成本着实有点高 但是把问题给解决了 总算还是有点收获 平时的学习过…

注意力机制 | CNN-BiLSTM-Attention基于卷积-双向长短期记忆网络结合注意力机制多输入单输出回归预测(Matlab程序)

注意力机制 | CNN-BiLSTM-Attention基于卷积-双向长短期记忆网络结合注意力机制多输入单输出回归预测(Matlab程序) 目录 注意力机制 | CNN-BiLSTM-Attention基于卷积-双向长短期记忆网络结合注意力机制多输入单输出回归预测(Matlab程序)预测结果评价指标基本介绍程序设计参…

qt 编译器 调试器

电脑版本&#xff1a;win10 64位 qt版本&#xff1a;based on Qt 5.14.0&#xff08;msvc 2017&#xff0c; 32位&#xff09; Qt Creator 4.11.0 qt安装包&#xff1a;qt-opensource-windows-x86-5.9.9.exe 安装过程一路next&#xff0c;安装完成后&#xff0c;默认使用的…

Spring IoC循环依赖问题

什么是循环依赖 循环依赖其实就是循环引⽤&#xff0c;也就是两个或者两个以上的 Bean 互相持有对⽅&#xff0c;最终形成闭环。⽐如A依赖于B&#xff0c;B依赖于C&#xff0c;C⼜依赖于A。 注意&#xff0c;这⾥不是函数的循环调⽤&#xff0c;是对象的相互依赖关系。循环调…

一个服务端同学的Vue框架入门及实践

做为服务端同学&#xff0c;接触前端代码较少&#xff0c;刚毕业的时候用过 jQuery Bootstrap2/3&#xff0c;当时的感觉就是&#xff0c;容易上手&#xff0c;学习门槛相对较低&#xff0c;另外就是有一个非常成熟的 jQuery 插件库&#xff0c;在这里&#xff0c;几乎可以找到…

vue集成tui.calendar日历组件

vue集成tui.calendar日历组件前言一、简介、效果图二、vue简单集成(集成js版本,没有使用官方的vue2版本)1.引包2.简单示例三、自定义功能1.需求分析、效果展示2.实现思路前言 vue2的集成在git上官方已经给出了demo这里就不贴代码了。本次主要是vue3集成 最近有个功能需要一个日…

重发布实验

基础配置&#xff1a; [r1]int l0 [r1-LoopBack0]ip add 1.1.1.1 24 [r1-LoopBack0]int g0/0/0 [r1-GigabitEthernet0/0/0]ip ad 192.168.12.1 24 [r1-GigabitEthernet0/0/0]int g0/0/1 [r1-GigabitEthernet0/0/1]ip add 192.168.123.1 24 [r1]ospf 1 router-id 1.1.1.1 [r1-o…

自学大数据第12天~Hbase

先留个问题~ERROR: KeeperErrorCode ConnectionLoss for /hbase/master 稍后解决 找到了问题的根因: 查看报错日志 事关tmp文件夹的配置,所以去找一下hbase配置文件中关于这个文件夹的配置项 我的策略是将这个配置项注销掉 然后启动hbase ,之后hmaster就成功启动了; 接着s…

熟练Redis之无处不在的锁

为了保证并发访问的正确性&#xff0c;Redis提供了两种方法,分别是加锁和原子操作 Redis加锁两个问题:一个是&#xff0c;如果加锁操作多&#xff0c;会降低系统的并发访问性能;第二个是&#xff0c;Redis客户端要加锁时&#xff0c;需要用到分布式锁&#xff0c;而分布式锁实…

Coremail奇安信发布2022中国企业邮箱安全性研究:应对ChatGPT带来的安全挑战

日前&#xff0c;广东盈世科技计算机有限公司与奇安信集团联合编写发布《2022中国企业邮箱安全性研究报告》。 报告数据显示&#xff1a;2022年&#xff0c;全国企业邮箱用户共收到各类钓鱼邮件约425.9亿封&#xff0c;相比2021年收到各类钓鱼邮件的342.2亿封增加了24.5%。 一…