首页 > 其他分享 >常见的并发陷阱

常见的并发陷阱

时间:2023-08-04 17:01:05浏览次数:34  
标签:synchronized lock DoubleLockSingleton 常见 并发 线程 陷阱 Lock CPU


常见的并发陷阱

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)。 锁的使用可以参考前面文章的描述和建议。


 


锁的边界

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


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的时间有助于降低负载;另外任务队列的长度以及任务队列的拒绝策略也会对任务的处理有一些影响.

 


标签:synchronized,lock,DoubleLockSingleton,常见,并发,线程,陷阱,Lock,CPU
From: https://blog.51cto.com/u_2650279/6964919

相关文章

  • k8s 常见面试题
    Kubernetes是什么?它解决了什么问题?       Kubernetes(简称K8s)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。它由Google开发并捐赠给CloudNativeComputingFoundation(CNCF)来进行维护。Kubernetes构建在容器技术(如Docker)的基......
  • 【现网事故】记一次多系统调用,并发冲突、请求放大导致的生产问题
    事故现象生产环境,转账相关请求失败量暴增。直接原因现网多个重试请求同时到达svr,导致内存数据库大量返回时间戳冲突。业务方收到时间戳冲突,自动进行业务重试,服务内部也存在重试,导致流量放大。转账首先我们一起了解一下转账。转账请求在支付场景中的应用频率非常高,它是现代金......
  • POE交换机常见几个问题
    问:哪些摄像机可以接POE交换机?答:符合IEEE802.3AF/AT标准的POE摄像机均可支持。如果不是POE摄像机,可以增加一个标准POE分线器来连接交换机与摄像机。问:摄像机白天正常,到晚上就没图像显示器黑屏?答:这是典型摄像机供电不足的情况,请确保你使用的网线是CAT5类或以上网线。建议用万......
  • 【知识点】JAVA之并发集合
    当涉及到多线程编程时,使用并发集合是一种常见的方式来处理多个线程同时访问和操作共享数据的问题。并发集合是一组线程安全的数据结构,可以同时被多个线程访问和修改,而不会导致数据不一致或竞争条件。以下是一些常见的并发集合及其特点:ConcurrentHashMap(并发哈希表):它是一个线程......
  • Java面试题 P48:框架篇:Spring框架常见注解(Spring、SpringBoot、SpringMvc)
        ......
  • Wi-Fi STA/STA 并发
    Android12引入了Wi-FiSTA/STA并发功能,使设备可同时连接到两个Wi-Fi网络。此可选功能支持以下功能。Make-before-break:设备会在断开现有连接之前连接到新的Wi-Fi网络。这使得Wi-Fi网络之间的切换更加顺畅并发仅本地和互联网连接:设备会连接到仅限本地的网络,而不中断设......
  • 直线导轨使用中常见的问题有哪些?
    直线导轨作为设备的核心部件之一,起着导向和支撑的作用功能。目前,已被广泛应用在各行各业中,大到机械设备,小到抽屉,我们都能看到直线导轨的身影,可以说,直线导轨已经悄无声息的进入到我们的生活了。任何零部件,在使用中都会遇到各种各样的问题,直线导轨也不例外,那么在使用直线导轨时,哪些问......
  • jQuery 自学笔记—10 常见特效 (终章)
    隐藏、显示、切换,滑动,淡入淡出,以及动画效果演示点击这里,隐藏/显示面板一寸光阴一寸金,因此,我们为您提供快捷易懂的学习内容。在这里,您可以通过一种易懂的便利的模式获得您需要的任何知识。实例jQueryhide()演示一个简单的jQueryhide()方法。jQueryhid......
  • 高并发性能指标:QPS、TPS、RT、吞吐量
    QPS,每秒查询QPS:QueriesPerSecond意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。互联网中,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。TPS,每秒事务TPS:是TransactionsPerSecond的缩写......
  • Linux Shell实现模拟多进程并发执行
        在bash中,使用后台任务来实现任务的“多进程化”。在不加控制的模式下,不管有多少任务,全部都后台执行。也就是说,在这种情况下,有多少任务就有多少“进程”在同时执行。我们就先实现第一种情况:实例一:正常情况脚本———————————————————————————–#......