线程安全问题


一、线程不安全的原因

对于多个线程,操作同一个共享数据(堆里边的对象,方法区中的数据,如静态变量)

  1. 如果都是读操作:没有赋值操作,只是获取值——没有安全问题
  2. 如果一个读,一个写
  3. 多个写(至少一个线程写操作,就会存在线程安全问题)

产生线程安全的原因:

  1. 原子性: 表示一组操作(可能是一行或多行代码),是不可拆分的最小执行单位,就表示这组操作是原子性的

某个线程对共享变量的多次操作,中间存在并发并行执行其他线程的对同一个共享变量的操作,就不具有原子性
一段代码具有原子性,类似打扫房间,没有打扫完,就不能进入其他的人

例子1:
在这里插入图片描述
例子2:

public class 线程安全问题_不安全 {
    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        //多个线程使用同样的共享变量
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //把num循环++10000次
                for(int i=0;i<10000;i++){
                    num++;
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //把num循环++10000次
                for(int i=0;i<10000;i++){
                    num--;
                }
            }
        });
        //++10000次
        t1.start();
        //--10000次
        t2.start();
        //让t1,t2线程执行完,按逻辑来说,num应该就是0
        t1.join();
        t2.join();
        System.out.println(num);
    }
}

实际结果并不为0,且每次运行结果都不一样
图解:
在这里插入图片描述
注意:一行Java代码,可能也不具有原子性
Java代码是一行,编译为class字节码,或由jvm把字节码翻译为机器码后,还是不是一行,不一定
比较典型的:
++,- -操作

线程作为系统调度cpu执行的最小单位
cpu执行时,使用内存数据还不够快
采取的方式,是把内存的数据加载到cpu寄存器中,再执行

  1. 可见性: 多个进程之间,使用自己的工作内存(cpu寄存器)来执行操作,互相之间是不可见的
  2. 有序性: jvm执行(翻译)字节码指令,cpu执行机器码指令,都可能对多行指令进行重排序优化执行效率

重排序:
1.有依赖关系的多行指令之间,不会重排序为不符合预期结果的顺序
2.单纯从一个线程视角看,人肉眼感知都是有序的(但实际存在重排序)
从多个线程视角看,代码都是乱序的(单个线程重排序,多个线程并发并行执行)

二、如何解决线程不安全的问题

涉及多个线程操作共享变量

1.synchronized关键字

语法:
(1)也叫同步代码块
代码块:synchronized(对象){
 …
}

synchronized英文翻译过来是同步,前端ajax学过一个名词:异步(设置一个回调函数,等条件满足,由其他代码来执行回调函数)
同步: 使用对象来进行加锁,多个线程需要先申请锁(synchronized自动申请),代码块结束(自动释放锁):多个线程只能有一个线程获取到同一个对象的锁;注意:多个对象的锁,就可以是多个线程获取到

(2)实例方法:(也叫同步实例方法)

public class A{
    public synchronized void doSomething(){
   		...
	}

等同于

public void doSomething(){
	synchronized(this){
		...
	}
}

A a=new A()
多个线程中,调用a.doSomething()
this就是a同一个对象,就可以让多个线程同步

A a1=new A()
A a2=new A()
线程1中:a1.doSomething():用a1加锁
线程2中:a2.doSomething():用a2加锁
不能同步,可以并发并行执行
(3)静态方法:(也叫同步静态方法)

public class A{
	public static synchronized void doSomething(){
	...
	}
}

等同于

public class A{
	public static void doSomething(){
		synchronized(A.class){
		...
		}
	}
}

多个线程调用a.doSomething就是同步的

加锁的对象,就是A这个类的类对象
JVM类加载:把一个class文件加载到java进程的内存,就是类加载,要做的事情:
(1)class文件的代码加载到方法区
(2)在堆中生成一个类对象
Class< A > aClass = A.class
没有采取其他类加载手段的时候,多个线程中,A.class获取到的类对象都是同一个类对象

对于synchronized,底层原理是:

基于对象头加锁(对象有对象头这块区域,其中有一个状态的字段,标识是否加锁)的方式,一个对象,在一个时间,只能有一个线程获取到该对象的锁

关于对象头加锁:是JVM基于对象头加锁:monitor lock监视器锁
本质上会使用到操作系统mutex lock锁来实现(还有一些底层原理会在之后提到)

注意:synchronized加锁,是对对象头加锁,不是对代码来加锁

多个线程执行同一段synchronized的代码,是否具有同步的特性,要取决于是否是同一个对象

synchronized作用:
前提:多个线程申请同一个对象锁

