线程安全问题详解

目录

一、多线程的安全问题

1、观察线程不安全

2、线程安全的概念

3、JMM 

 4、多线程的安全保证

(1)原子性 

(2)可见性 

(3)防止指令重排 

二、多线程安全问题的解决 

1、多线程对象不同时

2、synchronized关键字的使用

 3、synchronized关键字的说明

4、synchronized关键字的特性 

 (1)互斥性

(2)刷新内存        

(3)可重入

5、synchronized 使用示例

(1)直接修饰普通方法:锁的是Counter对象

         (2)修饰静态方法: 锁的 Synchronize_static 类的对象

(3)修饰代码块: 明确指定锁哪个对象

6、java标准库中的线程安全类

 7、volatile关键字

(1)volatile关键字可以保证共享变量的可见性

(2)volatile关键字修饰的变量相当于一个内存屏障 

 三、相关代码


一、多线程的安全问题

1、观察线程不安全

/**
 * 观察多线程的安全问题
 */
public class ThreadSafeorUnsafe {
    public static class Counter{
        int count = 0;

        void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //主线程走到此处,t1和t2都已经执行结束,理性状态是count和等于1万
        System.out.println("t1和t2已经执行完毕~~");
        System.out.println(counter.count);
    }
}

此时的代码跑好多次都不是1万,并且每次都不一样

2、线程安全的概念

线程安全就是代码的串行和并行执行的结果是完全一致的,就说明当前线程是安全的

多个线程串行执行的结果和并行执行的结果不同,这就是线程不安全

第一次的是并行,会出现结果并不是理性的一万

此时改为串行执行后,就是1万了

所谓的线程安全问题就是在多线程并发的场景下,实际运行结果和单线程下的预期运行结果不符所出现的问题

3、JMM 

JMM--java的内存模型,描述多线程场景下java的线程内存(CPU的高速缓存和寄存器和主内存的关系),注意这个和JVM的呢村区域划分不是一个概念。

 每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的全局变量)都是先从主内存将变量加载到自己的工作内存中,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写会主内存。

共享变量:类中的成员变量,静态变量,常量都属于共享变量,也就是在堆和方法区中存储的变量

 4、多线程的安全保证

一段代码要保证是线程安全的(多线程场景下和单线程场景下的运行结果保持一致),需要同时满足以下三个特性:

(1)原子性 

该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么就不执行,不会存在中间状态,这种操作就是一个原子性操作。

例如:

①对于int a=10 这个操来说,就是直接将变量赋值给a变量,要么赋值成功,要么就没有赋值

②对于a+=10这个操作来说,先要读取当前变量的值,再将a+10计算,最后将计算得出的值重新赋值给a变量(对应了三个原子性操作)

(2)可见性 

一个线程对共享变量的修改,能够及时被其他线程看到,这种特性就是可见性

两个小问题

①为啥需要整这么多内存?

实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法. 所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.

② 为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也 就是几千倍, 上万倍).

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果 只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问 内存了. 效率就大大提高了. 那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥?? 答案就是:CPU很贵 。

(3)防止指令重排 

代码的书写顺序不一定就是最终JVM或者CPU的执行顺序

一段代码是这样的:

1. 去前台取下 U 盘

2. 去教室写 10 分钟作业

3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问 题,可以少跑一次前台。这种叫做指令重排序

对于单线程来说,指令重排是不会出现太大的问题的,但是在多线程的场景下,就有可能因为指令重排导致错误,一般就是对象还没初始化就被别的线程给用了

二、多线程安全问题的解决 

1、多线程对象不同时

当多个线程使用了不同的线程对象时,多个线程之间就互不影响,因此就不会产生线程安全问题,是否会导致线程不安全,一定要注意,多个线程是否在操作同一个变量

2、synchronized关键字的使用

要确保一段代码的线程安全性,就需要同时满足原子性,可见性和防止指令重排现象。

synchronized这个关键字就可以同时满足这三个条件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值