JAVA多线程使用场景和注意事项简版

前锋JAVA发展学院我想昨天分享

我曾经对我的弟弟说过,如果你真的无法弄清楚何时使用HashMap以及何时使用ConcurrentHashMap,那么使用后者,你的代码bug会非常小。

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

编程不是一个技巧。在大多数情况下,如何编写代码很简单,就是能力。

多线程本身就很复杂且容易出错。一些难以理解的概念需要被规避。本文不涉及基础知识,因为您有源代码jdk。

image.php?url=0Mr1uQPWx0

螺纹

螺纹

第一个类是Thread类。每个人都知道有两种方法可以实现它。第一个可以继承Thread来覆盖它的run方法;二是实现Runnable接口实现其run方法;第三种创建线程的方法是通过线程池。

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

一,一是异常处理情况。

线程退出

执行一些run方法后,线程将退出。但有些运行方法永远不会结束。一个线程的结尾绝对不是通过Thread.stop方法,这个方法在java1.2版本中已经被弃用了。所以我们有两种方法来控制线程。

在while

中定义退出标志

代码通常很长。

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

通常使用volatile修改标志以使其可见,然后通过设置此值来控制线程的操作,这已成为例程。

使用interrupt方法终止线程

与此类似。

而(!isInterrupted){.}

对于InterruptedException,例如Thread.sleep抛出的异常,我们通常会补充它,然后默默地忽略它。中断允许可取消的任务清理正在进行的工作,然后通知其他任务它将被取消,并最终终止,在这种情况下,需要谨慎处理此类异常。

中断方法不一定真正“中断”线程,它只是一种协作机制。中断方法通常不会中断某些被阻止的I/O操作。如写文件或套接字传输。在这种情况下,您需要同时调用阻塞操作的close方法才能正常退出。

使用时必须使用中断系列,这会引入错误甚至死锁。

异常处理

在java中引发了两个异常。必须捕获一个,例如InterruptedException,否则无法编译;另一个可以处理或不处理,例如NullPointerException。

在我们的任务运行中,很可能会抛出这两个例外。对于第一个例外,它必须放在try,catch中。但是,如果未处理第二个异常,则会影响任务的正常运行。

许多学生在处理循环任务时没有捕获一些隐式异常,这导致在异常情况下任务无法继续执行。如果无法确定异常类型,则可以直接捕获Exception或更通用的Throwable。

虽然(!中断){try {.} catch(Exception){}}

同步

在java中实现同步的方法有很多种,大致可以分为以下几类。

同步关键字

等等,通知等。

并发包中的ReentrantLock

易变的关键字

ThreadLocal局部变量

生产者和消费者是等待和通知的最典型的应用场景。必须将这些函数的调用放在同步代码块中才能正常工作。与信号量一样,它们在大多数情况下都令人眼花缭乱,并且对代码的可读性有很大影响,因此不推荐使用。关于与ObjectMonitor相关的几个函数,只要你理解下图,你就会基本没问题。

image.php?url=0Mr1uQpcY3

使用ReentrantLock时最常见的错误是忘记关闭finally块中的锁。在大多数同步方案中,使用Lock就足够了,它还具有用于粒度控制的读写锁的概念。我们通常使用不公平的锁来让任务自由竞争。不公平的锁定性能高于公平锁定性能。不公平的锁可以充分利用CPU的时间片并尝试减少CPU的空闲状态。不公平的锁也可能导致饥饿:一些任务永远不会被锁定。

通过锁升级机制同步,速度不一定比锁慢。而且,通过jstack,你可以很容易地看到它的堆栈,它仍然被广泛使用。

易失性总是保证变量的读取是可见的,但它的目标是基类型和它锁定的基础对象。如果它是一个集合类,例如Map,那么它的保证读取作为地图引用而不是地图对象可见。必须注意这一点。

synchronized和volatile都反映在字节码(monitorenter,monitorexit)中,主要是增加了内存屏障。而Lock,是一个纯粹的java api。

ThreadLocal非常方便,每个线程一个线程,它也很安全,但要注意内存泄漏。如果线程存活了很长时间,我们必须确保每次使用ThreadLocal时,都会调用其remove方法(特别是expungeStaleEntry)来清除数据。

关于Concurrent Pack

并发包是建立在AQS之上的。 AQS提供了一个框架,用于实现阻塞锁和一系列依赖FIFO等待队列的同步器。

线程池

最完整的线程池有大约7个参数。如果要合理使用线程池,绝对不会错过这些参数的优化。

线程池参数

并发包最常见的用途是线程池。建议直接使用线程池,Thread类可以降低优先级。我们主要使用newSingleThreadExecutor,newFixedThreadPool,newCachedThreadPool,schedule等,使用Executors工厂类创建。

newSingleThreadExecutor可以用来快速创建异步线程,非常方便。永远不应在高度并发的在线环境中使用newCachedThreadPool。它使用无限制的队列来缓冲任务,并可能爆炸你的记忆。

