【JavaEE】 多线程的风险-线程安全

简介: 【JavaEE】 多线程的风险-线程安全

1. 观察线程不安全

开局我们先看一段代码:

public class testDemo {
    static class Counter{
        private int count;
        public void isAdd() {
            count++;
        }
        public int getCount() {
            return count;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                counter.isAdd();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                counter.isAdd();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

该代码是t1对Counter类中的count进程加5000操作,t2对Counter类中的count进程加5000操作,因为两个线程都是向count加5000,在我们的理解中count的值应该是10000,运行代码我们得到:


83fa572d1945493b8b9673cb8a3bc345.png


观察代码可以看出,虽然我们我们理论是将count共相加到10000,但是结果却告诉我们,count并没有相加到10000这个值,这是为啥呢?我们下面开始说。


2.线程安全的概念

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


3.线程不安全原因

回到2.1中,我们现在开始解释为啥没有加到10000?这是因为在isAdd()函数中,count++并不是原子性的,它这个操作本质上是三个cpu指令组成:


load,把内存中的数据读取到工作内存(寄存器或缓存)中

add,将工作内存中的值执行+1操作

save 把工作内存(寄存器或缓存)中的数据读取到内存中


因为在线程运行时,它们之间时并发执行的,我们无法确定它先运行那一步,也就是说,我们无法确定load、add、save这三个步骤谁先执行,因此我们就可能会出现以下情况。



可以看出它并不会仅仅按照我们想要的那要运行,那像情况1、2、3这样运行会由什么影响呢?

我们就拿情况1来举例子:

  • 先t1(load):将内存中的值读入到t1的工作内存当中;再t2(load):将内存中的值读入到t2的工作内存当中。



  • 然后执行t2(add):把t2的工作内存放入到内存当中;再执行t2(save):将t2工作内存中给的数据放入到内存当中。




  • 之后在执行t1(add):将t1工作内存中的数据进行+1操作;在执行t1(save):这里注意了,在此之前t2执行save时,内存中已经放入了1,这里我们仍放入的sh是1,因此,我们这样操作是导致我们count 的值不是10000原因之一。



上方线程不安全原因总结:由于当前这两个线程调度顺序是无序的,我们也不知道这两个线程自增过程中,到底经历了什么?有多少此"顺序执行",又有多少次“交错执行”,因此得到的结果是啥也就是变化的。

线程不安全原因可分为四种:

3.1抢占式执行

       这个就是我们上方举得例子,可谓是罪魁祸首,万恶之源了。

3.2修改操作,不是原子性的

原子性的概念:不可分割的最小单位称为原子性

一条java指令也有可能不是原子的,就像我们上方count++操作,它就是由3个cpu指令构成的。

3.3内存可见性,引发的线程不安全

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到

上方举得例子算是比较直白的了,初始情况下, 两个线程的工作内存内容一致,一旦线程1修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步,这个时候代码中就容易出现问题。

3.4指令重排序引发的线程不安全

为什么要进行指令重排序呢?这是因为,编译器觉得我们代码按照我们给定的顺序不是最高效了,因此就对我们的代码进行了指令重排序。

上方说的比较机械,举一个例子来帮助了解以下,我把我们 写的指令比喻成要完成的任务,映射到一个事务上,如买菜,我们需要购买1.黄瓜 、2.豆角、3.鸡肉、4.大米,如图:

按照我们代码的顺序应该就是先买黄瓜,之后去买豆角,再买鸡肉,最后买大米:



这样购买确实能满足购买需求,但是走的路程就多了。如果经过指令重排序后,它就是进来先购买大米,之后去买鸡肉,再买黄瓜,最后买豆角。这样就大大提升了效率。



3.5如何结果上方不安全的问题

两个字:加锁 !

在isAdd()方法前面加上synchronized关键字

public class testDemo {
    static class Counter{
        private int count;
        synchronized public void isAdd() {
            count++;
        }
        public int getCount() {
            return count;
        }
        public void setCount(int count) {
            this.count = count;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        int i=1;
        while (i<10) {
            Thread t1=new Thread(()->{
                for (int j = 0; j < 5000; j++) {
                    counter.isAdd();
                }
            });
            Thread t2=new Thread(()->{
                for (int j = 0; j < 5000; j++) {
                    counter.isAdd();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.print("第"+i+"次运行:");
            System.out.println(counter.getCount());
            i++;
            counter.setCount(0);
        }
    }
}


我们再次运行这个代码,代码结果就一直是10000了:



相关文章
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
260 0
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
201 1
|
2月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
226 1
|
5月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
6月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
450 5
|
10月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
407 20
|
10月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
【JavaEE】——多线程常用类
Callable的call方法,FutureTask类,ReentrantLock可重入锁和对比,Semaphore信号量(PV操作)CountDownLatch锁存器,
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
Java Go 调度
【JavaEE】——线程池大总结
线程数量问题解决方式,代码实现线程池,ThreadPoolExecutor(核心构造方法),参数的解释(面试:拒绝策略),Executors,工厂模式,工厂类

热门文章

最新文章