JAVA多线程的使用场景与注意事项总结

前言

我曾经对自己的小弟说,如果你实在搞不清楚什么时候用HashMap,什么时候用ConcurrentHashMap,那么就用后者,你的代码bug会很少。

他问我:ConcurrentHashMap是什么? -.-

编程不是炫技。大多数情况下,怎么把代码写简单,才是能力。

多线程生来就是复杂的,也是容易出错的。一些难以理解的概念,要规避。本文不讲基础知识,因为你手里就有jdk的源码。

线程

Thread

第一类就是Thread类。大家都知道有两种实现方式。第一可以继承Thread覆盖它的run方法;第二种是实现Runnable接口,实现它的run方法;而第三种创建线程的方法,就是通过线程池。

我们的具体代码实现,就放在run方法中。

我们关注两种情况。一个是线程退出条件,一个是异常处理情况。

线程退出

有的run方法执行完成后,线程就会退出。但有的run方法是永远不会结束的。结束一个线程肯定不是通过Thread.stop()方法,这个方法已经在java1.2版本就废弃了。所以我们大体有两种方式控制线程。

定义退出标志放在while中

代码一般长这样。

private volatile boolean flag= true;
public void run() {
 while (flag) {
 }
}

标志一般使用volatile进行修饰,使其读可见,然后通过设置这个值来控制线程的运行,这已经成了约定俗成的套路。

使用interrupt方法终止线程

类似这种。

while(!isInterrupted()){……}

对于InterruptedException,比如Thread.sleep所抛出的,我们一般是补获它,然后静悄悄的忽略。中断允许一个可取消任务来清理正在进行的工作,然后通知其他任务它要被取消,最后才终止,在这种情况下,此类异常需要被仔细处理。

interrupt方法不一定会真正”中断”线程,它只是一种协作机制。interrupt方法通常不能中断一些处于阻塞状态的I/O操作。比如写文件,或者socket传输等。这种情况,需要同时调用正在阻塞操作的close方法,才能够正常退出。

interrupt系列使用时候一定要注意,会引入bug,甚至死锁。

异常处理

java中会抛出两种异常。一种是必须要捕获的,比如InterruptedException,否则无法通过编译;另外一种是可以处理也可以不处理的,比如NullPointerException等。

在我们的任务运行中,很有可能抛出这两种异常。对于第一种异常,是必须放在try,catch中的。但第二种异常如果不去处理的话,会影响任务的正常运行。

有很多同学在处理循环的任务时,没有捕获一些隐式的异常,造成任务在遇到异常的情况下,并不能继续执行下去。如果不能确定异常的种类,可以直接捕获Exception或者更通用的Throwable。

while(!isInterrupted()){
 try{
 ……
 }catch(Exception ex){
 ……
 }
}

同步方式

java中实现同步的方式有很多,大体分为以下几种。

  • synchronized 关键字
  • wait、notify等
  • Concurrent包中的ReentrantLock
  • volatile关键字
  • ThreadLocal局部变量

生产者、消费者是wait、notify最典型的应用场景,这些函数的调用,是必须要放在synchronized代码块里才能够正常运行的。它们同信号量一样,大多数情况下属于炫技,对代码的可读性影响较大,不推荐。关于ObjectMonitor相关的几个函数,只要搞懂下面的图,就基本ok了。

使用ReentrantLock最容易发生错误的就是忘记在finally代码块里关闭锁。大多数同步场景下,使用Lock就足够了,而且它还有读写锁的概念进行粒度上的控制。我们一般都使用非公平锁,让任务自由竞争。非公平锁性能高于公平锁性能,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。非公平锁还会造成饿死现象:有些任务一直获取不到锁。

synchronized通过锁升级机制,速度不见得就比lock慢。而且,通过jstack,能够方便的看到其堆栈,使用还是比较广泛。

volatile总是能保证变量的读可见,但它的目标是基本类型和它锁的基本对象。假如是它修饰的是集合类,比如Map,那么它保证的读可见是map的引用,而不是map对象,这点一定要注意。

synchronized和volatile都体现在字节码上(monitorenter、monitorexit),主要是加入了内存屏障。而Lock,是纯粹的java api。

