首先关于Volatile修饰符:
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个 不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟C中的一样 禁止编译器进行优化~~~~其次一个问题 Wait 和 Sleep的区别: 可以参考一下大名鼎鼎的*上对此问题的讨论吧:Difference between wait and sleep
1、这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2、使用目的:wait常与notfiy等一起使用用于多线程同步,而sleep单纯用于线程休眠,与线程同步没有关系
3、使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常, 这个也不正确,通过下面的5可以看到wait和sleep都需要处理interruptedException
5、sleep与锁没有关系,即使将sleep放入synchronized块中也不会对锁进行操作,而wait释放了锁,使得其他线程可以进入由该锁控制的其他同步块或同步方法。
synchronized(LOCK) {
Thread.sleep(1000); // LOCK still is held
}
synchronized(LOCK) {
LOCK.wait(); // LOCK is not held
}
首先看看JDK 7 中的关于sleep和wait的官方解释:
public static void sleep(long millis) throws InterruptedException
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.The thread does not lose ownership of any monitors.public final void wait() throws InterruptedException
Causes the current thread to wait until another thread invokes the
notify()
method or the notifyAll()
method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0)
.The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake upeither through a call to the notify
method the notifyAll
method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.“
官方介绍中的monitor其实可以理解为对象或类锁的概念。
sleep不出让系统资源,线程被调用时,占着CPU去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源 ;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。 上面这句话是错误的,Sleep和wait都会释放CPU资源,让CPU去做其他的事情,但是Sleep结束后,CPU是否马上得到执行,要看当时的CPU资源情况,而wait被notfiy唤醒后,进入就绪队列,继而等待CPU的处理。
6. 一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
在一个synchronized()的代码块中不一定要拥有wait(),当从该synchronized块中离开时就可以释放锁,不管是执行完毕还是return,throw等,从该块内调用方法是不会失去锁的
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
除了Sleep和wait的比较之外,还有一个Yield方法也很棘手
public static void yield()
A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.
Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilise a CPU. Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.
It is rarely appropriate to use this method. It may be useful for debugging or testing purposes, where it may help to reproduce bugs due to race conditions. It may also be useful when designing concurrency control constructs such as the ones in the java.util.concurrent.locks package.
yield和sleep的区别:
虽然两者都是静态方法,也同样是让自己放弃CPU,让给其他线程机会,但是它们之间还是有区别滴!sleep就是线程睡眠一定的时间,也就是交出cpu一段时间,yield用来暗示系统交出cpu控制权。这两个函数在多线程开发的时候特别有用,可以合理的分配cpu,提高程序的运行效率。对于sleep来说,有一个用法可以代替yield函数——sleep(0)。调用这个函数也相当于告诉CPU交出cpu的控制权。
1,sleep让给别人机会以后,自己处于阻塞状态,而yield处于就绪状态
2,sleep方法使用时要抛出异常,而yield什么都不做。
3,sleep会不考虑优先级的给其他线程机会,而yield是考虑同等优先级地给予其他线程机会!
(可以类比为:一般情况下,同班同学才能让给他“扫把”使用,如果是外校的人,那么只有等我睡着了,才会有让让他得到“扫把”的可能,因为优先级不一样)
4,sleep的可移植性比yield好,不能靠yield来提高程序的并发性能,它的唯一用途是在测试期间
人为地提高程序的并发性能,以帮助发现一些隐藏的错误。
还有Join() 从字面上来看,join是加入的意思,可以理解为加入其他线程。但是,实际上,它的功能是主线程调用了Thread_Yield_jion线程的join方法,自己等待,等到Thread_Yield_jion执行完毕之后,主线程才能继续向下执行
public final void join(long millis) throws InterruptedExceptionWaits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever.
This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.
探索并发编程
基于线程安全的一些原则来编程当然可以避免并发问题,但不是所有人都能写出高质量的线程安全的代码,并且如果代码里到处都是线程安全的控制也极大地影响了代码可读性和可维护性。因此,Java平台为了解决这个问题,提供了很多线程安全的类和并发工具,通过这些类和工具就能更简便地写线程安全的代码。归纳一下有以下几种:
- 同步容器类
- 并发容器类
- 生产者和消费者模式
- 阻塞和可中断方法
- Synchronizer
这些类和方法的使用都可以从JDK DOC查到,但在具体使用中还是有很多问题需要注意
同步容器类
同步容器类就是一些经过同步处理了的容器类,比如List有Vector,Map有Hashtable,查看其源码发现其保证线程安全的方式就是把每个对外暴露的存取方法用synchronized关键字同步化,这样做我们立马会想到有以下问题:
1)性能有问题
同步化了所有存取方法,就表明所有对这个容器对象的操作将会串行,这样做来得倒是干净,但性能的代价也是很可观的
2)复合操作问题
同步容器类只是同步了单一操作,如果客户端是一组复合操作,它就没法同步了,依然需要客户端做额外同步,比如以下代码:
[java] view plaincopy
1. public static Object getLast(Vector list) {
2. int lastIndex = list.size() - 1;
3. return list.get(lastIndex);
4. }
5. public static void deleteLast(Vector list) {
6. int lastIndex = list.size() - 1;
7. list.remove(lastIndex);
8. }
getLast和deleteLast都是复合操作,由先前对原子性的分析可以判断,这依然存在线程安全问题,有可能会抛出ArrayIndexOutOfBoundsException的异常,错误产生的逻辑如下所示:
解决办法就是通过对这些复合操作加锁
3)迭代器并发问题
Java Collection进行迭代的标准时使用Iterator,无论是使用老的方式迭代循环,还是Java5提供for-each新方式,都需要对迭代的整个过程加锁,不然就会有Concurrentmodificationexception异常抛出。
此外有些迭代也是隐含的,比如容器类的toString方法,或containsAll, removeAll, retainAll等方法都会隐含地对容器进行迭代
并发容器类
正是由于同步容器类有以上问题,导致这些类成了鸡肋,于是Java5推出了并发容器类,Map对应的有ConcurrentHashMap,List对应的有CopyOnWriteArrayList。与同步容器类相比,它有以下特性:
- 更加细化的锁机制。同步容器直接把容器对象做为锁,这样就把所有操作串行化,其实这是没必要的,过于悲观,而并发容器采用更细粒度的锁机制,保证一些不会发生并发问题的操作进行并行执行
- 附加了一些原子性的复合操作。比如putIfAbsent方法
- 迭代器的弱一致性。它在迭代过程中不再抛出Concurrentmodificationexception异常,而是弱一致性。在并发高的情况下,有可能size和isEmpty方法不准确,但真正在并发环境下这些方法也没什么作用。
- CopyOnWriteArrayList采用写入时复制的方式避开并发问题。这其实是通过冗余和不可变性来解决并发问题,在性能上会有比较大的代价,但如果写入的操作远远小于迭代和读操作,那么性能就差别不大了
生产者和消费者模式
大学时学习操作系统多会为生产者和消费者模式而头痛,也是每次考试肯定会涉及到的,而Java知道大家很憷这个模式的并发复杂性,于是乎提供了阻塞队列(BlockingQueue)来满足这个模式的需求。阻塞队列说起来很简单,就是当队满的时候写线程会等待,直到队列不满的时候;当队空的时候读线程会等待,直到队不空的时候。实现这种模式的方法很多,其区别也就在于谁的消耗更低和等待的策略更优。 以ArrayBlockingQueue为例,来看一下源码,ArrayBlockingQueue源码解析。
在此有几点疑问:对于普通的Queue,put/get 和offer/poll的区别是前者对抛出异常,而后者返回特殊值,对于BlockqingQueue呢,当为空或已满的情况会怎样呢?
撇开其锁的具体实现,其流程就是我们在操作系统课上学习到的标准生产者模式。
阻塞和可中断方法
由LinkedBlockingQueue的put方法可知,它是通过线程的阻塞和中断阻塞来实现等待的。当调用一个会抛出InterruptedException的方法时,就成为了一个阻塞的方法,要为响应中断做好准备。处理中断可有以下方法:
- 传递InterruptedException。把捕获的InterruptedException再往上抛,使其调用者感知到,当然在抛之前需要完成你自己应该做的清理工作,LinkedBlockingQueue的put方法就是采取这种方式
- 中断其线程。在不能抛出异常的情况下,可以直接调用Thread.interrupt()将其中断。
Synchronizer
Synchronizer不是一个类,而是一种满足一个种规则的类的统称。它有以下特性:
- 它是一个对象
- 封装状态,而这些状态决定着线程执行到某一点是通过还是*等待
- 提供操作状态的方法
其实BlockingQueue就是一种Synchronizer。Java还提供了其他几种Synchronizer
1)CountDownLatch
CountDownLatch是一种闭锁,它通过内部一个计数器count来标示状态,当count>0时,所有调用其await方法的线程都需等待,当通过其countDown方法将count降为0时所有等待的线程将会被唤起。使用实例如下所示:
[java] view plaincopy
1. public class TestHarness {
2. public long timeTasks(int nThreads, final Runnable task)
3. throws InterruptedException {
4. final CountDownLatch startGate = new CountDownLatch(1);
5. final CountDownLatch endGate = new CountDownLatch(nThreads);
6. for (int i = 0; i < nThreads; i++) {
7. Thread t = new Thread() {
8. public void run() {
9. try {
10. startGate.await();
11. try {
12. task.run();
13. } finally {
14. endGate.countDown();
15. }
16. } catch (InterruptedException ignored) { }
17. }
18. };
19. t.start();
20. }
21. long start = System.nanoTime();
22. startGate.countDown();
23. endGate.await();
24. long end = System.nanoTime();
25. return end-start;
26. }
27. }
2)Semaphore
Semaphore类实际上就是操作系统中谈到的信号量的一种实现,其原理就不再累述,可见探索并发编程------操作系统篇
A counting semaphore.Conceptually, a semaphore maintains a set of permits. Eachacquire()
blocks if necessary until a permit is available, and then takes it. Eachrelease()
adds a permit, potentially releasing a blocking acquirer. However, no actualpermit objects are used; theSemaphore
just keeps a count of thenumber available and acts accordingly.
Semaphores are often used to restrict the number of threads than can accesssome (physical or logical) resource. For example, here is a class that uses a semaphore to control access to apool of items:
通过semaphore的构造方法可以确定所有权限的最大个数,使用Semaphore的acquire()方法(无参数)可以获得一个permit,只要线程获取的次数<创建的个数就无需阻塞,如果超过构造时最大的个数,就进行阻塞,而semaphore的release()方法进行释放一个permit。Acquire()发生release()之前,但并没有一个permit对象供acquire和release,只有一个semaphore对象进行管理。
class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
public void putItem(Object x) {
if (markAsUnused(x))// 标记可用,进而释放锁
available.release();
}
// Not a particularly efficient data structure; just for demo
protected Object[] items = ... whatever kinds of items being managed
protected boolean[] used = new boolean[MAX_AVAILABLE];
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (!used[i]) {
used[i] = true;
return items[i];
}
}
return null; // not reached
}
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (item == items[i]) {
if (used[i]) {
used[i] = false;
return true;
} else
return false;
}
}
return false;
}
}
Before obtaining an item eachthread must acquire a permit from the semaphore, guaranteeing that an item isavailable for use. When the thread has finished with the item it is returnedback to the pool and a permit is returned to the semaphore, allowing anotherthread to acquire that item. Note that no synchronization lock is held when acquire()
is called as that would prevent an item from being returned to the pool. Thesemaphore encapsulates the synchronization needed to restrict access to thepool, separately from any synchronization needed to maintain the consistency ofthe pool itself.
A semaphore initialized to one,and which is used such that it only has at most one permit available, can serveas a mutual exclusion lock. This is more commonly known as abinary semaphore,because it only has two states: one permit available, or zero permitsavailable. When used in this way, the binary semaphore has the property (unlikemanyLock
implementations), that the "lock" can be released by a thread otherthan the owner (assemaphores have no notion of ownership). This can be useful in somespecialized contexts, such as deadlock recovery.
(3)关卡
关卡和闭锁类似,也是阻塞一组线程,直到某件事情发生,而不同在于关卡是等到符合某种条件的所有线程都达到关卡点。具体使用上可以用CyclicBarrier来应用关卡
以上是Java提供的一些并发工具,既然是工具就有它所适用的场景,因此需要知道它的特性,这样才能在具体场景下选择最合适的工具。
分类: 技术积累2010-08-0416:58 7244人阅读 评论(3) 收藏 举报
很多开发者谈到Java多线程开发,仅仅停留在new Thread(...).start()或直接使用Executor框架这个层面,对于线程的管理和控制却不够深入,通过读《Java并发编程实践》了解到了很多不为我知但又非常重要的细节,今日整理如下。
不应用线程池的缺点
有些开发者图省事,遇到需要多线程处理的地方,直接newThread(...).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:
- 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
- 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的
- 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题
制定执行策略
在每个需要多线程处理的地方,不管并发量有多大,需要考虑线程的执行策略
- 任务以什么顺序执行
- 可以有多少个任何并发执行《=控制多少个并发执行,应该是使用线程池的最大个数
- 可以有多少个任务进入等待执行队列 《= 控制多少个可以进入等待队列?怎样控制呢?
- 系统过载的时候,应该放弃哪些任务?如何通知到应用程序?《=系统过载时如何放弃任务?怎样通知应用程序?
- 一个任务的执行前后应该做什么处理 《= 任务的执行应该确保对资源的占用和释放
线程池的类型
不管是通过Executors创建线程池,还是通过Spring来管理,都得清楚知道有哪几种线程池:
- FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程
- CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制
- SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行
- SecheduledThreadPool:周期性线程池。支持执行周期性线程任务
其实,这些不同类型的线程池都是通过构建一个ThreadPoolExecutor来完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。具体可以参见JDK DOC。
The Executor implementations provided in this packageimplementExecutorService
,which is a more extensive interface. TheThreadPoolExecutor
class provides an extensible thread pool implementation. TheExecutors
class provides convenient factory methods for these Executors.
ExecutorService:An Executor
thatprovides methods to manage termination and methods that can produce aFuture
for trackingprogress of one or more asynchronous tasks. Methodsubmit extends base methodExecutor.execute(java.lang.Runnable)
by creating and returning aFuture
that can beused to cancel execution and/or wait for completion. MethodsinvokeAny andinvokeAll performthe most commonly useful forms of bulk execution, executing a collection oftasks and then waiting for at least one, or all, to complete. Theshutdown()
method will allow previously submitted tasks to execute before terminating,while theshutdownNow()
method prevents waiting tasks from starting and attempts to stop currentlyexecuting tasks. 使用ExecutorService对象进行submit的时候可以返回代表执行线程的Future对象用来进行判断是否结束或进行取消。但是如果要想得到执行线程的执行结果,执行线程或者实现callable接口,或者使用submit(runnable,result)在成功完成的后将返回result对象,如果不实现callable接口,不指定result,则future执行成功后将返回null.而invokeAll和invokeAny的参数Collection都实现了Callable接口。
public class Executors
extends Object
Factory and utility methods for Executor, ExecutorService,ScheduledExecutorService,ThreadFactory, andCallable classes defined in thispackage. This class supports the following kinds of methods:
- Methods that create and return an ExecutorService set up with commonly useful configuration settings.
- Methods that create and return a ScheduledExecutorService set up with commonly useful configuration settings.
- Methods that create and return a "wrapped" ExecutorService, that disables reconfiguration by making implementation-specific methods inaccessible.
- Methods that create and return a ThreadFactory that sets newly created threads to a known state.
- Methods that create and return a Callable out of other closure-like forms, so they can be used in execution methods requiring Callable.
即通过Executors工程类方法可以很方便的创建FixedThreadPool, CachedThreadPool, SingleThreadExecutor, ScheduledThreadPool等,除了制定PoolSize,还可以支持ThreadFacotory用来以工厂方法创建所需线程。
ThreadLocal:This class provides thread-localvariables. These variables differ from their normal counterparts in that eachthread that accesses one (via itsget orset method) has itsown, independently initialized copy of the variable.ThreadLocal instances are typically privatestatic fields in classes that wish to associate state with a thread(e.g., a user ID or Transaction ID).
For example, the class belowgenerates unique identifiers local to each thread. A thread's id is assignedthe first time it invokesThreadId.get() and remains unchanged onsubsequent calls.
如下例可知,ThreadLocal 实例是在线程要访问到的类中进行初始化的,且初始化时只要创建ThreadLocal类即可,覆盖inintialValues方法。那么线程在访问该类时即得到一个线程局部变量。
import java.util.concurrent.atomic.AtomicInteger;(注意Int类型Int++不是线程安全的,有个AtomicInteger及系列操作类可以进行原子操作)
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
下面是Hibernate中ThreadLocal的应用,然后再线程访问含有ThreadLocal的变量后,而每个线程都会有自己的ThreadLocalMap,会自动将该ThreadLocal变量放到该Map中。在ThreadLocal的set实现中,通过调用Thread.getCurrentThread()得到当前的线程,然后将set的变量放到线程的ThreadLocalMap中。
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away,all of its copies of thread-local instances are subject to garbage collection(unless other references to these copies exist).
由于int++之类的,并非线程安全的,原因可以查看 本博中关于i++非线程安全的解释。所有才有了原子整型之类的这样类,AtomicInteger::
addAndGet(int delta):
Atomically adds the given value to the current value.
compareAndSet(int expect, int update)
:Atomically sets the value to the given updated value if the currentvalue==
the expected value. Return false: != expect.
decrementAndGet()
:Atomicallydecrements by one the current value.
getAndAdd(int delta)
: Atomically adds the given value to the current value.
getAndDecrement()
: Atomicallydecrements by one the current value.
getAndIncrement()
:Atomicallyincrements by one the current value.
getAndSet(int newValue)
: Atomically sets to the given value and returns the old value.
incrementAndGet()
: Atomicallyincrements by one the current value.
关于AtomicInteger各个方法的记忆点是compare,get,set,Add,increment,decrement
ThreadPoolExecutor: An ExecutorService
that executes each submitted task using one of possibly several pooled threads,normally configured usingExecutors
factorymethods.
Thread pools address two different problems: they usually provide improved performance when executinglarge numbers of asynchronous tasks, due to reduced per-task invocationoverhead, and they provide a means of bounding and managing the resources,including threads, consumed when executing a collection of tasks. Each ThreadPoolExecutor
also maintains some basic statistics, such as the number of completed tasks.
To be useful across a wide range of contexts, this class provides many adjustable parameters andextensibility hooks. However, programmers are urged to use the more convenientExecutors
factory methodsExecutors.newCachedThreadPool()
(unbounded thread pool, with automatic thread reclamation),Executors.newFixedThreadPool(int)
(fixed size thread pool) andExecutors.newSingleThreadExecutor()
(single background thread), that preconfigure settings for the most commonusage scenarios. 从中看出ThreadPoolExecutor比Executors提供了更详细的调优的参数,但是不如后者更加方便的使用。
Keep-alive times
If thepool currently has more than corePoolSize threads, excess threads will beterminated if they have been idle for more than the keepAliveTime (seegetKeepAliveTime(java.util.concurrent.TimeUnit)).
Queuing
Any BlockingQueue may be used to transfer and hold submitted tasks. The use of this queue interacts with pool sizing:
· If fewer than corePoolSize threadsare running, the Executor always prefers adding a new thread rather thanqueuing.
· If corePoolSize or more threads arerunning, the Executor always prefers queuing a request rather than adding a newthread.
· If a request cannot be queued, a newthread is created unless this would exceed maximumPoolSize, in which case, thetask will be rejected.
There are three general strategies for queuing:
1. Direct handoffs. A good default choice for a work queue is a SynchronousQueue that hands off tasks to threads without otherwise holding them. Here, an attempt to queue a task will fail if no threads are immediately available to run it, so a new thread will be constructed. This policy avoids lockups when handling sets of requests that might have internal dependencies. Direct handoffs generally require unbounded maximum PoolSizes to avoid rejection of new submitted tasks. This in turn admits the possibility of unbounded thread growth when commands continue to arrive on average faster than they can be processed.
2. Unbounded queues. Using an unbounded queue (forexample a LinkedBlockingQueue without a predefined capacity) will cause new tasks to wait in the queue when all corePoolSize threads are busy. Thus, no more than corePoolSize threads will ever be created. (And the value of the maximum PoolSize therefore doesn't have any effect.) This may be appropriate when each task is completely independentof others, so tasks cannot affect each others execution; for example, in a webpage server. While this style of queuing can be useful in smoothing outtransient bursts of requests, it admits the possibility of unbounded work queuegrowth when commands continue to arrive on average faster than they can beprocessed.
3. Bounded queues. A bounded queue (for example, an ArrayBlockingQueue) helps preventresource exhaustion when used with finite maximumPoolSizes, but can be moredifficult to tune and control. Queue sizes and maximum pool sizes may be tradedoff for each other: Using large queues and small pools minimizes CPU usage, OSresources, and context-switching overhead, but can lead to artificially low throughput.If tasks frequently block (for example if they are I/O bound), a system may beable to schedule time for more threads than you otherwise allow. Use of smallqueues generally requires larger pool sizes, which keeps CPUs busier but mayencounter unacceptable scheduling overhead, which also decreases throughput.
Rejected tasks
New taskssubmitted in method execute(java.lang.Runnable)will berejected when the Executor has been shut down, and also when theExecutor uses finite bounds for both maximum threads and work queue capacity,and is saturated. In either case, the execute method invokes theRejectedExecutionHandler.rejectedExecution(java.lang.Runnable,java.util.concurrent.ThreadPoolExecutor) method of itsRejectedExecutionHandler. Fourpredefined handler policies are provided:
1. In the default ThreadPoolExecutor.AbortPolicy, thehandler throws a runtime RejectedExecutionException uponrejection.
2. In ThreadPoolExecutor.CallerRunsPolicy,the thread that invokes execute itself runs the task. This provides a simplefeedback control mechanism that will slow down the rate that new tasks aresubmitted.
3. In ThreadPoolExecutor.DiscardPolicy, atask that cannot be executed is simply dropped.
4. In ThreadPoolExecutor.DiscardOldestPolicy,if the executor is not shut down, the task at the head of the work queue isdropped, and then execution is retried (which can fail again, causing this tobe repeated.)
It is possibleto define and use other kinds of RejectedExecutionHandler classes.Doing so requires some care especially when policies are designed to work onlyunder particular capacity or queuing policies.
Hook methods
This class provides protected overridable beforeExecute(java.lang.Thread,java.lang.Runnable) andafterExecute(java.lang.Runnable,java.lang.Throwable) methods that are called before and after execution ofeach task. These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals, gathering statistics, or adding logentries. Additionally, method terminated() can be overridden to perform any special processing that needs to be done once the Executor has fully terminated.
If hook or callback methods throw exceptions, internal worker threads may in turn fail and abruptly terminate.
Queue maintenance
Method getQueue()allows access to the work queue for purposes of monitoring and debugging. Useof this method for any other purpose is strongly discouraged. Two suppliedmethods,remove(java.lang.Runnable)andpurge()are available to assist in storage reclamation when large numbers of queuedtasks become cancelled.
Finalization
A poolthat is no longer referenced in a program AND has no remaining threadswill be shutdown automatically. If you would like to ensure that unreferencedpools are reclaimed even if users forget to callshutdown(),then you must arrange that unused threads eventually die, by settingappropriate keep-alive times, using a lower bound of zero core threads and/orsettingallowCoreThreadTimeOut(boolean).
线程池饱和策略
由以上线程池类型可知,除了CachedThreadPool其他线程池都有饱和的可能,当饱和以后就需要相应的策略处理请求线程的任务,ThreadPoolExecutor采取的方式通过队列来存储这些任务,当然会根据池类型不同选择不同的队列,比如FixedThreadPool和SingleThreadExecutor默认采用的是无限长度的LinkedBlockingQueue。但从系统可控性讲,最好的做法是使用定长的ArrayBlockingQueue或有限的LinkedBlockingQueue,并且当达到上限时通过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略,具体差异可见JDKDOC
线程无依赖性
多线程任务设计上尽量使得各任务是独立无依赖的,所谓依赖性可两个方面:
- 线程之间的依赖性。如果线程有依赖可能会造成死锁或饥饿
- 调用者与线程的依赖性。调用者得监视线程的完成情况,影响可并发量
当然,在有些业务里确实需要一定的依赖性,比如调用者需要得到线程完成后结果,传统的Thread是不便完成的,因为run方法无返回值,只能通过一些共享的变量来传递结果,但在Executor框架里可以通过Future和Callable实现需要有返回值的任务,当然线程的异步性导致需要有相应机制来保证调用者能等待任务完成,关于Future和Callable的用法见下面的实例就一目了然了:
[java] view plaincopy
1. public class FutureRenderer {
2. private final ExecutorService executor = ...;
3. void renderPage(CharSequence source) {
4. final List<ImageInfo> imageInfos = scanForImageInfo(source);
5. Callable<List<ImageData>> task =
6. new Callable<List<ImageData>>() {
7. public List<ImageData> call() {
8. List<ImageData> result
9. = new ArrayList<ImageData>();
10. for (ImageInfo imageInfo : imageInfos)
11. result.add(imageInfo.downloadImage());
12. return result;
13. }
14. };
15. Future<List<ImageData>> future = executor.submit(task);
16. renderText(source);
17. try {
18. List<ImageData> imageData = future.get();
19. for (ImageData data : imageData)
20. renderImage(data);
21. } catch (InterruptedException e) {
22. // Re-assert the thread's interrupted status
23. Thread.currentThread().interrupt();
24. // We don't need the result, so cancel the task too
25. future.cancel(true);
26. } catch (ExecutionException e) {
27. throw launderThrowable(e.getCause());
28. }
29. }
30. }
以上代码关键在于List<ImageData>imageData = future.get();如果Callable类型的任务没有执行完时,调用者会阻塞等待。不过这样的方式还是得谨慎使用,很容易造成不良设计。另外对于这种需要等待的场景,就需要设置一个最大容忍时间timeout,设置方法可以在 future.get()加上timeout参数,或是再调用ExecutorService.invokeAll加上timeout参数
线程的取消与关闭
一般的情况下是让线程运行完成后自行关闭,但有些时候也会中途取消或关闭线程,比如以下情况:
- 调用者强制取消。比如一个长时间运行的任务,用户点击"cancel"按钮强行取消
- 限时任务
- 发生不可处理的任务
- 整个应用程序或服务的关闭
因此需要有相应的取消或关闭的方法和策略来控制线程,一般有以下方法:
1)通过变量标识来控制
这种方式比较老土,但使用得非常广泛,主要缺点是对有阻塞的操作控制不好,代码示例如下所示:
[java] view plaincopy
1. public class PrimeGenerator implements Runnable {
2. @GuardedBy("this")
3. private final List<BigInteger> primes
4. = new ArrayList<BigInteger>();
5. private volatile boolean cancelled;
6. public void run() {
7. BigInteger p = BigInteger.ONE;
8. while (!cancelled ) {
9. p = p.nextProbablePrime();
10. synchronized (this) {
11. primes.add(p);
12. }
13. }
14. }
15. public void cancel() { cancelled = true; }
16. public synchronized List<BigInteger> get() {
17. return new ArrayList<BigInteger>(primes);
18. }
19. }
2)中断
中断通常是实现取消最明智的选择,但线程自身需要支持中断处理,并且要处理好中断策略,一般响应中断的方式有两种:
- 处理完中断清理后继续传递中断异常(InterruptedException)
- 调用interrupt方法,使得上层能感知到中断异常
3) 取消不可中断阻塞
存在一些不可中断的阻塞,比如:
- java.io和java.nio中同步读写IO
- Selector的异步IO
- 获取锁
对于这些线程的取消,则需要特定情况特定对待,比如对于socket阻塞,如果要安全取消,则需要调用socket.close()
4)JVM的关闭
如果有任务需要在JVM关闭之前做一些清理工作,而不是被JVM强硬关闭掉,可以使用JVM的钩子技术,其实JVM钩子也只是个很普通的技术,也就是用个map把一些需要JVM关闭前启动的任务保存下来,在JVM关闭过程中的某个环节来并发启动这些任务线程。具体使用示例如下:
[java] view plaincopy
1. public void start() {
2. Runtime.getRuntime().addShutdownHook(new Thread() {
3. public void run() {
4. try { LogService.this.stop(); }
5. catch (InterruptedException ignored) {}
6. }
7. });
8. }
大家使用多线程无非是为了提高性能,但如果多线程使用不当,不但性能提升不明显,而且会使得资源消耗更大。下面列举一下可能会造成多线程性能问题的点:
- 死锁
- 过多串行化
- 过多锁竞争
- 切换上下文
- 内存同步
下面分别解析以上性能隐患
死锁
关于死锁,我们在学习操作系统的时候就知道它产生的原因和危害,这里就不从原理上去累述了,可以从下面的代码和图示重温一下死锁产生的原因:
[java] view plaincopy
1. public class LeftRightDeadlock {
2. private final Object left = new Object();
3. private final Object right = new Object();
4. public void leftRight() {
5. synchronized (left) {
6. synchronized (right) {
7. doSomething();
8. }
9. }
10. }
11. public void rightLeft() {
12. synchronized (right) {
13. synchronized (left) {
14. doSomethingElse();
15. }
16. }
17. }
18. }
预防和处理死锁的方法:
1)尽量不要在释放锁之前竞争其他锁
一般可以通过细化同步方法来实现,只在真正需要保护共享资源的地方去拿锁,并尽快释放锁,这样可以有效降低在同步方法里调用其他同步方法的情况
2)顺序索取锁资源
如果实在无法避免嵌套索取锁资源,则需要制定一个索取锁资源的策略,先规划好有哪些锁,然后各个线程按照一个顺序去索取,不要出现上面那个例子中不同顺序,这样就会有潜在的死锁问题
3)尝试定时锁
Java 5提供了更灵活的锁工具,可以显式地索取和释放锁。那么在索取锁的时候可以设定一个超时时间,如果超过这个时间还没索取到锁,则不会继续堵塞而是放弃此次任务,示例代码如下:
[java] view plaincopy
1. public boolean trySendOnSharedLine(String message,
2. long timeout, TimeUnit unit)
3. throws InterruptedException {
4. long nanosToLock = unit.toNanos(timeout)
5. - estimatedNanosToSend(message);
6. if (!lock.tryLock(nanosToLock, NANOSECONDS))
7. return false;
8. try {
9. return sendOnSharedLine(message);
10. } finally {
11. lock.unlock();
12. }
13. }
这样可以有效打破死锁条件。
4)检查死锁
JVM采用threaddump的方式来识别死锁的方式,可以通过操作系统的命令来向JVM发送thread dump的信号,这样可以查询哪些线程死锁。
过多串行化
用多线程实际上就是想并行地做事情,但这些事情由于某些依赖性必须串行工作,导致很多环节得串行化,这实际上很局限系统的可扩展性,就算加CPU加线程,但性能却没有线性增长。有个Amdahl定理可以说明这个问题:
其中,F是串行化比例,N是处理器数量,由上可知,只有尽可能减少串行化,才能最大化地提高可扩展能力。降低串行化的关键就是降低锁竞争,当很多并行任务挂在锁的获取上,就是串行化的表现
过多锁竞争
过多锁竞争的危害是不言而喻的,那么看看有哪些办法来降低锁竞争
1)缩小锁的范围
前面也谈到这一点,尽量缩小锁保护的范围,快进快出,因此尽量不要直接在方法上使用synchronized关键字,而只是在真正需要线程安全保护的地方使用
2)减小锁的粒度
Java 5提供了显式锁后,可以更为灵活的来保护共享变量。synchronized关键字(用在方法上)是默认把整个对象作为锁,实际上很多时候没有必要用这么大一个锁,这会导致这个类所有synchronized都得串行执行。可以根据真正需要保护的共享变量作为锁,也可以使用更为精细的策略,目的就是要在真正需要串行的时候串行,举一个例子:
[java] view plaincopy
1. public class StripedMap {
2. // Synchronization policy: buckets[n] guarded by locks[n%N_LOCKS]
3. private static final int N_LOCKS = 16;
4. private final Node[] buckets;
5. private final Object[] locks;
6. private static class Node { ... }
7. public StripedMap(int numBuckets) {
8. buckets = new Node[numBuckets];
9. locks = new Object[N_LOCKS];
10. for (int i = 0; i < N_LOCKS; i++)
11. locks[i] = new Object();
12. }
13. private final int hash(Object key) {
14. return Math.abs(key.hashCode() % buckets.length);
15. }
16. public Object get(Object key) {
17. int hash = hash(key);
18. synchronized (locks[hash % N_LOCKS]) {
19. for (Node m = buckets[hash]; m != null; m = m.next)
20. if (m.key.equals(key))
21. return m.value;
22. }
23. return null;
24. }
25. public void clear() {
26. for (int i = 0; i < buckets.length; i++) {
27. synchronized (locks[i % N_LOCKS]) {
28. buckets[i] = null;
29. }
30. }
31. }
32. ...
33. }
上面这个例子是通过hash算法来把存取的值所对应的hash值来作为锁,这样就只需要对hash值相同的对象存取串行化,而不是像HashTable那样对任何对象任何操作都串行化。
3)减少共享资源的依赖
共享资源是竞争锁的源头,在多线程开发中尽量减少对共享资源的依赖,比如对象池的技术应该慎重考虑,新的JVM对新建对象以做了足够的优化,性能非常好,如果用对象池不但不能提高多少性能,反而会因为锁竞争导致降低线程的可并发性。
4)使用读写分离锁来替换独占锁
Java 5提供了一个读写分离锁(ReadWriteLock)来实现读-读并发,读-写串行,写-写串行的特性。这种方式更进一步提高了可并发性,因为有些场景大部分是读操作,因此没必要串行工作。关于ReadWriteLock的具体使用可以参加一下示例:
[java] view plaincopy
1. public class ReadWriteMap<K,V> {
2. private final Map<K,V> map;
3. private final ReadWriteLock lock = new ReentrantReadWriteLock();
4. private final Lock r = lock.readLock();
5. private final Lock w = lock.writeLock();
6. public ReadWriteMap(Map<K,V> map) {
7. this.map = map;
8. }
9. public V put(K key, V value) {
10. w.lock();
11. try {
12. return map.put(key, value);
13. } finally {
14. w.unlock();
15. }
16. }
17. // Do the same for remove(), putAll(), clear()
18. public V get(Object key) {
19. r.lock();
20. try {
21. return map.get(key);
22. } finally {
23. r.unlock();
24. }
25. }
26. // Do the same for other read-only Map methods
27. }
切换上下文
线程比较多的时候,操作系统切换线程上下文的性能消耗是不能忽略的,在构建高性能web之路------web服务器长连接 可以看出在进程切换上的代价,当然线程会更轻量一些,不过道理是类似的
内存同步
当使用到synchronized、volatile或Lock的时候,都会为了保证可见性导致更多的内存同步,这就无法享受到JMM结构带来了性能优化。
Reentrant: 凹角的,重入的
ReentrantLock:A reentrant mutual exclusion Lock
with the same basic behavior and semantics asthe implicit monitor lock accessed using synchronized
methods and statements, but with extendedcapabilities[Such as the fairness parameter]. AReentrantLock
isowned by the thread lastsuccessfully locking, but not yet unlocking it. (This is not same with the Semaphore and CountDownLatch).The constructor for this class accepts an optional fairness parameter.When set true
, under contention, locks favor granting access to the longest-waiting thread.Otherwise this lock does not guarantee any particular access order.
hasQueuedThreads()
:Queries whether any threads are waiting to acquire this lock.
tryLock(long timeout,TimeUnit unit)
: Acquires the lock if it isnot held by another thread within the given waiting time and the current threadhas not beeninterrupted.There is another method with no parameters
unlock()
: Attempts to release this lock.
Lock
implementations provide more extensivelocking operations than can be obtained using synchronized
methodsand statements. They allow more flexible structuring, may have quite differentproperties, and may support multiple associated Condition
objects.
A lock is a tool for controlling access to ashared resource by multiple threads. Commonly, a lock provides exclusive accessto a shared resource: only one thread at a time can acquire the lock and allaccess to the shared resource requires that the lock be acquired first.However, some locks may allow concurrent access to a shared resource, such asthe read lock of a ReadWriteLock
.
The use of synchronized
methods or statements provides access to the implicit monitor lock associatedwith every object, butforcesall lock acquisition and release to occur in a block-structured way:when multiple locks are acquired they must be released in the opposite order,and all locks must be released in the same lexical scope in which they wereacquired. 即使用synchronized时 lock的顺序是固定的,而使用lock对象,可以在任何地方引用lock,进入调用其上锁和释放的操作。
When locking and unlocking occur indifferent scopes, care must be taken to ensure that all code that is executed while the lock is held is protected by try-finally or try-catch to ensure thatthe lock is released when necessary.
Lock implementations provide additional functionality over the use of synchronized methods and statements by providing a non-blocking attempt to acquire a lock (tryLock()),an attempt to acquire the lock that can be interrupted (lockInterruptibly(),and an attempt to acquire the lock that can timeout (tryLock(long,TimeUnit)).
The three forms of lock acquisition (interruptible, non-interruptible, and timed) may differ in their performance characteristics, ordering guarantees, or other implementationqualities.
ReentrantReadWriteLock:An implementation of ReadWriteLock
supporting similar semantics toReentrantLock
.
This class has the followingproperties:
- Acquisition order
This class does not impose a reader or writer preference ordering for lock access. However, it does support an optional fairness policy.
- Reentrancy
This lock allows both readers and writers to reacquire read or write locks in the style of a ReentrantLock. Non-reentrant readers are not allowed until all write locks held by the writing thread have been released. 即非reentrancy锁只允许读操作在写锁被释放之后才能进行。
Additionally, a writer can acquire the read lock, but not vice-versa. Among other applications, reentrancy can be useful when write locks are held during calls or callbacks to methods that perform reads under read locks. If a reader tries to acquire the write lock it will never succeed.
- Lock downgrading
Reentrancy also allows downgrading from the write lock to a read lock, by acquiring the write lock, then the readlock and then releasing the write lock. However, upgrading from a read lock to the write lock isnot possible.
- Interruption of lock acquisition
The read lock and write lock bothsupport interruption during lock acquisition.
- Condition support
The write lock provides a Condition implementation that behaves in the same way, with respect to the write lock, as the Condition implementation providedby ReentrantLock.newCondition() does for ReentrantLock. This Condition can, of course,only be used with the write lock.
The read lock does not support a Condition and readLock().newCondition() throws UnsupportedOperationException.
- Instrumentation
This class supports methods to determine whether locks are held or contended. These methods are designed for monitoring system state, not for synchronization control.Serialization of this class behaves in the same wayas built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
Sample usages. Here is a code sketch showing how to perform lockdowngrading after updating a cache (exception handling is particularly trickywhen handling multiple locks in a non-nested fashion):
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock beforeacquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because anotherthread might have
// acquired write lock and changedstate before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lockbefore releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlockwrite, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
ReentrantReadWriteLocks can be usedto improve concurrency in some uses of some kinds of Collections. This istypically worthwhile only when the collections are expected to be large,accessed by more reader threads than writer threads, and entail operations withoverhead that outweighs synchronization overhead. For example, here is a classusing a TreeMap that is expected to be large and concurrently accessed.
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String,Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
3)分清原子性操作和复合操作
所谓原子性,是说一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。如果所有程序块都是原子性的,那么就不存在任何并发问题。而很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count++)和check-then-act,这些都是很容易被忽视的,例如大家所常用的惰性初始化模式,以下代码就不是线程安全的:
[java] view plaincopy
1. @NotThreadSafe
2. public class LazyInitRace {
3. private ExpensiveObject instance = null;
4. public ExpensiveObject getInstance() {
5. if (instance == null)
6. instance = new ExpensiveObject();
7. return instance;
8. }
9. }
这段代码具体问题在于没有认识到if(instance==null)和instance = new ExpensiveObject();是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance==null)后会被中断,另一个线程也去执行if(instance==null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。
虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。
在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。
另外,java1.5以后提供了一套提供原子性操作的类,有兴趣的可以研究一下它是如何在软件层面保证原子性的。
JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下:
- 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
- 对象监视器的解锁发生在等待获取对象锁的线程之前.
- 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
- 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
- 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.
有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:
- 使用 synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
- 使用 volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
- final变量。常量内也是存于主存中
共享变量发布
共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:
- 将对象引用存储到公共静态域
- 初始化一个可以被外部访问的对象
- 将对象引用存储到一个集合里
安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。
线程安全
其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:
- swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全
- JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用
- ThreadLocal把变量限制在本线程*享
类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:
(1)取出a和b
(2)计算a+b
(3)将计算结果写入内存
类似的,像"a++"这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作
利用Happen-Before规则分析DCL
下面是一个典型的使用DCL的例子:
1. public class LazySingleton {
2. private int someField;
3.
4. private static LazySingleton instance;
5.
6. private LazySingleton() {
7. this.someField = new Random().nextInt(200)+1; // (1)
8. }
9.
10. public static LazySingleton getInstance() {
11. if (instance == null) { // (2)
12. synchronized(LazySingleton.class) { // (3)
13. if (instance == null) { // (4)
14. instance = new LazySingleton(); // (5)
15. }
16. }
17. }
18. return instance; // (6)
19. }
20.
21. public int getSomeField() {
22. return this.someField; // (7)
23. }
24. }
为了分析DCL,我需要预先陈述上面程序运行时几个事实:
1. 语句(5)只会被执行一次,也就是LazySingleton只会存在一个实例,这是由于它和语句(4)被放在同步块中被执行的缘故,如果去掉语句(3)处的同步块,那么这个假设便不成立了。
2. instance只有两种“曾经可能存在”的值,要么为null,也就是初始值,要么为执行语句(5)时构造的对象引用。这个结论由事实1很容易推出来。
3. getInstance()总是返回非空值,并且每次调用返回相同的引用。如果getInstance()是初次调用,它会执行语句(5)构造一个LazySingleton实例并返回,如果getInstance()不是初次调用,如果不能在语句(2)处检测到非空值,那么必定将在语句(4)处就能检测到instance的非空值,因为语句(4)处于同步块中,对instance的写入--语句(5)也处于同一个同步块中。
有读者可能要问了,既然根据第3条事实getInstance()总是返回相同的正确的引用,为什么还说DCL有问题呢?这里的关键是 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量 的 不正确值 ,具体来说LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句(1)和语句(7)并不存在happen-before关系。
假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们无法利用第1条和第2条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系,这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,这就是DCL的问题所在。很荒谬,是吧?DCL原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到是DCL所引起的。
前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--LazySingleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对LazySingleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。
对DCL的分析也告诉我们一条经验原则,对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
再稍微对DCL探讨一下,这个例子中的LazySingleton是一个不变类,它只有get方法而没有set方法。由对DCL的分析我们知道,即使一个对象是不变的,在不同的线程中它的同一个方法也可能返回不同的值 。之所以会造成这个问题,是因为LazySingleton实例没有被安全发布,所谓“被安全的发布”是指所有的线程应该在同步块中获得这个实例。这样我们又得到一个经验原则,即使对于不可变对象,它也必须被安全的发布,才能被安全地共享。 所谓“安全的共享”就是说不需要同步也不会遇到数据竞争的问题。在Java5或以后,将someField声明成final的,即使它不被安全的发布,也能被安全地共享,而在Java1.4或以前则必须被安全地发布。
关于DCL的修正
既然理解了DCL的根本原因,或许我们就可以修正它。
既然原因是线程Ⅱ执行getInstance()可能根本没有在同步块中执行,那就将整个方法都同步吧。这个毫无疑问是正确的,但是这却回到最初的起点(返朴归真了),也完全违背了DCL的初衷,尽可能少的减少同步。虽然这不能带任何意义,却也说明一个道理,最简单的往往是最好的。
如果我们尝试不改动getInstance()方法,而是在getSomeField()上做文章,那么首先想到的应该是将getSomeField设置成同步,如下所示:
1. public synchronized int getSomeField() {
2. return this.someField; // (7)
3. }
这种修改是不是正确的呢?答案是不正确。这是因为,第2条happen-before规则的前提条件并不成立。语句(5)所在同步块和语句(7)所在同步块并不是使用同一个锁。像下面这样修改才是对的:
1. public int getSomeField() {
2. synchronized(LazySingleton.class) {
3. return this.someField;
4. }
5. }
但是这样的修改虽然能保证正确性却不能保证高性能。因为现在每次读访问getSomeField()都要同步,如果使用简单的方法,将整个getInstance()同步,只需要在getInstance()时同步一次,之后调用getSomeField()就不需要同步了。另外getSomeField()方法也显得很奇怪,明明是要返回实例变量却要使用Class锁。这也再次验证了一个道理,简单的才是好的。
好了,由于我的想象力有限,我能想到的修正也就仅限于此了,让我们来看看网上提供的修正吧。
首先看Lucas Lee的修正(这里 是原帖):
1. private static LazySingleton instance;
2. private static int hasInitialized = 0;
3.
4. public static LazySingleton getInstance() {
5. if (hasInitialized == 0) { // (4)
6. synchronized(LazySingleton.class) { // (5)
7. if (instance == null) { // (6)
8. instance = new LazySingleton(); // (7)
9. hasInitialized = 1;
10. }
11. }
12. }
13. return instance; // (8)
14. }
如果你明白我前面所讲的,那么很容易看出这里根本就是一个伪修正,线程Ⅱ仍然完全有可能在非同步状态下返回instance。Lucas Lee的理由是对int变量的赋值是原子的,但实际上对instance的赋值也是原子的,Java语言规范规定对任何引用变量和基本变量的赋值都是原子的,除了long和double以外。使用hasInitialized==0和instance==null来判断LazySingleton有没有初始化没有任何区别。Lucas Lee对http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 中的最后一个例子有些误解,里面的计算hashCode的例子之所以是正确的,是因为它返回的是int而不是对象的引用,因而不存在访问到不正确成员变量值的问题。
neuzhujf的修正:
1. public static LazySingleton getInstance() {
2. if (instance == null) { // (4)
3. synchronized(LazySingleton.class) { // (5)
4. if (instance == null) { // (6)
5. LazySingleton localRef = new LazySingleton();
6. instance = localRef; // (7)
7. }
8. }
9. }
10. return instance; // (8)
11. }
这里只是引入了一个局部变量,这也容易看出来只是一个伪修正,如果你弄明白了我前面所讲的。
既然提到DCL,就不得不提到一个经典的而且正确的修正。就是使用一个static holder,kilik在回复中给出了这样的一个修正。由于这里一种完全不同的思路,与我这里讲的内容也没有太大的关系,暂时略了吧。另外一个修正是使用是threadlocal,都可以参见这篇文章 。
步入Java5
前面所讲的都是基于Java1.4及以前的版本,java5对内存模型作了重要的改动,其中最主要的改动就是对volatile和final语义的改变。本文使用的happen-before规则实际上是从Java5中借鉴而来,然后再移花接木到Java1.4中,因此也就不得不谈下Java5中的多线程了。
在java 5中多增加了一条happen-before规则:
· 对volatile字段的写操作happen-before后续的对同一个字段的读操作。
利用这条规则我们可以将instance声明为volatile,即:
1. private volatile static LazySingleton instance;
根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。
遭遇同样错误
在Java世界里,框架似乎做了很多事情来隐藏多线程,以至于很多程序员认为不再需要关注多线程了。 这实际上是个陷阱,这它只会使我们对多线程程序的bug反应迟钝。大部分程序员(包括我)都不 会特别留意类文档中的线程不安全警告,自己写程序时也不会考虑将该类是否线程安全写入文档中。做个测试,你知道java.text.SimpleDateFormat不是线程安全的吗?如果你不知道,也不要感到奇怪,我也是在《Java Concurrent In Practice 》这书中才看到的。
现在我们已经明白了DCL中的问题,很多人都只认为这只不过是不切实际的理论者整天谈论的话题,殊不知这样的错误其实很常见。我就犯过,下面是从我同一个项目中所写的代码中摘录出来的,读者也不妨拿此来检验一下自己,你自己犯过吗?即使没有,你会毫不犹豫的这样写吗?
第一个例子:
1. public class TableConfig {
2. //....
3. private FieldConfig[] allFields;
4.
5. private transient FieldConfig[] _editFields;
6.
7. //....
8.
9. public FieldConfig[] getEditFields() {
10. if (_editFields == null) {
11. List<FieldConfig> editFields = new ArrayList<FieldConfig>();
12. for (int i = 0; i < allFields.length; i++) {
13. if (allFields[i].editable) editFields.add(allFields[i]);
14. }
15. _editFields = editFields.toArray(new FieldConfig[editFields.size()]);
16. }
17. return _editFields;
18. }
19. }
这里缓存了TableConfig的_editFields,免得以后再取要重新遍历allFields。这里存在和DCL同样的问题,_editFields数组的引用可能是正确的值,但是数组成员却可能null! 与DCL不同的是 ,由于对_editFields的赋值没有同步,它可能被赋值多次,但是在这里没有问题,因为每次赋值虽然其引用值不同,但是其数组成员是相同的,对于我的业务来说,它们都等价的。由于我的代码是要用在java1.4中,因此唯一的修复方法就是将整个方法声明为同步。
第二个例子:
1. private Map selectSqls = new HashMap();
2.
3. public Map executeSelect(final TableConfig tableConfig, Map keys) {
4. if (selectSqls.get(tableConfig.getId()) == null) {
5. selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
6. }
7. PreparedSql psql = (PreparedSql) selectSqls.get(tableConfig.getId());
8.
9. List result = executeSql(...);
10.
11. return result.isEmpty() ? null : (Map) result.get(0);
12. }
上面的代码用constructSelectSql()方法来动态构造SQL语句,为了避免构造的开销,将先前构造的结果缓存在selectSqls这个Map中,下次直接从缓存取就可以了。显然由于没有同步,这段代码会遭遇和DCL同样的问题,虽然selectSqls.get(...)可能能够返回正确的引用,但是却有可能返回该引用成员变量的非法值。另外selectSqls使用了非同步的Map,并发调用时可能会破坏它的内部状态,这会造成严重的后果,甚至程序崩溃。可能的修复就是将整个方法声明为同步:
1. public synchronized Map executeSelect(final TableConfig tableConfig, Map keys) {
2. // ....
3. }
但是这样马上会遭遇吞吐量的问题,这里在同步块执行了数据库查询,执行数据库查询是是个很慢的操作,这会导致其它线程执行同样的操作时造成不必要的等待,因此较好的方法是减少同步块的作用域,将数据库查询操作排除在同步块之外:
1. public Map executeSelect(final TableConfig tableConfig, Map keys) {
2. PreparedSql psql = null;
3. synchronized(this) {
4. if (selectSqls.get(tableConfig.getId()) == null) {
5. selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
6. }
7. psql = (PreparedSql) selectSqls.get(tableConfig.getId());
8. }
9.
10. List result = executeSql(...);
11.
12. return result.isEmpty() ? null : (Map) result.get(0);
13. }
现在情况已经改善了很多,毕竟我们将数据库查询操作拿到同步块外面来了。但是仔细观察会发现将this作为同步锁并不是一个好主意,同步块的目的是保证从selectSqls这个Map中取到的是一致的对象,因此用selectSqls作为同步锁会更好,这能够提高性能。这个类中还存在很多类似的方法executeUpdate,executeInsert时,它们都有自己的sql缓存,如果它们都采用this作为同步锁,那么在执行executeSelect方法时需要等待executeUpdate方法,而这种等待原本是不必要的。使用细粒度的锁,可以消除这种等待,最后得到修改后的代码:
1. private Map selectSqls = Collections.synchronizedMap(new HashMap())
2. public Map executeSelect(final TableConfig tableConfig, Map keys) {
3. PreparedSql psql = null;
4. synchronized(selectSqls) {
5. if (selectSqls.get(tableConfig.getId()) == null) {
6. selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
7. }
8. psql = (PreparedSql) selectSqls.get(tableConfig.getId());
9. }
10.
11. List result = executeSql(...);
12.
13. return result.isEmpty() ? null : (Map) result.get(0);
14. }
我对selectSqls使用了同步Map,如果它只被这个方法使用,这就不是必须的。作为一种防范措施,虽然这会稍微降低性能,即便当它被其它方法使用了也能够保护它的内部结构不被破坏。并且由于Map的内部锁是非竞争性锁,根据官方说法,这对性能影响很小,可以忽略不计。这里我有意无意地提到了编写高性能的两个原则,尽量减少同步块的作用域,以及使用细粒度的锁 ,关于细粒度锁的最经典例子莫过于读写锁了。这两个原则要慎用,除非你能保证你的程序是正确的。
结束语
在这篇文章中我主要讲到happen-before规则,并运用它来分析DCL问题,最后我用例子来说明DCL问题并不只是理论上的讨论,在实际程序中其实很常见。我希望读者能够明白用happen-before规则比使用时间的先后顺序来分析线程安全性要有效得多,作为对比,你可以看看这篇经典的文章 中是如何分析DCL的线程安全性的。它是否讲明白了呢?如果它讲明白了,你是否又能理解?我想答案很可能是否定的,不然的话就不会出现这么多对DCL的误解了。当然我也并不是说要用happen-before规则来分析所有程序的线程安全性,如果你试着分析几个程序就会发现这是件很困难的事,因为这个规则实在是太底层了,要想更高效的分析程序的线程安全性,还得总结和利用了一些高层的经验规则。关于这些经验规则,我在文中也谈到了一些,很零碎也不完全。
http://lifethinker.iteye.com/blog/260515