  1. 互斥:同一个时间,只能有一个线程执行同步代码
  2. 刷新主存
  3. 可重入性:同一个线程可以多次申请成功同一个对象锁

图解:在这里插入图片描述

2.volatile关键字

语法: 修饰一个变量的
作用: 保证可见性、有序性
注意: 不保证原子性——n++,n- -,所以即使使用volatile修饰n,也是线程不安全的
使用场景:

  1. 读操作:读操作本身是原子性,所以使用volatile这行语句就是线程安全
  2. 写操作:赋值操作是一个常量值,也可以保证线程安全(写到主存)。n++——n=n+1(这个操作不是原子性的,先读,修改,写回)

如:我们之前自定义标志位来中断线程,其实应该加上volatile修饰,才是线程安全(一个读,一个写常量值)

三、总结

关于使用多线程,需要考虑:

  1. 提高效率——多线程作用就是充分利用cpu资源,提高任务的执行效率
  2. 线程安全——多线程程序的底线

实际设计多线程代码:在满足多线程安全的前提下,尽可能的提高任务效率
所以,常见的设计方式:

单例设计模式

某个类的对象,只创建一个对象,所有使用的地方,都使用同一个对象
典型的:DataSource(数据库连接池)对一个数据库来说,只需要一个连接池对象(里边包含了多个数据库连接对象)

写法:
(1)饿汉式: 静态变量=new 对象 (类加载时,就创建)
(2)懒汉式: 静态变量=null 获取对象的方法中,if(静态变量=null) 静态变量=new 对象 (类加载时不创建,而是第一次调用方法获取对象时,创建)用于单线程,多线程中存在线程安全问题
(3)懒汉式(线程安全但效率低): synchronized修饰方法
(4)双重校验锁的写法:
在这里插入图片描述

语言表述:volatile修饰,synchronized加锁时,前后使用if判断
第一个情况:instance=null,多线程同时调用getInstance

volatile的原理:
volatile保证可见性,有序性;在Java层面看,volatile是无锁操作的(在Java层面,多个线程对volatile修饰的变量进行读可以并发并行执行,和无锁的执行效率差不多)

volatile修饰的变量,cpu中,使用了缓存一致性协议来保证读取的都是最新的主存数据
如果其他地方修改了这个volatile修饰的变量,就会把cpu缓存中的变量置为无效,要操作这个变量就要从主存重新读取

假设volatile可以保证可见性,但不保证有序性,是否双重校验锁写法会有问题(了解)?

首先,变量 = new 对象();也会分为三条指令
(1)分配对象的内存空间
(2)实例化对象——存放在内存空间中(new操作,执行构造方法)
(3)赋值给变量(=)

在这个过程中,可能重排序为132:
分配内存空间,对象还没有实例化完成,就可以赋值给instance
引用指向这块内存区域,此时,对象还没有实例化完成,重排序只保证本线程调用对象.属性/实例方法时,是实例化完成,不保证其他线程
此时instance指向了一块未实例化完成的内存
线程B执行第一个if判断
instance==null不满足,执行return
线程B拿到instance未完成实例化的对象,使用它的属性/方法,就会出错

使用volatile,保证有序性之后:
确保线程A,不管123啥顺序,但保证线程B使用这个变量的时候,线程A的123(这里不管是不是重排序),但必须全部完成,再指向线程B的使用instance的代码(第1行)
保证多个线程,指令之间,有一定的顺序
cpu里边,基于volatile的变量操作,是有加锁机制(线程A,123全部执行完,写回主存,再执行其他线程对该变量的操作)

所以,常见的设计方式:

  1. 加锁细粒度化(加锁的代码行少一点,让部分代码行可以并发并行执行)
  2. 考虑线程安全:没有共享变量操作的代码,没有多线程安全问题;共享变量的读,使用volatile就行;共享变量的写,加锁
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dhdhdhdhg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值