并发编程面试题1

简介: 并发编程面试题

并发

为什么要使用并发编程(并发编程的优点)

  • 并发编程可以提升 CPU 的计算能力的利用率,通过并发编程的形式可以将多核CPU 的计算能力发挥到极致
  • 提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
  • 并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务,提升系统并发能力和性能


并发编程有什么缺点

并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。


频繁的上下文切换消耗性能

时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。


减少上下文切换的解决方案


无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。

CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。

使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。


线程安全容易导致死锁

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";
    public static void main(String[] args) {
        deadLock();
    }
    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_b) {
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();
    }
}

在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b并等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。


如上所述,完全可以看出当前死锁的情况。


通常可以用如下方式避免死锁的情况:


避免一个线程同时获得多个锁;

避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;

尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;

对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况


内存泄露

内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。


并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?

并发编程三要素(线程的安全性问题体现在)

  • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,其他线程能够立刻看到。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)


出现线程安全问题的原因

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题


解决办法

  • JDK Atomic开头的原子类、synchronized、LOCK可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题


并行和并发有什么区别?

并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的

并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生,并行是真正意义上的同时进行,而并发只是让用户看起来是同时进行的,实际上还是一个一个的进行的

并发是指单个CPU处理多个任务,并行是指多个CPU处理多个任务


并发

并发 指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的(串行化是指有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题);其实就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。它只是用户看起来是同时进行的,实际上不上同时进行,而是一个个的的进行(多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行)


并行

并行是指多个处理器或者是多核的处理器同时处理多个不同的任务,例如当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行


这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。


就像上面这张图,只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。


多线程

多线程环境下为什么要引入同步的机制

当时用多线程访问同一个资源时,非常容易出现线程安全的问题,引用同步机制保证在多线程并发的情况下共享资源只能被一个线程所持有,从而避免了多线程冲突导致数据混乱

多线程什么场景下会发生死锁

当两个线程(进程)相互持有对方所需要的的资源,又不主动释放,导致所有线程(进程)都无法继续前进,导致程序陷入无尽的阻塞

什么是多线程?多线程的优劣?

多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

多线程的好处:

可以提高 CPU 的利用率,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的劣势:

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。

线程池

什么是线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。所以提高服务程序效率的一个手段就是尽可能减少线程创建和销毁的次数, 这就是”池化资源”技术产生的原因,池化技术主要是为了减少每次获取资源的消耗,提高对资源的利用率。


线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销。


JDK 中线程池框架的继承关系

线程池有什么作用/为什么要使用线程池/优点?

提高响应速度。同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行 ,创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。

提高线程的可管理性。可有效的控制最大并发线程数。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

提高系统资源的使用率,重用存在的线程,减少线程创建销毁的开销。

附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。


线程池创建的参数含义?

为了彻底了解线程池的时候,我们需要弄清楚线程池创建的几个参数

corepoolsize : 核心线程数,默认情况下,在创建线程池后,每当有新的任务来的时候,如果此时线程池中的线程数小于核心线程数,就会去创建一个线程执行(就算有空线程也不复用),当创建的线程数达到核心线程数之后,再有任务进来就会放入任务缓存队列中。当任务缓存队列也满了的时候,就会继续创建线程,知道达到最大线程数。如果达到最大线程数之后再有任务过来,那么就会采取拒绝服务策略。

Maximumpoolsize : 线程池中最多可以创建的线程数

keeplivetime : 线程空闲状态时,最多保持多久的时间会终止。默认情况下,当线程池中的线程数大于corepollsize 时,才会起作用 ,直到线程数不大于 corepollsize 。

workQueue: 阻塞队列,用来存放等待的任务

rejectedExecutionHandler :任务拒绝处理器(这个注意一下),有四种

abortpolicy 丢弃任务,抛出异常

discardpolicy 拒绝执行,不抛异常

discardoldestpolicy 丢弃任务缓存队列中最老的任务

CallerRunsPolicy 线程池不执行这个任务,主线程自己执行。


