线程的安全性可能是非常复杂的,在没有充足同步的情况下,由于多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果(非预期的)。下面的Tools工具类的plus方法会使计数加一,为了方便,这里的num和plus()都是static的:
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次:
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,为什么?
问题在于,如果执行的时机不对,那么两个线程会在调用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; } }
这次我们启动两个线程就能看出问题:
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了。
除了安全性,多线程程序还有可能出现活跃性问题(死锁等),性能问题(上下文切换等),这些问题我们后续再详细说明。