当前位置: 首页>后端>正文

java 并发编程

什么是线程和进程?

  • 进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行程序是一个进程从创建到消亡的过程。在java中,当我们启动main函数,其实就是启动了一个jvm的进程。main函数所在的线程就是这个进程中的一个线程,这个线程也加主线程。

  • 线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

请简要描述线程与进程的关系,区别及优缺点?

  • 一个进程中可以有多个线程,多个线程共享进程的堆、方法区(jdk1.8之后的元空间),但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈。

  • 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

程序计数器为什么是私有的?

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

  • 如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

  • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

一句话简单了解堆和方法区?

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、以及编译器编译后的代码等数据。

说说并发与并行的区别?

并发:某一个时间段同时进行多个事情,但是同一时刻可能并没有同时进行 在计算机科学中,多个线程或者是多个进程同时在单核或者多核cpu上执行时,执行路径具有不确定性的这种情形。

并行:同一时刻,同时进行多个事情。

为什么要使用多线程呢?

从计算机底层来说: 单核时代,主要是为了提高单线程利用CPU和IO系统的效率,当线程被IO阻塞时,能充分利用CPU。多核时代,主要是为了提高单线程对多核CPU能力的利用,例如,假设在多核CPU上只有一个线程执行,其他CPU资源就会被浪费掉。

使用多线程可能带来什么问题?

并发编程是为了提高程序的执行效率和运行速度(多核多线程),但是多线程不一定都能提高运行速度(单核多线程)。并且多线程可能会遇到内存泄漏、死锁、线程不安全等问题。

说说线程的生命周期和状态?

java 并发编程,第1张
img

什么是上下文切换?

  • 线程在执行过程中,会有自己的运行条件和状态,例如程序计数器,栈信息等。当出现如下条件时,线程会从CPU占用状态退出。

  • 主动退出CPU,比如调用了sleep,wait等

  • 时间片用完

  • 调用了阻塞类系统中断,比如IO,线程被阻塞、被终止或结束运行

  • 前面三种情况会发生线程上下文切换

什么是线程死锁?如何避免死锁?

  • 死锁:两个及以上的线程同时阻塞,并互相等待对方释放对方占用的资源,并且会一直等待下去,导致程序无法正常结束。

  • 死锁产生必须具备以下四个条件:

    • 互斥条件:该资源任意一个时刻只由一个线程占用

    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

    • 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源

    • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系

  • 如何预防死锁?破坏死锁的产生的必要条件即可:

    • 破坏请求与保持条件(不释放条件) :一次性申请所有的资源

    • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

    • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。例如要求按照A、B、C的顺序申请A、B、C三个资源,则不会发生死锁

  • 如何避免死锁?

    • 避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

    • 安全状态: 指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列.

说说 sleep() 方法和 wait() 方法区别和共同点?

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。

  • 两者都可以暂停线程的执行。

  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

  • new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

  • 总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

说一说自己对于 synchronized 关键字的了解?

  • synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • 在 Java 早期版本中,synchronized 属于 重量级锁,效率低下:

    • 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的,Java 的线程是映射到操作系统的原生线程之上的,而线程切换是需要从用户态切换到内核态,这个状态转换的时间成本比较高。

    • DK1.6 对锁的实现引入了大量的优化,如自旋锁、自适应锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

    • 所以,目前不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

说说自己是怎么使用 synchronized 关键字?

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁,静态方法锁和实例方法锁可以同时调用,一个锁类,一个锁实例,不会形成互斥

  • 修饰代码块 :指定加锁对象,对给定对象/类加锁

  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

双重校验锁实现对象单例,为什么需要用volatile关键字修饰成员变量?

  • new对象分为三步完成:

    • 为新对象分配地址空间

    • 初始化新对象

    • 将分配的地址空间指向新对象

  • new对象的过程存在指令重拍,如果顺序为1->3->2可能导致其他线程获取到没有正常初始化的对象。

  • 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

构造方法可以使用 synchronized 关键字修饰么?

  • 构造方法不能使用 synchronized 关键字修饰。

讲一下 synchronized 关键字的底层原理?

  • synchronized 同步代码块的情况:

    • synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置

    • 当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权

    • wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因

    • 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1

    • 在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • synchronized 修饰方法的的情况:

    • synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
  • 不过两者的本质都是对对象监视器 monitor 的获取。

说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?

  • JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、自适应性锁、锁消除、锁粗化等技术来减少锁操作的开销。

  • 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

