Java语言中的线程安全--《深入理解Java虚拟机》笔记

来源:互联网 发布:淘宝township充值原理 编辑:程序博客网 时间:2024/06/06 12:40

《Java Concurrency In Practice》作者Brian Goetz对“线程安全”有一个比较恰当的定义:当多个线程访问访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。


Java语言中的线程安全

 按照线程安全的“安全程度”由强至弱来排序,可以将Java语言中的各种操作共享的数据分为以下5类,也就是指Java中的类可以分为5类。

1.不可变对象

  在JDK5以后,不可变对象一定是线程安全的。无论是对象的方法实现还是方法的调用者,都不需要采取任何的线程安全保障措施(各种同步措施,加锁,synchronized关键字等),只要一个不可变对象被正确的构建出来(没有发生this引用逃逸的情况),那么其外部的可见状态永远也不会改变,永远也不会看到他在多个线程之中处于不一致的状态。不可变带来的安全性是最简单和最纯粹的。如果共享数据是基本类型,那么只需要给基本类型加上final修饰符就能保证不可变,如果是对象,那么要保证对象的行为不会对对象数据产生改变,可以参考java.lang.String类,String类内部的数据是使用final修饰的char数组,且String类的方法(如substring,replace)都不会改变当前对象的值,而是会返回一个新的String对象。

所以只要String被构建出来,那么这个对象就不可变。

如 String str = new String("hello"); //str指向的String对象永远不会发生改变,就算后面进行了 str = "abc"的操作,原对象也只是失去了str对其的引用。

Java中除了String类是不可变,不可变的还有枚举类型(Enum),以及java.lang.Number的部分子类,如Long、Integer、BigInteger等类型。Number的其他子类型如AtomicInteger和AtomicLong则不是不可变对象。

所以一个类要为不可变,要做到两点:1、值域不可变,如果是基本数据类型加上final修饰,如果是对象类型,要保证对象的行为不会对对象的数据造成改变。

    2、类的方法(行为)不会对类的数据(属性)造成改变。

2、绝对线程安全

  绝对线程安全完全满足上面Brian Goetz给出的线程安全的定义。不需要调用者做任何额外的同步操作。在java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。

 eg、java.util.Vector类,它的add、get、size方法都是使用synchronized关键字修饰的,尽管这样效率很低,但确实是安全的。但是这样并不意味着调用它的时候永远都不在需要同步手段了。

package org.yamikaze.test.thread;import java.util.Vector;public class TestVector {private static Vector<Integer> vector = new Vector<Integer>();public static void main(String[] args) {for(;;) {for(int i = 0; i < 10; i++) {vector.add(i);}Thread removeThread = new Thread(new Runnable(){@Overridepublic void run() {for(int i = 0; i < vector.size(); i++) {vector.remove(i);}}});Thread printThread = new Thread(new Runnable(){@Overridepublic void run() {for(int i = 0; i < vector.size(); i++) {System.out.print(vector.get(i));;}}});removeThread.start();printThread.start();while(Thread.activeCount() > 20);}}}
运行一会儿会出现ArrayIndexOutOfBoundsException,虽然方法加上了关键字synchronized修饰,同一个时刻只会有一个线程调用vector的方法,但是上面的程序操作要依赖于外部的变量i,要么就有可能当两个线程remove和print的i都相同时,remove线程先做了移除操作,导致i不可用,那么等到print线程时就会出现这个错误,所以仍需要做同步操作保证Vector调用的顺序。程序可以改成下面这样
package org.yamikaze.test.thread;import java.util.Vector;public class TestVector {private static Vector<Integer> vector = new Vector<Integer>();public static void main(String[] args) {for(;;) {for(int i = 0; i < 10; i++) {vector.add(i);}Thread removeThread = new Thread(new Runnable(){@Overridepublic void run() {synchronized (vector) {for (int i = 0; i < vector.size(); i++) {vector.remove(i);}}}});Thread printThread = new Thread(new Runnable(){@Overridepublic void run() {synchronized (vector) {for (int i = 0; i < vector.size(); i++) {System.out.print(vector.get(i));}}}});removeThread.start();printThread.start();//System.out.println("=======" + Thread.activeCount() + "========");while(Thread.activeCount() > 20);}}}
这样就不会出现上述异常。

ps:当synchronized中的是同步代码块时,那么锁就是括号中的对象。

        当synchronized修饰的是方法时,那么锁就是当前对象。

        当synchronized修饰的是静态方法时,那么锁就是当前类.class。

3、相对线程安全

   相对线程安全就是我们通常意义上所讲的线程安全,他需要保证对这个对象单独的操作时线程安全的,我们在调用时不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性,如上面的Vector操作,它就是一个相对线程安全的类,在Java中大部分线程安全类都属于这种类型,例如Vector,StringBuffer、HashTable等,这些类的会改变数据的方法都做了同步操作,所以速度会比没有做同步操作慢,毕竟同步操作就意味着性能下降,所以在对字符串做拼接操作时,尽量用StringBuilder而不是StringBuffer。

4、线程兼容

   线程兼容指的是对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用。通常说的线程不安全,指的就是这种类型,Java API中的大部分类都是线程兼容的,如StringBuilder、ArrayList、HashMap等。

5、线程对立

  线程对立指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码,由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应该尽量避免。(ps:你叫我写个线程对立的例子,我也写不出来啊!/(ㄒoㄒ)/~~)

 一个线程对立的例子是Thread类的suspend和resume方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的。如果suspend中断的线程就是resume要恢复的线程,那就肯定要产生死锁了。基于这个原因,这两个方法已经被废弃了。常见的线程对立操作还有System.setIn(), System.setOut()和System.runFinalizersOnExit()等