线程池哪几种?分别说一下

CachedThreadPool(可缓冲线程池)

线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)(只有非核心线程,最大线程数很大),每新来一个任务,当没有空余线程的时候就会重新创建一个线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话就会被回收,它可以一定程度减少频繁创建/销毁线程,减少系统开销,适用于执行时间短并且数量多的任务场景。


Executors.newCachedThreadPool();

特征


(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)


(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)


(3)当线程池中,没有可用线程,会重新创建一个线程


FixedThreadPool(定长线程池)

一个有指定的线程数的线程池,线程数量是固定的,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。核心线程是没有超时机制的,队列大小没有限制,除非线程池关闭了核心线程才会被回收。

(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量 
(2)Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式

特征

(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量

(2)线程可以重复被使用,在显示关闭之前,都将一直存在

(3)超出一定量的线程被提交时候需在队列中等待


SingleThreadExecutor(单任务线程池)

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,每次任务到来后都会进入阻塞队列,然后按指定顺序执行。


注意:如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务


 (1)Executors.newSingleThreadExecutor() ; 
 (2)Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式


特征

(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行

ScheduleThreadPool(周期线程池)

创建一个定长线程池,支持定时及周期性任务执行,通过过schedule(s gei4 酒)方法可以设置任务的周期执行

(1)Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数 
(2)newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);
// corePoolSize线程的个数,threadFactory创建线程的工厂

特征

(1)线程池中具有指定数量的线程,即便是空线程也将保留

(2)可定时或者延迟执行线程活动


一个任务进来后,线程池是怎么处理的(线程池的实现原理)?


任务被提交到线程池,先看看有没有空闲的线程可以用,有就直接用,没有就会先判断当前线程数量是否小于corePoolSize(线程池核心线程数量),如果小于则创建线程来执行提交的任务,否则将任务放入workQueue(线程堵塞队列)缓存队列,如果workQueue缓存队列满了,则判断当前线程数量是否小于maximumPoolSize(线程池最大线程数量),如果小于则创建线程执行任务,否则就会调用handler(拒绝策略)来拒绝接收任务。


线程池的拒绝策略有哪几种?

当线程池中正在运行的线程已经达到了指定的最大线程数量maximumPoolSize且线程池的阻塞队列也已经满了时,向线程池提交任务将触发拒绝处理逻辑。提供了四种拒绝策略,它们分别是AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy和DiscardPolicy


1. AbortPolicy(a 不 t)

拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。

/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
 new AbortPolicy();

可以看出线程池的默认拒绝策略为AbortPolicy

2.DiscardPolicy

这种拒绝策略直接把被提交的新任务直接被丢弃掉,也不会报错,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。安静的扔掉多余的新任务。

3.DiscardOldestPolicy

丢弃最早未处理请求策略,丢弃最先进入阻塞队列的任务以腾出空间让新的任务入队列。

4.CallerRunsPolicy

当有新任务提交后把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。


这样做主要有两点好处


新提交的任务不会被丢弃,这样也就不会造成业务损失。

由于谁提交任务谁就要负责执行任务,那提交任务的线程就会一直被占着,线程池的其他线程在此期间也会执行掉一部分任务,那就腾出一定的空间,相当于是给了线程池一定的缓冲期(其实就是提交任务的线程执行提交的新任务时,其他线程可能就有的执行完任务了,原本是没有多余的线程,但是现在就有了多余空闲的线程)


使用也很容易,在配置的线程池中指定对应的拒绝策略就行

如果你提交任务时,线程池队列已满,这时会发生什么

如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,则判断当前线程数量是否小于maximumPoolSize(线程池最大线程数量),如果小于则创建线程执行任务,否则就会调用handler(拒绝策略)来拒绝接收任务,默认是 AbortPolicy策略


线程池都有哪些状态?

RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。

SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。

STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。

TIDYING(太顶):

SHUTDOWN 状态下,当已经排队的所有任务都被处理完了,任务数为 0, 线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。

线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。

TERMINATED(te1 米 nei3 ti3 d):线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态


Executor 框架

什么是 Executor线程池框架?

Executor框架是指JDK 1.5中引入的一系列并发库中与线程池相关的功能类,包括Executor、Executors、ExecutorService、Future、Callable等。可以通过该框架来控制线程的启动,执行,关闭,简化并发编程。Executor框架把任务提交和执行解耦,要执行任务的人只需要把任务提交即可,任务的执行不需要去关心。通过Executor框架来启动线程比使用Thread更好,更易管理,效率高,避免this逃逸问题。


Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。简单来说,Executor框架可以非常方便的创建一个线程池



Executor框架结构

Executor框架包括3大部分


(1)任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;


(2)任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。


(3)异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。


Executor框架的成员及其关系可以用一下的关系图表示


还是蛮好理解的,正如Java优秀框架的一贯设计思路,顶级接口-次级接口-虚拟实现类-实现类。


Executor:执行者,java线程池框架的最上层父接口,地位类似于spring的BeanFactry、集合框架的Collection接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行。


ExecutorService:该接口继承自Executor接口,添加了shutdown、shutdownAll、submit、invokeAll等一系列对线程的操作方法,该接口比较重要,在使用线程池框架的时候,经常用到该接口。


ExecutorService的生命周期包括三种状态:运行,关闭,终止。创建后便进入运行状态,当调用了shutdown()方法时,进入关闭状态,此时不再接受新任务,但是它还在执行已经提交了的任务,当所有的任务执行完后,便达到了终止状态。shutdownNow()方法关闭当前服务,尚未执行的任务,不再执行;正在执行的任务,通过线程中断thread.interrupt。方法返回等待执行的任务列表。


AbstractExecutorService:这是一个抽象类,实现ExecuotrService接口,


ThreadPoolExecutor:这是Java线程池最核心的一个类,该类继承自AbstractExecutorService,主要功能是创建线程池,给任务分配线程资源,执行任务。


ScheduledExecutorSerivce 和 ScheduledThreadPoolExecutor :延迟执行和周期性执行的线程池。


Executors:这是一个静态工厂类,该类定义了一系列静态工厂方法,通过这些工厂方法可以返回各种不同的线程池。


Executor框架的使用示意图

为什么要使用Executor线程池框架

每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。

调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。

直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。


使用Executor线程池框架的优点

  • 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
  • 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
  • 框架中已经有定时、定期、单线程、并发数控制等功能。
  • 综上所述使用线程池框架Executor能更好的管理线程、提供系统资源使用率。


在 Java 中 Executor 和 Executors 的区别?

Executor它是"执行者",java线程池框架的最上层父接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行,Executor 接口对象能执行我们的线程任务。准确的说,Executor提供了execute()接口来执行已提交的 Runnable 任务的对象。ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

Executors是个静态工厂类,是个工具类,可以通过这个工具类按照我们的需求创建了不同的线程池,来满足业务的需求。它通过静态工厂方法返回ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 等类的对象。


线程池中 submit() 和 execute() 方法有什么区别?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行Runnable 和 Callable 类型的任务。

返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有异常处理:submit()方便Exception处理

什么是线程组?为什么在 Java 中不推荐使用?

什么是线程组?

线程组(ThreadGroup)简单来说就是多个线程的集合。线程组的出现是为了更方便地管理线程。Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制,默认情况下,所有的线程都属于主线程组(main线程组)。


线程组是父子结构的,一个线程组可以集成其他线程组,同时也可以拥有其他子线程组。从结构上看,线程组是一个树形结构,每个线程都隶属于一个线程组,线程组又有父线程组,这样追溯下去,可以追溯到一个根线程组——System线程组。


下面介绍一下线程组树的结构:


JVM创建的system线程组是用来处理JVM的系统任务的线程组,例如对象的销毁等。

system线程组的直接子线程组是main线程组,这个线程组至少包含一个main线程,用于执行main方法。

main线程组的子线程组就是应用程序创建的线程组。

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。


为什么不推荐使用线程组?

虽然线程组看上去很有用处,实际上现在的程序开发中已经不推荐使用它了,主要有两个原因:


线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。

线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

虽然线程组现在已经不被推荐使用了,但是它在线程的异常处理方面还是做出了一定的贡献。

当线程运行过程中出现异常情况时,在某些情况下JVM会把线程的控制权交到线程关联的线程组对象上来进行处理。


线程池之ThreadPoolExecutor详解

最原始的创建线程池的方式,它包含了 7 个参数可供设置。

public static void myThreadPoolExecutor() {
    // 创建线程池
    ThreadPoolExecutor threadPool = 
        new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

执行结果如下:



ThreadPoolExecutor 参数介绍

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

7 个参数代表的含义如下:


参数 1:corePoolSize


核心线程数,线程池中始终存活的线程数。


参数 2:maximumPoolSize


最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。


参数 3:keepAliveTime


最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。


参数 4:unit


单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:


TimeUnit.DAYS:天

TimeUnit.HOURS:小时

TimeUnit.MINUTES:分

TimeUnit.SECONDS:秒

TimeUnit.MILLISECONDS:毫秒

TimeUnit.MICROSECONDS:微妙

TimeUnit.NANOSECONDS:纳秒

参数 5:workQueue


一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:


ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue:一个由链表结构组成的无界阻塞队列(默认)。

SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。

PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。


参数 6:threadFactory


线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。


参数 7:handler


拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:


AbortPolicy:拒绝并抛出异常。


CallerRunsPolicy:使用当前调用的线程来执行此任务。


DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。


DiscardPolicy:忽略并抛弃当前任务。


默认策略为 AbortPolicy。


线程池的执行流程

ThreadPoolExecutor 关键节点的执行流程如下:


当线程数小于核心线程数时,创建线程。

当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。

线程池的执行流程如下图所示:

自定义拒绝策略演示

public static void main(String[] args) {
    // 任务的具体方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("当前任务被执行,执行时间:" + new Date() +
                               " 执行线程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 创建线程,线程的任务队列的长度为 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
         100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
           new RejectedExecutionHandler() {
           @Override
           public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
           // 执行自定义拒绝策略的相关操作
           System.out.println("我是自定义拒绝策略~");
         }
      });
    // 添加并执行 4 个任务
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}


总结

最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略


你知道怎么创建线程池吗?

线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):


Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;

Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;

Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;

Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;

Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;

Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。

ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。

