线程安全的问题以及解决方案

简介: 线程安全的问题以及解决方案

线程安全

线程安全的定义

线程安全:某个代码无论是在单线程上运行还是在多线程上运行,都不会产生bug.

线程不安全:单线程上运行正常,多线程上运行会产生bug.

观察线程不安全

看看下面的代码:

public class ThreadTest1 {
    public static int count = 0;
 
    public static void main(String[] args)throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t1");
 
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
 
        System.out.println(count);
    }
}

按照常理来讲,运行的结果应该是20000,但让我们来看看实际的运行结果:

显然结果与我们预期的不一致,但为什么会出现这种问题呢?

让我们来看一下线程不安全的原因:

线程不安全的原因

重点:线程调度是随机的

1.根本原因:操作系统上线程是"抢占式执行",而且是"随机调度"的,执行顺序会有很多变数.(罪魁祸首)

2.代码结构:多个线程同时修改一个变量(1. 一个线程修改一个变量(没事) 2.多个线程同时读取一个变量(没事) 3.多个线程同时修改不同的变量(没事))

3.直接原因:上述线程的修改操作本身不是"原子的",比如count++这条语句,它本身包含多个cpu指令(这个例子后面会详细讲).执行了一半可能会调度走.

4.内存可见性问题(例子里的代码还没有),后面的文章会讲.

5.指令重排序问题

分析例子代码中的问题

这个问题就主要出现在count++这条语句中.它本身包含这些cpu指令:LOAD,ADD,SAVE

让我们回顾一下这几条指令的含义:

(1)load:从内存中读取数据到cpu的寄存器

(2)add:把寄存器中的值+1,

(3)save:把寄存器的值写回到内存中.

因此count++这条语句的执行的流程如下:

这是一个count++的执行流程,但是在多进程程序中,这三条指令一定会连贯执行吗(规范的按照一个load->add->save执行)? ,留着这个问题,来看看后面的内容:

修改共享数据

在例子中,显然是符合多个线程修改同一个变量的.

上面线程不安全的代码中,涉及到多个线程对count变量进行的修改.

此时这个count是一个多线程都能访问到的共享数据,因此t1和t2都可以对count进行修改.

原子性

什么是原子性

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

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其它的线程插进来了,如果这个操作被打断了,结果可能就是错误的.

这点也和线程的抢占式调度密切相关,如果线程不是"抢占"的,就算没有原子性,也问题不大.

综合以上,我们可以得到引起问题的原因:共享数据的修改以及数据并非原子的.

通过下面这个图就可以看出来:

等等还有很多种执行顺序(无数种).

比如图二:由于t2的load抢占在t1的add前执行,因此导入时count值都一样,那么执行的结果最后就是+1,而不是理想中的各自线程都给count+1,最后执行完两个就是+2了.那么有没有一种情况执行结果是正常的,当然有:

类似这种每个线程执行时,三条指令都是在一块的,这种运行是正确的,那么有没有一种方法能按照这样运行呢?有的.

只要将count++操作上锁,使得这三条一起指令执行完之后,才会执行下一个操作.

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

解决上面的问题
public class ThreadTest {
    public static final Object locker = new Object();
    public static int count = 0;
 
    public static void main(String[] args) throws InterruptedException {
 
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                //进入大括号会上锁
                synchronized (locker) {
                    count++;
                }//出大括号会解锁
            }
        });
 
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
 
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里用到的机制(synchronized)后面的文章会解释.

可见性

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

Java内存模型(JMM) :Java虚拟机规范中定义了java内存模型.

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

线程之间的共享变量存在主内存(可以看作为上面的内存)

每一个线程都有自己的工作内存(并不是真正的内存,可以看作为上面的cpu寄存器(也有可能是cpu缓存,不过都差不多))

当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存种读取数据.

当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存.

(1)初始情况下,两个线程的工作内存一致

(2)一旦线程1修改了a的值,此时主内存并不一定可以及时同步过来(是在寄存器中改动的,因为寄存器比较快)

此时引入了一个问题:

为什么要在主内存和工作内存种麻烦的拷来拷去?

因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了几千至上万倍)


比如某个代码种要连续10次读取某个变量的值,如果10次都从内存中度,速度是很慢的.但如果只是第一次从内存中读,读到的结果缓存到CPU某个寄存器中,那么后面9次就不需要从内存中读了,效率就大大提高了.

那么问题又来了,既然寄存器速度这么快,还要内存干嘛?

贵!

后面我们将用更详细的方法解决线程安全问题,敬请期待.

相关文章
|
20天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
1月前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
6月前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1014 1
|
2月前
|
JavaScript 前端开发 安全
轻松上手Web Worker:多线程解决方案的使用方法与实战指南
轻松上手Web Worker:多线程解决方案的使用方法与实战指南
53 0
|
6月前
|
安全 Java 调度
多线程编程的挑战与解决方案
多线程编程的挑战与解决方案
|
5月前
|
缓存 安全 Java
Java中的线程安全问题及解决方案
Java中的线程安全问题及解决方案
|
6月前
|
安全 Java
Java多线程编程实践中的常见问题与解决方案
Java多线程编程实践中的常见问题与解决方案
|
5月前
|
安全 Java 调度
多线程编程的挑战与解决方案
多线程编程的挑战与解决方案
|
5月前
|
安全 Java 开发者
Java多线程编程实践中的常见问题与解决方案
Java多线程编程实践中的常见问题与解决方案
|
5月前
|
设计模式 安全 Java
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
125 0