多线程安全

文章讨论了线程安全的概念,指出在多线程环境下因随机调度可能导致的不安全问题,如抢占式执行、非原子操作和内存可见性等。通过示例展示了count变量在无同步机制下的并发问题,并引出了synchronized关键字来确保线程安全,实现原子性和内存可见性。同时提到了volatile关键字的作用,它能解决内存可见性和指令重排序问题,但不保证原子性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

什么是线程安全?

synchronized 关键字

线程不安全问题

1) 抢占式执行

2) 多线程修改

3) 变量不是原子

4) 内存可见性

5) 指令重排序

什么是线程安全?

在操作系统中,调度线程的时候,是随机的 (线程在系统中是抢占式执行)

正是因为这样的随机性,就可能导致程序的执行出现一些bug

因此我们认为如果程序在多线程的情况下,因为调度的随机性,引入了bug , 导致结果与我们的预料的结果相差甚远, 则认为代码是线程不安全的 ! 

相反如果这样的调度的随机性 , 没有带来bug, 则我们认为代码是线程安全的 ! 

引入一个例子: 在多线程的情况下 , 让count遍历自增10W次.

public class Demo19 {
    private static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        //让 main线程等待t1与t2线程执行完
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

 多次运行结果: 

通过多次重复运行 我们看到count的是并不是我们预期的10W, 而貌似是一个随机数.

上述就是我们说到的在多线程下, 由于线程调度的随机性, 导致了代码出现了bug

问题如下 :

 下面的过程是我们以为(期望)的执行顺序 : 

但多线程它是抢占式执行 , 大多数的执行顺序如下 : 

由于多线程的抢占式执行 , 上面的只是一部分交错执行顺序

例如 : 还有以下

 

 等等...还有许多交错执行顺序. 但是符合我们预期的也就只有两种情况 (要么是t1先执行完,

要么是t2先执行完)

synchronized 关键字

要解决以上的问题我们就要引出 synchronized 关键字!!!!!

加锁

class Count {
    private int m = 0;
    //进行加锁操作
    synchronized public void add() {
        m++;
    }
    public int getM() {
        return this.m;
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Count count = new Count();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.getM());
    }
}

 输出结果 : 

 

这就属于线程安全的一种 , 多个线程对同一个变量进行操作, 我们给它加上synchronized关键字就可以保证 add()这个方法在运行的时候只能同时被一个线程调用 , 同时也就保证了count++ 的原子性(原子性 (不可分割的最小单位称为原子)---> 原子性就是在cpu中只有一个指令).

对于加锁操作 , 如果有了锁竞争 , 那就肯定有线程处于线程阻塞状态

总结 : 对于多线程来说并发性越高, 速度越快,但是同时可能就会出现一些问题 (bug)

          加了锁之后, 并发程度就降低了, 此时数据就更靠谱了, 同时速度就慢了.

线程不安全问题

1) 抢占式执行

线程的抢占式执行 , 线程间的调度充满随机性 . (这个原因也就是线程不安全的核心)

2) 多线程修改

多线程对同一个变量进行修改操作 . (如果多个线程针对不同的变量进行修改, 那样没事,

如果多个线程对同一个变量进行读 , 那也没事).

3) 变量不是原子

例如上面的count++  它就是3条cpu指令,  不是原子性, 就导致了多线程抢占式执行引起的bug

通过加锁操作就是把好几个指令给打包成为了一个原子的操作.

4) 内存可见性

举个栗子 : 

public class Demo2 {
    private static boolean flag = false;
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (!flag) {
                //空
            }
        });
        t.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("main线程结束");
    }
}

我们预期的效果就是通过将flag = true , 从而终止 t 线程的循环.

我们发现在main 线程中改变flag , 貌似对 t 线程中的while循环好像不起作用. 

这就是内存可见性

解释:  

 对此我们给flag加上volatile关键字, 以禁止编译器进行优化,让编译器每次都从内存中取值.

 

5) 指令重排序

指令重排序,也是编译器优化的策略 , 调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变 .

谈到优化,都得保证,调整之后的结果和之前是不变的 , 单线程下容易保证. 但如果是多线程的话,就不好说了

解决指令重排序的 , 也是加volatile 关键字, 禁止指令重排序.

synchronized 补充 :

 综上 : volatile 不保证原子性, 但是可以禁止内存可见性, 禁止指令重排序

           volatile 适用的场景,是一个线程读,一个线程写的情况~

           synchronized 适用于多个写   锁竞争

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值