线程不安全
众所周知,多线程访问同一公共资源会带来线程的不安全,本文探讨一下这个问题的若干细节。
关于线程安全的基本问题
有关线程安全常涉及两个概念:
竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。 |
线程安全与竞态条件:线程安全的代码区不存在竞态条件,线程不安全的代码区(临界区)存在竞态条件。
为什么会出现线程安全问题?
道理很简单,多个线程同时访问(写操作)同一个公共资源必然带来问题,我们举2个例子:
例1:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B也对i加1,那么线程A,线程B提交后,i的结果为1,而不是我们期望的结果2。
例2:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B随后读取i,那么线程B读取的结果依然为0,而不是我们期望的结果1。这就是多线程并发导致的可见性问题。
如何解决线程不安全?
临界区进行同步,从而避免竞态条件。如:使用synchronized或JUC中的Lock对临界区加锁。给临界区加锁好比给公园的一个厕所加了一把锁,避免了人们共同进入厕所的问题,而必须是只有拿到钥匙的人才能进入(这个例子有点那个,但是我总会联想到这个例子)。
举例说明
既然多线程引发的安全问题是因为同时访问同一个公共资源导致的,相应的,如果多线程访问的不是公共资源也就不会发生线程安全问题。下面按资源是否公共举几个例子来说明问题。
公共资源:对象的成员变量
对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。
示例代码:
public class ThreadSafe_ { public static void main(String[] args) { Obj obj = new Obj(); MyRunnable task = new MyRunnable(obj); new Thread(task,"t1").start(); new Thread(task,"t2").start(); } } class Obj { StringBuilder noSafeBuilder = new StringBuilder();// 对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。 void add(String text){ noSafeBuilder.append(text); } } class MyRunnable implements Runnable{ private Obj obj = null; MyRunnable(Obj obj){ this.obj = obj; } @Override public void run() {// 临界区(线程不安全的代码区,因为多个线程可能同时修改成员变量noSafeBulider,所以会带来问题) this.obj.add(" "+Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString()); } }
打印结果:
t2=> t1 t2 |
当然,上面run方法因为线程不安全,无法保证线程的执行顺序,所以上面的代码运行多次可能带来多个结果:在这段代码中,我们期望的是,Obj对象的noSafeBuilder属性先追加1个线程名然后打印这个线程名,然后noSafeBuilder再追加第2个线程名并打印追加的2个线程名,但从打印结果看,第1个线程在执行完run方法后就已经打印了2个线程名——这就是线程不安全所带来的问题。
从上图可以知道,线程不安全会带来多少问题。
解决办法:
class MyRunnable implements Runnable{ private Obj obj = null; MyRunnable(Obj obj){ this.obj = obj; } private final Object lock = new Object(); @Override public void run() { synchronized (lock) {//临界区 加锁同步 this.obj.add(" "+Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString()); } } }
打印结果:
t1=> t1 t2=> t1 t2 |
由于t2可能先执行,所以上面代码的打印结果也可能为:
t2=> t2 t1=> t2 t1 |
非公共资源:局部基本类型变量
局部变量存储在线程自己的栈中,也就是说,局部变量永远也不会被多个线程共享。如:
public class ThreadTest { public static void main(String[]args){ MyThread share = new MyThread(); for (int i=0;i<50;i++){ new Thread(share,"线程"+i).start(); } } } class MyThread implements Runnable{ public void run() { int a =0; ++a; System.out.println(Thread.currentThread().getName()+":"+a); } }
无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据。
特殊资源:局部的对象引用
为什么这个是特殊示例呢?因为对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。这就意味着访问对象的引用的代码可能是线程安全的,也有可能是线程不安全的——这主要取决于:在某个方法中创建的对象是否会逃逸(即该对象不会被其它方法获得,也不会被非局部变量引用到),如果不会,那么这个方法就是线程安全的,反之就是不安全的。
实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。如:
public void method1(){ LocalObject localObject = new LocalObject(); localObject.callMethod(); method2(localObject); } public void method2(LocalObject localObject){ localObject.setValue("value"); }