我习惯于自定义ThreadPoolExecutor,它是具有最完整参数的那个。

Public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueueworkQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

如果我的任务可以估算,corePoolSize,maximumPoolSize通常设置为相同的大小,然后生存时间设置得特别长。您可以避免频繁创建和关闭线程的开销。 I/O密集型和CPU密集型应用程序线程的大小不同。通常,可以更多地打开I/O密集型应用程序线程。

我通常定义一个threadFactory,主要是给线程一个名字。这样,当使用jstack等工具时,我可以直观地看到我创建的主题。

监测

最好地监视高度并发的线程池。它可以通过使用日志,存储等来保存,这对解决后续问题非常有帮助。

通常,您可以通过继承ThreadPoolExecutor来控制和监视线程行为来覆盖beforeExecute,afterExecute和terminate方法。

线程池饱和策略

也许最容易被遗忘的是线程的饱和策略。也就是说,线程和缓冲区队列的空间完全耗尽,以及如何处置新添加的任务。 Jdk默认实现4种策略。默认实现是AbortPolicy,它直接抛出异常。其他的如下所述。

DiscardPolicy比中止更激进,直接丢弃任务,并且没有异常信息。

CallerRunsPolicy由调用线程处理。例如,在Web应用程序中,在线程池资源已满后,新任务将在tomcat线程中运行。这种方法可以延迟某些任务的执行压力,但在更多情况下,它会直接阻塞主线程的操作。

DiscardOldestPolicy丢弃队列的最高任务,然后重试该任务(重复此过程)。

在许多情况下,这些饱和策略可能无法满足您的需求。您可以自定义自己的策略,例如将任务保存到某个存储。

阻止队列

阻塞队列阻塞当前线程。当队列中有元素时,被阻塞的线程将被自动唤醒,这极大地方便了编码的灵活性。在并发编程中,通常建议使用阻塞队列,以便实现可以尝试避免程序中的意外错误。阻塞队列的最经典方案是读取和解析套接字数据。读取数据的线程不断地将数据放入队列,解析线程不断从队列中提取数据进行处理。

默认情况下,对访问者的ArrayBlockingQueue调用是不公平的,我们可以通过设置构造函数参数将其更改为公平的阻塞队列。

LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用作线程池队列时很危险。

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

DelayQueue是一个无限制的阻塞队列,支持延迟的get元素。放置在DelayQueue中的对象需要实现Delayed接口,主要是为了提供延迟时间,并延迟队列的内部排序。这种方法通常比大多数非阻塞的while循环更节省cpu资源。

还有PriorityBlockingQueue和LinkedTransferQueue等,基于字面意思来猜测其目的。在线程池的构造参数中,我们使用队列,必须注意其特性和边界。例如,即使是最简单的newFixedThreadPool在某些情况下也是不安全的,因为它使用了无界的队列。

CountDownLatch

如果有一堆接口A-Y,每个接口最多需要200ms,最小接口为100ms。

我的一个服务需要提供一个接口Z,它调用A-Y接口来聚合结果。接口调用没有订单要求。接口Z如何在300ms内返回此数据?

这种类型的问题通常是赛马问题,并且只有通过并行计算才能完成问题。它可以分为两类:

实现任务的并行性

在开始执行之前等待n个线程完成任务

在并发包出现之前,手动编写这些同步过程非常复杂。您现在可以使用CountDownLatch和CyclicBarrier进行简单编码。

CountDownLatch由计数器实现,其初始值是线程数。每当线程完成自己的任务时,计数器的值减1。当计数器值达到0时,表示所有线程都已完成任务,然后等待锁的线程可以恢复执行任务。

CyclicBarrier在实现相同功能方面的能力相似。但是,在日常工作中,使用CountDownLatch会更频繁。

旗语

虽然信号量有一些应用场景,但大多数属于传奇,应该在编码中谨慎使用。

信号量可以实现限流功能,但它只是常用的限流方法之一。另外两个是漏桶算法和令牌桶算法。

Hystrix的融合功能还使用信号量进行资源控制。

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

但是,对于初级和中级农民来说,这些课程和职能很难理解,并且容易出错,应严格禁止在商业法规中使用。但是,在网络编程或某些框架项目中,这些功能是必要的,而且这部分工作不得分配给弟弟。

无论是等待,通知还是同步关键字或锁定,您都不需要它们,因为它们会导致程序复杂化。最好的方法是使用并发包提供的机制来避免一些编码问题。

并发包中的CAS概念在某种程度上是没有锁的实现。更专业的有一个类似于disruptor的无锁队列框架,但它仍然建立在CAS的编程模型上。近年来,像AKKA这样的事件驱动模型越来越受欢迎,但编程模型很简单,并不意味着实现很简单,其背后的工作仍然需要多线程来协调。

在golang引入了coroutine的概念之后,它为多线程添加了一个更轻量级的补充。 Java可以加载quasar来通过javaagent技术添加一些函数,但是我认为你不会为了这个效率而牺牲代码的可读性。

收集报告投诉