ThreadLocal很方便,每个线程一份数据,也很安全,但要注意内存泄露。假如线程存活时间长,我们要保证每次使用完ThreadLocal,都调用它的remove()方法(具体来说是expungeStaleEntry),来清除数据。

关于Concurrent包

concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。

线程池

最全的线程池大概有7个参数,想要合理使用线程池,肯定不会不会放过这些参数的优化。

线程池参数

concurrent包最常用的就是线程池,平常工作建议直接使用线程池,Thread类就可以降低优先级了。我们常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类创建。

newSingleThreadExecutor可以用于快速创建一个异步线程,非常方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。

我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。

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

假如我的任务可以预估,corePoolSize,maximumPoolSize一般都设成一样大的,然后存活时间设的特别的长。可以避免线程频繁创建、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不一样的,一般I/O密集型的应用线程就可以开的多一些。

threadFactory我一般也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,能够直观的看到我所创建的线程。

监控

高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。

通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。

线程池饱和策略

最容易被遗忘的可能就是线程的饱和策略了。也就是线程和缓冲队列的空间全部用完了,新加入的任务将如何处置。jdk默认实现了4种策略,默认实现的是AbortPolicy,也就是直接抛出异常。下面介绍其他几种。

DiscardPolicy 比abort更加激进,直接丢掉任务,连异常信息都没有。

CallerRunsPolicy 由调用的线程来处理这个任务。比如一个web应用中,线程池资源占满后,新进的任务将会在tomcat线程中运行。这种方式能够延缓部分任务的执行压力,但在更多情况下,会直接阻塞主线程的运行。

DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。

很多情况下,这些饱和策略可能并不能满足你的需求,你可以自定义自己的策略,比如将任务持久化到一些存储中。

阻塞队列

阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提高的编码的灵活性,非常方便。在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。

ArrayBlockingQueue对访问者的调用默认是不公平的,我们可以通过设置构造方法参数将其改成公平阻塞队列。

LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。

SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身不存储任何元素,吞吐量非常高。对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。它更像是一个管道,在一些通讯框架中(比如rpc),通常用来快速处理某个请求,应用较为广泛。

DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象需要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式通常能够比大多数非阻塞的while循环更加节省cpu资源。

另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜测它的用途。在线程池的构造参数中,我们使用的队列,一定要注意其特性和边界。比如,即使是最简单的newFixedThreadPool,在某些场景下,也是不安全的,因为它使用了无界队列。

CountDownLatch

假如有一堆接口A-Y,每个接口的耗时最大是200ms,最小是100ms。

我的一个服务,需要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?

此类问题典型的还有赛马问题,只有通过并行计算才能完成问题。归结起来可以分为两类:

  • 实现任务的并行性
  • 开始执行前等待n个线程完成任务

在concurrent包出现之前,需要手工的编写这些同步过程,非常复杂。现在就可以使用CountDownLatch和CyclicBarrier进行便捷的编码。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

CyclicBarrier与其类似,可以实现同样的功能。不过在日常的工作中,使用CountDownLatch会更频繁一些。

信号量

Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽量少用。

信号量可以实现限流的功能,但它只是常用限流方式的一种。其他两种是漏桶算法、令牌桶算法。

hystrix的熔断功能,也有使用信号量进行资源的控制。

Lock && Condition

在Java中,对于Lock和Condition可以理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。

但这些类和函数对于初中级码农来说,难以理解,容易产生bug,应该在业务代码中严格禁止。但在网络编程、或者一些框架类工程中,这些功能是必须的,万不可将这部分的工作随便分配给某个小弟。

End

不管是wait、notify,还是同步关键字或者锁,能不用就不用,因为它们会引发程序的复杂性。最好的方式,是直接使用concurrent包所提供的机制,来规避一些编码方面的问题。

concurrent包中的CAS概念,在一定程度上算是无锁的一种实现。更专业的有类似disruptor的无锁队列框架,但它依然是建立在CAS的编程模型上的。近些年,类似AKKA这样的事件驱动模型正在走红,但编程模型简单,不代表实现简单,背后的工作依然需要多线程去协调。

