并发基础

目录

一、Java内存模型

1.并发需要解决的问题及问题产生的根源

可见性

原子性

有序性

2.Java是怎么解决并发问题的: JMM(Java内存模型)

什么是Java内存模型

Java内存模型的实现

volatile

synchronized

final

happens-before 规则

ThreadLocal

总结线程安全的实现方法

二、线程基础

1.线程的6种状态

2.线程的实现方式

3.基础线程机制

Daemon线程

sleep()

yield()

4.线程中断

interrupted()

Executor 的中断操作

5.线程同步

synchronized

ReentrantLock

6.线程间通信

join()

wait() notify() notifyAll()

三、JUC

1.Locks

类结构总览

接口Condition

接口: Lock

接口: ReadWriteLock(读写锁)

抽象类: AbstractOwnableSynchonizer

抽象类: AbstractQueuedSynchonizer(AQS)

抽象类(long): AbstractQueuedLongSynchronizer

锁常用类: ReentrantLock

锁常用类: ReentrantReadWriteLock

锁常用类: LockSupport

并发工具类:CountDownLatch

并发工具类:CyclicBarrier

并发工具类:Semaphore

2.原子类

基础类型:AtomicBoolean,AtomicInteger,AtomicLong

数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

引用:AtomicReference,AtomicMarkedReference,AtomicStampedReference

3.并发集合

BlockingQueue和BlockingDeque

Queue: ArrayBlockingQueue

Queue: LinkedBlockingQueue

Queue: SynchronousQueue

List: CopyOnWriteArrayList

Set: CopyOnWriteArraySet

Map: ConcurrentHashMap

4.线程池相关类

四、线程池

1.线程池的优势

2.线程池ThreadPoolExecutor的主要参数

corePoolSize:核心线程数

maxPoolSize:最大线程数

keepAliveTime:线程空闲时间

unit:keepAliveTime的单位

workQueue:阻塞队列

threadFactory:创建线程的工厂

handler:拒绝策略

3.线程池流程

4.常见的线程池

类结构关系

线程池1:ThreadPoolExecutor

线程池2:ScheduledThreadPoolExecutor

线程池3:ForkJoinPool

5.线程池使用技巧

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

配置线程池需要考虑的因素

五、各种锁的总结

1.悲观锁 VS 乐观锁

悲观锁

乐观锁

2.自旋锁 VS 适应性自旋锁

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

4. 公平锁 VS 非公平锁

5. 可重入锁 VS 非可重入锁

6. 独享锁 VS 共享锁


一、Java内存模型

1.并发需要解决的问题及问题产生的根源

可见性

      可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
      根源:CPU高速缓存、寄存器等引起,变量的修改没有立即写入主存。

原子性

      原子性:即一个操作或者多个操作,要么全部执行成功,要么不执行。
      根源:操作系统增加了进程、线程,以分时复用 CPU,导致可能存在非原子操作。

有序性

      有序性:即程序执行的顺序按照代码的先后顺序执行。
      根源:编译器和处理器常常会对指令做重排序。但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。

2.Java是怎么解决并发问题的: JMM(Java内存模型)

什么是Java内存模型

      JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
      所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

      

Java内存模型的实现

      Java内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障。Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。比如关键字:volatile、synchronized、final。
      原子性:synchronized来保证方法和代码块内的操作是原子性的。
      可见性:volatile修改立即同步主内存,使用之前都从主内存刷新;还可以使用synchronized、final保证可见性。
      有序性:volatile关键字会禁止指令重排;synchronized保证同一时刻只允许一条线程操作。

volatile

      当一个变量定义为 volatile 之后,将具备两种特性:
      1.保证多线程操作时变量的可见性。被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新原理:当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中,但其他处理器缓存的值还是旧的。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议(缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。)
      2.禁止指令重排序优化(有序性)。原理:Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
      3.无原子性。(对volatile修饰的变量的单次读/写操作可以保证原子性的,如long和double类型变量(高32位和低32位),但是并不能保证i++的原子性,因为本质上i++是读、写两次操作。)