简单来说就两种创建线程池的方法,一种是用Executors工具类来创建,一种是用ThreadPoolExecutor来创建,推荐使用ThreadPoolExecutor创建线程池


为什么不推荐使用Executors创建线程池,而是推荐使用ThreadPoolExecutor来创建线程池?

因为JDK 自带工具类创建的线程池Executors存在的问题,最大的两个问题如下所示

  • 有的线程池可以无限添加任务或线程,容易导致 OOM;
public static ExecutorService newFixedThreadPool(int nThreads) {
 return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

可见其任务队列用的是LinkedBlockingQueue,且没有指定容量,相当于无界队列,这种情况下就可以添加大量的任务,甚至达到Integer.MAX_VALUE的数量级,如果任务大量堆积,可能会导致 OOM。

public static ExecutorService newCachedThreadPool() {
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}

这个虽然使用了有界队列SynchronousQueue,但是最大线程数设置成了Integer.MAX_VALUE,这就意味着可以创建大量的线程,也有可能导致 OOM。


还有一个问题就是这些线程池的线程都是使用 JDK 自带的线程工厂 (ThreadFactory)创建的,线程名称都是类似pool-1-thread-1的形式,第一个数字是线程池编号,第二个数字是线程编号,这样很不利于系统异常时排查问题。

所以推荐使用ThreadPoolExecutor方法来创建线程池

Executors和ThreaPoolExecutor创建线程池的区别】

Executors类和ThreadPoolExecutor都是util.concurrent并发包下面的类, Executos下面的newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newCachedThreadPool底层的实现都是用的ThreadPoolExecutor实现的,所以ThreadPoolExecutor更加灵活。


我觉得最大的区别就是Executors使用起来很方便,但是容易出现问题,例如像OOM,这个里面的线程池都是固定类型的,可能所有的类型在某个场合都不合适使用,所以这个时候还是要ThreadPoolExecutor来,而ThreadPoolExecutor因为是最原始的,所以使用起来更灵活,可以定制线程池,ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。


《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险Executors 各个方法的弊端:


newFixedThreadPool 和 newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。newCachedThreadPool 和 newScheduledThreadPool:

主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。


线程池之ScheduledThreadPoolExecutor详解

线程池之ScheduledThreadPoolExecutor详解_ThinkWon的博客-CSDN博客_scheduledthreadpoolexecutor


ScheduledThreadPoolExecutor详解 - 知乎

什么是 FutureTask

FutureTask是一种可以取消的异步的计算任务,FutureTask实现了Runnable和Future这两个接口,FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作,只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。由于实现了Runnable接口,所以 FutureTask 也可以放入线程池中。


FutureTask实现了RunnableFuture接口,而RunnableFuture接口扩展自Future和Runnable接口,在创建FutureTask时可以使用Callable接口的实例或者Lambda表达式,也可以使用Runnable的实例,但内部还是会使用适配器模式转换成Callable实例类型。


使用FutureTask的优势有

  • 可以获取线程执行后的返回结果;
  • 提供了超时控制功能。
  • 它实现了Runnable接口和Future接口


什么是异步计算呢?

异步计算是指在让该任务执行时,不需要一直等待其运行结束返回结果,而是可以先去处理其他的事情,然后再获取返回结果。例如你想下载一个很大的文件,这时很耗时的操作,没必要一直等待着文件下载完,你可以先去吃个饭,然后再回来看下文件是否下载完成,如果下载完成就可以使用了,否则还需要继续等待。异步计算实现原理就是因为FutureTask实现了Runnable和Future这两个接口


创建FutureTask实例的两种方式

第一种:使用Callable创建一个FutureTask实例:

FutureTask<Boolean> future = new FutureTask<>(new Callable<Boolean>() {
   @Override
   public Boolean call() throws Exception {
     return true;
   }
 });


通过new一个对象的方法可以直接创建一个FutureTask实例,如果直接调用run方法将直接在当前线程中运行,不会开启新线程。

第二种:使用ExecutorService或者线程可以让FutureTask进行托管进行,示例如下:

//托管给线程池处理
Future<?> futureResult = Executors.newCachedThreadPool().submit(future);
//托管给单独线程处理
new Thread(future).start();

因为FutureTask继承了Runnable接口,所以它可以通过new Thread()的方式进行运行,再由future变量来检索结果值或者取消任务等操作,通过线程池托管的方式也可以适用。


取消任务

遇到特殊情况时需要对没有运行的或者已经运行的任务进行取消操作,这时可以调用cancel()方法,取消方法有一个布尔类型的参数mayInterruptIfRunning;当值为ture时将尝试中断托管的线程(调用托管线程的interrupt尝试中断)。


取消任务使用的是线程的中断操作,如果任务是可取消的,在任务中存在阻塞线程的地方需要加上InterruptedException的异常捕获来处理中断异常或者在任务可取消点使用Thread.currentThread().isInterrupted()方法来判断任务是否已经发送的中断请求以正常取消任务。


检索结果值

使用FutureTask的一个重要目的是为了能获取到任务的结果值,使用Callable使用一个任务调用点时可以在任务中返回一个引用类型。


在FutureTask内部使用outcome变量存储Callable的结果,调用FutureTask.get()方法将检索结果值,但get()方法也会阻塞调用线程直到任务执行完成或者取消。


get()方法还可以通过设置超时时间来指定等待的时长,超过等待时间后将会抛出TimeoutException异常。

总结

使用FutureTask可以完成任务的取消、检查结果值,这两项也是FutureTask的特色,但FutureTask的底层还是托管给Thread完成;相对于Thread检查结果值会更新的方便,不再需要管理线程执行的状态与值。


在使用cancel方法需要注意任务是否可以取消,在任务内部需要使用Thread.currentThread().isInterrupted()检查中断状态并在Thread.sleep() 、condition.wait()、 Thread.join() 、Thread.wait()使用线程进行阻塞以及可中断的I/O操作方法中捕获InterruptedException异常以避免不必要的情况发生,在大多数任务中是不应该被中断的,所以最好在可中断的任务中设置好检查点;在任务线程会被阻塞点捕获InterruptedException异常,根据情况判断是否需要取消任务。


什么是 Callable 和 Future?

Callable 是类似于 Runnable的接口,Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值和抛异常


Future 接口就是可以拿到Callable的返回值,Future 可以拿到异步执行任务的返回值。表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。


public class CallableAndFuture {
  public static void main(String[] args) {
    Callable<Integer> callable = new Callable<Integer>() {
      public Integer call() throws Exception {
        return new Random().nextInt(100);
      }
    };
    FutureTask<Integer> future = new FutureTask<Integer>(callable);
    new Thread(future).start();
    try {
      Thread.sleep(5000);// 可能做一些事情
      System.out.println(future.get());
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
}

线程

什么是线程和进程?

进程

进程是操作系统资源分配的基本单位,进程是一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程

线程是处理器任务调度和执行的基本单位,线程是进程中的一个控制单元,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

进程与线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。


1、根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位


2、资源开销:进程之间的切换会有较大的开销,线程之间切换的开销小。


3、包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线


(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。


4、内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的


5、影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。


6、执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行


一般怎么样才能做到线程安全?


1. 无状态

我们都知道只有多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?

public class NoStatusService {
    public void add(String status) {
        System.out.println("add status:" + status);
    }
    public void update(String status) {
        System.out.println("update status:" + status);
    }
}

这个例子中NoStatusService没有定义公共资源,换句话说是无状态的。

这种场景中,NoStatusService类肯定是线程安全的。

2. 不可变

如果多个线程访问的公共资源是不可变的,也不会出现数据的安全性问题。

public class NoChangeService {
    public static final String DEFAULT_NAME = "abc";
    public void add(String status) {
        System.out.println(DEFAULT_NAME);
    }
}

EFAULT_NAME被定义成了static final的常量,在多线程中环境中不会被修改,所以这种情况,也不会出现线程安全问题。

3. 无修改权限

有时候,我们定义了公共资源,但是该资源只暴露了读取的权限,没有暴露修改的权限,这样也是线程安全的。

public class SafePublishService {
    private String name;
    public String getName() {
        return name;
    }
    public void add(String status) {
        System.out.println("add status:" + status);
    }
}

这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。

4. synchronized


使用JDK内部提供的同步机制,这也是使用比较多的手段,分为:同步方法 和 同步代码块。我们优先使用同步代码块,因为同步方法的粒度是整个方法,范围太大,相对来说,更消耗代码的性能。


其实,每个对象内部都有一把锁,只有抢到那把锁的线程,才被允许进入对应的代码块执行相应的代码。当代码块执行完之后,JVM底层会自动释放那把锁。

public class SyncService {
    private int age = 1;
    private Object object = new Object();
    //同步方法
    public synchronized void add(int i) {
        age = age + i;        
        System.out.println("age:" + age);
    }
    public void update(int i) {
        //同步代码块,对象锁
        synchronized (object) {
            age = age + i;                     
            System.out.println("age:" + age);
        }    
     }
     public void update(int i) {
        //同步代码块,类锁
        synchronized (SyncService.class) {
            age = age + i;                     
            System.out.println("age:" + age);
        }    
     }
}

5. Lock

除了使用synchronized关键字实现同步功能之外,JDK还提供了Lock接口,这种显示锁的方式。

通常我们会使用Lock接口的实现类:ReentrantLock,它包含了:公平锁、非公平锁、可重入锁、读写锁 等更多更强大的功能。

public class LockService {
    private ReentrantLock reentrantLock = new ReentrantLock();
    public int age = 1;
    public void add(int i) {
        try {
            reentrantLock.lock();
            age = age + i;           
            System.out.println("age:" + age);
        } finally {
            reentrantLock.unlock();        
        }    
   }
}

但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁。

不过说句实话,在使用Lock显示锁的方式,解决线程安全问题,给开发人员提供了更多的灵活性。

6. 分布式锁

如果是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。


但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全。


这就需要使用:分布式锁了。


分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。


其中我个人更推荐使用redis分布式锁,其效率相对来说更高一些。


try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}  

同样需要在finally代码块中释放锁。

目录
相关文章
|
9月前
|
安全 算法 Java
去某东面试遇到并发编程问题:如何安全地中断一个正在运行的线程
一个位5年的小伙伴去某东面试被一道并发编程的面试题给Pass了,说”如何中断一个正在运行中的线程?,这个问题很多工作2年的都知道,实在是有些遗憾。 今天,我给大家来分享一下我的回答。
69 0
|
10月前
|
资源调度
JUC并发编程之同步器(Semaphore、CountDownLatch、CyclicBarrier、Exchanger、CompletableFuture)附带相关面试题
1.Semaphore(资源调度) 2.CountDownLatch(子线程优先) 3.CyclicBarrier(栅栏) 4.Exchanger(公共交换区) 5.CompletableFuture(异步编程)
109 0
|
15天前
|
机器学习/深度学习 数据采集 自然语言处理
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
|
20天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
26 1
|
20天前
|
调度 Python
Python并发编程模型:面试中的重点考察点
【4月更文挑战第14天】Python并发编程包括多线程、多进程和协程,常用于提高系统响应和资源利用率。多线程简单但受限于GIL;多进程可规避GIL,但通信开销大;协程适合IO密集型任务,学习成本较高。面试常见问题涉及并发并行概念、GIL影响、进程间通信同步及协程的异步IO理解。掌握并发模型的选择与应用,能有效提升面试表现。
28 0
|
20天前
|
Java Go 调度
Go语言并发编程原理与实践:面试经验与必备知识点解析
【4月更文挑战第12天】本文分享了Go语言并发编程在面试中的重要性,包括必备知识点和面试经验。核心知识点涵盖Goroutines、Channels、Select、Mutex、Sync包、Context和错误处理。面试策略强调结构化回答、代码示例及实战经历。同时,解析了Goroutine与线程的区别、Channel实现生产者消费者模式、避免死锁的方法以及Context包的作用和应用场景。通过理论与实践的结合,助你成功应对Go并发编程面试。
31 3
|
20天前
真实并发编程问题-1.钉钉面试题
真实并发编程问题-1.钉钉面试题
43 0
|
20天前
|
NoSQL Java 关系型数据库
2024最新500道Java高岗面试题:数据库+微服务 +SSM+并发编程+..
今天分享给大家的都是目前主流企业使用最高频的面试题库,也都是 Java 版本升级之后,重新整理归纳的最新答案,会让面试者少走很多不必要的弯路。同时每个专题都做到了详尽的面试解析文档,以确保每个阶段的读者都能看得懂。
|
8月前
全到哭!从面试到架构,阿里大佬用五部分就把高并发编程讲清楚了
不知道大家最近去面试过没有?有去面试过的小伙伴应该会知道现在互联网企业招聘对于“高并发”这块的考察可以说是越来越注重了。基本上你简历上有高并发相关经验,就能成为企业优先考虑的候选人。其原因在于,企业真正需要的是能独立解决问题的人才。每年面试找工作的人很多,技术水平也是高低不一,而并发编程却一直是让大家很头疼的事情,很多人总觉得自己似乎掌握了并发编程的知识,但实际在面试或者工作中,都会被它吊打虐哭。
115 0
|
9月前
|
缓存 安全 Java
Java并发编程必知必会面试连环炮
Java并发编程必知必会面试连环炮
122 0