线程安全(Java语言)

在刚学习Java线程安全这节内容的时候,一直以为"线程安全"这个命题是一个非真即假的二元排他选项,即要么是安全的,要么是不安全的;


其实不然,Brian Goetz在IBM developWorkers上发表的一篇论文中,他把各种操作共享的数据分成5类:

1、不可变的

在讲述不可变之前,先来了解一个知识:在JDK5.0之后,Java内存模型被修正之后,不可变(Immutable)的对象一定是线程安全的;

因此,无论是对象的方法实现,又或者是方法的调用者,都不需要再采取任何的线程安全保障措施。

好,现在回到我们"不可变"的这话题上来,说到不可变,很多童鞋第一反应可能就是关键字final,也确实是final;

不过要注意的一点:当一个不可变的对象被正确地创建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会改变。


可能看文字大伙觉得不是那么舒服,那我们来看一下下面的这段代码:

private final int value = 1;
当value被构建出来后,其的值永远都是1,这个是不会改变的。


但是有的人会说,以下的代码可以通过编译,可以运行:

final StringBuffer buffer = new StringBuffer("immutable");
buffer.append("csdn");
第一个例子的情况是value为基本数据类型,而第二个例子buffer则是一个引用变量;

我们这里讲的是,value和buffer变量所对应的那块内存空间的内容不可改变,

并不是buffer所指的另一块内存空间的值。简单来说,就是指"引用变量"不能变,引用变量所指的对象中的内容是可以改变的。


像我们经常接触到的java.lang.String类则是一个典型的不可变的对象,我们通过调用它的substring()、replace()和concat()这些方法都不会影响原来的值,

这些方法所返回的是一个新构造的字符串对象。


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


2、绝对线程安全的

如果想达到绝对线程安全这个级别,需要付出的代价往往是很大的,有的时候甚至是不切实际的代价。

在Java API中声明自己是线程安全的类,大多数都不是绝对线程安全的。

例如java.util.Vector类,它的add()、get()、remove()和size()等方法都是被synchronized修饰的,不要以为使用这些方法的时候,就不需要额外的同步手段了;

如果是这样想的话,那代价一定是很大的;不信我们来看看下面这段代码:

package com.gdut.test;

import java.util.Vector;

public class ThreadSecurity {

	private static Vector<Integer> vector = new Vector<Integer>();
	
	public static void main(String[] args) {
		
		while(true) {
			// 往vector里面添加元素
			for(int i = 0; i < 10; i++) {
				vector.add(i);
			}
			
			// 移除vector内元素的线程
			Thread removeThread = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i = 0; i < vector.size(); i++) {
						// 移除元素
						vector.remove(i);
					}
				}
			});
			

			// 打印vector内元素的线程
			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-9755404" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 4

导致这个异常的原因就是:

当removeThread线程恰好在错误的时候删除了一个元素,导致序号i已经不再可用,再用i去访问数组就会抛出一个 java.lang.ArrayIndexOutOfBoundsException


所以,我们只要修改一下代码即可:

// 移除vector内元素的线程
Thread removeThread = new Thread(new Runnable() {
	@Override
	public void run() {
		synchronized(vector) {
			for(int i = 0; i < vector.size(); i++) {
				// 移除元素
				vector.remove(i);
			}
		}
		
	}
});


// 打印vector内元素的线程
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));
			}
		}
	}
});

这样就保证了vector访问的线程安全性。

这里的"绝对"的意思就是:一个类,不管运行时环境如何,调用者都不需要任何额外的同步措施


3、相对线程安全的

相对线程安全就是我们通常意义上所讲的线程安全,即上面vector例子中,它的remove、add等方法和HashTable等都是属于这一类型的。


4、线程兼容的

线程兼容是指对象本身并不是线程安全的,例如ArrayList和HashMap等类,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。

我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。


5、线程对立的

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码;而这种线程对立的代码,往往是有害的,应该尽量去避免。

例如Thread.suspend()和Thread.resume()方法,如果suspend()中断的线程就是即将要执行resume的那个线程,这样就产生死锁了。





内容参考于:周志明  深入理解java虚拟机

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章