synchronized

      synchronized的作用

      保证原子性:在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断
      保证可见性:释放锁之前会将对变量的修改刷新到主存
      保证有序性:每个时刻都只有一个线程访问同步代码块
      具备可重入性:一个线程拥有了锁仍然还可以重复申请锁

      synchronized的使用

      修饰代码块,对synchronized括号内的对象加锁
      修饰实例方法,对当前实例对象加锁,监视器锁monitor是this
      修饰静态方法,对这个类的所有对象加锁,监视器锁monitor是Class对象
      修饰类,对这个类的所有对象加锁

      synchronized底层实现

      synchronized是通过对象内部的监视器锁(monitor)来实现的。具体的:
      1.同步代码块:利用monitorenter和monitorexit这两个字节码指令。执行monitorenter时,尝试获取监视器锁monitor的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。
      2.同步方法:字节码多了ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。

      理解Java对象头

      在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

      
      实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
      填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
      对象头:包括Mark WordClass MetaData Address。Mark Word存储对象的哈希码、GC分代年龄、锁信息等;Class MetaData Address存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度。【对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。锁的类型和状态无锁状态、偏向锁、轻量级锁、重量级锁,JDK6之后对synchronized进行了优化,新增了偏向锁、轻量级锁两种状态)在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

虚拟机位数对象头结构描述
32位/64位Mark Word存储对象的哈希码、GC分代年龄、锁信息等
32位/64位Class MetaData Address指向对象类型数据的指针
32位/64位数组长度如果是数组对象的话,有这一部分,否则没有

      synchronized优化

      为什么优化?synchronized是通过对象内部的监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
      单线程重复获取锁-->偏向锁
      不同线程交替执行-->轻量级锁
      多线程竞争锁-->重量级锁

      1.锁膨胀

      1.1偏向锁
      引入偏向锁主要目的是减少只有一个线程的情况下获取锁的代价,减少轻量级锁的消耗。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。(CAS是乐观锁,synchronized是悲观锁)
      偏向锁的获取:如果一个线程获得了锁,那么锁就进入偏向模式,对象头的Mark Word的标志位设置为“01”,即偏向模式,再使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word中。当该线程再次请求锁时,无需再做任何同步操作,只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
      偏向锁的释放:偏向锁使用了遇到竞争才释放锁的机制。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码),具体是,先暂停拥有偏向锁的线程,然后判断锁对象是否还处于被锁定状态,如果是,则升级为轻量级锁,如果否,则恢复到无锁。

      1.2轻量级锁
      引入轻量级锁主要目的是在没有多线程竞争的情况下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统重量级锁开销更大,升级为重量级锁。
      轻量级锁加锁和解锁:依赖多次CAS操作。

      1.3重量级锁
      存在多线程竞争时,锁会升级为重量级锁。

      2.锁消除

      消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

      3.锁粗化

      锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

      4.自旋锁与自适应自旋锁

      轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
      自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起。缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。(自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。)
      自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

      synchronized和volatile区别

      1.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
      2.volatile保证变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
      3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

final

      final基础使用

      修饰类:final类不能被继承
      修饰方法:final方法不能被重写,但可以被重载
      修饰参数:将参数指明为final,这意味这你无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。
      修饰变量
      1.修饰基本类型变量,变量只能被初始化一次,大多是我们平时static final的用法,存在方法区(元空间)
      2.修饰引用类型,其在内存中指向的地址不可更改(或者说一直指向某个内存地址)。例如:
            StringBuffer sb1 = new StringBuffer("sb");
            final StringBuffer finalSb = sb1;
      finalSb会一直指向new StringBuffer("sb")开辟的堆空间地址,即使sb1指向了其他地址,finalSb也还是指向一开始的内存地址。
      使用final修饰引用变量的目的:JVM可以对final变量的使用进行优化(指提升速度层面的,因为不可变,所以直接进入高速缓存区,且不用考虑线程间可见性等问题)

      final域重排序规则(针对有序性)

      按照final修饰的数据类型分类(final域重排序规则一般都是指修饰成员变量的情况):
      1.基本数据类型:
      final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
      final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。确保在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
      2.引用数据类型:
      额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量重排序。
      例如下面代码,左边是修饰基本数据类型,右边是修饰引用,帮助下理解:

            

      final域重排序规则实现原理:
      写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
      很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器。

happens-before 规则

      什么是happens-before

      在Java内存模型中,happens-before的意思是前一个操作的结果可以被后续操作获取。

      为什么需要happens-before

      JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

      有哪些happens-before规则

      单一线程原则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
      管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果。
      volatile 变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
      线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
      线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
      线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
      传递性规则:happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
      对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

