3.线程不安全原因-bite

本文深入探讨了Java中线程不安全的原因,包括线程抢占执行、非原子操作、内存可见性和指令重排序,并提供了相应的解决方案,如使用`synchronized`和`volatile`关键字来确保线程安全和内存可见性。
摘要由CSDN通过智能技术生成

线程不安全原因1:线程的抢占式执行

1.线程是抢占式执行,线程间的调度充满随机性.[线程不安全的万恶之源!]

class Counter {
	public int count;
    public void increase() {
        count++;
    }
}

public class Demo15 {
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        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 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();

        // 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
        System.out.println(counter.count);
    }
}

上面这一段代码的执行结果大概率并不是10 0000 ,只要多个线程操作的是同一个变量,就有问题

count++ 在cpu角度来看是3个指令:

1.load(把内存中的值加载到,cpu寄存器中)

2.add(将寄存器中的值加一)

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

因为抢占式执行,就导致两个线程同时执行这3个指令时,顺序上充满随机性…

情况1:
在这里插入图片描述

情况2:
在这里插入图片描述
情况3:…

如果类似,情况2出现多次,就会出现线程不安全

极端情况下:
如果所有的操作都是串行的, 此时结果就是10w(可能出现的,但是小概率事件)
如果所有的操作都是交错的,此时结果就是5w(可能出现的,也是小概率事件)

解决原因1的办法

将多条指令打包成一个原子操作
即:
给线程加锁,使用synchronized关键字

class Counter {
	public int count;
   	synchronized public void increase() {
        count++;
    }
}

public class Demo15 {
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        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 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();

        // 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
        System.out.println(counter.count);
    }
}

给方法直接加上 synchronized关键字.当一个线程进入方法后,就会自动加锁,离开方法后,就会自动解锁.

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BLOCKED状态)
阻塞会一直持续到,占用锁的线程把锁释放为止

线程不安全原因2:多个线程对同一个变量进行修改操作,3:针对变量的操作不是原子的~

线程不安全原因4:内存的可见性

public class Demo16 {
    private static int isQuit = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("循环结束! t 线程退出!");
        });
        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值: ");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕!");
    }
}

上述代码在运行一段时间后,输入1赋值给isQuit ,但 t 线程并没有结束运行

原因:
t 这个线程在循环读取这个变量,是先从内存中读取后,加载到寄存器,最后打印到控制台,但是从内存中读取,相比于直接从寄存器中读取是非常低效的.

因此在t1中频繁的读取这里的内存的值,就会非常低效!!
而且如果t2线程迟迟不修改, t1线程读到的值又始终是一样的值!!

因此, t1就有了一个大胆的想法:就会不再从内存读数据了,而是直接从寄存器里读(不执行load 了)一旦t1做出了这种大胆的假设, 此时万一t2修改了count值, t1就不能感知到了

这是java编译器进行优化的效果

解决原因4的办法:使用synchronized或volatile关键字

1.使用synchronized关键字:
synchronized 不光能保证指令的原子性,同时也能保证内存可见性,被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设,相当于手动禁用了编译器的优化

使用volatile关键字:
volatile和原子性无关,但是能够保证内存可见性.禁止编译器做出上述优化.编译器每次执行判定相等,都会重新从内存读取isQuit的值.

public class Demo16 {
    private volatile static int isQuit = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("循环结束! t 线程退出!");
        });
        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值: ");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕!");
    }
}

线程不安全原因5:指令重排序

指令重排序,也是编译器优化中的一种操作

咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓,编译器就会智能的调整这里代码的前后顺序从而提高程序的效率,保证逻辑不变的前提,再去调整顺序

如果代码是单线程的程序,编译器的判定一般都是很准,但是如果代码是多线程的,编译器也可能产生误判

解决原因5的办法:synchronized

synchronized:不光能保证原子性,同时还能保证内存可见性,同时还能禁止指令重排序

对象头

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值