什么是线程安全
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。
进程间交换数据
套接字,信号处理器,共享内存,信号量以及文件等。
竞态条件
在多线程环境下,getNext是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式。
读取-修改-写入
public class RaceCondition {
private int value;
public int getNext() {
return value++;
}
}
先检查后执行
public class LazyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}
注:由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使得他们得到相同的值
复合操作
为了确保线程安全,“先检查后执行”(例如延迟初始化)和"读取-修改-写入"(例如递增运算)等操作必须是原子的。我们将”先检查后执行“以及”读取-修改-写入“等操作统称为符合操作:包含了一组必须以原子方式执行的操作以确保线程安全。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量
//非线程安全
public class UnsafeCachingFactoryizer impelemtns Servlet{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger> lastFactors = new AtomicReference<BigInteger>();
public void service(ServletRequest req, ServletResponse res) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get()) {
encodeIntoResponse(res,lastFactors.get());
}else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(res,Factors);
}
}
}
注:在某种执行时序中,可能无法同时更新lastNumber和lastFactors。同样也无法保证会同时获取两个值。
线程切换
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文 切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度、而不是线程运行上。
Timer
Timer类的作用是使任务在稍后的时刻运行,或者运行一次,或者周期性地运行。TimerTask将在Timer管理的线程中执行,而不是由应用程序来管理。如果某个TimerTask访问了应用程序中其他线程访问的数据,那么TimerTask需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问数据。通常,要实现这个目标,最简单的方式是确保TimerTask访问的对象本身是线程安全的,从而就能把线程安全性封装在共享对象内部。
JAVA同步机制
Java中主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显示锁 (Explict Lock)以及原子变量。
加锁的含义
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
重入
内置锁时可以重入的,因此某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。
重排序
假设线程A需要看到线程B更新了number,ready,但实际上却只看到了其中一个值更新。
关于锁住哪个对象的问题
无论List使用哪个锁来保护它的状态,可以肯定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E e) {
boolean absent = !list.contains(e);
if(absent) {
list.add(e);
}
return absent;
}
}
- 正确的实现方式
要想使这个方法能正确的执行,必须使list在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端端代码,使用X本身用于保护其状态的锁来保护这段代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E e) {
synchronized (list) {
boolean absent = !list.contains(e);
if (absent) {
list.add(e);
}
return absent;
}
}
}
- synchronized
java1.6之前重量级锁机制
java1.6之后synchronized锁升级过程
并发编程三大特性:可见性,有序性,原子性
volatile
volatile可以保证可见性,有序性,但是无法保证原子性
- 可见性
- 有序性
计算机为了提高代码的执行效率,会对机器指令重排优化,volatile修饰的变量禁止指令重排。
内存屏障技术:lock前缀指令禁止重排序
指令重排遵循as-if-serial,happens-before语义
-
as-if-serial
-
happens-before
public class VolatileSerialTest {
volatile static int x = 0;
volatile static int y = 0;
public static void main(String[] args) throws InterruptedException {
Set<String> resultSet = new HashSet<>();
HashMap<String, Integer> resultMap = new HashMap<>();
for (int i = 0; i < 100000; i++) {
x = 0;
y = 0;
resultMap.clear();
Thread one = new Thread(() -> {
int a = y;
x = 1;
resultMap.put("a", a);
});
Thread other = new Thread(() -> {
int b = x;
y = 1;
resultMap.put("b", b);
});
one.start();
other.start();
one.join();
other.join();
resultSet.add("a=" + resultMap.get("a") + ":" + "b=" + resultMap.get("b"));
System.out.println(resultSet);
}
}
}
- 原子性
volatile修饰的共享变量无法保证原子性
public class VolatileAtomicTest {
private static volatile int num = 0;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println(num);
}
}
用来确保将变量的更新操作通知到其他线程,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作一起重排序,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会执行线程阻塞,因此volatile变量时一种比synchronized关键字更轻量级的同步机制。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
Java监视器模式
Java内置锁也称为监视器或监监视锁。
Java监视器模式实现方式一
public class Counter {
private long value = 0;
public synchronized long getValue() {
return value;
}
public synchronized long increatement() {
return ++value;
}
}
java监视器模式实现方式二
public class Counter {
private Object lock = new Object();
private long value = 0;
public long getValue() {
synchronized (lock) {
return value;
}
}
public long increatement() {
synchronized (lock) {
return ++value;
}
}
}
同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
synchronized(lock){
}
只要有数据在多个线程之间共享,就使用正确的同步
非线程安全
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run() {
while(!ready) {
Thread.yield();
System.out.println(number);
}
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
安全发布的常用模式
由于JVM 内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全发布。
public static Holder holder = new Holder(42);
栈封闭
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numberPairs = 0;
Animal cadidate = null;
//animals 被封闭在方法中,不要使它逸出
animals = new ThreeSet<Animal>(new SpeciesGenderComparator)());
animals.addAll(candidates);
return 0;
}
ThreadLocal类
闭锁
CountDownLatch
使用场景一: 并发执行测试
public void test(int nThread,final Runnable task) throws InterruptedException {
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(10);
for(int i=0 ;i<10;i++) {
Thread t = new Thread() {
public void run() {
try {
start.await();
} catch (InterruptedException e) {
}
task.run();
end.countDown();
}
};
t.start();
}
start.countDown();
end.await();
}
- LongAdder 分段CAS
LongAdder longAdder = new LongAdder();
FutureTask
FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。
public class PreLoader {
FutureTask<List<String>> future = new FutureTask<List<String>>(new Task());
Thread thread = new Thread(future);
public void start() {
thread.start();
}
public List<String> get() {
try {
return future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return new ArrayList<String>();
}
public static void main(String[] args) {
PreLoader pl = new PreLoader();
pl.start();
List<String> list = pl.get();
System.out.println(list.get(10000));
}
private static class Task implements Callable<List<String>>{
public List<String> call() throws Exception {
List<String> list = new ArrayList<String>();
for(int i=0;i<100000;i++) {
list.add(Integer.toString(i));
}
return list;
}
}
}
线程池
newFixedThreadPool
创建固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)
newCachedThreadPool
创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,将会回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
newSingleThreadPool
将创建一个单线程的Executor,它创建单个工作这线程来执行任务,如果这个线程一场结束,会创建另一个线程来代替。
newScheduledThreadPool
类似与Timer
ThreadPoolExecutor
ThreadPoolExecutor中各参数的含义:
corePoolSize:线程池中维持的线程个数,即使在线程闲置的情况
maximumPoolSize:线程池中最大的线程数
keepAliveTime:当线程池中的线程数超过corePoolSize时,闲置的线程可存活的时间
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Executor的生命周期
JVM只有在所有非守护线程全部终止后才会退出。因此,如果无法则会那个却的关闭Executor,那么JVM将无法结束。由于Executor以异步的方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的。有些任务可能已经完成,有些可能证字啊运行,而其他的任务可能在队列中等待执行。关闭应用程序时,可能采用平缓的关闭形式(完成所有已经启动的任务,并且不再接受任何新的任务)。ExecutorService的生命周期有三种状态:运行,关闭和已终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成。shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。在ExecutorService关闭后,提交的任务将由“拒绝执行处理器(Rejected Executor Handler)”来处理,他会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务完成后,ExecutorService将转入终止状态。
延迟任务与周期任务
Timeer类负责管理延迟任务(在100ms后执行任务)以及周期任务(没10ms执行一次该任务),然而,Timier存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。Timer在执行所有定时任务时指挥创建要给线程,如果某个任务执行时间过长,那么将会破环其他的TimerTask的定时精确性。Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因此当TimerTask抛出未检查异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误的认为整个Timer都被取消了。因此,已经被调用但尚未执行的TimerTask将不会再执行,新的任务也不能被调度(这个为题称之为线程泄漏(Thread Leakage))
Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统的时中变化很敏感,而ScheduledThreadPoolExecutor只支持基于相对时间的调度。
CompletionService
CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable的任务提交给它来执行,然后只用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
为任务设置时限
在有限时间内执行任务的主要困难在于,要确保得到答案的时间不会超过限定的时间,或者在限定的时间无法获得答案。在支持时间限制的Future.get中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
中断
通过中断来取消任务
public class BrokenPrimeProducer extends Thread{
private final BlockingQueue<BigInteger> queue;
public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
BigInteger p = BigInteger.ONE;
while(!Thread.currentThread().isInterrupted()) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void cancel() {
interrupt();
}
}
在Java的API或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑更大的应用。对中断操作的正确理解是:它并不会真正的中断一个正在运行的线程,而只是发出中断起你供求,然后由线程在下一个合适的时刻中断自己(这些时刻也称为取消点)。通常,中断是实现取消的最合理方式。
不可取消的任务在退出前恢复中断状态
如果不想或无法传递InterruptedException(或许通过Runnable来定义任务),那么需要寻找另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt来恢复中断状态。
public Integer getNextTask(BlockingQueue<Integer> queue) {
boolean interrupted = false;
try {
while(Boolean.TRUE) {
return queue.take();
}
} catch (Exception e) {
interrupted = false;
}finally {
if(interrupted) {
Thread.currentThread().interrupt();
}
}
return null;
}
通过Future来取消任务
public static void timeRun(Runnable r,long timeout,TimeUnit unit) throws InterruptedException{
ExecutorService taskExec = Executors.newFixedThreadPool(2);
Future<?> future = taskExec.submit(r);
try {
future.get(timeout, unit);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
//接下来任务将被额取消
}finally {
//如果任务已经结束,那么执行取消操作也不会带来任何影响
//如果任务正在运行,那么将被中断
future.cancel(true);
}
}
通过注册一个关闭钩子来停止服务
public void stop() {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
LogService.stop();
} catch (Exception e) {
//ignored
}
}
});
}
线程池配置与调优
只有在任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成“拥塞”。如果提交的任务依赖于其他任务,那么除非线程池无限大(确保他们依赖任务不会被放入等待队列中或被拒绝),否则将可能造成死锁。
如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池堵塞,甚至还会增加执行时间较短任务的服务时间。
对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。
配置ThreadPoolExecutor
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有三种:无界队列,有界队列和同步移交(Synchronized Handoff)。newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果任务持续快速地达到,并且超过了线程池的处理速度,那么队列将无限制的增加。
一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。在使用有界的工作队列时,如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。
使用信号量(Semaphore)来控制任务的提交速率
信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。可以用来实现某种资源池,或者对容器施加边界。
public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore;
public BoundedExecutor(Executor exec, Semaphore semaphore) {
this.exec = exec;
this.semaphore = semaphore;
}
public void submitTask(final Runnable cmd) throws InterruptedException {
semaphore.acquire();
try {
exec.execute(new Runnable() {
public void run() {
try {
cmd.run();
} finally {
semaphore.release();
}
}
});
} catch (Exception e) {
semaphore.release();
}
}
}
死锁的避免
如果一个程序每次至多只能获得一个锁,那么就不会 产生死锁,当然这种情况并不现实。但如果能能够避免这种情况,那么就能省去很多工作。还有一项技术可以检测死锁和从死锁中恢复过来,即显示使用Lock类中的定时tryLock功能来嗲提内置锁机制。显示锁可以指定一个超时时间,在等待超过该时间后tryLock会返回一个失败信息,如果超市时限比或取锁的时间要长的多,那么就可以在发生某个意外情况后重新获得控制权。
对性能的思考
与单线程相比,使用多个线程总会引入一些额外的性能开销。造成这些开销的操作包括:线程之间的协调(例如加锁,出发信号以及内存同步等),增加的上下文切换,线程的创建和销毁,以及线程的调度等。如果过度的使用线程,那么这些开销甚至会超过由于提高吞吐量、响应性或这计算能力锁带来的性能提升。
锁分段
锁分段的一个短处在于当ConcurrentHashMap(java1.7使用的是分段锁)需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合中时,就需要获取分段所有集合中的所有锁。(要获取内置所的一个集合,能采取的唯一方法就是递归)
一些代替独占锁的方法
使用并发容器、读-写锁、不可变对象以及原子变量。
ReadWriteLock实现了一种在多个读取操作以及单个写入操作情况下的加锁规则:如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行写入操作时必须以独占方式来获取锁。
通过把I/O操作从处理请求的线程转移到一个专门的线程,类似于两种不同的救火方案之间的差异:第一种方案时所有人排成一队,通过传递水桶来救火:第二中方案时每个人都拿着一个水桶去救火。在第二中方案中,每个人都可能在水源和着火点上存在更大的惊蛰(结果导致了只能将更少的水传递到着火点),此外救火的效率也更低,因为每个人都在不停的切换模式(装水、跑步、倒水、跑步。。。)。在第一中解决方案中,水不断的从水源传递到燃烧的建筑物,人们付出更少的体力却传递了更多的水,并且每个人从头至尾只需做一项工作。正如中断会干扰人们的工作并降低效率、阻塞和上下文切换同样会干扰线程的正常执行。
在构造函数中启动一个线程
如果在构造函数中启动一个下昵称,那么将可能带来子类化问题,同时还会导致this引用从构造函数中逸出。
显示锁
Java 5.0增加了一种新的机制:ReetrantLock。ReetrantLock并不是一种替代内置锁的方法,而是当内置锁机制不适用时,作为一种可选的高级功能。
AbstractQueuedSynchronizer(AQS)
AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易实现并且高效地构建出来。不仅ReentrantLock和Semaphore是基于AQS构建的,还包括CountDownLatch,ReetrantReadWriteLock,SynchronousQueue和FutureTask.
原子变量
原子变量采用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性。与基于锁的方案相比,非阻塞算法在设计和实现上都要复杂的多,但他们在可伸缩性和活跃性上却拥有巨大的优势。由于非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在力度更细的层次上进行协调,并且极大的减少调度开销。而且在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或者自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。
锁还存在一个缺点。当一个线程正在等待锁时,它不能做任何其他事情。
独占锁是一项悲观锁(它假设最坏的情况),对于细粒度的操作,还有另外一种更高效的方法,也是一种乐观的方法,通过这种方法剋在不发生干扰的情况下完成更新操作,如果存在,这个操作将失败,并且可以重试(也可以不重试)
CAS
CAS包含了3个操作数:需要读写的内存位置V,进行比较的值A和拟写入的新值B。CAS的含义是:我认为V的值应该是A,如果是那么将V的值更新为B,否则不修改并告诉你V的值实际为多少。
ABA问题
ABA问题是一种异常现象:如果在算法中的节点可以被循环利用,那么在使用“比较并交换”指令时就可能初夏你这种问题(主要在没有垃圾回收机制的环境中)。在CAS操作中将判断"V的值是否任然为A",并且如果时的化就继续操作。在大多数情况下,之中判断是完全足够的。然后,有时候还是需要知道“自从上次看到V的值为A以来,这个值是否发生了变化”,在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。一个相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号,即使这个值由A变为B,然后又变为A,版本号也将是不同的。AtomicStampedReference和AtomicMarkableReference支持在两个变量上执行原子的条件更新。
解决ABA问题
使用带有版本号的CAS,例如
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<Integer>(0,0);
安全的发布对象
提前初始化
public class EagerInitialization {
private static EagerInitialization instance = new EagerInitialization();
public static EagerInitialization getInstance() {
return instance;
}
}
延长初始化占位类模式
public class EagerInitializationFactory {
private static class EagerInitializationHolder{
private static EagerInitialization instance = new EagerInitialization();
}
public static EagerInitialization getInstance() {
return EagerInitializationHolder.instance;
}
public static void main(String[] args) {
EagerInitializationFactory.getInstance();
}
}
注意事项
当执行时间较长的计算或者可能无法快速完成的操作时(网络I/O或者控制台I/O),一定不要持有锁。