IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 【JavaWeb】为什么线程是不安全的?(原子性,可见性,顺序性) -> 正文阅读

[Java知识库]【JavaWeb】为什么线程是不安全的?(原子性,可见性,顺序性)

1??观察线程不安全

// 演示线程不安全的现象
public class Main {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 100000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();

        add.join();
        sub.join();

        System.out.println(r);
    }
}

第一次运行结果:
-22577
第二次运行结果:
10823
第三次运行结果:
-651

理论上,r 被加了 COUNT 次,也被减了 COUNT 次
所以,结果应该是 0,但是结果却和预期的不一致,而且每次的结果还不一样,这就是多线程带来的风险。后面会具体分析原因。

2??线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet、StringBuilder等等都不是线程安全的。

Vector、Stack、Dictionary、StringBuffer等是线程安全的,但是线程安全必定会带来性能损失,这几个类都是 Java 设计失败的产品,尽量不要使用。

想要线程安全,程序员在设计的时候尽量就不要带入线程不安全的情况,或者自己解决线程不安全的问题。

3??当前线程不安全的原因

1.站在开发者角度

  1. 多个线程之间操作同一块数据(共享的),不仅是内存数据
  2. 多个线程中至少有一个线程在修改(写操作)这块共享数据

即使在多线程的代码中,哪些情况不需要考虑线程安全问题?

1.几个线程之间互相没有任何数据共享的情况下,天生就是线程安全的;
2.几个线程之间即使有共享数据,但只有读操作,没有写操作时,天生也是线程安全的。

2.系统角度

java代码(高级语言)中的一条语句,可能对应了多条指令。

r++实质就是r = r + 1

变成指令动作:
1.把 r 的数据从内存中加载到寄存器中 LOAD_A
2.完成对数据加一的操作       ADD 1
3.把寄存器中的值写回到内存中    STORE_A

但是线程的调度是可能发生在任意时刻的,可能在这三条指令其中之一发生调度,但是不会切割指令!

4??线程安全需满足的条件

1.原子性(atomic)

什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

解释刚才的现象

刚才我们看到的 r++或者r--就是一个原子性的操作,要么全部完成或全部没完成。
但是实际起来,不能保证原子性,所以就出了错。

为什么 COUNT 越大,出错的概率就越大?

因为 COUNT 越大,线程执行需要跨时间片的概率越大(遇到线程调度的概率越大),导致中间出错的概率越大!
这点和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.

原子性被破坏是线程不安全的最常见的原因

2.内存可见性

可见性指, 一个线程对共享数据的修改,能够及时地被其他线程看到.

前置知识

CPU 为了提升数据获取速度,一般在 CPU 中设置了缓存,因为缓存速度远大于内存速度,内存容量远大于缓存容量。
快和慢都是相对的,这样设计是为了兼顾效率和成本。
在这里插入图片描述
Java 内存模型 (JMM)

Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

主存储:真实内存
工作内存:CPU中缓存的模拟(不区分几级缓存)

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 “工作内存” (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据,并允许在工作内存中处理很长时间.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.

  1. 初始情况下, 两个线程的工作内存内容一致.
  1. 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步.
    甚至在某些情况下,会被优化成完全看不到的结果!

这个时候代码中就容易出现问题.

3.代码顺序性

什么是代码重排序?

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
现象就是可能重排序过后的执行指令和书写指令不一致。

我们写的程序,往往中间是经过很多环节优化后的结果,并不保证最终执行的语句和我们写的语句一模一样。

JVM规定了一些重排序的基本原则:happens-before规则
简单理解:JVM要求,无论怎么优化,对于单线程,优化前后的结果不应该有变化。但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

举个例子:
在这里插入图片描述

重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论

5??解决之前的线程不安全问题

使用 synchronized 锁
这里用到的机制,内容比较多,下一篇博客再讲解。

public class UseSynchronized {
    // 定义一个共享的数据
    static int r = 0;

    // 定义加减的次数
    static final int COUNT = 1000000;

    //定义一个公共的静态对象
    static Object object = new Object();

    static class Add extends Thread {
        @Override
        public void run() {
            //对该对象进行加锁
            synchronized (object) {
                for (int i = 0; i < COUNT; i++) {
                    r++;
                }
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            //同一把锁,有互斥性
            synchronized (object) {
                for (int i = 0; i < COUNT; i++) {
                    r--;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();

        add.join();
        sub.join();

        //结果始终为预期结果0
        System.out.println(r);
    }
}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-10 11:43:27  更:2022-05-10 11:45:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:03:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码