文章2:线程安全和synchronized

65 阅读4分钟

什么是线程安全?

当多个线程修改同一个共享变量的时候,如果对该变量修改的结果不能确定,那么称:该变量线程不安全

比如这个例子:

public class Task implements Runnable {

    // 共享变量
    static int num = 0;

    @Override
    public void run() {
        // 对共享变量的修改
        for (int i = 0; i < 100_000; i++) {
            num++;
        }
    }

    public static void main(String[] args) {
        final Thread t1 = new Thread(new Task());
        final Thread t2 = new Thread(new Task());
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) { }
        System.out.println("num = " + num);
    }
}

显然最后的打印结果不会跟预期结果一样,两个线程同时对一个变量 num 做十万次 ++ 操作,最后的结果 num 应该是等于二十万才对。

但是上面的程序最终结果都是在 [100_000, 200_000] 这个区间内的,显然与预期结果不符。

为什么不符合预期,其实原因很简单:

上面代码 num++ 的这行语句在执行的时候是有三个步骤的,如下:

  1. 将 num 的值读到缓存中
  2. 将 num 的值加1
  3. 将 num 的值写回内存

上述例子中,两个线程同时都在执行 num++ 的操作,也就是同时在执行这三个步骤,如果 t1 和 t2 同时都执行了第1步,拿到 num 的值为1;再往下执行第二步,t1 和 t2 都把 num 的值从1加到2;最后 t1 和 t2 再把 num 的值写回内存,此时 num 的值为2

但也有不出错的情况,那最后 num 的结果就是3,num 最后的值具体是2还是3不好确定,所以称变量 num 线程不安全。

num 的范围:

既然 num 变量线程不安全,但也总是会有一个范围,这个范围由执行出错的“次数”维护。

最大值:如果每次执行都没出错,结果都是确定的两个线程都成功加1,那结果就是 num = 200,000,也就是上述这个例子中num的最大值。

最小值:相反如果每次执行都出错了,两个线程的执行结果总是只保存了一个下来,那结果就是 num = 100,000,也就是上述这个例子中num的最小值。

使用synchronized关键字保证线程安全

一句话说明 synchronized 关键字的作用:

保证在 同一时刻 最多只有 一个 线程执行该段代码,以达到保证 并发安全 的效果。

本质上是一个锁的作用,用来保护需要同步执行的代码,只有拿到锁的线程才能执行被保护的代码。如果使用 synchronized 来保护 num++ 这行代码 ,那就可以保证 num 变量的线程安全。因为这样同一个时刻就只有一个线程能执行 num++ 了。

使用方式

synchronized 既然是一把锁,那肯定就需要一个对象来充当这个锁对象,这个对象可以是类对象或者是实例对象。由此又衍生出了两种使用方式:普通方法/代码块加锁;静态方法/代码块锁。

由于 Java 中的类对象是全局唯一的对象,而实例对象则可以存在多个。于是,类对象锁是全局唯一的,被类对象锁保护的代码在同一时刻肯定只有唯一一个线程能执行该代码;实例对象锁在全局可以同时存在多个,被实例对象锁保护的代码在同一时刻可能会被多个线程同时执行,但这些线程肯定持有不同的实例对象锁。

普通方法/代码块加锁

普通方法加锁:

// 将 synchronized 关键字添加在方法签名上,以 this 对象为锁
@Override
public synchronized void run() {
    for (int i = 0; i < 100; i++) {
        num++;
    }
}

普通代码块加锁:

@Override
public void run() {
    for (int i = 0; i < 100; i++) {
        synchronized (this) {
            num++;
        }
    }
}

静态方法/代码块锁

静态方法加锁:

// 将 synchronized 关键字添加在静态方法签名上,以 Task.class 类对象为锁
public static synchronized void incr() {
    for (int i = 0; i < 100; i++) {
        num++;
    }
}

静态代码块加锁:

@Override
public synchronized void run() {
    for (int i = 0; i < 100; i++) {
        synchronized (Task.class) {
            num++;
        }
    }
}

使用方式小结

synchronized 关键字就是将指定的对象作为锁去保护某一段代码的机制,这个指定的对象可以是普通对象也可以是类对象。根据指定的对象的功能不同可以分为对象锁和类锁,对象锁和类锁的区别就是在内存中,类锁是只有一个的,而对象锁可以创建多个,从而在某种特定的条件下可能在同一个时间段内多个线程都在执行同一个方法。