深入浅出 Java Concurrency (39): 并发总结 part 3 常见的并发陷阱

常见的并发陷阱

volatile

volatile只能强调数据的可见性,并不能保证原子操作和线程安全,因此volatile不是万能的。参考指令重排序

volatile最常见于下面两种场景。

a. 循环检测机制

volatile boolean done = false;

while( ! done ){
        dosomething();
    }

b. 单例模型 (http://www.blogjava.net/xylz/archive/2009/12/18/306622.html)

public class DoubleLockSingleton {

private static volatile DoubleLockSingleton instance = null;

private DoubleLockSingleton() {
    }

public static DoubleLockSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}

synchronized/Lock

看起来Lock有更好的性能以及更灵活的控制,是否完全可以替换synchronized?

锁的一些其它问题中说过,synchronized的性能随着JDK版本的升级会越来越高,而Lock优化的空间受限于CPU的性能,很有限。另外JDK内部的工具(线程转储)对synchronized是有一些支持的(方便发现死锁等),而对Lock是没有任何支持的。

也就说简单的逻辑使用synchronized完全没有问题,随着机器的性能的提高,这点开销是可以忽略的。而且从代码结构上讲是更简单的。简单就是美。

对于复杂的逻辑,如果涉及到读写锁、条件变量、更高的吞吐量以及更灵活、动态的用法,那么就可以考虑使用Lock。当然这里尤其需要注意Lock的正确用法。

Lock lock = 
lock.lock();
try{
    //do something
}finally{
    lock.unlock();
}

一定要将Lock的释放放入finally块中,否则一旦发生异常或者逻辑跳转,很有可能会导致锁没有释放,从而发生死锁。而且这种死锁是难以排查的。

如果需要synchronized无法做到的尝试锁机制,或者说担心发生死锁无法自恢复,那么使用tryLock()是一个比较明智的选择的。

Lock lock = 
if(lock.tryLock()){
    try{
        //do something
    }finally{
        lock.unlock();
    }
}

甚至可以使用获取锁一段时间内超时的机制Lock.tryLock(long,TimeUnit)。 锁的使用可以参考前面文章的描述和建议。

锁的边界

一个流行的错误是这样的。

ConcurrentMap<String,String> map = new ConcurrentHashMap<String,String>();

if(!map.containsKey(key)){
    map.put(key,value);
}

看起来很合理的,对于一个线程安全的Map实现,要存取一个不重复的结果,先检测是否存在然后加入。 其实我们知道两个原子操作和在一起的指令序列不代表就是线程安全的。 割裂的多个原子操作放在一起在多线程的情况下就有可能发生错误。

实际上ConcurrentMap提供了putIfAbsent(K, V)的“原子操作”机制,这等价于下面的逻辑:

if(map.containsKey(key)){
    return map.get(key);
}else{
    return map.put(k,v);
}

除了putIfAbsent还有replace(K, V)以及replace(K, V, V)两种机制来完成组合的操作。

提到Map,这里有一篇谈HashMap读写并发的问题。

构造函数启动线程

下面的实例是在构造函数中启动一个线程。

public class Runner{
   int x,y;
   Thread thread;
   public Runner(){
      this.x=1;
      this.y=2;
      this.thread=new MyThread();
      this.thread.start();
   }
}

这里可能存在的陷阱是如果此类被继承,那么启动的线程可能无法正确读取子类的初始化操作。

因此一个简单的原则是,禁止在构造函数中启动线程,可以考虑但是提供一个方法来启动线程。如果非要这么做,最好将类设置为final,禁止继承。

丢失通知的问题

这篇文章里面提到过notify丢失通知的问题。

对于wait/notify/notifyAll以及await/singal/singalAll,如果不确定到底是否能够正确的收到消息,担心丢失通知,简单一点就是总是通知所有。

如果担心只收到一次消息,使用循环一直监听是不错的选择。

非常主用性能的系统,可能就需要区分到底是通知单个还是通知所有的挂起者。

线程数

并不是线程数越多越好,在下一篇文章里面会具体了解下性能和可伸缩性。 简单的说,线程数多少没有一个固定的结论,受限于CPU的内核数,IO的性能以及依赖的服务等等。因此选择一个合适的线程数有助于提高吞吐量。

对于CPU密集型应用,线程数和CPU的内核数一致有助于提高吞吐量,所有CPU都很繁忙,效率就很高。 对于IO密集型应用,线程数受限于IO的性能,某些时候单线程可能比多线程效率更高。但通常情况下适当提高线程数,有利于提高网络IO的效率,因为我们总是认为网络IO的效率比较低。

对于线程池而言,选择合适的线程数以及任务队列是提高线程池效率的手段。

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

对于线程池来说,如果任务总是有积压,那么可以适当提高corePoolSize大小;如果机器负载较低,那么可以适当提高maximumPoolSize的大小;任务队列不长的情况下减小keepAliveTime的时间有助于降低负载;另外任务队列的长度以及任务队列的拒绝策略也会对任务的处理有一些影响。

时间: 2024-10-16 01:31:52

深入浅出 Java Concurrency (39): 并发总结 part 3 常见的并发陷阱的相关文章

深入浅出 Java Concurrency (17): 并发容器 part 2 ConcurrentMap (2)

本来想比较全面和深入的谈谈ConcurrentHashMap的,发现网上有很多对HashMap和ConcurrentHashMap分析的文章,因此本小节尽可能的分析其中的细节,少一点理论的东西,多谈谈内部设计的原理和思想. 要谈ConcurrentHashMap的构造,就不得不谈HashMap的构造,因此先从HashMap开始简单介绍. HashMap原理 我们从头开始设想.要将对象存放在一起,如何设计这个容器.目前只有两条路可以走,一种是采用分格技术,每一个对象存放于一个格子中,这样通过对格子

深入浅出 Java Concurrency - 目录 [转]

这是一份完整的Java 并发整理笔记,记录了我最近几年学习Java并发的一些心得和体会. J.U.C 整体认识 原子操作 part 1 从AtomicInteger开始 原子操作 part 2 数组.引用的原子操作 原子操作 part 3 指令重排序与happens-before法则 原子操作 part 4 CAS操作 锁机制 part 1 Lock与ReentrantLock 锁机制 part 2 AQS 锁机制 part 3 加锁的原理 (Lock.lock) 锁机制 part 4 锁释放与

深入浅出 Java Concurrency (18): 并发容器 part 3 ConcurrentMap (3)[转]

在上一篇中介绍了HashMap的原理,这一节是ConcurrentMap的最后一节,所以会完整的介绍ConcurrentHashMap的实现. ConcurrentHashMap原理 在读写锁章节部分介绍过一种是用读写锁实现Map的方法.此种方法看起来可以实现Map响应的功能,而且吞吐量也应该不错.但是通过前面对读写锁原理的分析后知道,读写锁的适合场景是读操作>>写操作,也就是读操作应该占据大部分操作,另外读写锁存在一个很严重的问题是读写操作不能同时发生.要想解决读写同时进行问题(至少不同元素

深入浅出 Java Concurrency (38): 并发总结 part 2 常见的并发场景[转]

常见的并发场景 线程池 并发最常见用于线程池,显然使用线程池可以有效的提高吞吐量. 最常见.比较复杂一个场景是Web容器的线程池.Web容器使用线程池同步或者异步处理HTTP请求,同时这也可以有效的复用HTTP连接,降低资源申请的开销.通常我们认为HTTP请求时非常昂贵的,并且也是比较耗费资源和性能的,所以线程池在这里就扮演了非常重要的角色. 在线程池的章节中非常详细的讨论了线程池的原理和使用,同时也提到了,线程池的配置和参数对性能的影响是巨大的.不尽如此,受限于资源(机器的性能.网络的带宽等等

深入浅出 Java Concurrency (35): 线程池 part 8 线程池的实现及原理 (3)[转]

线程池任务执行结果 这一节来探讨下线程池中任务执行的结果以及如何阻塞线程.取消任务等等. 1 package info.imxylz.study.concurrency.future;2 3 public class SleepForResultDemo implements Runnable {4 5     static boolean result = false;6 7     static void sleepWhile(long ms) {8         try {9      

深入浅出 Java Concurrency (33): 线程池 part 6 线程池的实现及原理 (1)[转]

线程池数据结构与线程构造方法 由于已经看到了ThreadPoolExecutor的源码,因此很容易就看到了ThreadPoolExecutor线程池的数据结构.图1描述了这种数据结构. 图1 ThreadPoolExecutor 数据结构 其实,即使没有上述图形描述ThreadPoolExecutor的数据结构,我们根据线程池的要求也很能够猜测出其数据结构出来. 线程池需要支持多个线程并发执行,因此有一个线程集合Collection<Thread>来执行线程任务: 涉及任务的异步执行,因此需要

深入浅出 Java Concurrency (28): 线程池 part 1 简介[转]

从这一节开始正式进入线程池的部分.其实整个体系已经拖了很长的时间,因此后面的章节会加快速度,甚至只是一个半成品或者简单化,以后有时间的慢慢补充.完善. 其实线程池是并发包里面很重要的一部分,在实际情况中也是使用很多的一个重要组件. 下图描述的是线程池API的一部分.广义上的完整线程池可能还包括Thread/Runnable.Timer/TimerTask等部分.这里只介绍主要的和高级的API以及架构和原理. 大多数并发应用程序是围绕执行任务(Task)进行管理的.所谓任务就是抽象.离散的工作单元

深入浅出 Java Concurrency (15): 锁机制 part 10 锁的一些其它问题[转]

主要谈谈锁的性能以及其它一些理论知识,内容主要的出处是<Java Concurrency in Practice>,结合自己的理解和实际应用对锁机制进行一个小小的总结. 首先需要强调的一点是:所有锁(包括内置锁和高级锁)都是有性能消耗的,也就是说在高并发的情况下,由于锁机制带来的上下文切换.资源同步等消耗是非常可观的.在某些极端情况下,线程在锁上的消耗可能比线程本身的消耗还要多.所以如果可能的话,在任何情况下都尽量少用锁,如果不可避免那么采用非阻塞算法是一个不错的解决方案,但是却也不是绝对的.

深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则[转]

在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念. 在Java Concurrency in Practice中是这样定义线程安全的: 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的. 显然只有资源竞争时才会导致线程不安全,因此无状态对象永远是线程安全的. 原子操作的描述是: 多个线程执行一个操作时,其