ThreadLocal

      ThreadLocal简介

      ThreadLocal来维护变量时,ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。

      ThreadLocal使用场景

      1.数据库连接(图1)
      2.Session管理(图2)
      3.SimpleDateFormat线程不安全,用ThreadLocal存放保证每个线程单独一份

         

      【延伸知识点】
      ThreadLocal和数据库连接池解决的是两个不同的问题:
      private static final ThreadLocal <Connection> ct = new ThreadLocal <Connnection>(); 
      这行代码保证每一个线程都有一个自己独立的Connection;Connection可以去新建立,也可以从连接池取出来,从连接池取是为避免不必要的创建和销毁。
      如果不用ThreadLocal,即使从连接池取也有可能发生多个线程使用同一个Connection的问题。

      ThreadLocal原理

      利用ThreadLocal的静态内部类ThreadLocalMap存放每个线程的变量副本,通过当前线程的引用获取到线程的ThreadLocalMap 类型的 threadLocals 属性,再通过ThreadLocal的this引用获取到当前线程的变量副本。

      get方法:
      1.先获取到当前线程的引用
      2.利用这个引用来获取到 ThreadLocalMap(即Thread的属性ThreadLocalMap,Map的key即是ThreadLocal的引用,为什么需要Map,因为一个线程可能有多个ThreadLocal,见图4)
      3.如果 map 存在,则获取当前 ThreadLocal 对应的 value 值
      4.如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化

                      

      set方法:
      1.先获取到当前线程的引用
      2.利用这个引用来获取到 ThreadLocalMap
      3.如果 map 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中
      4.如果 map 为空,则去创建一个 ThreadLocalMap

         

      ThreadLocal使用不当导致内存泄漏

      ThreadLocalMap的key为ThreadLocal的弱引用,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链才会断掉(Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value)。如果线程一直不结束,永远无法回收,造成内存泄漏。(内存泄漏的根源是ThreadLocalMap的生命周期跟Thread一样长,不是弱引用)

      

      key 使用弱引用
      ThreadLocalMap的key为ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。这样会导致ThreadLocalMap中key为null, 而value还存在着强引用,产生内存泄漏。但是下一次ThreadLocalMap调用set()、get()、remove()方法的时候会清除value值。使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏。
      key 使用强引用
      ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
      ThreadLocal正确的使用方法:每次使用完ThreadLocal都调用它的remove()方法清除数据。

      ThreadLocal不支持继承性

      同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量(即当前线程Thread的属性),所以二者自然是不能共享的)。
      如果需要继承,则使用InheritableThreadLocal。(InheritableThreadLocal原理:InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals属性中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。)

总结线程安全的实现方法

      悲观锁

      synchronized 和 ReentrantLock

      乐观锁

      CAS和原子类

      线程本地存储

      ThreadLocal

二、线程基础

1.线程的6种状态

      新建(New):new一个Thread或Runnable实例出来,线程就进入了初始状态。
      可运行(Runnable):可能正在运行,也可能正在等待 CPU 时间片。包括Running 和 Ready。
      阻塞(Blocked):等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
      无限期等待(Waiting):等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
      超时等待(Timed Waiting):无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。(注意区别:Thread.sleep(long)不会释放锁,Object.wait(long)会释放锁)
      死亡(Terminated):线程结束任务之后自己结束,或者产生了异常而结束。

      

2.线程的实现方式

      继承 Thread 类
      实现 Runnable 接口
      实现 Callable 接口

3.基础线程机制

Daemon线程

      当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

sleep()

      主动放弃CPU(但不释放锁),该线程进入阻塞状态(Blocked 状态)。

yield()

      主动放弃CPU(但不释放锁),线程直接进入就绪状态(Runnable)。yield()方法执行后,有可能线程调度器又将该线程调度执行。

4.线程中断

interrupted()

      当线程调用interrupt()时,线程中断状态被置位(可通过isInterrupted()或interrupted()判断,区别是后者会将当前线程中断状态变成false),可能有两种情况:
      1.如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程;
      2.如果没有执行 sleep() 等会抛出 InterruptedException 的操作,就无法使线程提前结束。

Executor 的中断操作

      调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

5.线程同步

synchronized

      见上一章节。

ReentrantLock

      后续JUC中详细讲解,包括和synchronized区别。

6.线程间通信

join()

      在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。如将一个大问题分割为许多小问题,要等待所有的小问题处理后,再进行下一步操作。

wait() notify() notifyAll()

      调用 wait() 使得线程A挂起等待某个条件满足,当线程B的运行使得这个条件满足时,会调用 notify() 或者 notifyAll() 来唤醒挂起的线程A。一般用在synchronized同步块中。

      wait() 和 sleep() 的区别:
      wait() 是 Object 的方法,会释放锁;
      sleep() 是 Thread 的静态方法,不会释放锁。

      与await() signal() signalAll()区别:
      JUC的Condition提供,调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

