如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。
线程安全性的前提:对『线程安全性』的讨论必须建立在对象内部存在共享变量这一前提,若对象在多条线程间没有共享数据,那这个对象一定是线程安全的!
上述线程安全性的定义即为绝对线程安全的情况,即:一个对象在构造完之后,调用者无需任何额外的操作,就可以在多线程环境下随意使用。 绝对的线程安全是一种理想的状态,若要达到这一状态,往往需要付出巨大的代价。 通常并不需要达到绝对的线程安全。
我们通常所说的『线程安全』即为『相对的线程安全』,JDK中标注为线程安全的类通常就是『相对的线程安全』,如:Vector、HashTable、Collections.synchronizedXXX。 对于相对线程安全的类,使用它们时一般不需要使用额外的保障措施,但对于一些特定的使用场景,仍然需要额外的操作来保证线程安全,如:
// 读线程
Thread t1 = new Thread( new Runnable(){
public void run(){
for(int i=0; i<vector.size(); i++){
System.out.println( vector.get(i) );
}
}
}).start();
// 写线程
Thread t2 = new Thread( new Runnable(){
public void run(){
for(int i=0; i<vector.size(); i++){
vector.remove(i);
}
}
}).start();
vector是一个线程安全的容器,它所提供的方法均为同步方法,但上述代码仍然会出现线程安全性问题: 若线程1读了一半的元素后暂停,线程2开始执行,并删除了所有的元素,然后线程1继续执行,此时发生角标越界异常! 修改方案:加上额外的同步
// 读线程
Thread t1 = new Thread( new Runnable(){
public void run(){
synchronized( vector ){
for(int i=0; i<vector.size(); i++){
System.out.println( vector.get(i) );
}
}
}
}).start();
// 写线程
Thread t2 = new Thread( new Runnable(){
public void run(){
synchronized( vector ){
for(int i=0; i<vector.size(); i++){
vector.remove(i);
}
}
}
}).start();
线程对立指的是:不论调用者采用何种同步措施,都无法达到线程安全的目的。 如Thread类的suspend、resume方法就是线程对立的方法。 suspend方法会暂停线程,但它不会释放资源,若resume需要请求到该资源才会被运行的话,系统就会进入死锁状态。
同步指的是同一时刻,只有一条线程操作『共享变量』。
实现同步的方式有很多:互斥访问、CAS操作。 互斥会引起阻塞,当一条线程请求一个已经被另一线程使用的锁时,就会进入阻塞态;而进入阻塞态会涉及上下文切换。因此,使用互斥来实现同步的开销是很大的。
互斥同步(阻塞式同步)是一种『悲观锁』,即它认为总是存在多条线程竞争资源的情况,因此它不管当前是不是真的有多条线程在竞争共享资源,它总是先上锁,然后再处理。
Java中有两种实现互斥同步的方式:synchronized和ReentrantLock。
synchronized和ReentrantLock如何选择? 优先选择synchronized! JDK1.6已经对synchronized做了很多优化,性能与ReentrantLock相差不大。在条件允许的请况下应优先选择synchronized。
它是一种『乐观锁』,即它总是认为当前没有线程使用共享资源,因此它不管当前的状态,直接操作共享资源,若发现产生了冲突,那么再采取补偿措施(如:CAS的补偿措施就是不断尝试,直到不发生冲突为止),这种方式线程无需进入阻塞态(挂起态),因此称为『非阻塞同步』。
JUC中各种整形原子类的自增、自减等操作就使用了CAS。
CAS操作过程:CAS操作存在3个值:共享变量V、预期的旧值A、新值B,若V与A相同,则将V更新成B,否则就不更新,继续循环比较,直到更新完成为止。
CAS操作可能引发的问题:ABA问题。 若V一开始的值为A,但在准备赋新值的过程中A变成了B,又变成了A,而CAS操作误认为V没有被改过。
『阻塞式同步』和『非阻塞式同步』都是同一时刻只让一条线程处理共享数据,而下面的方案使得多条线程之间不存在共享数据,从而无需同步。
WEB服务器采用的就是这种方式,它把每个请求封装在一条线程中处理,从而不存在线程安全性问题。
不可变对象一定是线程安全的。