多线程并发的一些常见的使用规范

简介: 多线程并发的一些常见的使用规范

1. 多线程并发使用规范

1.1 指定线程名称

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

创建单条线程时直接指定线程名称

      Thread thread = new Thread();
      thread.setName("a");

    2. 线程池则使用guava或自行封装的ThreadFactory,指定命名规则。

7095be8f75784c5b8fcf357054736550.png

2. 尽量使用线程池

【推荐】尽量使用线程池来创建线程

   除特殊情况,尽量不要自行创建线程,更好的保护线程资源。

28a99528b6254725a726313782f29fda.png

   同理,定时器也不要使用Timer,而应该使用ScheduledExecutorService。因为Timer只有单线程,不能并发的执行多个在其中定义的任务,而且如果其中一个任务抛出异常,整个Timer也会挂掉,而ScheduledExecutorService只有那个没捕获到异常的任务不再定时执行,其他任务不受影响。

3.不允许使用Executors

【强制】线程池不允许使用Executors去创建,避资源耗尽风险

Executors返回的线程池对象的弊端:

1.FixedThreadPool和SingleThreadPool:

允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

2.CachedThreadPool和ScheduledThreadPool:

允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

应通过newThreadPoolExecutor(xxx,xxx,xxx,xxx)这样的方式,更加明确线程池的运行规则,合理设置Queue及线程池的coresize和maxsize,建议使用vjkit封装的ThreadPoolBuilder。

4. 正确停止线程

【强制】正确停止线程

Thread.stop()不推荐使用,强行的退出太不安全,会导致逻辑不完整,操作不原子,已被定义成Deprecate方法。

停止单条线程,执行Thread.interrupt()。

停止线程池:

ExecutorService.shutdown():不允许提交新任务,等待当前任务及队列中的任务全部执行完毕后退出;

ExecutorService.shutdownNow():通过Thread.interrupt()试图停止所有正在执行的线程,并不再处理还在队列中等待的任务。

最优雅的退出方式是先执行shutdown(),再执行shutdownNow(),vjkit的ThreadPoolUtil进行了封装。

注意,Thread.interrupt()并不保证能中断正在运行的线程,需编写可中断退出的Runnable,见规则5。

5. 编写可停止的Runnable

【强制】编写可停止的Runnable

执行Thread.interrupt()时,如果线程处于sleep(),wait(),join(),lock.lockInterruptibly()等blocking状态,会抛出InterruptedException,如果线程未处于上述状态,则将线程状态设为interrupted。

因此,如下的代码无法中断线程:

public void run(){
         while (true){
            sleep();
         }
      }
      public void sleep(){
         try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
      }

1.正确处理InterruptException

       因为InterruptException异常是个必须处理的CheckedException,所以run()所调用的子函数很容易吃掉异常并简单的处理成打印日志,但这等于停止了中断的传递,外层函数将收不到中断请求,继续原有循环或进入下一个堵塞。

       正确处理是调用Thread.currentThread().interrupt();将中断往外传递

 

public void sleep(){
         try {
            ....
         } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
         }
      }

1.主循环及进入阻塞状态前要判断线程状态

50c15010e84843b6a21632ebe396c9d8.png

6 . Runnable中必须捕获一切异常

【强制】Runnable中必须捕获一切异常

如果Runnable中没有捕获RuntimeException而向外抛出,会发生下列情况:

1.ScheduledExecutorService执行定时任务,任务会被中断,该任务将不再定时调度,但线程池里的线程还能用于其他任务。

2.ExecutorService执行任务,当前线程会中断,线程池需要创建新的线程来响应后续任务。

3.如果没有在ThreadFactory设置自定义的UncaughtExceptionHanlder,则异常最终只打印在System.err,而不会打印在项目的日志中。

因此建议自写的Runnable都要保证捕获异常;如果是第三方的Runnable,可以将其再包裹一层vjkit中的SafeRunnable。

def04ddacaa5496996c6200a97d45ad2.png

7. 可考虑使用ThreadLocal

【强制】全局的非线程安全的对象可考虑使用ThreadLocal存放

全局变量包括单例对象,static成员变量。

著名的非线程安全类包括SimpleDateFormat,MD5/SHA1的Digest。

对这些类,需要每次使用时创建。

但如果创建有一定成本,可以使用ThreadLocal存放并重用。

ThreadLocal变量需要定义成static,并在每次使用前重置。

d2a650ba902b40d49f8e5c06705f0e7c.png

8. 缩短锁

【推荐】缩短锁

1.能锁区块,就不要锁整个方法体;

619301a8a6804e0795682060b712d6a1.png

1.能用对象锁,就不要用类锁。

4ea59b0e65394786a01a86ef88699a3b.png

9. 选择分离锁,分散锁甚至无锁的数据结构

【推荐】选择分离锁,分散锁甚至无锁的数据结构

分离锁:

1)读写分离锁ReentrantReadWriteLock,读读之间不加锁,仅在写读和写写之间加锁;

2)ArrayBase的queue一般是全局一把锁,而LinkedBase的queue一般是队头队尾两把锁。

分散锁(又称分段锁):

1)如JDK7的ConcurrentHashMap,分散成16把锁;

2)对于经常写,少量读的计数器,推荐使用JDK8或vjkit封装的LongAdder对象性能更好(内部分散成多个counter,减少乐观锁的使用,取值时再相加所有counter)

无锁的数据结构:

1)完全无锁无等待的结构,如JDK8的ConcurrentHashMap;

2)基于CAS的无锁有等待的数据结构,如AtomicXXX系列。

基于ThreadLocal来避免锁

10. 推荐】基于ThreadLocal来避免锁

比如Random实例虽然是线程安全的,但其实它的seed的访问是有锁保护的。因此建议使用JDK7的ThreadLocalRandom,通过在每个线程里放一个seed来避免了加锁。

11. 规避死锁风险

【推荐】规避死锁风险

对多个资源多个对象的加锁顺序要一致。

如果无法确定完全避免死锁,可以使用带超时控制的tryLock语句加锁。

12. volatile修饰符,AtomicXX系列的正确使用

【推荐】volatile修饰符,AtomicXX系列的正确使用

多线程共享的对象,在单一线程内的修改并不保证对所有线程可见。使用volatile定义变量可以解决(解决了可见性)。

但是如果多条线程并发进行基于当前值的修改,如并发的counter++,volatile则无能为力(解决不了原子性)。

此时可使用Atomic*系列:

34256831dbd848888553ddea65bc7446.png

但如果需要原子地同时对多个AtomicXXX的Counter进行操作,则仍然需要使用synchronized将改动代码块加锁。

13. 延时初始化的正确写法

【推荐】延时初始化的正确写法

通过双重检查锁(double-checkedlocking)实现延迟初始化存在隐患,需要将目标属性声明为volatile型,为了更高的性能,还要把volatile属性赋予给临时变量,写法复杂。

所以如果只是想简单的延迟初始化,可用下面的静态类的做法,利用JDK本身的class加载机制保证唯一初始化。

3e30bc2c57c040d6bed9ef69b9b3bb94.png


目录
相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
221 0
|
1月前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
190 59
|
24天前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
35 6
|
28天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
52 6
|
27天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
4月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
95 0
|
1月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
2月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
32 1
|
4月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
3月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。