锁升级过程?

  • 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

  • 偏向锁:

    • 线程获取到锁对象,如果此时没有其他线程占用锁对象,就将锁对象头中的标志位置为01,并将自己的线程ID记录在锁对象头的Mark Work的偏向锁线程ID中,同时将偏向锁状态置为1。

    • 当前当前线程再次加锁,直接锁加1,可重入。如果其他线程竞争,判断当前线程是否还需要锁,需要则释放偏向锁,升级为轻量级锁。如果不需要锁,则释放锁,其他线程竞争,继续偏向锁。

  • 轻量级锁:

    • 当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
  • 重量级锁:

    • 当前锁处于轻量级锁时,如果其他线程来竞争锁,此时会进行自旋。自旋锁重试之后如果抢锁依然失败,轻量级锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。
  • 偏向锁:适用于单线程适用锁的情况

    轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似)

    重量级锁:适用于竞争激烈的情况

谈谈 synchronized 和 ReentrantLock 的区别?

  • 两者都是可重入锁,是指线程可以再次获取自己已经获取的锁。比如一个线程已经获取到了A对象上的锁,此时这个线程还能再次获取A对象上的锁,只是锁计数加1,释放锁时,依次释放直到锁计数为0,才算释放锁。如果不支持重入的话,会造成死锁。

  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API,synchronized的监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的,Java1.6对synchronized的优化都是在jvm层面实现的;ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)。

  • ReentrantLock 比 synchronized 增加了一些高级功能,可以实现更精细的锁控制:

    • 等待可中断: ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情

    • 可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的

    • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。notify()/notifyAll()具体唤醒的线程依赖于操作系统的线程调度。而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程

volatile 关键字的作用?

  • 禁止指令重排

  • 保证共享变量在多线程环境下的可见性

为什么要使用CPU缓存?

  • CPU缓存在寄存器和主存直接设置了一层CPU缓存,寄存器每次都先从寄存器中获取数据,如果获取不到才到主存中读取数据。

  • CPU缓存为了解决CPU处理速度和内存处理速度不匹配的问题,内存缓存是用于解决内存访问太慢的问题。

谈一谈JMM?

JMM是java内存模型,模型是说java的线程都有自己的本地内存,可以将数据缓存到本地内存中,在线程执行过程中,可以先去本地内存中读取数据,而不是直接去主存中读取。在多线程的环境下,如果一个线程修改了主存中的数据,而另一个线程从它本地内存中读取到的是没有修改过的缓存数据,从而导致数据不一致。

并发编程的三个重要特征?

  • 原子性: 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。

  • 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。

  • 有序性:代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

说说 synchronized 关键字和 volatile 关键字的区别?

  • synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。

  • volatile 关键字能保证数据的可见性,但不能保证原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

ThreadLocal?

  • ThreadLocal是线程保存线程事由变量的对象,在当前线程中对当前线程ThreadLocal中的变量进行修改,只会影响到当前线程的ThreadLocal中的变量,不会影响到其他线程的ThreadLocal变量。

  • ThreadLocal内部持有一个ThreadLocalMap静态内部类,这个类类似java map的实现,保存的变量的K值是当前的线程的ThreadLocal<?>对象实例,value是要保存的值对象,一个对象可以持有多个ThreadLocal对象。

ThreadLocal 内存泄露问题?

ThreadLocalMap 中使用的 key 为 ThreadLocal<?>对象实例的弱引用,而 value 是强引用。所以,如果弱引用对象实例没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

ThreadlocalMap的key设置成弱引用的原因?

在一个Thread的生命周期中,如果Thread中的某个对象持有一个ThreadLocal对象,那么这个线程就有一个线程私有的变量ThreadLocal。

如果在线程生命周期中,持有ThreadLocal的对象被回收了,那么ThreadLocal也应该被回收掉,但是ThreadLocal是存放在Thread的ThreadLocalMap中的,如果ThreadLocalMap的key不是弱引用,则应该被回收掉的ThreadLocal会一直被Thread的ThreadLocalMap持有,则在线程生命周期能访问到一个应该被回收的对象,是不应该的。

一个ThreadLocal对象被设置到线程中时,至少有两个引用,一个是ThreadLocalMap中的key对它有弱引用,一个是持有ThreadLocal的对象对它有强引用。所以ThreadLocalMap的key被设置成弱引用,当引用ThreadLocal的对象被回收后,即强引用消失后,ThreadLocal对象只剩弱引用,会被回收掉。ThreadLocalMap没有对外提供操作key、value的api也是为了保证数据被回收的数据不能再被操作。

