多个线程执行相同的程序自己不会有安全问题,问题在于访问了相同的资源。资源能够是内存区域(变量,数组,对象),系统(数据库,web 服务)或文件等。实际上多个线程读取不会变化的资源也不会有问题,问题在于一到多个线程对资源进行写操做。html
如下给出累加器并发访问实例,多个线程对同一个累加器进行累加操做。分别打印出各个线程运行中数值先后变化,以及在主线程暂停2s后,给出最终的结果以及预期结果。java
done like this:web
public class ThreadSecurityProblem {
// 累加器
public static class Counter {
private int count = 0;
// 该方法将会产生竞态条件(临界区代码)
public void add(int val) {
int result = this.count + val;
System.out.println(Thread.currentThread().getName() + "-" + "before: " + this.count);
this.count = result;
System.out.println(Thread.currentThread().getName() + "-" + "after: " + this.count);
}
public int getCount() {
return this.count;
}
}
// 线程调度代码
public static class MyRunnable implements Runnable {
private Counter counter;
private int val;
MyRunnable(Counter counter, int val) {
this.counter = counter;
this.val = val;
}
@Override
public void run() {
counter.add(val);
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
// 开启5个线程,调用同一个累加器
IntStream.range(1, 6)
.forEach(i -> {
final MyRunnable myRunnable = new MyRunnable(counter, i);
new Thread(myRunnable, "thread-" + i)
.start();
});
Thread.sleep(2000L);
System.out.println(Thread.currentThread().getName() + "-final: " + counter.getCount());
// 预期值
int normalResult = IntStream.range(1, 6)
.sum();
System.out.println(Thread.currentThread().getName() + "-expected: " + normalResult);
}
}
复制代码
运行结果:数据库
thread-1-before: 0
thread-3-before: 0
thread-2-before: 0
thread-2-after: 2
thread-3-after: 3
thread-1-after: 1
thread-4-before: 2
thread-4-after: 6
thread-5-before: 6
thread-5-after: 11
main-final: 11
main-expected: 15数组
如结果所示,线程1/2/3前后取得this.count的初始值0,同时进行累加操做(顺序没法预估)。线程1/2/3中的最后一次累加赋值后this.count变为2,随后第4个线程开始累加赋值this.count变为6,最后第5个线程累加赋值this.count变为11.因此5个线程执行完毕后的结果为11,并不是预期的15.安全
线程1/2/3在执行Counter对象的add()方法时,在没有任何同步机制的状况下
,没法预估操做系统与JVM什么时候会切换线程运行。此时代码的运行轨迹相似下面的顺序:bash
从主存加载this.count的值放到各自的工做内存中
各自将工做内存中的值累加val
将各自工做内存中的值写回主存
复制代码
线程1/2/3交替状况模拟:多线程
this.count = 0;
线程1: 读取this.count到工做内存中,此时this.count为0
线程2: 读取this.count到工做内存中,此时this.count为0
线程3: 读取this.count到工做内存中,此时this.count为0
线程3: cpu将工做内存的值更新为3
线程2: cpu将工做内存的值更新为2
线程1: cpu将工做内存的值更新为1
线程3: 回写工做内存中的值到主存,此时主存中this.count为3
线程1: 回写工做内存中的值到主存,此时主存中this.count为1
线程2: 回写工做内存中的值到主存,此时主存中this.count为2
最终主存中的this.count被更新为2
复制代码
三个线程执行完毕后,this.count最后写回内存的值为最后一个线程的累加值(实例中为线程2,最后回写到内存的值为2)。并发
多线程访问顺序敏感的区域称为临界区,该区域代码会造成竞态条件。如实例ThreadSecurityProblem
中的Counter
对象的add()
方法。对于临界区的代码不加上适当的同步措施将会造成竞态条件,其运行结果彻底没法预估。ide
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial