非线程安全其实是在多个线程对同一个对象实例的变量进行并发访问的时候发生,产生的后果就是脏读,也就是取到的数据是修改过的。而线程安全就是获得的实例变量的值是经过同步处理的,从而不会出现脏读现象。
1.1.1、实例变量非线程安全
如果我们把多个线程并发访问的实例变量转化成方法里面的局部变量,那么就不会产生线程不安全的情况了。因为每个线程拿到的变量都是该线程自己拥有,类似于ThreadLocal类的思想。下面这个例子将变量变为局部变量从而实现线程安全。
1 package soarhu; 2 import java.util.concurrent.TimeUnit; 3 class ObjectMonitor{ 4 void add(String name){ 5 try { 6 int num = 0; //方法里面的局部变量,不会出现并发竞争的情况。 7 if(name.equals("a")){ 8 num = 100; 9 System.out.println("a set finish"); 10 TimeUnit.MILLISECONDS.sleep(2); 11 }else{ 12 num = 200; 13 System.out.println("b set finish"); 14 } 15 System.out.println("user: "+name+" num: "+num); 16 }catch (InterruptedException e){ 17 e.printStackTrace(); 18 } 19 } 20 } 21 22 class ThreadA extends Thread{ 23 private ObjectMonitor monitor ; 24 25 ThreadA(ObjectMonitor monitor) { 26 super(); 27 this.monitor = monitor; 28 } 29 30 @Override 31 public void run() { 32 monitor.add("a"); 33 } 34 } 35 36 class ThreadB extends Thread{ 37 private ObjectMonitor monitor ; 38 39 ThreadB(ObjectMonitor monitor) { 40 super(); 41 this.monitor = monitor; 42 } 43 44 @Override 45 public void run() { 46 monitor.add("B"); 47 } 48 } 49 public class Test { 50 public static void main(String[] args) throws InterruptedException { 51 ObjectMonitor objectMonitor = new ObjectMonitor(); 52 ThreadA threadA = new ThreadA(objectMonitor); 53 ThreadB threadB = new ThreadB(objectMonitor); 54 threadA.start(); 55 threadB.start(); 56 } 57 }
输出结果:
a set finish
b set finish
user: B num: 200
user: a num: 100
如果将第6行的代码移到方法外面,则会出现线程安全的问题,改完后,运行结果:
a set finish
b set finish
user: B num: 200
user: a num: 200
因为此时add()方法访问的变量num为类成员变量,而且add方法没有进行同步,那么a,b两个线程就会同时进入add()方法对num变量进行修改。当a线程把num设置成100后。执行打印语句之前,这时候b线程进入了add()方法,设置num的值为200,那么最终就出现了脏读。解决方法是在add()方法上加入synchronized。使其成为同步方法。这样就会同步访问该方法,从而避免线程不安全的问题了。加入后程序的执行结果为:
a set finish
user: a num: 100
b set finish
user: B num: 200
1.1.2、多个对象多个锁
稍微修改一下上面方法,使其a,b两个线程拥有不同的锁对象,那么add()同步方法的同步意义对现在a,b两个线程没有意义了。因为锁对象不同。
package soarhu; import java.util.concurrent.TimeUnit; class ObjectMonitor{ int num = 0; synchronized void add(String name){ try { if(name.equals("a")){ num = 100; System.out.println("a set finish"); TimeUnit.MILLISECONDS.sleep(2); }else{ num = 200; System.out.println("b set finish"); } System.out.println("user: "+name+" num: "+num); }catch (InterruptedException e){ e.printStackTrace(); } } } class ThreadA extends Thread{ private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) { super(); this.monitor = monitor; } @Override public void run() { monitor.add("a"); } } class ThreadB extends Thread{ private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) { super(); this.monitor = monitor; } @Override public void run() { monitor.add("B"); } } public class Test { public static void main(String[] args) throws InterruptedException { ObjectMonitor objectMonitor = new ObjectMonitor(); ObjectMonitor objectMonitor2 = new ObjectMonitor(); ThreadA threadA = new ThreadA(objectMonitor); ThreadB threadB = new ThreadB(objectMonitor2);//使用不同的同步对象 threadA.start(); threadB.start(); } }
输出结果:
a set finish
b set finish
user: B num: 200
user: a num: 100
结果正确,且执行方式不是同步方式,而是异步方式。那么如何使这个方式仍然按照同步的方式进行访问呢?很简单,用同步代码块
修改部分代码,如下:
class ObjectMonitor{ int num = 0; void add(String name){ synchronized (Integer.TYPE) { //这里使用同步代码块,这里的监视器jvm里只有一份,故可以起到监视器同步的作用 try { if (name.equals("a")) { num = 100; System.out.println("a set finish"); TimeUnit.MILLISECONDS.sleep(2); } else { num = 200; System.out.println("b set finish"); } System.out.println("user: " + name + " num: " + num); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1.1.3、synchronized方法与锁对象
有上面的代码可以知道,synchronized方法的锁监视器对象就是该方法所属的对象。下面看看对多个方法的调用。
package soarhu; import java.util.concurrent.TimeUnit; class ObjectMonitor{ synchronized void a(String name){ try { System.out.println("a method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis()); TimeUnit.MILLISECONDS.sleep(5); System.out.println("a end time: " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } void b(String name){ try { System.out.println("b method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis()); TimeUnit.MILLISECONDS.sleep(5); System.out.println("b end time: " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadA extends Thread{ private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) { super(); this.monitor = monitor; } @Override public void run() { monitor.a("a"); } } class ThreadB extends Thread{ private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) { super(); this.monitor = monitor; } @Override public void run() { monitor.b("B"); } } public class Test { public static void main(String[] args) throws InterruptedException { ObjectMonitor objectMonitor = new ObjectMonitor(); ThreadA threadA = new ThreadA(objectMonitor); ThreadB threadB = new ThreadB(objectMonitor); threadA.start(); threadB.start(); } }
输出结果:
a method start: Thread-0 begin time 1492413285921
b method start: Thread-1 begin time 1492413285921
b end time: 1492413285927
a end time: 1492413285927
可以看到a,b两个线程同步访问。虽然a线程拥有objectMonitor的锁,但是b方法并没有加同步块,所以b线程任然可以访问该监视器对象的其他非同步方法。
在b()方法加上同步关键字后,
synchronized void b(String name){ try { System.out.println("b method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis()); TimeUnit.MILLISECONDS.sleep(5); System.out.println("b end time: " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } }
输出结果:
a method start: Thread-0 begin time 1492413504482
a end time: 1492413504489
b method start: Thread-1 begin time 1492413504489
b end time: 1492413504494
已经同步了,那么我们可以得到结论:
A线程现持有监视器对象的锁,B线程可以异步的调用该监视器对象中的非同步方法,如果B线程需要调用该监视器对象的同步方法则需要等待A线程释放锁。
1.1.4、脏读