Java语言中的线程安全
资源限制的挑战
(1)什么是资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。
(2)资源限制引发的问题
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
(3)如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
(4)在资源限制情况下进行并发编程
如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。
线程安全
并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力的最有力武器。但是我们必须保证并发的安全性,在此基础上实现的高效并发才有意义。一般而言,并发的安全性也就是我们常说的线程安全。
Java语言中的线程安全
按照线程安全的安全程度由强到弱,将Java语言中的共享数据的分为如下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1、不可变
在Java语言中(JDK 1.5之后),不可变(Immutable)的对象一定是线程安全的。无论是对象的方法实现还是方法的调用者,都不需要额外采取任何的线程安全保障措施。只要一个不可变对象被正确的构建出来,那么其外部的可见状态永远也不会改变,永远也不会看到它在多线程之中处于不一致的状态。“不可变”带来的安全性是最简答和最纯粹的。
在Java语言中,如果共享数据是一个基本数据类型,那么只需要在定义时使用final关键字修饰它就可以保证它是不可变的。
如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。例如,java.lang.String类的对象,它就是一个典型的不可变对象,我们调用它的substring() 、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构建的字符串对象。
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。例如,java.lang.Integer构造函数,它通过将内部状态变量value定义为final来保障状态不变。
private final int value;
public Integer(int value) {
this.value = value;
}
2、绝对线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或在调用方进行其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是绝对线程安全的。一个类要达到绝对的线程安全,往往需要付出很大的代价,甚至有时候是不切实际的代价。在Java API中标注自己是线程安全的类,大多数都不是绝对线程安全的。
比如说java.util.Vector是一个线程安全的容器,相信大家都不会有异议。因为它的add(),get()和size()这类方法都被synchronized修饰,尽管这样效率低下,但确实是线程安全的。但是,即使它所有的方法都被修饰成同步的,也不意味着调用它的时候永远都不再需要同步手段了。
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);
}
}
}
尽管这里使用的Vector的get()、remove()和size()方法都是同步的,但是在多线程环境下,如果不在方法调用端做额外的同步操作的话,使用这段代码仍然不是线程安全的,因为如果另一个线程恰好在错误的时间删除了一个元素,导致打印线程中的序列i已经不再可用的话,再用序列i访问数组就会抛出一个ArrayIndeOutOfBoundsException。
如果要保证这段代码的线程安全,我们可以将代码改为:
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));
}
}
}
});
3、相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保护措施。但是,对于一些特定顺序的连续调用,就可能需要在调用端使用额外的手段来保证调用的正确性。在Java语言中,大部分的线程安全类都是属于这种类型。例如Vector、HashTable和利用Collections的synchronizedCollection()方法包装的集合。
4、线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境下可以安全的使用。我们平常说的一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API中大部分类都是属于线程兼容的,例如ArrayList和HashMap。
5、线程对立
线程对立是指,无论调用端是否采用同步手段,都无法在多线程环境中并发使用。这样的情况通常是有害的,应当尽量避免。
- 点赞
- 收藏
- 关注作者
评论(0)