线程—线程安全(多图理解+代码)

1.线程不安全

在多线程环境下执行出现bug的代码,就是线程不安全的

线程不安全的几个原因:
1.抢占式执行
在系统调度的时候,具体调度哪个线程,是一个"全随机"的,所以我们不知道是哪个线程在执行,所以也无法预测走势。

2.多个线程修改同一个变量
多个线程修改同一个变量,比如说让count++,因为这个操作不是原子性的,所以会出现线程不安全问题
3.修改操作不是原子性的
解决方法就是把这个操作包装成一个原子操作,也就是上锁,执行完或者完全不执行。
4.内存可见性
JVM的代码优化可能会导致bug。
4.指令重排序

2.对代码加锁使其线程安全(加锁(synchronized关键字)+代码+画图)

现在有两个线程,每个线程都循环10000次,每次让count++,期望输入20000.

public class HomeWork9_06 {


    //public static int tmp = 0;
    public static void main(String[] args) throws InterruptedException {

        Operator operator = new Operator();//实例化对象
        Thread t  = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    operator.Add();
                    //tmp++;tmp也可以
                }
            }
        });

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000;i++){
                    operator.Add();
                    //tmp++;
                }
            }
        });

        t.start();
        t1.start();
        t.join();//不写join让main线程等待的话 t和t1线程还没有走完 main也会接着走下去
        t1.join();
        System.out.println(operator.count);
        //System.out.println(tmp);
    }

}
class Operator{//自定义一个类 实现++
    public int count;
    public  void Add(){
        count ++;
    }

}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
结果每次都没有达到20000

因为count++这个操作,具体可以分为三个指令,1.load 把内存中的数据给cpu的寄存器。 2.add cpu对其加法运算。3.save 把结果放回内存中。

对执行指令画图,下面只花2种,剩下还有很多种,可以自己花花看,
在这里插入图片描述

第一个的结果最终是3,因为两个线程加加, 第二个结果是2,但是也是两个线程加加。这就是因为线程不安全导致的,所以我们要把load,add,save这三个指令打包成一个原子(加锁),这样就不会有这种情况出现,加锁可以让这个加锁的这个线程执行完之前,另一个线程处于堵塞状态
在这里插入图片描述

加锁的关键字是synchronized,使用方法有两种
1.一种是修饰方法
2.一种是修饰代码块,将要加锁的代码逻辑放在synchronized修饰的代码块中

第一种修饰方法的写法
在这里插入图片描述
第二种修饰代码块的写法
在这里插入图片描述
这时候再看一下上锁以后 执行的代码,就不会有bug了

public class HomeWork9_06 {
    //public static int tmp = 0;
    public static void main(String[] args) throws InterruptedException {

        Operator operator = new Operator();
        Thread t  = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    operator.Add();
                    //tmp++;
                }
            }
        });

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000;i++){
                    operator.Add();
                    //tmp++;
                }
            }
        });

        t.start();
        t1.start();
        t.join();//不写join让main线程等待的话 t和t1线程还没有走完 main也会接着走下去
        t1.join();
        System.out.println(operator.count);
        //System.out.println(tmp);
    }

}
class Operator{
    public int count;
    public void Add(){//加锁关键字,synchronized
        synchronized (this) {
            count++;
        }
    }
}

在这里插入图片描述

3. 内存可见性(volatile关键字)

while(count == 0)
这个代码会执行两个指令,一个是load读取,一个cmp比较
load是在内存上读取到cpu中,cmp是在比较cpu中寄存器的值。前者比后面慢1w倍左右

读取一个数据的话,那么就要从内存读取到cpu寄存器中,因为从内存读取 这一操作上,相比与cpu是比较慢的,所以如果这个操作是会重复的话,因为JVM的代码优化机制,那么只会再内存中读取一次,就不会再从内存上读取了,就只会再cpu中的寄存器或者缓存中读取(更快)。
假如你对count++了一次,如果你要的比较的话,修改以后是在内存上存储的数据,这时已经修改的数据就和cpu上的数据对不上了,就会出现bug,(比较的是原来寄存器中的值,count还是没有变,因为没有从内存中读取到cpu寄存器里),所以我们不希望出现这种问题,我们就会使用volatile这个关键字,让它每次操作都是从内存上读取,不在cpu寄存器上读取。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

keild

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

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

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

打赏作者

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

抵扣说明:

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

余额充值