如何让线程持有一个贯穿线程生命周期的ThreadLocal?

  • 将ThreadLocal设置成静态变量(static修饰),这样ThreadLocal对象会被类的Class对象持有,会贯穿线程生命周期中。此时多个线程持有一个相同的ThreadLocal,每个ThreadLocal在各自的Thread中ThreadLocalMap中对应的值是不一样的,所以线程私有变量的实现是依赖线程的ThreadLocalMap来实现的。

  • 设计为非static的,长对象(比如被spring管理的对象)的内部,也不会被回收。

为什么要用线程池?

  • 线程池、数据库连接池、Http 连接池等等都是池化技术的应用。池化技术主要是为了减少每次获取资源的消耗,提高对资源的利用率。例如数据库连接,如果每次连接都去创建,开销非常大。

  • 线程池提供了对线程资源的限制和管理。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

实现 Runnable 接口和 Callable 接口的区别?

  • Runnable 接口 不会返回结果或抛出检查异常。

  • Callable 接口 可以回结果或抛出检查异常。

  • Executors 可以实现将 Runnable 对象转换成 Callable 对象 (Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result).

执行 execute()方法和 submit()方法的区别是什么呢?

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。

  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

如何创建线程池?

  • 通过ThreadPoolExecutor构造方法实现:

    • ThreadPoolExecutor一共有三个构造方法,一般使用参数最长的构造方法,自己指定各种参数来创建线程池。
  • 通过 Executor 框架的工具类 Executors 来实现:

    • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

    • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

    • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

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

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

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

ThreadPoolExecutor 最多参数构造方法分析?

  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数

  • workQueue:缓存队列

  • keepAliveTime:当线程池中的线程数大于corePoolSize核心线程数时,线程不会立即销毁,而是等超过keepAliveTime时间后才会销毁

  • unit:keepAliveTime的时间单位

  • threadFactory:线程工厂executor 创建新线程的时候会用到

  • handler:饱和策略

ThreadPoolExecutor 饱和策略定义?

  • 当同时运行的线程数达到maximumPoolSize最大线程数,且workQueue也已经满了时,再进来新的任务时,ThreadPoolTaskExecutor定义的一些拒绝策略。

饱和策略?

  • AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理,这是默认策略。

  • CallerRunsPolicy:直接在调用execute方法的线程中运行被拒绝的任务,这种策略会降低对于新任务的提交速度,影响整体性能。如果程序能够承受延迟,并且要求每个任务都被执行,可以使用这种策略。

  • DiscardPolicy:不处理新任务,直接丢弃掉。

  • DiscardOldestPolicy:抛弃最早的,未处理的任务。

线程池执行过程分析?

  • 提交任务。

  • 判断corePoolSize是否已经满了,没有满,创建新线程执行,满了交给workQueue。

  • 判断workQueue是否已经满了,没有满,添加到workQueue中,满了交给maximumPoolSize。

  • 判断maximumPoolSize是否已经满了,没有满,创建新线程执行,满了交给handler。

  • 判断handler使用的哪种拒绝策略,按照拒绝策略进行拒绝。

介绍一下 Atomic 原子类?

  • 原子类就是提供了一系列不可被中断的操作的类。

java.util.concurrent,JUC 包中的原子类是哪 4 类?

  • 基本类型:

    • AtomicInteger:整形原子类

    • AtomicLong:长整型原子类

    • AtomicBoolean:布尔型原子类

  • 数组类型:

    • AtomicIntegerArray:整形数组原子类

    • AtomicLongArray:长整形数组原子类

    • AtomicReferenceArray:引用类型数组原子类

  • 引用类型:

    • AtomicReference:引用类型原子类

    • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

    • AtomicMarkableReference :原子更新带有标记位的引用类型

  • 对象的属性修改类型:

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

    • AtomicLongFieldUpdater:原子更新长整形字段的更新器

    • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

      讲讲 AtomicInteger 的使用?

  • public final int get() //获取当前的值

  • public final int getAndSet(int newValue) //获取当前的值,并设置新的值

  • public final int getAndIncrement() //获取当前的值,并自增

  • public final int getAndDecrement() //获取当前的值,并自减

  • public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

  • boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

  • public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger 线程安全原理简单分析?

  • AtomicInteger主要使用了CAS + volatile + native方法来保证原子操作的,从而避免 synchronized 的高开销,执行效率大为提升。
  • CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。
  • UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。
  • 另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

