多线程之线程安全问题

首先我们先看一个线程不安全的例子:

class Counter {
    public int count = 0;
    public void add() {
        count++;
    }
    public int getCount() {
        return count;
    }
}
public class ThreadDemo10 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
​
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}
//结果本来应该是10000,结果却为63996
//这种多线程的实际结果与预期结果不一致就是bug。

上述代码出现的BUG的原因是:

1.线程之间的调度是不确定的(抢占式执行)

2.多个线程修改同一个变量

3.修改操作,不是原子的

例如count++ 操作就不是原子的,它被分为了三个操作~load,add,save

load,将count的值从内存上读到寄存器上

add,给寄存器里的count值加一

save,把寄存器里的值重新写入内存当中

如果是使用 = 直接赋值,那么就是一个原子的操作

那么什么叫原子操作?

某个操作,对应的cpu指令是多个,那么就不是原子的,反之,如果是一个cpu指令,那么就是原子的

还有两个导致线程不安全的原因:

内存可见性引起的线程不安全(和count++的例子无关)

指令重排序引起的线程不安全(和count++的例子无关)

解决不安全的问题

加锁,即保证”原子性“

一旦某个线程加锁了之后,其他线程也想加锁,就不能直接加上,就需要阻塞等待,一直等到拿到锁的线程释放锁了为止

Java中如何进行加锁

synchronized(同步):java中的一个关键字,可使用这个关键字来实现加锁效果

以下代码是对上述例子的修改:

public void add() {
    //这里的this表示的是锁对象
    这里的this就是counter对象
    synchronized (this) {
        count++;
    }
}
//相当于:
synchronized public void add() {  
    count++;
}

此处使用代码块的方式来表示。

进入synchronized修饰的代码块就会触发加锁

出了synchronized代码块就会触发解锁

加锁本质上是把并发的变成了串行的

join是将两个线程完整的进行串行,加锁是两个线程的某个小部分串行,大部分是并发的

使用this,就是谁调用的this就是给谁加锁

如果直接给普通方法使用synchronized修饰,此时就相当于以this为锁对象。

如果synchronized修饰静态方法,此时就不是给this加锁了,而是给类对象加锁

如果多个线程尝试对同一个锁对象加锁,那么就会出现锁竞争

针对不同对象加锁,就不会产生锁竞争

1>static修饰的方法是啥意思?

标志类方法。static在c语言中代表特殊的内存空间。而c++引入了面向对象,为了表示普通实例方法和类方法,必须引入一个关键字,但是为了兼容代码,所以就用static表示。实际上类属性/类方法 和 ‘静态’ 没有关系

2>类对象是啥?

javac会把Java文件编译成.class文件,.class文件要先把文件内容读取到内存中(类加载),类对象就可以来表示这个.class文件的内容(所有的详细信息)。

由于内存可见性引起的线程不安全

先看一个BUG:

public class ThreadTest3 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 0) {
​
            }
            System.out.println("t1线程结束循环");
        });
        Thread t2 = new Thread(() -> {
           Scanner in = new Scanner(System.in);
            System.out.println("请输入一个整数:");
           int flag = in.nextInt();
        });
        t1.start();
        t2.start();
    }
}
//预期结果是输入一个数之后t1线程结束,输出"t1线程结束循环"
//但是实际的结果是输入一个数之后,t1仍然在死循环状态

以上BUG就是因为内存可见性引起的

上述代码在执行的过程中,由于flag == 0,这个代码实际上是两个操作

一是load加载,将flag的值从内存加载到寄存器上

二是cmp比较,在寄存器里比较0和flag的值

但是由于load的开销很大,编译器就会把load优化掉

所以只有第一次编译器执行了load,后续代码都只执行cmp,不执行load

编译器优化:就是能够智能的调整我的代码执行逻辑,在保证程序结果不变的前提下,通过加减语句,语句变换,一系列操作让程序效率提高

但是编译器对于‘程序结果不变’在单线程下的判断十分准确,但是多线程就不一定了

所谓的内存可见性就是在多线程环境下,编译器优化产生了误判,从而引起了bug

处理方式:让编译器在这个场景下暂停优化。使用volatile关键字

语法:volatile public .....

volatile public static int flag = 0;

volatile不保证原子性!保证内存可见性

适用场景:必须是一个线程读一个线程写的情况

synchronized则是多个线程写

volatile还有一个效果:禁止指令重排序

指令重排序也是编译器优化的一个策略,调整代码执行顺序,让程序更高效。

例如  Student s = new Student();  就会导致指令重排序

new 对象的步骤可以分为三步:

1.申请内存空间

2.调用构造方法(初始化内存的数据)

3.把对象的引用赋值给s(内存地址的赋值)

需要注意的是发生指令重排序的可能性很小

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值