golang引入协程(coroutine)概念以后,对多线程加入了更加轻量级的补充。java中可以通过javaagent技术加载quasar补充一些功能,但我觉得你不会为了这丁点效率去牺牲编码的可读性。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Java编程实现多线程TCP服务器完整实例

    相关Java类 Socket public class Socket extends Object ·功能:TCP客户端套接字 ·构造方法: Socket(InetAddress address, int port) 创建一个流套接字并将其连接到指定 IP 地址的指定端口号 ·常用方法: 1.getInetAddress 获得InetAddress的相关信息 2.getInputStream 获得此TCP连接的输入流 3.getOutPutStream 获得此TCP连接的输出流 ServerSo

  • java多线程实现下载图片并压缩

    最近在做一个需求:从其他系统的ftp目录下载存储图片url的文件,然后读取文件中的url地址,根据地址下载图片后按天压缩成一个包,平均一个地址文件中包含4000个地址左右,也就是说一个文件扫描后需要下载4000个左右的图片,然后压缩,下面把我的实现方式和优化过程记录下来,如果大家有什么更好的方式可以分享. 使用框架:SpringMVC 定时任务实现:继承org.springframework.scheduling.quartz.QuartzJobBean; ftp环境搭建就不说了,在其他博客记录

  • Java常见面试题之多线程和高并发详解

    volatile 对 volatile的理解 volatile 是一种轻量级的同步机制. 保证数据可见性 不保证原子性 禁止指令重排序 JMM JMM(Java 内存模型)是一种抽象的概念,描述了一组规则或规范,定义了程序中各个变量的访问方式. JVM运行程序的实体是线程,每个线程创建时 JVM 都会为其创建一个工作内存,是线程的私有数据区域.JMM中规定所有变量都存储在主内存,主内存是共享内存.线程对变量的操作在工作内存中进行,首先将变量从主内存拷贝到工作内存,操作完成后写会主内存.不同线程间

  • Java并发编程示例(十):线程组

    对线程分组是Java并发API提供的一个有趣功能.我们可以将一组线程看成一个独立单元,并且可以随意操纵线程组中的线程对象.比如,可以控制一组线程来运行同样的任务,无需关心有多少线程还在运行,还可以使用一次中断调用中断所有线程的执行. Java提供了ThreadGroup类来控制一个线程组.一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程. 根据<Effective Java>的说明,不再建议使用ThreadGroup.建议使用Executor. --D瓜哥特此

  • 详解Java多线程处理List数据

    实例1: 解决问题:如何让n个线程顺序遍历含有n个元素的List集合 import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.ArrayUtils; public class Test_4 { /** * 多线程处理list * * @param data 数据list * @param threadNum 线程数 */ public synchronized void handleLi

  • Java多线程 线程组原理及实例详解

    线程组 线程组可以批量管理线程和线程组对象. 一级关联 例子如下,建立一级关联. public class MyThread43 implements Runnable{ public void run() { try { while (!Thread.currentThread().isInterrupted()) { System.out.println("ThreadName = " + Thread.currentThread().getName()); Thread.slee

  • Java多线程——之一创建线程的四种方法

    1.实现Runnable接口,重载run(),无返回值 package thread; public class ThreadRunnable implements Runnable { public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } package thread; public clas

  • JAVA多线程的使用场景与注意事项总结

    前言 我曾经对自己的小弟说,如果你实在搞不清楚什么时候用HashMap,什么时候用ConcurrentHashMap,那么就用后者,你的代码bug会很少. 他问我:ConcurrentHashMap是什么? -.- 编程不是炫技.大多数情况下,怎么把代码写简单,才是能力. 多线程生来就是复杂的,也是容易出错的.一些难以理解的概念,要规避.本文不讲基础知识,因为你手里就有jdk的源码. 线程 Thread 第一类就是Thread类.大家都知道有两种实现方式.第一可以继承Thread覆盖它的run方

  • java多线程开发ScheduledExecutorService简化方式

    目录 前言 java多线程的应用场景 应用场景一: 应用场景二: ScheduledExecutorService方法简介 实例 实例结果 前言 java开发,多多少少会接触到多线程的应用开发场景,博主之前做多线程的时候就两种方式,一个是继承Thread一个是实现Runnable接口,不过用的多的还是实现Runnable接口,因为实现Runnable接口可以做多线程资源共享!而java.util.concurrent.ScheduledExecutorService接口将大大简化多线程应用的开发

  • Java多线程Thread类的使用及注意事项

    目录 Thread类的基本用法 线程指标 run和start的区别 中断线程 线程等待 进程状态 线程安全问题 synchronized用法 1.直接修饰普通的方法 2.修饰一个代码块 3.修饰一个静态方法 监视器锁monitor lock 死锁的其他场景 volatile Thread类的基本用法 1.创建子类,继承自Thread并且重写run方法: class MyThread extends Thread { @Override public void run() { System.out

  • java多线程join()方法的作用和实现原理解析(应用场景)

    1.join() 方法的作用 这个方法的作用是先将当前线程挂起,待其他线程结束后在执行当前线程的代码: 2.应用场景 比如有三个人小红.小李.小王, 三个人相约一起去酒店吃饭,菜已经点好了, 三个人从不同的地方出发,只有三个人都到了酒店之后才会开始上菜:那么这三个人就分别代表三个线程,这三个线程执行完之后才会执行 "上菜" 的代码逻辑, 代码示例 package com.Lock; /** * join方法示例 * 比如有三个人小红.小李.小王, 三个人相约一起去酒店吃饭,菜已经点好了

  • Java ThreadLocal的使用场景总结

    使用场景1:本地变量 我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作. ① 2个线程格式化 当有 2 个线程进行时间格式化时,我们可以这样写: import java.text.SimpleDateFormat; import java.util.Date; public class Test { public static void main(String[] args) throws InterruptedExcept

  • Java多线程的具体介绍与使用笔记小结

    一.基本概念:线程.进程 1.1.进程与线程的具体介绍 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径. 若一个进程同一时间并行执行多个线程,就是支持多线程的 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小 一个进程中的多个线程共享相同的内存单元/内存地址空间à它们从同一堆中分配对象,可以访问相同的变量和对象.这就使得线程间通信更简便.高效.但多个线程操作共享的系统资源可能就会带来安全的隐患. 进程(process),是程序的

  • Java多线程 ThreadLocal原理解析

    目录 1.什么是ThreadLocal变量 2.ThreadLocal实现原理 3.内存泄漏问题 4.使用场景 1)存储用户Session 2)解决线程安全的问题 3)使用ThreadLocal重新设计一个上下文设计模式 4)ThreadLocal注意事项 脏数据 内存泄漏 父子线程共享线程变量 1.什么是ThreadLocal变量 ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本. 这里有几点需要注意: 因为每个 T

  • Java多线程CountDownLatch的实现

    介绍 CountDownLatch是一个同步辅助类,它允许一个或多个线程一直等待直到其他线程执行完毕才开始执行. 用给定的计数初始化CountDownLatch,其含义是要被等待执行完的线程个数. 每次调用CountDown(),计数减1 主程序执行到await()函数会阻塞等待线程的执行,直到计数为0 场景 1:多线程读取批量文件, 并且读取完成之后汇总处理 2:多线程读取Excel多个sheet,读取完成之后获取汇总获取的结果 3:多个人一起一起来吃饭,主人等待客人到来,客人一个个从不同地方

  • Java多线程死锁问题详解(wait和notify)

    目录 一. synchronnized 的特性 1. 互斥性 2. 可重入性 二. 死锁问题 1. 什么是死锁 2. 死锁的四个必要条件 3. 常见的死锁场景及解决 3.1 不可重入造成的死锁 3.2 循环等待的场景 三. Object类中提供线程等待的方法 1. 常用方法 2. wait和notify的搭配使用 3. wait 和 sleep 的区别 4. 练习: 顺序打印ABC 总结 一. synchronnized 的特性 1. 互斥性 synchronized 会起到互斥效果, 这里的互

  • java多线程编程必备volatile与synchronized深入理解

    目录 Volatile概述 Synchronized概述 Volatile与Synchronized的区别 使用场景 1 Volatile的使用场景 2 Synchronized的使用场景 注意事项 相关面试问题 Volatile概述 Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排.当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到.也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见

随机推荐