三、JUC

1.Locks

类结构总览

      接口:Condition、Lock、ReadWriteLock
      抽象类:AbstractOwnableSynchonizer、AbstractQueuedSynchonizer(AQS)、AbstractQueuedLongSynchronizer
      锁常用类:ReentrantLock、ReentrantReadWriteLock、LockSupport、StampedLock
      工具常用类:CountDownLatch、CyclicBarrier、Semaphore、Phaser、Exchanger

      

接口Condition

      Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法(wait、notify 和 notifyAll)的使用。可以通过await()、signal()来休眠/唤醒线程,且可以实现更精确的控制。

      原理

      Condition具体实现在AbstractQueuedSynchronizer类中。这个类中管理了一个阻塞队列和N多个条件队列。阻塞队列记录了等待获取锁的线程,头结点记录了当前正在运行的线程。条件队列记录了由Condition.await()阻塞的线程,一个Lock可以有多个Condition,每个Condition是一个队列。Condition是AbstractQueuedSynchronizer的一个内部类ConditionObject,所以创建的Condition对象中是可以访问整个AbstractQueuedSynchronizer对象的属性的,通过这样将Condition与Lock相关联。
      await()阻塞线程:await()方法释放锁,并将当前线程封装成一个node加入到条件队列的尾部。同时阻塞当前线程,等待唤醒。
      signal()唤醒线程:signal()将条件队列中的第一个节点加入到阻塞队列的尾端,表示可以被唤醒执行。

      

接口: Lock

      Lock为接口类型,具体使用参见其实现类ReentrantLock的用法。

接口: ReadWriteLock(读写锁)

      ReadWriteLock为接口类型, 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。其实现是ReentrantReadWriteLock。

抽象类: AbstractOwnableSynchonizer

      此类为创建锁和相关同步器(伴随着所有权的概念)提供了基础。

抽象类: AbstractQueuedSynchonizer(AQS)

      简介

      队列同步器AQS是用来构建锁或其他同步组件的基础框架,内部使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。平时用到的ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等等皆是基于AQS的。我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

      核心思想

      AQS维护了一个 state(代表共享资源)一个FIFO线程等待队列(CLH,是一个虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系。多线程争用资源被阻塞时会被封装成一个结点(Node),进入此队列)。
      
      代码里面即是:
      
      state通过protected类型的getState,setState,compareAndSetState进行操作,如下:
      

      核心思想主要是上面这些,数据结构这块了解即可。数据结构层面包括一个Sync queue(双向链表,即上述的CLH队列)和Condition queue(单向链表,不是必须的,只有当使用Condition时,才会存在此单向链表,并且可能会有多个Condition queue,代码其实就是ConditionObject)。
      

      AQS 对资源的共享方式

      Exclusive(独占):只有一个线程能执行,如ReentrantLock。(ReentrantReadWriteLock 可以看成是独占+共享组合式)
      Share(共享):多个线程可同时执行,如Semaphore、CountDownLatch。

      AQS源码介绍

      AQS类有两个内部类,分别为Node类ConditionObject类。每个线程被阻塞的线程都会被封装成一个Node结点,放入sync queue。ConditionObject实现了Condition接口的await、signal方法,用来等待条件、释放条件。
      AQS类核心方法,acquire()和release()acquireShared()和releaseShared()
      独占模式-同步状态的获取:
      1.当线程调用acquire()(里面会调用子类自定义实现的tryAcquire获取同步状态)申请获取锁资源,如果成功,则进入临界区。
      2.当获取锁失败时(线程被封装成Node),则进入一个FIFO等待队列,然后被挂起等待唤醒。
      3.当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则进入临界区,否则继续挂起等待。
      独占模式-同步状态的释放:
      1.当线程调用release()(里面会调用子类的tryRelease()方法释放锁)进行锁资源释放时,如果没有其他线程在等待锁资源,则释放完成。
      2.如果队列中有其他等待锁资源的线程需要唤醒,则唤醒队列中的第一个等待节点(先入先出)。
      共享模式-同步状态的获取:
      1.当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。
      2.当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。
      3.当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。
      共享模式-同步状态的释放:
      1.当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。

      如何利用AQS实现同步组件

      自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。自定义同步器实现时主要实现以下几种方法:
      
      以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()获取独占锁(state=0时,CAS置state=state+1):