为什么会设计AQS同步器类?

  • AQS(AbstractQueuedSynchronizer)同步器的作者在论文中阐明了,几乎任意一个同步器都可以基于它去实现其他类型的同步器。例如我们可以基于ReentrantLock去实现Semaphore,反过来也可以实现。但是这种设计会带来很大的复杂性和不灵活性。所以需要构建一个各种同步器的基础框架,即基于这个基础框架,可以实现各种同步器。
  • 由于java内置的同步锁synchronized关键字,存在潜在的饥饿问题,AQS的主要性能指标之一就是要解决饥饿问题,同时AQS也支持非公平的同步器。

介绍一下AQS?

  • AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面
  • AQS 是一个用来构建锁和同步器的基础框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等都是基于 AQS 实现的。

AQS是基于CLH实现的,介绍一下CLH?

CLH是排队式自旋锁论文三个作者的首字母,排队时式自旋锁要解决的问题是如何高效的访问多核CPU的共享资源。无论是一致性内存访问架构(CPU -> 高速缓存 -> 总线 -> 内存),还是非一致性内存访问架构(内存 -> CPU -> 总线)多线程之间的共享资源访问在高并发的情况下,都是系统瓶颈。CLH就是为了解决这个问题而设计的。

AQS原理介绍?

AQS使用一个int型的成员变量state来表示同步状态,且这个状态是volatile修饰的,保证线程可见的。所有要获取锁的线程都会被封装成一个PNode添加到一个FIFO的CLH队列中,后一个节点总是自旋监视前一个节点的状态,如果前一个节点的状态是释放锁,则后一个节点可以尝试去获取锁。

AQS支持的两种资源共享方式?

  • 独占(Exclusive):只有一个线程执行,又可以分为公平锁和非公平锁

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • 共享(Share):多个线程可以同时执行,例如CountDownLatch、Semaphore、 CyclicBarrier、ReadWriteLock

  • ReentrantReadWriteLock可以看做是组合式,因为读锁是多线程的,写锁是单线程的

AQS 组件总结?

  • 信号量(Semaphore)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源
  • 倒计时器(CountDownLatch): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行
  • 循环栅栏(CyclicBarrier): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似

用过 CountDownLatch 么?什么场景下用的?

CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch 。具体场景是下面这样的: 我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 为此我们定义了一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

shutdown() 与 shutdownNow() 对比?

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List

isTerminated() 与 isShutdown() 对比?

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。

ScheduledThreadPoolExecutor 和 Timer 的比较?

  • Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不是。
  • Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程。
  • 在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机( 即计划任务将不再运行),ScheduledThreadPoolExecutor 不仅捕获运行时异常,还允许在需要时处理它们。抛出异常的任务将被取消,但其他任务将继续正常运行。

线程池大小确定?

  • 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
  • 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上

JDK 提供的并发容器总结?

  • JDK 提供的这些容器大部分在 java.util.concurrent 包中。
  • ConcurrentHashMap : 线程安全的 HashMap
  • CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
  • ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。

ConcurrentHashMap?

  • HashMap 不是线程安全的。
  • 使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
  • 在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
  • 读操作时,只有是作为红黑树旋转时,才会加锁。其他时候都不用加锁,且value是volatile类型变量,是线程可见的。
  • 写操作通过分段加锁,没有hash冲突时,使用cas乐观锁,段出现hash冲突升级成synchronized悲观锁。

CopyOnWriteArrayList?

  • 写时复制技术
  • ReentrantReadWriteLock读写锁,解决了读读互斥的问题,但是存在读写互斥
  • CopyOnWriteArrayList让所有的写操作互斥,且写时,先将原来的数组复制一份(只能有一个写操作,只能复制一份副本),将数据添加进去,然后用新数据替换原数组,从而达到读读不互斥,读写不互斥,写写互斥。
  • 源码分析,读不做操作,写全部加上同步一把重入锁

ConcurrentLinkedQueue?

  • Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue。阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。
  • ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全。

BlockingQueue?

  • 阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中。
  • BlockingQueue 提供了可阻塞的插入和移除的方法。
  • 当队列容器已满,生产者线程会被阻塞,直到队列未满。
  • 当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

