- Java中的主要同步机制:
synchronized
、Lock
、volatile
、原子变量
。 - java.util.concurrent.atomic
- 内存可见性:读线程可立即看到写线程的写入结果。
volatile
- 禁止指令重排序:不会将该变量上的操作与其他内存操作一起重排序。
- 可见性:volatile变量不会缓存在寄存器或其他处理器不可见的地方。
- volatile不足以确保递增操作(++)的原子性。
- 加锁机制可以确保可见性及原子性,volatile只能确保可见性。
ThreadLocal(防止共享)
- ThreadLocal有get、set方法。为每个使用ThreadLocal变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
- ThreadLocal常用于防止对
可变的单实例变量
或全局变量
进行共享。- 如全局数据库链接Connection对象,将链接保存到ThreadLocal中,每个线程都会拥有自己的连接。
- 将单线程程序移植到多线程,将共享的全局变量转为ThreadLocal,可维持线程安全性。然而,禁止的这些变量的共享,在业务上可能就不满足需求了。
- ThreadLocal
可视为Map ,其中保存了特定于线程的值。但真实的实现并非如此,这些特定于线程的值保存在Thread对象中,当线程结束,这些值会被gc。 - 在实现应用程序框架时大量使用了ThreadLocal。如在EJB调用期间,J2EE容器需将一个事务上下文(Transaction Context)与某执行线程关联。将事务上下文保存在ThreadLocal。
- ThreadLocal类似于全局变量,降低代码可重用性,并在类间引入隐含的耦合性,因此在使用时要格外小心。
不变性
- 不可变对象一定是线程安全的。
- 对象中所有域均final,对象也可能是可变的。因为final域中可以保存对可变对象的引用。
基础构建模块
同步容器类
- Vector、HashTable、Collections.synchronizedXxx
- 将类中的状态封装起来,并对每个public方法进行同步,每次仅有一个线程可访问。
同步容器类的问题
- 同步容器类均线程安全,但某些情况需额外的客户端加锁来保护
复合操作
。如Vector迭代
迭代器与ConcurrentModificationException
- 对容器类进行迭代的标准方式都是使用Iterator,但若有其他线程并发修改容器,使用迭代器也无法避免在迭代期间对容器加锁。
- 若不想在迭代期间对容器加锁,可“克隆”一个副本进行迭代。但克隆过程本身需加锁。克隆存在显著的性能开销。
隐藏迭代器
- containsAll、removeAll、retainAll
并发容器
- 同步容器为串行访问,吞吐量低。使用并发容器可极大提高伸缩性并降低风险。
- ConcurrentHashMap增加了对一些常见复合操作的支持,如“若没有则添加”、替换、有条件删除等。
- CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步List。
- ConcurrentSkipListMap代替同步的SortedMap(如synchronizedMap包装的TreeMap)
- ConcurrentSkipListSet代替同步的SortedSet(如synchronizedMap包装的TreeSet)
- Queue用来临时保存一组待处理元素,ConcurrentLinkedQueue先进先出,PriorityQueue为非并发的优先队列。
- Queue是通过LinkedList实现的,去掉了List的随机访问需求,实现高效并发。
- BlockingQueue
ConcurrentHashMap
- ConcurrentHashMap使用了分段锁。
- ConcurrentHashMap的迭代器不会抛出CoucurrentModificationException,
不需在迭代过程中对容器加锁
。 - HashTable、synchronizedMap,对Map加锁并独占访问。
- “若没有则添加”、“若相等则删除”、“若相等则替换”均为原子操作。
CopyOnWriteArrayList
- CopyOnWriteArrayList迭代期间不需对容器加锁、复制(修改时需复制)。
- 当修改容器时会复制底层数组,这需要一定开销。
- 仅当迭代操作远多于修改操作时使用。
同步工具类
闭锁
- CountDownLatch为一种闭锁实现,使一个或多个线程等待一组事件发生。countDown方法递减计数器,await方法等待计数器达到0。
- 闭锁为一次性对象,一旦进入终止状态,就不能被重置。
- 闭锁拦截所有线程,直到某个状态满足条件。
栅栏Barrier
- 闭锁用于等待事件(满足某结束状态),栅栏用于等待其他线程。
信号量Semaphore
- 计数信号量用于控制同时访问某特定资源的操作数,或同时执行某指定操作的数量。
- Semaphore管理一组虚拟许可(permit),执行操作时首先获得许可,使用后释放许可。
- acquire获得许可,release释放许可。
FutureTask
- FutureTask表示一种可生成结果的计算,计算是通过Callable实现(相当于可生成结果的Runnable)。
- Future.get获取结果,若任务未完成,则一直阻塞直到完成后返回结果。
阻塞队列和生产者-消费者
- 阻塞队列的put、take可阻塞。无界队列永不会满,无界队列上的put永远不会阻塞。
- BlockingQueue简化了生产者-消费者的实现,支持任意数量的生产者和消费者。
- 如果阻塞队列不能完全符合设计需求,还可以通过信号量(Semaphore)来创建其他的阻塞数据结构。
- ArrayBlockingQueue、LinkedBlockingQueue为FIFO队列,比同步List拥有更好并发性。
- PriorityBlockingQueue既可根据元素的自然序比较元素(实现了Comparable方法),也可用Comparator来比较。
- io密集型与cpu密集型的任务,使用生产者-消费者,提高吞吐率。
- Java增加了Deque和BlockingDeque对Queue、BlockingQueue进行扩展。Deque为双端队列,实现了在队列头、尾的高效插入、移除。具体实现包括ArrayDeque、LinkedBlockingDeque。
- 阻塞队列用于生产者-消费者,双端队列用于工作密取模式。生产者-消费者,所有消费者共享一个工作队列,而工作密取的每个消费者有自己的双端队列。
阻塞方法和中断方法
- 线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作-前提是B愿意停下来。
构建高效且可伸缩的结果缓存
- ConcurrentHashMap存储缓存数据
- Future
- FutureTask
- Callable
任务执行
在线程中执行任务
- 大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。如Web服务器、EJB容器、邮件服务器、文件服务器、数据库服务器。
串行地执行任务
:性能太差,每次只处理1个请求。显式地为任务创建线程
:为每个请求创建一个新线程。无限制创建线程的不足
- 线程生命周期的开销非常高
- 线程的创建与销毁是有代价的。
- 若请求到达率非常高且请求处理过程是轻量级的,如大多数服务器应用,为每个请求创建一个线程将消耗大量计算资源。
- 资源消耗
- 活跃的线程会消耗资源,特别是内存。若可运行线程多于cpu核数,有些线程将闲置。闲置线程占用内存较多。
- 若已有足够多的线程使cpu保持忙碌,创建更多线程将降低性能。
- 稳定性
- 可创建线程数有上限。与平台、jvm启动参数、Thread构造函数中请求的栈大小、底层操作系统对线程的限制等有关。
- 线程生命周期的开销非常高
Executor框架
- 在Java类库中,任务执行的主要抽象不是Thread,而是Executor。
Executor基于生产者-消费者,提前任务相当于生产者,执行任务相当于消费者。
public interface Executor { void execute(Runnable command); }
示例:基于Executor的Web服务器
- private static final Executor exec = Executors.newFixedThreadPool(THREADS_NUM);
- ServerSocket socket = new ServerSocket(80);
- final Socket connection = socket.accept();Runnable task = new Runnable(){connection};
- exec.execute(task);
执行策略
- 将任务的提交与执行解耦。
线程池
- 重用现有线程而不是创建新线程,可在处理多个请求时分摊在线程创建与销毁过程中产生的巨大开销。
- 可通过Executors中静态工厂方法之一来创建一个线程池。
- newFixedThreadPool
- 固定大小
- 每次提交任务创建新线程直到达到线程池最大数量。
- newCachedThreadPool
- 可缓存线程池
- 无大小
- 若线程数量多于处理需求,则回收
- newSingleThreadPool
- 单线程
- 串行执行任务队列中的任务
- newScheduledThreadPool
- 固定大小
- 以延迟或定时的方式来执行任务
- newFixedThreadPool
Executor的生命周期
- Executor通常会创建线程来执行任务。但JVM只有在所有(非守护)线程全部终止后才退出。如无法正确关闭Executor,JVM将无法结束。
- ExecutorService接口扩展了Executor接口,增加了用于生命周期管理的方法。
- shutdown:平缓关闭,不再接受新任务,等待正在执行及待执行任务执行完毕。
- shutdownNow:粗暴关闭,尝试取消正在执行的所有任务,不再启动待执行任务。
延迟任务与周期任务
- Timer类负责管理延迟任务与周期任务。但Timer有缺陷,使用
ScheduledThreadPoolExecutor
代替。
找出可利用的并行性
示例:串行的页面渲染器
携带结果的任务Callable与Future
示例:使用Future实现页面渲染器
在异构任务并行化中存在的局限
CompletionService:Executor与BlockingQueue
示例:使用CompletionService实现页面渲染器
为任务设置时限
示例:旅行预订门户网站
取消与关闭
线程池的使用
- 只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而
在线程池的线程中不应该使用ThreadLocal在任务间传递值
。 - 只有当任务是同类型且相互独立时,线程池的性能才能达到最佳。若将运行时间较长与较短的任务混在一起,可能“拥塞”,若任务依赖于其他任务,可能死锁。
设置线程池大小
- 代码中通常不会固定线程池大小,而应通过某动态机制来提供,或Runtime.availableProcessors来动态计算。
- 线程池大小:cpu数、内存大小、计算密集还是IO密集、是否需JDBC这样的稀缺资源。
- 计算密集型:线程数=CPU数+1。(当线程偶尔由于页缺失故障或其他原因暂停,额外的+1线程可确保CPU时钟周期不会浪费)
- IO密集型:线程数比计算密集型更大。
- 数据库连接池与任务线程池:相互影响。
ThreadPoolExecutor
若默认执行策略(newCachedThreadPool、newFixedThreadPool、newScheduledThreadExecutor)不满足需求,可使用构造方法自定义一个对象。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler ){}
- corePoolSize:线程池最小大小。
- maximumPoolSize:最大值
- keepAliveTime:空闲线程存活时间,超出后线程可被回收,但不一定立即回收。
- newCachedThreadPool线程数量0-Integer.MAX_VALUE,超时时间1分钟,自动扩展与收缩。
- newFixedThreadPool、newSingleThreadExecutor默认使用无界LinkedBlockingQueue。
- 使用有界队列ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue有助于避免资源耗尽。
图形用户界面应用程序
避免活跃性危险
性能与可伸缩性
并发程序的测试
显示锁
- ReentrantLock可重入,
必须在finally中释放锁
。 - Lock lock = new ReentrantLock();lock.lock();try{}finally{lock.unlock();}
- 可定时与可轮询的锁获取模式是由tryLock()方法实现的。
- ReadWriteLock允许多个读操作同时进行,但每次只允许一个写操作。
- 当写操作时,其他线程无法读、写数据,而当读操作时,其它线程无法写数据,但可读数据
构建自定义的同步工具
原子变量与非阻塞同步机制
Java内存模型
- 在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并定期与主内存进行协调。