深入理解JVM(③)再谈线程安全

我们在编写程序的时候,一般是有个顺序的,就是先实现再优化,并不是所有的牛P程序都是一次就写出来的,肯定都是不断的优化完善来持续实现的。因此我们在考虑实现高并发程序的时候,要先保证并发的正确性,然后在此基础上来实现高效。所以线程安全是高并发程序首先需要保证的。

线程安全定义

对于线程安全的定义可以理解为:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
这个定义是很严谨且有可操作性,它要求线程安全的代码都必须具备一个共同特征:代码本身封装了所有必要的正确性保障手段(互斥、同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用。

Java中的线程安全

要讨论Java中的线程安全,我们要以多个线程之间存在共享数据访问为前提。我们可以不把线程安全当作一个非真即假的二元排他选项来看待,而是按照线程安全的“安全程度”由强至弱来排序,将Java中各操作共享的数据分为以下五类:不可变、绝对线程安全、相对相对安全、线程兼容和线程对立

不可变

Java内存模型中,不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。在学习Java内存模型这一篇文章中我们在介绍Java内存模型的三个特性的可见性的时候说到,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有吧“this”的引用传递出去,那么在其他线程中就能看见final字段的值。并且外部可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接、最纯粹的。

在Java中如果共享数据是一个基本类型,那么在定义时使用final修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要对象自行保证其行为不会对其状态产生任何影响才行。例如java.lang.String类的对象实例,它的substring()、replace()、concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都声明为final,这样在构造函数结束后,他就是不可变的。
例如java.lang.Integer构造函数。

/** * The value of the {@code Integer}. * * @serial */ private final int value; /** * Constructs a newly allocated {@code Integer} object that * represents the specified {@code int} value. * * @param value the value to be represented by the * {@code Integer} object. */ public Integer(int value) { this.value = value; }

除了String之外,还有枚举类型以及java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。

绝对线程安全

绝对线程安全是能够完全满足上面的线程安全的定义,这个绝对线程安全的定义是很严格的:“不管运行时环境如何,调用者都不需要任何额外的同步措施”。Java的API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
例如java.util.Vector是一个线程安全的容器,相信所有的Java程序员对此都不会有异议,因为它的add()、get()、和size()等方法都被synhronized修饰。但是这样并不意味着调用它的时候,就永远不再需要同步手段了。

public class VectorTest { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){ for (int i=0;i<10;i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable() { @Override public void run() { for (int i=0;i<vector.size();i++){ vector.remove(i); } } }); Thread printThread = new Thread(new Runnable() { @Override public void run() { for(int i=0;i<vector.size();i++){ System.out.println(vector.get(i)); } } }); removeThread.start(); printThread.start(); while (Thread.activeCount() > 20); } } }

运行结果:

Exception in thread "Thread-653" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 18 at java.util.Vector.get(Vector.java:748) at com.eurekaclient2.test.jvm3.VectorTest$2.run(VectorTest.java:33) at java.lang.Thread.run(Thread.java:748)

通过上述代码的例子,就可以看出来,尽管Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境中,如果调用端不做额外的同步措施,使用这段代码仍然是不安全的。因为在并发运行中,如果提前删除了一个元素,而后面还要去打印它,就会抛出数组越界的异常。
如果非要这段代码正确执行下去,就必须把removeThread和printThread进行加锁操作。

Thread removeThread = new Thread(new Runnable() { @Override public void run() { synchronized (vector){ for (int i=0;i<vector.size();i++){ vector.remove(i); } } } }); Thread printThread = new Thread(new Runnable() { @Override public void run() { synchronized (vector){ for(int i=0;i<vector.size();i++){ System.out.println(vector.get(i)); } } } }); 相对线程安全

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygddx.html