如果获取成功,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止;(释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。)
如果获取失败,线程会被加入到CLH队列,同时挂起自己。当锁被释放后,非公平锁直接CAS抢占该锁;公平锁则会判断自己是否是否则CLH队列头部,如果不是则加入CLH队列尾部,由头部线程获取锁。
      以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后续动作。

抽象类(long): AbstractQueuedLongSynchronizer

      以 long 形式维护同步状态state的一个AQS。

锁常用类: ReentrantLock

      ReentrantLock使用

      

      ReentrantLock几个概念

      1.可重入锁:可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。(原理是加锁时不断增加state的值,解锁相反)
      2.可中断锁:可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。(比如两个线程死锁时,可中断一个)
      3.公平锁与非公平锁
            公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
            非公平锁:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。
            synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

      ReentrantLock实现原理及源码分析

      内部类Sync继承了AQS,提供NonfairSync、FairSync两个类均继承了Sync。
      
      非公平锁:
      调用lock(),直接CAS操作抢占锁(state=0时,置为1),CAS操作成功则抢到了锁;(非公平即体现在这里,排队线程还未唤醒时,新来的线程就直接抢占了锁)
      若未抢到锁则去调用acquire(),首先直接CAS抢占锁,成功直接返回,失败则入队列。
            
      公平锁:
      调用lock(),先让CLH队列队头的线程获取锁,自己加入CLH队列尾部;如果CLH队列头部没有线程,自己在CAS操作获取锁。
            
      总结:
      非公平锁和公平锁的区别是调用lock()时,1.非公平锁直接CAS先尝试获取锁,失败再调用acquire(),而公平锁直接调用acquire()
                                                                  2.tryAcquire()时,非公平锁直接CAS获取锁,而公平锁会优先队列头部的先获取锁。
      如果判断当前锁被占用(state!=0),这时会去检查是否是自己占用了,state=state+1,体现了可重入性。

      synchronized和ReentrantLock区别

      1.synchronized是Java关键字,是JVM层面的锁;ReentrantLock 是JUC提供的API层面的锁。(synchronized是基于对象的监视器锁monitor,ReentrantLock是通过设置state)
      2.是否可中断,synchronized不可中断;ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
      3.是否可手动释放,synchronized不需要释放锁;ReentrantLock则需要手动释放锁。
      4.是否公平锁,synchronized为非公平锁;ReentrantLock提供公平锁和非公平锁。
      5.synchronized通过Object类的wait()/notify()/notifyAll()随机唤醒一个线程或唤醒全部线程;ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒。

锁常用类: ReentrantReadWriteLock

      ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。底层也是基于AQS来实现的。

锁常用类: LockSupport

      LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。LockSupport.park()不会释放锁资源。
      应用:
      1.AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。
      2.Condition.await()和CountDownLatch.countDown()等组件底层都是调用LockSupport.park()来实现阻塞当前线程的。

并发工具类:CountDownLatch

      CountDownLatch的两种使用场景

      场景1:让多个线程等待:模拟并发,让并发线程一起执行。(见左图)
      让一组线程在指定时刻(秒杀时间)执行抢购,这些线程在准备就绪后,进行等待(CountDownLatch.await()),直到秒杀时刻的到来,然后一拥而上。
      场景2: 让单个线程等待:多个线程(任务)完成后,进行汇总合并。(见右图)
      并发任务存在前后依赖关系,比如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后、需要进行结果合并;或者多个数据操作完成后,需要数据check。
            

      CountDownLatch 工作原理

      CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。调用await()方法的线程会被阻塞(阻塞在Latch栅栏上),直到计数器 减到 0 的时候(countDown() N次,将计数减为0),才能继续往下执行。
      源码层面,底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到state为0,其就会唤醒在await()方法中等待的线程。

      

      CountDownLatch与Thread.join

      CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。CountDownLatch可以控制多个线程执行完后主线程再执行,也可以多个线程一起等待发令枪然后同时进行,而join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。

并发工具类:CyclicBarrier

      CyclicBarrier用法

      CyclicBarrier字面意思是“可重复使用的栅栏”,利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
      与CountDownLatch对比:
      1.CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。CyclicBarrier可以实现CountDownLatch的功能,而反之则不能。
      2.两者内部都有计数器,CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制。

            

      CyclicBarrier原理

      CyclicBarrier 的源码实现和 CountDownLatch大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 是 ReentrantLock 和 Condition 的组合使用。
      具体是:CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。

      

 

并发工具类:Semaphore

      Semaphore用法

      Semaphore通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量。常用于限流,比如:数据库连接池等。

      

      Semaphore原理

      Semaphore与ReentrantLock的内部类的结构相同,是基于AQS来做的(Semaphore是共享模式,ReentrantLock是独占模式),有一个内部类Sync继承了AQS,两个内部类FairSync和NonfairSync继承Sync,即有公平锁和非公平锁之分。
      1.Semaphore初始化(new Semaphore(10))。默认会创建一个非公平的锁的同步阻塞队列;初始令牌数量赋值给同步队列的state状态,state就代表当前所剩余的令牌数量。
      2.获取令牌(semaphore.acquire())。即CAS操作修改state=state-1,若state>=0,则代表获取令牌成功;若state<0,则代表令牌数量不足,此时封装线程为Node节点加入阻塞队列,挂起当前线程。
      3.释放令牌(semaphore.release())。即CAS操作修改state=state+1,释放令牌成功之后,同时会唤醒同步队列的所有阻塞节点线程。

2.原子类

基础类型:AtomicBoolean,AtomicInteger,AtomicLong

      AtomicInteger原理及源码解析

      主要是保存了一个volatile的int值,并利用unsafe类提供的方法,最核心的就是CAS方法(compareAndSwapInt)来原子地设置值,主要包含三个操作数(其实4个) —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相等,就更新为新值;不相等,则不作操作。其中CAS是一条CPU的原子指令,是靠硬件实现的。
      CAS是乐观锁,存在几个问题
      1.ABA问题(A->B->A),CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。解决思路是使用版本号(1A->2B->3A)。Atomic包提供了一个类AtomicStampedReference来解决ABA问题。(AtomicStampedReference内部使用Pair来存储元素值及其版本号,元素值和版本号才更新)
      2.自旋CAS如果长时间不成功,导致CPU开销大。
      3.只能保证变量的原子性,无法保证代码块原子。
      AtomicInteger源码

      
      
      
      unsafe类源码
      为什么这里会导致使用var5 = getIntVolatile获取偏移地址中的值后,后面compareAndSwapInt方法var5又会出现不等于偏移量地址上的值呢?多线程下getIntVolatile获取完内存值后,其他线程可能执行compareAndSwapInt改掉内存中的值了。

      

数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

      分别是原子整型数组、原子长整型数组、原子引用类型数组。

引用:AtomicReference,AtomicMarkedReference,AtomicStampedReference

      AtomicReference: 原子更新引用类型。
      AtomicStampedReference: 原子更新引用类型,内部使用Pair来存储元素值及其版本号
      AtomicMarkableReferce: 原子更新带有标记位的引用类型。

3.并发集合

BlockingQueue和BlockingDeque

      BlockingQueue是单向队列,只能一头插入,另一头获取。
      BlockingDeque是双端队列,即你可以从任意一端插入或者获取元素的队列。

Queue: ArrayBlockingQueue

      ArrayBlockingQueue是基于数组的先进先出(FIFO)有界阻塞队列。队列创建时,必须要指定容量,后期不能改变容量。
      实现原理:基于数组,利用ReentrantLock + condition 阻塞控制。封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的await/signal操作了。

            

Queue: LinkedBlockingQueue

      LinkedBlockingQueue是基于链表的先进先出(FIFO)的可选界的阻塞队列。若不指定容量,默认为Integer.MAX_VALUE
      与ArrayBlockingQueue对比:
      1.LinkedBlockingQueue实现原理基于链表,导致的容量区别。
      2.与ArrayBlockingQueue不同的是,LinkedBlockingQueue队列中有两把锁,读锁和写锁是分离的。
      3.LinkedBlockingQueue理论上来说比ArrayBlockingQueue有更高的吞吐量,但是在大多数的实际应用场景中,却没有很好的表现。
      实现原理:基于链表,利用ReentrantLock + condition 阻塞控制。
      使用场景:生产者消费者、线程池。
      实现生产者消费者示例:

         

Queue: SynchronousQueue

      SynchronousQueue是一个没有数据缓冲的BlockingQueue,即内部同时只能够容纳单个元素。Executors.newCachedThreadPool()中就使用了SynchronousQueue

List: CopyOnWriteArrayList

      ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的
      原理:读写分离,写时加锁,同时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array。读的时候未加锁,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
      缺点:
      1.由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
      2.不能用于实时读的场景,多线程向CopyOnWriteArrayList添加数据,可能读到旧数据。合适读多写少的场景,最好慎用

Set: CopyOnWriteArraySet

      将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。

Map: ConcurrentHashMap

      见Java集合部分。

4.线程池相关类

      后续单独一章节讲解。

四、线程池

1.线程池的优势

      1.降低系统资源消耗,通过复用已存在的线程,降低线程创建和销毁造成的消耗。
      2.提高响应速度。 如果任务到达了,复用已存在的线程,无需重新创建。
      3.方便管理,控制并发数、制定排队策略和拒绝策略。
      4.提供扩展功能,比如延时定时线程池。

2.线程池ThreadPoolExecutor的主要参数

      

corePoolSize:核心线程数

      核心线程会一直存活,及时没有任务需要执行。
      当线程数<corePoolSize,即使有线程空闲,线程池也会优先创建新线程处理;当前线程数>=corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。
      如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maxPoolSize:最大线程数

      线程池所允许的最大线程个数。
      当线程数<maxPoolSize,如果当前阻塞队列满了,则创建新的线程执行任务;当线程数>maxPoolSize,会执行RejectedExecutionHandler来拒绝这个任务。(队列里的不算在线程池的线程数,例如core=5,max=10,队列size=10,实际是先创建5个核心,再来加入队列10,再来可再创建5个)
      当阻塞队列是无界队列,则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue。

      

keepAliveTime:线程空闲时间

      当线程池中线程数大于核心线程数时,线程的空闲时间。如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

unit:keepAliveTime的单位

      keepAliveTime的单位

workQueue:阻塞队列

      ArrayBlockingQueue:基于数组结构的有界阻塞队列,先进先出(FIFO)。
      LinkedBlockingQueue:基于链表结构的阻塞队列,先进先出(FIFO),吞吐量通常要高于ArrayBlockingQuene。Executors.newFixedThreadPool()使用了这个队列。
      SynchronousQueue:不存储元素的阻塞队列,吞吐量通常要高于LinkedBlockingQueue。Executors.newCachedThreadPool()使用了这个队列。
      PriorityBlockingQueue:具有优先级的无限阻塞队列。

threadFactory:创建线程的工厂

      通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory,创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

handler:拒绝策略

      两种情况会执行拒绝策略

      1.线程池里的线程数达到最大线程数。
      2.在调用shutdown()和线程池真正关闭之间,如有任务提交。

      JDK内置拒绝策略

      AbortPolicy(中止策略): 直接抛出异常,默认策略。场景:比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
      CallerRunsPolicy(调用者运行策略): 用调用者所在的线程来执行任务。场景:不允许失败场景(对性能要求不高、并发量较小)
      DiscardOldestPolicy(弃老策略): 丢弃阻塞队列中靠最前的任务,并执行当前任务。场景:发布消息。
      DiscardPolicy(丢弃策略): 直接丢弃任务。场景:无关紧要的任务,所以这个策略基本上不用了。

      也可以实现RejectedExecutionHandler接口或者继承JDK内置策略,自定义拒绝策略,如记录日志或持久化存储不能处理的任务。

      第三方实现的拒绝策略

      dubbo中的线程拒绝策略:
      1.输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。
      2.输出当前线程堆栈详情,当你通过上面的日志信息还不能定位问题时,线程堆栈详情就是你发现问题的救命稻草。
      3.继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性。

     

      Netty中的线程池拒绝策略:
      Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的,相较于CallerRunsPolicy,就可以扩展到支持高效率高性能的场景了。

     

      activeMq中的线程池拒绝策略:
      activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。

      

3.线程池流程

      

4.常见的线程池

类结构关系

      

线程池1:ThreadPoolExecutor

      Executors提供的创建线程方法

      newFixedThreadPool:一个固定大小的线程池(线程池里的线程数量永远不会变化),可以用于已知并发压力的情况下,对线程数做限制。
      
      newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
      
      newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务,且负载较轻的场景。
      

      ThreadPoolExecutor实现原理

      ThreadPoolExecutor将会一方面维护自身的生命周期(即线程池的运行状态),另一方面同时管理线程和任务。(即三块:生命周期管理、线程管理、任务管理)
      线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦。
      任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:1.直接申请线程执行该任务;2.缓冲到队列中等待线程执行;3.拒绝该任务。-------这块详细流程对应execute(Runnable var1)源码一起看。
      线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

      

      ThreadPoolExecutor源码解析

      1.生命周期管理及几个关键属性
      线程池内部使用一个变量ctl维护两个值(高3位和低29位):运行状态(runState)和线程数量 (workerCount)。
      运行状态包括以下5种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。

      

      2.任务的执行execute(不带返回值)
      execute –> addWorker –>内部类Worker的runworker (getTask)

      execute方法:

      

      addWorker方法:

            

      内部类Worker及runWorker方法:

      
      

      3.任务的提交submit(带返回值)

      submit其实就是对任务做了一层封装,将其封装成Future,然后提交给线程池执行,最后返回这个future。
      这里的 newTaskFor(task) 方法会将其封装成一个FutureTask类。
      外部的线程拿到这个future,执行get()方法的时候,如果任务本身没有执行完,执行线程就会被阻塞,直到任务执行完。

      

      4.shutdown和shutdownNow方法

      shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完。
      shutdownNow将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,然后清空任务队列。

线程池2:ScheduledThreadPoolExecutor

      Executors提供的创建方法

      newScheduledThreadPool:适用于执行延时或者周期性任务。

      ScheduledThreadPoolExecutor原理

      ScheduledFutureTask 来执行周期任务;DelayedWorkQueue 来存储任务。

线程池3:ForkJoinPool

      ForkJoinPool线程池思想

      Fork/Join框架是用于并行执行任务的框架, 采用分治思想,把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果。
      Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。
      采用工作窃取算法,当一个线程的任务执行完了之后,如果其他线程还有没执行完毕的任务,会去窃取过来执行。维护的是一个双端队列,窃取的时候从队列的底部取任务(本身执行从顶部,减少了冲突)。优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,且创建多个线程和多个双端队列消耗更多的系统资源。

      

      ForkJoinPool的使用

      创建ForkJoinPool实例,调用ForkJoinPool的submit(ForkJoinTask<T> task)或者invoke(ForkJoinTask<T> task)来执行指定任务。ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask。
      RecursiveAction:没有返回值,只是执行任务。
      RecursiveTask:有返回值,小任务结束后,返回结果。大任务可将小任务返回结果进行整合。
      两种使用分别如下图:

                     
                    

5.线程池使用技巧

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

      线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
      Executors各个方法的弊端
      newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
      newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
      推荐方式 1:
      首先引入:commons-lang3包
      

      推荐方式 2:
      首先引入:com.google.guava包
      

      推荐方式 3:
      spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可
      

配置线程池需要考虑的因素

      任务的性质

      CPU 密集型任务:Ncpu+1     (加1是为了当计算线程出现偶尔的故障,或者偶尔的I/O、发送数据、写日志等情况时,这个额外的线程可以保证CPU时钟周期不被浪费。)
      IO 密集型任务:Ncpu*2
      混合型任务:CPU密集型任务与IO密集型任务的执行时间差别较小,则拆分为两个线程池;否则没有必要拆分。

      任务的执行时间

      如果任务执行时间长,则增加线程数量。

      任务的优先级

      高并发应当增加任务队列上限,因为任务生产的较快。
      此外,对于并发高、业务执行时间长这种情形单纯靠线程池解决方案是不合适的,即使服务器有再高的资源配置,每个任务长周期地占用着资源,最终服务器资源也会很快被耗尽,因此对于这种情况,应该配合业务解耦,做些模块拆分优化整个系统结构。

五、各种锁的总结

1.悲观锁 VS 乐观锁

悲观锁

      什么是悲观锁

      总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。

      悲观锁的实现

      1.传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
      2.synchronized 和 ReentrantLock。

乐观锁

      什么是乐观锁

      乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

      乐观锁的实现

      1.CAS 实现:具体实现是原子类等。
      2.版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

      悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

2.自旋锁 VS 适应性自旋锁

      见synchronized的优化

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

      见synchronized的优化

4. 公平锁 VS 非公平锁

      见ReentrantLock的概念。

5. 可重入锁 VS 非可重入锁

      可重入锁是指同一个线程可以多次获取同一把锁。例如执行doSomething()时,执行到doOthers()时可顺利获得锁。(ReentrantLock和synchronized都是可重入锁。)

      

6. 独享锁 VS 共享锁

      独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。
      共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
      例如ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,读锁是共享锁,写锁是独享锁。

 

 

 

 

 

 

 

 

 

感谢:

https://www.pdai.tech/md/java/thread/java-thread-x-overview.html

https://blog.csdn.net/forwardsailing/article/details/107146472

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值