名词解析
1. 上下文切换
调度程序临时挂起当前运行的线程时,另一个线程开始运行。也就是线程切换,在应用程序中很常见,带来的系统开销巨大。
2. 原子性
程序可以作为单独的、不可分割的一次操作执行,称为原子操作,具有原子性,例如赋值操作:a = 3,而看似紧凑的操作有的并不是原子操作:例如 a++,可看成“读-改-写”三步,非原子操作可能遗失更新。java.util.concurrent.atomic
包下的AtomicLong
等类可以保证变量操作的原子性。
3. 可见性
当前线程对变量的修改操作对其他线程可见,具有可见性的操作读取时不会产生并发问题,但是写入时要处理并发。
可见性问题涉及到变量在内存中的存取情况,长话短说。内存分为主内存以及各自变量占有的内存(副本),以a++
为例,首先变量会从主内存中读取a
的初值到副本中,然后对副本中的值加1操作,再将结果写入主内存中。这个过程如果有两个线程介入的话,很显然容易产生并发问题。
但是如果变量可见,在线程访问该变量时,操作系统会增加store
和load
指令,在读取的时候store
指令强制从主存中重新读取,写入时load
会强制写入主存,保证了任何时候线程看到的变量值都是一致的。注意,在读写这段时间内,如果有其它线程执行写操作,依然会有并发问题,可见性只是保证变量的一致性。
4. 竞争条件
当计算的正确性依赖于运行时的相关时序或者多线程的交替时(“幸运”时序), 会产生竞争条件。最常见的竞争条件是:“先检查再运行”,也即if
等判断操作。检查到运行的这段时间,变量的值如果被另一个线程修改,就会导致非期望的运行结果。例如:“惰性初始化”
private Test mTest;
public static Test getInstance(){
if(mTest == null)
return new Test();
}
5. 重进入
避免死锁的一种手段。对锁的请求是基于“每线程”而不是“每调用”,意味着同一线程可以多次请求同一个锁。每个锁关联了一个请求计数和一个占有它的线程,该线程每重进入该代码块,计数加1,退出则减1,为0时释放锁。如果没有重进入,以下例子会造成死锁:
public class Widget {
public synchronized void doSomething(){
......
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething(){
...
super.doSomthing();
}
}
6. 重排序
在单个线程中,为了使java虚拟机能够充分利用CPU等资源,只要结果与原来相比没有产生别的影响,即使对其他线程有影响,虚拟机也会对编写的代码进行重排序,
7. Volatile关键字
用来修饰变量,变量被Volitale
关键字修饰时,编译器与运行时会监视这个变量,它具有以下几个特点:
- 对它的操作不会与其他的内存操作一起被重新排序,这意味着使用
Volatile
会屏蔽虚拟机的一些优化操作,降低些许性能。 Volatile
修饰的变量不会缓存在寄存器或者其他对处理器隐蔽的地方,所以可以保证变量的可见性- 不能保证变量的原子性,这是与锁机制的不同点
- 多用于当做标识完成、中断、状态的标记使用
8. 逸出
首先“发布”的意思是一个对象能够被当前范围之外的代码所引用。一个对象在还没准备好的时候就被发布出去,称为逸出。
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
});
}
private void doSomething(Event e){
}
}
上面的程序在构造方法中隐式持有了外部对象的引用,然而对象还没创建完全,对象的状态不一定是可预知的、稳定的,造成了逸出。书中建议:不要让this
引用在构造期间逸出。
此外,比较常见的一种错误是在构造方法中去创建并开启一个线程,无论是显式还是隐式的,this引用总会被线程共享,这样在新线程开启的时候就可以看见当前实例,但是当前类的构造方法不一定执行完了。总而言之,不要在构造方法中去开启一个线程,可以创建,然后通过工厂方法发布出去。
9. ThreadLocal
允许使用者将每个线程与持有数值的对象关联在一起,为每个使用它的线程维护了一份单独的拷贝,Thread (T) 在概念上与Map (Thread, T)相同,其提供了get和set访问器,用来设置和获取单独的拷贝。
线程安全
不要妄图只使用原子类型的数据去解决所有的并发问题,原子类型只保证当前变量的原子性,对整个程序的执行时序等等没有保障。
获得锁的唯一途径是进入锁保护的代码块中,而无论是正常退出还是抛出异常,线程都会在离开代码块后释放锁。
无论是类锁还是对象锁实际上都是对象锁,类锁持有的是
ClassLoader
加载的当前类对象。获得和释放锁都存在开销,并且使用锁会导致代码块串行运行,性能大大降低,尽量锁代码块而不是整个方法。
共享对象
为了维护程序的封装性,只在必要的时候提供变量的
set
方法,避免过多的私有变量能够被外界获取。不可变的对象永远是线程安全的,但是不可变并不意味着将对象的所有域都声明为
final
类型,因为final
修饰的引用虽然不可变,但是引用指向的对象的属性是可以变化的。
一些线程安全的类
在源码中会见到一些线程安全相关的类,比如Hashtable
,但是由于在不考虑线程安全下HashMap
比它操作、效率高,在多线程下Hashtable
线程安全的机制效率很低,所以已经被sychronizedMap
、ConcurrentMap
替代。
以数组为结构的CopyOnWriteArrayList
,以及其衍生出来的CopyOnWriteSet
,没有用到锁,属于效率高的线程安全集合。
队列有BlockingQueue
、ConcurrentLinkedQueue
。