多线程——线程安全问题

概念

线程安全的定义是复杂的,但是我们可以这样认为:
如果在多线程环境下代码运行的结果是符合我们要求的,也就是和单线程环境运行的结果一致,那么我们就认为这个程序是线程安全的。

先看一段线程不安全的代码:SUM是一个共享变量。期待结果应为200000,但结果并不是200000,这是由于此时线程不安全导致的。

//有一个共享变量,初始为0,启动20个线程,每个线程循环10000次,每次循环将共享变量++
public class UnsafeThread {
    private static int SUM;
    public static void main(String[] args){
        //for循环只是启动线程
        for (int i = 0; i < 20; i++){
            new Thread(()->{
                for (int j = 0; j<10000;j++){
                    SUM++;
                }
            }).start();
        }
        //所有子线程执行完毕后,打印SUM的值
        //idea中,运行方式:run运行写>2;debug运行写>1
        while ( Thread.activeCount() > 2) {
            //判断线程活跃数,>1表示当前有子线程在运行,所有让main让步
            Thread.yield();
            //yield()让步,让当前main线程由运行态转变为就绪态
        }
        System.out.println(SUM);

    }

}

线程不安全的原因:

1.原子性

原子性概念:一组操作中不能插入其他修改指令。

不保证原子性所带来的问题:

  • 在多个线程并行或并发执行指令中,如果在某个线程执行的过程中,被其他线程修改了内部的共享变量,导致前一个线程的运行结果发生了错误。
  • 可以通过加锁操作来保证代码的原子性(synchronized关键字)

一条JAVA语句,不一定就是原子的,也不一定只是一条指令
在这里插入图片描述
像上代码中的SUM++,其实就是三步操作完成的:
1.从内存中把数据读到CPU中
2.把SUM里面的数据进行++更新
3.再把数据写回到CPU中
看似是一条代码,但其实有3步,就很有可能存在线程不安全问题。

2.可见性

2.1线程的相关方法栈

  • 方法和栈有关
    方法进入时,会生成和方法相关的栈信息(入栈)
    方法退出时,会消除相关的栈信息(出栈)
    线程和方法栈的关系同上

jdk1.7内存模型
在这里插入图片描述
JDK1.8内存模型,1.8以后没有方法区,有元数据区
在这里插入图片描述
每个线程都具有自己的线程栈,每次方法调用都会生成对应的方法栈。
在这里插入图片描述
通过方法栈分析:下代码打印结果为A,B在这里插入图片描述

2.2可见性分析

为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到变化,这个就是不可见性带来的线程安全问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.代码顺序性

顺序性就是:
(1)一段代码的执行顺序
(2)指令执行的顺序

什么是重排序? 这是JVM,CPU的优化结果

java程序 —>java进程运行的过程:
javaC命令编译java文件为class字节码文件,进程运行以后,JVM会解释字节码为机器码,交给CPU执行指令。

保证线程安全的方法:

1.synchronized关键字——监视器锁

在最开始线程不安全的代码中加入synchronized关键字,令其线程安全
在这里插入图片描述
在这里插入图片描述
synchronized语法演示:

//演示synchronized关键字使用方式
public class SynchronizedDemo {
    //语法1.在静态方法上:静态同步方法
    public synchronized static void test1(){

    }
    //test1写法等于test1_2
    public static void test1_2(){
        synchronized (SynchronizedDemo.class){

        }
    }

    //语法2.在实例方法中:实例同步方法
    public void test2(){

    }
    //test2写法等于test2_2
    public void test2_2(){
        synchronized (this){

        }
    }
    //语法3.在代码块:同步代码快
    public static void main(String[] args){
        Object o = new Object();
       
        synchronized (o){

        }
    }

}
1.对象锁,每个对象都有一个对象头,包含锁
2.synchronized是获取对象锁操作;
  我要获取这个对象就要知道这个对象的对象锁,如果不知道对象锁,就会发生阻塞
      (1)申请对象锁
     (2)申请成功的话:
a:持有对象锁,并进入代码块中并执行代码,
b:退出代码块时,是需要归还对象锁的
c:退出代码块归还对象锁后,JVM会将刚才竞争失败的线程唤醒,重新竞争,循环(1)(2)(3)的逻辑
      (3)申请失败的话:竞争失败的线程进入一个同步队列中,全部发生阻塞
    同步队列:JVM管理synchronize竞争失败线程时,
将失败线程由运行态转变为阻塞态,然后将他们放到一个同步队列的数据结构中去,
当对象锁再次可用再次竞争时,再将之前所有失败线程唤醒(只会
 唤醒之前竞争该对象锁失败的线程,竞争别的对象锁的线程也会被存储进同步队列,但是不会被唤醒),(阻塞态转变为被唤醒状态)
     (4)对象:普通的java对象,类对象(JVM把类当做对象来管理)
     (5)注意事项:
 a:多个线程只有对同一个对象申请对象锁时,才会有同步互斥的作用
  ————同步互斥:同步代码块,在一个时间点,只有一个线程运行
  b:多个线程竞争的不同对象的对象锁,不会产生同步互斥作用
     (6)对于静态同步方法、实例同步方法都可以转换为同步代码快的写法。

基于上语法,针对代码执行顺序进行分析:
在这里插入图片描述
分析不同线程运行是否有同步互斥的效果:在这里插入图片描述
什么时候使用synchronized关键字?

  • 线程数量——根据代码,线程数量在动态变化时使用。
  • synchronized加锁时,执行同步代码块的时间和任务量越多,对性能的优化越好。

Volatile关键字:

分析加上volatile关键字不会存在线程安全问题:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值