<基础-2> 构建线程安全应用程序
程序员文章站
2022-07-12 18:49:44
...
上一篇主要介绍了程序,进程,线程的概念和区别,进程的组成。然后是线程的创建和基本控制。接着本篇就介绍下什么是线程安全,怎样去保证线程安全的基本方法。
二、 构建线程安全应用程序
2.1 什么是线程安全性
线程安全很难给出一个准确的定义。大都是从不同的方面进行一个描述。当对一个复杂对象进行某种操作时,从操作开始到操作结束,该对象中间肯定会经历若干个非法的中间状态。能保证多线程在使用该对象时,每个开始和结束都是稳定合法状态,中间状态不会被其他线程访问,则是线程安全的。
1) 类要成为线程安全的,则首先必须在单线程环境有正确的行为。
2) 正确性和安全性的关系非常类似事务(ACID)的一致性和独立性之间的关系。
Bloch给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。
1.不可变
不可变的对象一定是线程安全的,如String。
2.线程安全
访问类时不需要做任何同步的类是线程安全的。这个条件很苛刻,HashTable或Vector都不满足这种严格的定义。
考虑下面的代码片段,它迭代一个 Vector 中的元素。尽管 Vector 的所有方法都是同步的,但是在多线程的环境中不做额外的同步就使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,则 get() 会抛出一个 ArrayIndexOutOfBoundsException 。
Vector v = new Vector();
// contains race conditions -- may require external synchronization
for (int i=0; i<v.size(); i++) {
doSomething(v.get(i));
}
3.有条件的线程安全
有条件的线程安全类对于单独的操作可以是线程安全的,但某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历Hashtable或Vector.
4.线程兼容
线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全使用。这可能意味着用一个synchronized块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的。许多常见的类都是线程兼容的,如ArrayList,HashMap等。
5.线程对立
是那些不管是否调用了外部同步都不能在并发使用时安全的呈现的类。线程对立很少见,其中一个例子是调用System.setOut()的类。
2.1 servlet的线程安全性
Servlet/jsp默认是以多线程模式执行的,所以在编写代码时需要非常细致地考虑多线程的安全性问题。
Servlet体系结构是建立在java多线程机制之上的,它的生命周期是由web容器负责的。当客户端第一次请求某个servlet时,servlet容器会根据web.xml实例化这个servlet类,当有新的客户端请求该servlet时,一般不会再实例化该类(这个也要看web容器里的配置),也就是多个线程在使用这个实例。
2.3 同步和互斥
线程通信主要通过共享访问字段完成,通常有可能出现两种错误:线程干扰和内存一致性错误。用来防止这些错误的工具是同步(synchronization)。
当两个线程需要使用同一个对象时,存在交叉操作而破坏数据的可能性。这种潜在的干扰在术语上称作:临界区(critical section)。通过同步对临界区的访问可以避免这种线程干扰。
注:
这里有关于临界区,互斥量,信号量的相关概念介绍:
http://blog.csdn.net/bao_qibiao/article/details/4516196
同步是围绕被称为内在锁(intrinsic lock)或者监视器锁(monitor lock)的内部实体构建的,强制对对象状态的独占访问,以及建立可见性所需的发生前关系。
每个对象都具有与其关联的内在锁,按照约定,需要对对象的字段进行独占和一致性访问的线程在访问之前,必须获得这个对象的内在锁,访问完成之后必须释放内在锁。从获得锁到释放锁的时间段内,线程被称为拥有内在锁。只要有线程拥有内在锁,其他线程就不能获得同一个锁,试图获得锁的其他线程将被阻塞。注意一定是试图获取同一个锁才会阻塞,比如锁的是类实例,则同一个类的对象A,B之间不构成竞争;如果锁的是类(MyClass.class),则和类实例之间不构成竞争。
Java提供了synchronized关键字来支持内在锁。Synchronized可以放在对象,方法,类的前面。
注:这个关键字在方法前面不能被继承,不是方法签名的一部分。
1) 放在普通方法(非static)前面,锁住this对象都是锁的类实例,效果是一样的。
Public synchronized void ff() {}
Public void ff()
{
Synchronized(this)
{
….
}
}
2) 放在普通(非static)成员变量前面(只能在同步块中锁成员变量,不能在声明变量时用synchronized,static变量也是一样)则锁住的只是该成员变量。注意锁住成员变量后不要对该变量进行重新赋值,赋值后这个方法获得的锁就不能跟其他线程再次访问这个方法构成锁竞争了,新的请求会获得新的对象锁。
如同步块:
synchronized(o)
{
// 这里就是严重错误了。
O = new Object();
}
同样,下面的写法毫无意义,jvm通常也会优化掉这种锁同步。
synchronized(new Object())
{
...
}
3) Synchronized放在static变量(只能在同步块中锁static变量,不能在声明变量时用synchronized)、static方法前面或类前面或用同步块同步Class.forName(“MyClass”)都是获取的类锁,跟类实例对象锁不一样。放在类前面则该类所有的方法都是同步的。
4) Synchronized块锁住myClass.getClass()跟上面的类锁不一样。
类锁和实例锁可以同时获得,并且互不干扰:
public class Something(){
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
那么,对于Something类的两个实例a与b,那么下列组方法何以被1个以上线程同时访问呢
1. x.isSyncA()与x.isSyncB()
2. x.isSyncA()与y.isSyncA()
3. x.cSyncA()与y.cSyncB()
4. x.isSyncA()与Something.cSyncA()
答案:
1. 都是对同一个实例的synchronized域访问,因此不能被同时访问
2. 是针对不同实例的,因此可以同时被访问
3. 因为是static synchronized,所以不同实例之间仍然会被限制,相当于Something.isSyncA()与 Something.isSyncB()了,因此不能被同时访问。
4. 能够同时访问,因为一个是实例锁,一个是类锁。
5)使用锁同步时,我们要尽量减少锁的竞争
通常可以采取:
(1)减小锁的范围(快进快出)
(2)减小锁的粒度,比如ConcurrentHashMap里采取的分段锁。当采取一个锁来保护多个相互独立的状态时,可以将锁分解成多个锁。
(3)一些替代锁的方法,比如使用并发容器或原子变量。
还可以参考:
http://yoush25-163-com.iteye.com/blog/999157
http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
可重入(reentrant)同步:线程可以重新获得他已经拥有的锁。
2.4 同步与volatile
线程读取的所有变量的值都是由内存模型来决定的,因为内存模型定义了变量被读取时允许返回的值集合。从程序员的角度看每个值几何应该只包含一个确定的值,即由某个线程最近写入的值,然而在缺乏同步时,实际获得的值集合可能包含许多不同的值。
先了解下内存模型吧。
Java内存模型 ( java memory model )
根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
其中, 工作内存里的变量, 在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时, 也是不可见的.
内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)
jvm怎么体现可视性(Visibility) ?
在jvm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.
jvm怎么体现有序性(Ordering) ?
通过Java提供的同步机制或volatile关键字, 来保证内存的访问顺序.
详细请参考:http://developer.51cto.com/art/200906/131393.htm
Java提供了一种同步机制,它不提供对锁的独占访问,但同样可以确保对变量的每一个读取操作都返回最近写入的值,这种机制就是用volatile变量。与synchronized相比,volatile变量所需的编码较少,并且运行时开销也少,但它的功能只是synchronized的一部分,只具备可见性不具备原子性,很容易被误用。
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
• 对变量的写操作不依赖于当前值。
• 该变量没有包含在具有其他变量的不变式中。
volatile可以解决可见性问题,但不能解决原子性问题,比如i++的操作,即使是volatile类型的也不能保证并发安全。
详细可以参考:
http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
2.5 活性
并发应用程序按照及时方式执行的能力成为活性,一般包括三种类型的问题,死锁,饿死和活锁。
1) 死锁
互相等待资源而都不能运行,比如经典的哲学家用餐问题。
2) 饿死
一个线程永远无法获得共享资源的使用。
3) 活锁
2.6 threadLocal变量
ThreadLocal并不是一个线程,而是线程的局部变量,也许叫做ThreadLocalVariable更容易让人理解。ThreadLocal变量为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本而不影响其他线程。
Jdk2.0开始就提供了ThreadLocal,后来在速度上改进了,Jdk5.0提供了泛型支持,ThreadLoacal也被定义为支持泛型:
Public class ThreadLocal<T> extends Object
该类定义了4个方法:
1) protected T initialValue():返回此线程局部变量的当前线程的“初始值”。该方法定义为protected的就是为了重写的。线程第一次使用get()方法访问变量时调用此方法,但如果线程线程之前调用了set(T)方法则不会对该线程再调用此方法。通常此方法对每个线程最多调用一次,但如果在调用get()后又调用了remove()则可能再次调用此方法。通常使用匿名内部类重写此方法。
2) public T get():返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值则先将其初始化为调用initialValue方法返回的值。
3) public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,他们只依靠initialValue方法来设置线程局部变量的值。
4) public void remove():移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其initialValue方法重新初始化。
注意在线程池中尽量不要使用ThreadLocal,或要谨慎使用,使用完一定要remove,因为线程池中的线程是重复使用的,ThreadLocal被用过之后后面的线程再获得,里面的值已经不是初始化时的值,是之前被处理过的值。
还可以参考:http://www.iteye.com/topic/103804
http://lavasoft.blog.51cto.com/62575/51926/
2.7 高级并发对象
上面的重点是讲述低级别的API,都是Java平台最基本的组成部分,这些足以胜任基本的任务,但是更加高级的任务需要更高级别的API,对应充分利用现代多处理器和多核心系统的大规模并发应用程序来说尤其重要。
Jdk5.0之后引入了高级并发特性,并且不断完善。大多数特性在java.util.concurrent包中实现,java集合框架中也有新的并发数据结构补充进来。
主要新增的高级并发对象有:Lock对象,执行器,并发集合、原子变量和同步器。
1.)Lock对象
对应前面synchronized里的内部锁,lock对象是显式的,支持更加复杂的锁定语法。也支持wait和notify.
2.)执行器
增加了对线程池的支持,Executors,ExecutorService等。
3.)并发集合
主要有BlockingQueue, ConcurrentMap等。
4.)原子变量
对应在java.util.concurrent.atomic包中。
5.)同步器
提供了一些帮助在线程间协调的类,包括semaphores,mutexes,latches等。
上面讲的这些高级对象在后面会介绍,这里只是引入一下。
二、 构建线程安全应用程序
2.1 什么是线程安全性
线程安全很难给出一个准确的定义。大都是从不同的方面进行一个描述。当对一个复杂对象进行某种操作时,从操作开始到操作结束,该对象中间肯定会经历若干个非法的中间状态。能保证多线程在使用该对象时,每个开始和结束都是稳定合法状态,中间状态不会被其他线程访问,则是线程安全的。
1) 类要成为线程安全的,则首先必须在单线程环境有正确的行为。
2) 正确性和安全性的关系非常类似事务(ACID)的一致性和独立性之间的关系。
Bloch给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。
1.不可变
不可变的对象一定是线程安全的,如String。
2.线程安全
访问类时不需要做任何同步的类是线程安全的。这个条件很苛刻,HashTable或Vector都不满足这种严格的定义。
考虑下面的代码片段,它迭代一个 Vector 中的元素。尽管 Vector 的所有方法都是同步的,但是在多线程的环境中不做额外的同步就使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,则 get() 会抛出一个 ArrayIndexOutOfBoundsException 。
Vector v = new Vector();
// contains race conditions -- may require external synchronization
for (int i=0; i<v.size(); i++) {
doSomething(v.get(i));
}
3.有条件的线程安全
有条件的线程安全类对于单独的操作可以是线程安全的,但某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历Hashtable或Vector.
4.线程兼容
线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全使用。这可能意味着用一个synchronized块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的。许多常见的类都是线程兼容的,如ArrayList,HashMap等。
5.线程对立
是那些不管是否调用了外部同步都不能在并发使用时安全的呈现的类。线程对立很少见,其中一个例子是调用System.setOut()的类。
2.1 servlet的线程安全性
Servlet/jsp默认是以多线程模式执行的,所以在编写代码时需要非常细致地考虑多线程的安全性问题。
Servlet体系结构是建立在java多线程机制之上的,它的生命周期是由web容器负责的。当客户端第一次请求某个servlet时,servlet容器会根据web.xml实例化这个servlet类,当有新的客户端请求该servlet时,一般不会再实例化该类(这个也要看web容器里的配置),也就是多个线程在使用这个实例。
2.3 同步和互斥
线程通信主要通过共享访问字段完成,通常有可能出现两种错误:线程干扰和内存一致性错误。用来防止这些错误的工具是同步(synchronization)。
当两个线程需要使用同一个对象时,存在交叉操作而破坏数据的可能性。这种潜在的干扰在术语上称作:临界区(critical section)。通过同步对临界区的访问可以避免这种线程干扰。
注:
这里有关于临界区,互斥量,信号量的相关概念介绍:
http://blog.csdn.net/bao_qibiao/article/details/4516196
同步是围绕被称为内在锁(intrinsic lock)或者监视器锁(monitor lock)的内部实体构建的,强制对对象状态的独占访问,以及建立可见性所需的发生前关系。
每个对象都具有与其关联的内在锁,按照约定,需要对对象的字段进行独占和一致性访问的线程在访问之前,必须获得这个对象的内在锁,访问完成之后必须释放内在锁。从获得锁到释放锁的时间段内,线程被称为拥有内在锁。只要有线程拥有内在锁,其他线程就不能获得同一个锁,试图获得锁的其他线程将被阻塞。注意一定是试图获取同一个锁才会阻塞,比如锁的是类实例,则同一个类的对象A,B之间不构成竞争;如果锁的是类(MyClass.class),则和类实例之间不构成竞争。
Java提供了synchronized关键字来支持内在锁。Synchronized可以放在对象,方法,类的前面。
注:这个关键字在方法前面不能被继承,不是方法签名的一部分。
1) 放在普通方法(非static)前面,锁住this对象都是锁的类实例,效果是一样的。
Public synchronized void ff() {}
Public void ff()
{
Synchronized(this)
{
….
}
}
2) 放在普通(非static)成员变量前面(只能在同步块中锁成员变量,不能在声明变量时用synchronized,static变量也是一样)则锁住的只是该成员变量。注意锁住成员变量后不要对该变量进行重新赋值,赋值后这个方法获得的锁就不能跟其他线程再次访问这个方法构成锁竞争了,新的请求会获得新的对象锁。
如同步块:
synchronized(o)
{
// 这里就是严重错误了。
O = new Object();
}
同样,下面的写法毫无意义,jvm通常也会优化掉这种锁同步。
synchronized(new Object())
{
...
}
3) Synchronized放在static变量(只能在同步块中锁static变量,不能在声明变量时用synchronized)、static方法前面或类前面或用同步块同步Class.forName(“MyClass”)都是获取的类锁,跟类实例对象锁不一样。放在类前面则该类所有的方法都是同步的。
4) Synchronized块锁住myClass.getClass()跟上面的类锁不一样。
类锁和实例锁可以同时获得,并且互不干扰:
public class Something(){
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
那么,对于Something类的两个实例a与b,那么下列组方法何以被1个以上线程同时访问呢
1. x.isSyncA()与x.isSyncB()
2. x.isSyncA()与y.isSyncA()
3. x.cSyncA()与y.cSyncB()
4. x.isSyncA()与Something.cSyncA()
答案:
1. 都是对同一个实例的synchronized域访问,因此不能被同时访问
2. 是针对不同实例的,因此可以同时被访问
3. 因为是static synchronized,所以不同实例之间仍然会被限制,相当于Something.isSyncA()与 Something.isSyncB()了,因此不能被同时访问。
4. 能够同时访问,因为一个是实例锁,一个是类锁。
5)使用锁同步时,我们要尽量减少锁的竞争
通常可以采取:
(1)减小锁的范围(快进快出)
(2)减小锁的粒度,比如ConcurrentHashMap里采取的分段锁。当采取一个锁来保护多个相互独立的状态时,可以将锁分解成多个锁。
(3)一些替代锁的方法,比如使用并发容器或原子变量。
还可以参考:
http://yoush25-163-com.iteye.com/blog/999157
http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
可重入(reentrant)同步:线程可以重新获得他已经拥有的锁。
2.4 同步与volatile
线程读取的所有变量的值都是由内存模型来决定的,因为内存模型定义了变量被读取时允许返回的值集合。从程序员的角度看每个值几何应该只包含一个确定的值,即由某个线程最近写入的值,然而在缺乏同步时,实际获得的值集合可能包含许多不同的值。
先了解下内存模型吧。
Java内存模型 ( java memory model )
根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
其中, 工作内存里的变量, 在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时, 也是不可见的.
内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)
jvm怎么体现可视性(Visibility) ?
在jvm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.
jvm怎么体现有序性(Ordering) ?
通过Java提供的同步机制或volatile关键字, 来保证内存的访问顺序.
详细请参考:http://developer.51cto.com/art/200906/131393.htm
Java提供了一种同步机制,它不提供对锁的独占访问,但同样可以确保对变量的每一个读取操作都返回最近写入的值,这种机制就是用volatile变量。与synchronized相比,volatile变量所需的编码较少,并且运行时开销也少,但它的功能只是synchronized的一部分,只具备可见性不具备原子性,很容易被误用。
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
• 对变量的写操作不依赖于当前值。
• 该变量没有包含在具有其他变量的不变式中。
volatile可以解决可见性问题,但不能解决原子性问题,比如i++的操作,即使是volatile类型的也不能保证并发安全。
详细可以参考:
http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
2.5 活性
并发应用程序按照及时方式执行的能力成为活性,一般包括三种类型的问题,死锁,饿死和活锁。
1) 死锁
互相等待资源而都不能运行,比如经典的哲学家用餐问题。
2) 饿死
一个线程永远无法获得共享资源的使用。
3) 活锁
2.6 threadLocal变量
ThreadLocal并不是一个线程,而是线程的局部变量,也许叫做ThreadLocalVariable更容易让人理解。ThreadLocal变量为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本而不影响其他线程。
Jdk2.0开始就提供了ThreadLocal,后来在速度上改进了,Jdk5.0提供了泛型支持,ThreadLoacal也被定义为支持泛型:
Public class ThreadLocal<T> extends Object
该类定义了4个方法:
1) protected T initialValue():返回此线程局部变量的当前线程的“初始值”。该方法定义为protected的就是为了重写的。线程第一次使用get()方法访问变量时调用此方法,但如果线程线程之前调用了set(T)方法则不会对该线程再调用此方法。通常此方法对每个线程最多调用一次,但如果在调用get()后又调用了remove()则可能再次调用此方法。通常使用匿名内部类重写此方法。
2) public T get():返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值则先将其初始化为调用initialValue方法返回的值。
3) public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,他们只依靠initialValue方法来设置线程局部变量的值。
4) public void remove():移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其initialValue方法重新初始化。
注意在线程池中尽量不要使用ThreadLocal,或要谨慎使用,使用完一定要remove,因为线程池中的线程是重复使用的,ThreadLocal被用过之后后面的线程再获得,里面的值已经不是初始化时的值,是之前被处理过的值。
还可以参考:http://www.iteye.com/topic/103804
http://lavasoft.blog.51cto.com/62575/51926/
2.7 高级并发对象
上面的重点是讲述低级别的API,都是Java平台最基本的组成部分,这些足以胜任基本的任务,但是更加高级的任务需要更高级别的API,对应充分利用现代多处理器和多核心系统的大规模并发应用程序来说尤其重要。
Jdk5.0之后引入了高级并发特性,并且不断完善。大多数特性在java.util.concurrent包中实现,java集合框架中也有新的并发数据结构补充进来。
主要新增的高级并发对象有:Lock对象,执行器,并发集合、原子变量和同步器。
1.)Lock对象
对应前面synchronized里的内部锁,lock对象是显式的,支持更加复杂的锁定语法。也支持wait和notify.
2.)执行器
增加了对线程池的支持,Executors,ExecutorService等。
3.)并发集合
主要有BlockingQueue, ConcurrentMap等。
4.)原子变量
对应在java.util.concurrent.atomic包中。
5.)同步器
提供了一些帮助在线程间协调的类,包括semaphores,mutexes,latches等。
上面讲的这些高级对象在后面会介绍,这里只是引入一下。