线程安全性:num++操做为何也会出问题?

  线程的安全性多是很是复杂的,在没有充足同步的状况下,因为多个线程中的操做执行顺序是不可预测的,甚至会产生奇怪的结果(非预期的)。下面的Tools工具类的plus方法会使计数加一,为了方便,这里的num和plus()都是static的:java

public class Tools {

    private static int num = 0;

    public  static int plus() {
        num++;
        return num;
    }

}

  咱们再编写一个任务,调用这个plus()方法并输出计数:安全

public class Task implements Runnable {

    @Override
    public void run(){
        int num = Tools.plus();
        System.out.println(num);
    }
}

  最后建立10个线程,驱动任务:多线程

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Task()).start();
        }
    }
}

  输出:并发

2
4
3
1
5
6
7
8
9
10

  看起来一切正常,10个线程并发地执行,获得了0累加10次的结果。咱们把10次改成10000次:ide

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(new Task()).start();
        }
    }
}

  输出:工具

...
9994
9995
9996
9997
9998

  在个人电脑上,这个程序只能偶尔输出10000,为何?spa

  问题在于,若是执行的时机不对,那么两个线程会在调用plus()方法时获得相同的值,num++看上去是单个操做,但事实上包含三个操做:读取num,将num加一,将计算结果写入num。因为运行时可能多个线程之间的操做交替执行,所以这多个线程可能会同时执行读操做,从而使它们获得相同的值,并将这个值加1,结果就是,在不一样的线程调用中返回了相同的数值。线程

A线程:num=9→→→9+1=10→→→num=10
B线程:→→→→num=9→→→9+1=10→→→num=10

  若是把这个操做换一种写法,会看的更清晰,num加一后赋值给一个临时变量tmp,并睡眠一秒,最后将tmp赋值给num:设计

public class Tools {

    private static int num = 0;

    public static int plus() {
        int tmp = num + 1;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = tmp;
        return num;
    }

}

  此次咱们启动两个线程就能看出问题:code

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Task()).start();
        }
    }
}

  启动程序后,控制台1s后输出:

1
1
A线程:num=0→→→0+1=1→→→num=1
B线程:→num=0→→→0+1=1→→→num=1

  上面的例子是一种常见的并发安全问题,称为竞态条件(Race Condition),在多线程环境下,plus()是否会返回惟一的值,取决于运行时对线程中操做的交替执行方式,这并非咱们但愿看到的状况。

  因为多个线程要共享相同的内存地址空间,而且是并发运行,所以它们可能会访问或修改其余线程正在使用的变量,线程会因为没法预料的数据变化而发生错误。要使多线程程序的行为能够预测,必须对共享变量的访问操做进行协同,这样才不会在线程之间发生彼此干扰。幸运的是,java提供了各类同步机制来协同这种访问。

  将plus()修改成一个同步方法,同一时间只有一个线程能够进入该方法,能够修复错误:

public class Tools {

    private static int num = 0;

    public synchronized static int plus() {
        int tmp = num + 1;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = tmp;
        return num;
    }

}

  控制台前后输出:

1
2

  这时若是将plus()方法改成num++,驱动10000个线程去执行,也能够保证每次都能输出到10000了。

  那么如何设计一个线程安全的类避免出现此类问题呢?

  若是咱们写了这样一个Tools工具类,没有考虑并发的状况,其余调用者可能就会在多线程调用plus()方法中产生问题,咱们也不但愿在多线程调用其余开发者编写的类时产生和单线程调用不同的结果,咱们但愿不管单线程仍是多线程调用一个类时,无须使用额外的同步,这个类即能表现出正确的行为,这样的类是线程安全的。

  观察上面程序,咱们在对num变量进行操做时出了问题,首先,num变量具备两个特色:共享的(Shared)和可变的(Mutable)。“共享”意味着变量能够由多个线程同时访问,而“可变”意味着变量的值在其生命周期内能够发生变化;其次,看一下咱们对num的操做,读取num,将num加一,将计算结果写入num,这是一个“读取-修改-写入”的操做,而且其结果依赖于以前的状态。这是一种最多见的竞态条件——“先检查后执行(Check-Then-Act)”操做,首先观察某个条件为真(num为0),而后根据观察结果采起相应的动做(将num加1),可是,当咱们采起相应动做的时候,系统的状态就可能发生变化,观察结果可能变得无效(另外一个线程在这期间将num加1),这样的例子还有不少,好比观察某路径不存在文件夹X,线程A开始建立文件夹X,可是当线程A开始建立文件夹X的时候,它先前观察的结果就失效了,可能会有另外一个线程B在这期间建立了文件夹X,这样问题就出现了。

  所以,咱们能够从两个方面来考虑设计线程安全的类

  1、状态变量方面:(对象的状态是指存储在实例变量与静态域成员中的数据,还可能包括其余依赖对象的域。例如,某HashMap的状态不只存储在HashMap自己,还存储在许多Map.Entry对象中。)多线程访问同一个可变的状态变量没有使用合适的同步会出现问题,所以:

  1.不在线程之间共享该状态变量(即每一个线程都有独自的状态变量)

  2.将状态变量修改成不可变的变量

  3.在访问状态变量时使用同步

  2、操做方面:在某个线程须要复合操做修改状态变量时,经过某种方式防止其它线程使用这个变量,从而确保其它线程只能在修改操做完成以前或者以后读取和修改状态,而不是在修改状态的过程当中。

相关文章
相关标签/搜索