三个常见的 BlockingQueue 的实现类?

  • ArrayBlockingQueue:
    • ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现
    • ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞
    • ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,即线程访问不准寻FIFO。因此可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue可以在new的时候,通过参数指出
  • LinkedBlockingQueue:
    • 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量
    • 为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE
  • PriorityBlockingQueue:
    • 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则
    • PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列,后面插入元素的时候,如果空间不够的话会自动扩容
    • 简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)

ConcurrentSkipListMap?

  • 跳表又叫跳跃表,是一种可以进行二分查找的有序链表。跳表是在原来的有序链表上添加了多层有序链表实现的。如果严格按照二分查找构建跳表,每次添加数据后都必须重建i>1层的所有链表,会让插入和删除的时间复杂度退化成O(n),所以一般采用插入数据后,随机数据所处在层数,即第1到随机层数都有这个值(只需维护每层这个值的索引关系),从而达到简化跳表插入和删除。
  • 由于满足二分查找,所以跳表的查找复杂度是O(logn)
  • 跳表是一种以时间换空间的算法

线程池最佳实践

为什么要使用线程池?

  • 线程池、数据库连接池、Http 连接池等等,主要是为了减少每次获取资源的消耗,提高对资源的利用率。
  • 线程池提供了对线程使用的限制和管理。 同时每个线程池还维护一些基本统计信息,例如已完成任务的数量
  • 使用线程池的好处:
    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池在实际项目的使用场景?

线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。例如:批量发送邮件。

如何使用线程池?

一般是通过 ThreadPoolExecutor 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。创建线程池时需要注意核心线程数,最大线程数,队列长度,拒绝策略。

线程池最佳实践?

  • 使用 ThreadPoolExecutor 的构造函数声明线程池。
  • 避免使用Executors 类:
    • Executors 类的FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
    • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
  • 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  • 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
  • 总结:使用有界队列,控制线程创建数量。

监测线程池运行状态?

可以利用 ThreadPoolExecutor 的相关 API做一个的监控。打印线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。

建议不同类别的业务用不同的线程池?

一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。

别忘记给线程池命名?

  • 初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题
  • 自己实现 ThreadFactor,对线程命名

正确配置线程池参数?

CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 I/O 密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

AQS 简单介绍?

  • AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。
  • AQS 就是一个抽象类,主要用来构建锁和同步器。
  • AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。

AQS 原理概览?

  • AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  • AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

AQS 对资源的共享方式?

  • Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁

    • 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒
    • 公平锁和非公平锁只有两处不同:
      • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了
      • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面
      • 公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒
      • 相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态
  • Share(共享):

        多个线程可同时执行,如Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock
    

Semaphore(信号量)?

  • synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • Semaphore 有两种模式,公平模式和非公平模式:
    • 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO
    • 非公平模式: 抢占式的

CountDownLatch(倒计时器)?

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕,或者允许count线程同时运行。

CountDownLatch 的两种典型用法?

  • 某一线程在开始运行前等待 n 个线程执行完毕
    • 例子:
      • 主线程提交线程后,立即CountDownLatch.await()
      • 多个线程执行,每执行完成一个后,countdownlatch.countDown()
      • 当count个线程执行完成后,主线程会被唤醒,然后继续执行
  • 实现多个线程开始执行任务的最大并行性
    • 例子:
    • 需要一个startCountDownLatch和一个endCountDownLatch,startCountDownLatch用于控制所有线程同时执行,endCountDownLatch用于控制允许多少个线程同时执行
    • startCountDownLatch的count为1,每加入一个线程后,都startCountDownLatch.await(),同时endCountDownLatch.countDown()
    • 当所有线程加入后,在主线程上startCountDownLatch.countDown()

CountDownLatch 的不足?

CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用

CyclicBarrier(循环栅栏)?

  • CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
  • CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。
  • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活

CyclicBarrier 的应用场景?

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。

CyclicBarrier 和 CountDownLatch 的区别?

  • CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。
  • CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

ReentrantLock 与 Synchronized 对比?

锁的实现机制 AQS 监视器模式
灵活性 支持响应中断、超时、尝试获取锁 不灵活
释放锁的形式 必须显示的调用unlock释放锁 自动释放监视器
锁类型 公平锁、非公平锁 非公平锁
条件队列 可以关联多个条件队列 关联一个条件队列
可重入性 可重入 可重入

https://www.xamrdz.com/backend/3yw1924920.html

相关文章: