多线程原理

30次阅读

共计 22075 个字符,预计需要花费 56 分钟才能阅读完成。

一、进程 与 线程

A. 进程

 在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程;类似地,音乐播放器和 Word 都是进程。

B. 线程

 某些进程内部还需要同时执行多个子任务,我们把子任务称为线程。

C. 进程 VS 线程

 进程和线程是包含关系,但是多任务即可以由多进程实现,也可以由单进程内的多线程实现,还可以混合 多进程 \+ 多线程。多进程的优点:稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其它进程,而在多线程情况下,任何一个线程崩溃会直接导致整个进程崩溃。多进程的缺点:(1)创建进程比创建线程开销大,尤其是在 Windows 系统上。(2)进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

二、多线程

 一个 Java 程序实际上是一个 JVM 进程,JVM 进程用一个主线程来执行 main() 方法,在 main() 方法内部,我们又可以启动多个线程.
此外,JVM 还有负责垃圾回收的其它工作线程等。和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。

A. 线程的创建

Java 用 Thread 对象表示一个线程,通过调用 start() 启动一个新线程;一个线程对象只能调用一次 start() 方法。线程的执行代码写在 run() 方法中;线程调度由操作系统决定。Thread.sleep() 可以把当前线程暂停一段时间。
1. 从 Thread 派生一个自定义类,然后覆写 run() 方法。
Thread t = new MyThread();
t.start();

class MyThread extends Thread{
    @Override
    public void run(){}
}
2. 创建 Thread 实例时,传入一个 Runnable 实例。
Thread t = new Thread(new MyRunnable());
t.start();

class MyRunnable implements Runnable{
    @Override
    public void run(){}
}

B. 线程的状态

 在 Java 程序中,一个线程对象只能调用一次 start() 方法启动新线程,并在新线程中执行 run() 方法。一旦 run() 方法执行完毕,线程就结束了。
1. Java 线程的状态有以下几种
新建(new)
 新创建的线程,尚未执行。
运行(Runable)
 运行中的线程,正在执行 run() 方法的 Java 代码。
无限期等待(Waiting)
 运行中的线程,因为某些操作在等待中。没有设置 Timeout 参数的 Object.wait() 方法。没有设置 Timeout 参数的 Thread.join() 方法。LookSupport.park() 方法。
限期等待(Timed Waiting)
 运行中的线程,因为 sleep() 方法正在计时等待。Thread.sleep() 方法。设置了 Timeout 参数的 Object.wait() 方法。设置了 Timeout 参数的 Thread.join() 方法。LockSupport.parkNanos() 方法。LockSupport.parkUntil() 方法。
阻塞(Blocked)
 运行中的线程,因为某些操作被阻塞而挂起。
结束(Terminated)
 线程已终止,因为 run() 方法执行完毕。

C. 线程的方法

Thread.start()
 启动一个线程。
Thread.join()
 等待一个线程执行结束。
Thread.interrupt()
 中断线程,通过(isInterrupted)方法判断此线程是否中断。
Thread.setDaemon(true)
 设置线程为守护线程,JVM 退出时不考虑守护线程。
Thread.setPriority(int n)
 设置线程优先级(1~10,默认值 5)。
Thread.currentThread()
 获取当前线程。
Synchronized
 加锁、解锁。找出修改共享变量的线程代码块。选择一个共享实例作为锁。使用 synchronized(lockObject){…}。

三、线程池

 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务;每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。**

A. 使用线程池的优势

1.  创建 / 销毁 线程伴随着系统开销,过于频繁的创建 / 销毁线程,会很大程度上影响系统处理效率;使用线程池可以降低线程创建 / 销毁造成的系统消耗。2.  提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行。3.  方便线程并发数的管控,因为线程若是无限制的创建,可能会导致内存占用过多而产生 OOM,并且会造成 cpu 过度切换。

B. 线程池的构造函数

 线程池的概念是 Executor 这个接口,具体实现是 ThreadPoolExecutor 类。
ThreadPoolExecutor 提供了四个构造函数
1. public ThreadPoolExecutor
        (
        int    corePoolSize,
        int    maximumPoolSize,
        long   keepAliveTime,
        TimeUnit   unit,
        BlockingQueue<Runnable>  workQueue
        )

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

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

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

C. 构造函数参数详解

1. int corePoolSize(线程池基本大小)
 该线程池中核心线程数最大值。核心线程:线程池新建线程的时候,如果当前线程总数小于 corePoolSize,则新建的是核心线程。如果超过 corePoolSize,则新建的是非核心线程。核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。如果指定 ThreadPoolExecutor 的 allowCoreThreadTimeOut 这个属性为 true,那么核心线程如果处于闲置状态,超过一定时间,就会被销毁掉。
2. int maximumPoolSize(线程池最大大小)
 线程池所允许的最大线程个数,对于无界队列(LinkedBlockingQueue),可忽略该参数。线程总数 = 核心线程数 + 非核心线程数。非核心线程:当队列满了,且已创建的线程数小于 maximumPoolSize,则线程池会创建新的非核心线程来执行任务。非核心线程,如果闲置的时长超过参数(keepAliveTime)所设定的时长,就会被销毁。
3. long keepAliveTime(非核心线程的存活保持时间)
 当线程池中非核心线程的空闲时间超过了线程存活时间,那么这个线程就会被销毁。
4. TimeUnit unit(keepAliveTime 的单位)
NANOSECONDS :  1 微毫秒 = 1 微秒 / 1000
MICROSECONDS:1 微秒 = 1 毫秒 / 1000
MILLISECONDS:1 毫秒 = 1 秒 / 1000
SECONDS:秒
MINUTES:分
HOURS:小时
DAYS:天
5. BlockingQueue< Runnable > workQueue(任务队列)
 该线程池中的任务队列,维护着等待执行的 Runnable 对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
常见的 workQueue 类型:
SynchronousQueue
 这个队列接收到任务的时候,会直接提交给线程处理,而不保留它。如果所有核心线程都在工作,就会新建一个线程来处理这个任务。为了保证不出现(线程总数达到了 maximumPoolSize 而不能新建线程)错误,使用这个类型队列的时候,maximumPoolSize 一般指定成 Integer.MAX_VALUE。
LinkedBlockingQueue
 这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建核心线程处理任务。如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效。
ArrayBlockingQueue
 可以限定队列的长度,接收到任务的时候,如果没有达到 corePoolSize 的值,则新建核心线程执行任务。如果达到了,则入队等候。如果队列已满,则新建非核心线程执行任务。如果总线程数达到了 maximumPoolSize,并且队列也满了,则报错。
DelayQueue
 队列内元素必须实现 Delayed 接口,这就意味着新添加的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入列,只有达到了指定的延时时间,才会执行任务。
6. ThreadFactory threadFactory(线程工厂)
 用于创建新线程:
threadFactory 创建的线程也是采用 new Thread() 方式。threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n(m 为线程池的编号,n 为线程池内的线程编号)。可以使用 Thread.currentThread().getName() 查看当前线程。public Thread new Thread(Runnable r){} 或使用 Executors.defaultThreadFactory()。
7. RejectedExecutionHandler handler(线程饱和策略)
 当线程池和队列都满了,再加入线程会执行此策略。ThreadPoolExecutor.AbortPolicy():不执行新任务,直接抛出异常,提示线程池已满。ThreadPoolExecutor.DisCardPolicy():不执行新任务,也不抛出异常。ThreadPoolExecutor.DisCardOldSetPolicy():将消息队列中的第一个任务替换为当前新进来的任务执行。ThreadPoolExecutor.CallerRunsPolicy():直接调用 execute 来执行当前任务。

D. ThreadPoolExecutor 的执行策略

如上图所示,当一个任务被添加进线程池时:
 首先判断线程池中是否有线程处于空闲状态,如果有、则直接执行任务。如果没有,则判断线程数量是否达到 corePoolSize,如果没有、则新建一个线程(核心线程)执行任务。如果线程数量达到了 corePoolSize,则将任务移入队列等待。队列已满,总线程数未达到 maximumPoolSize 时,新建线程(非核心线程)执行任务。队列已满,总线程数达到了 maximumPoolSize 时,则调用 handler 实现拒绝策略。

E. JAVA 中常见的四种线程池

Java 通过 Executors 提供了四种线程池,这四种线程池都是直接或间接配置 ThreadPoolExecutor 的参数实现的。
1. CachedThreadPool()
 可缓存线程池。线程数为 Integer.max_value,也就是无限制大小。有空闲线程则复用空闲线程,无空闲线程则新建线程。适用于耗时少,任务量大的情况。
源码:
public static ExecutorService newCachedThreadPool(){return new ThreadPoolExecutor ( 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>() );
}
创建方法:
ExecutorService   cachedThreadPool  =  Executors.newCachedThreadPool() ;
2. FixedThreadPool()
 定长线程池。可控制线程最大并发数(同时执行的线程数)。超出的线程会在队列中等待。
源码:
public static ExecutorService newFixedThreadPool(int nThreads){return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() );
}
创建方法:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
3. ScheduledThreadPool()
 定时及周期性任务执行的线程池。
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){return new ScheduledThreadPoolExecutor(corePoolSize);}

public ScheduledThreadPoolExecutor(int corePoolSize){super ( corePoolSize, Integer.MAX_VALUE, DEFAULT\_KEEPALLIVE\_MILLS, MILLISECONDS, new DelayedWorkQueue() );
}
创建方法:
ExecutorService  scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
执行方法:
1 秒后执行一次任务
    scheduledThreadPool.schedule(new Task, 1, TimeUnit.SECONDS);

2 秒后开始执行定时任务,每 3 秒执行一次(不管任务需要执行多长时间)scheduledThreadPool.scheduledAtFixedRate(new Task, 2, 3, TimeUnit.SECONDS);

2 秒后开始执行定时任务,以 3 秒为间隔执行(上一次任务执行完毕后)scheduledThreadPool.**scheduledWithFixedDelay**(new Task, 2, 3, TimeUnit.SECONDS);
4. SingleThreadExecutor()
 单线程化的线程池。有且仅有一个工作线程执行任务。所有任务按照指定顺序执行,即遵循队列的入队出队规则。
源码:
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor ( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() ));
}
创建方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();

F. 执行与关闭

execute()
 执行一个任务,没有返回值。
submit()
 提交一个线程任务,有返回值。
shutdown()
 关闭线程池,等待正在执行的任务先完成,然后再关闭。
shutdownNow()
 立刻停止正在执行的任务。
awaitTermination()
 等待指定的时间让线程池关闭。

四、Java 内存模型

Java 内存模型的主要目标是定义程序中变量的访问规则,即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。

A. 主内存

 虚拟机的主内存是虚拟机内存中的一部分。Java 虚拟机规定所有的变量(这里的变量是指实例字段、静态字段、构成数组对象的元素,但不包括局部变量和方法参数)必须在主内存中产生。

B. 工作内存

Java 虚拟机中每个线程都有自己的工作内存,该内存是线程私有的。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存;如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

C. Java 虚拟机中主内存和工作内存之间的交互

 就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。

lock(锁定)
 把一个变量标识为一条线程独占的状态;作用对象在主内存。
unlock(解锁)
 把一个处于锁定状态的变量释放出来,释放后才可被其它线程锁定;作用对象在主内存。
read(读取)
 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用;作用对象在主内存。
load(载入)
 把 read 操作从主内存中得到的变量值放入工作内存中;作用对象在工作内存。
use(使用)
 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作;作用对象在工作内存。
assign(赋值)
 把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。作用对象在工作内存。
store(存储)
 把工作内存中的一个变量的值传送到主内存中,以便 write 操作;作用对象在 ** 工作内存 **。
write(写入)
 把 store 操作从工作内存中得到的变量的值放入主内存的变量中;作用对象在 ** 主内存 **。

D. 以上 8 种操作的使用规范

Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。例:变量 a、b
将变量 a 从主内存复制到工作内存,按顺序访问是:read a,   load a。将变量 b 从工作内存同步回主内存,按顺序访问是:store b,  write b。若将 a、b 从主内存复制到工作内存,也有可能的顺序是:read a,  read b,  load b,  load a。
1. 不允许 read 和 load、store 和 write 操作之一单独出现
 也就是不允许从主内存读取了变量的值、但是工作内存不接收的情况,或者是不允许从工作内存将变量的值写回到主内存、但是主内存不接收的情况。
2. 不允许一个线程丢弃最近的 assign 操作
 也就是不允许线程在自己的工作线程中修改了变量的值却不写回到主内存。
3. 不允许一个线程写回没有修改的变量到主内存
 也就是如果线程的工作内存中变量没有发生过任何 assign 操作,是不允许将该变量的值写回主内存。
4. 变量只能在主内存中产生
 不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行 load 或者 assign 操作。
5. 一个变量在同一时刻只能被一个线程对其进行 lock 操作
 也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其它线程是不能对其加锁的。但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
6. 对变量执行 lock 操作,就会清空工作空间该变量的值
 执行引擎使用这个变量之前,需要重新 load 或者 assign 操作初始化变量的值。
7. 不允许对没有 lock 的变量执行 unlock 操作
 如果一个变量没有被 lock 操作,那也不能对其执行 unlock 操作,当然一个线程也不能对被其它线程 lock 的变量执行 unlock 操作。
8. 对一个变量执行 unlock 之前,必须先把变量同步回主内存中
 也就是执行 store 和 write 操作。

E. 重排序

 重排序是指为了提高指令运行的性能,在编译时或者运行时对指令执行顺序进行调整的机制。
1. 编译重排序
 是指编译器在编译源代码的时候就对代码执行顺序进行分析,在遵循 as-if-serial 的原则前提下对源码的执行顺序进行调整。
As-if-serial 原则
 是指在单线程环境下,无论怎么重排序,代码的执行结果都是确定的。
2. 运行时重排序
 是指为了提高执行的运行速度,系统对机器的执行指令的执行顺序进行调整。

F. volatile 关键字

volatile 是 Java 虚拟机提供的最轻量级的同步机制。在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,也就不能保证操作的原子性。volatile 修饰的变量对所有的线程具有可见性,当一个线程修改了被 volatile 修饰的变量值时,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。volatile 修饰的变量禁止指令重排序。volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢。因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

五、线程同步

 多线程协调运行的原则是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。**
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
volatile 保证变量对所有线程的可见性,但是操作并非原子操作,并发情况下不安全。
重量级的同步机制使用 Synchronize。

A. 并发操作下需注意的特性

1. 原子性(Atomicity)
 原子是最小单位,具有不可分割性。例:a=0;(a 非 long 和 double 类型)这个操作是不可分割的,那么我们说这个操作是原子操作。例:a++;(a++ 也可以写成:a = a + 1)这个操作是可分割的,所以它不是一个原子操作。非原子操作都会存在线程安全问题,需要使用 sychronized 来让它变成一个原子操作。Java 的 concurrent 包下提供了一些原子类。
2. 可见性(Visibility)
 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程的修改结果,另一个线程立即就能看到,主要实现方式是修改值后将值同步至主内存。在 Java 中,volatile、synchronized、final 修饰的变量都具有可见性。volatile 只能让被它修饰的内容具有可见性,但不能保证它具有原子性。
3. 有序性(Ordering)
 如果在线程内部,所有操作都是有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。volatile 和 synchronized 两个关键字可以保证线程之间操作的有序性。volatile 是因为其本身包含“禁止指令重排序”的语义。synchronized 是由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”这条规则获得的。

B. 线程阻塞的代价

1.  如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这样的切换会消耗大量的系统资源。2.  因为用户态与内核态都有各自专用的内存空间、专用的寄存器等。3.  用户态切换至内核态需要传递给许多变量、参数给内核。4.  内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

C. 锁的类型

1. 乐观锁
 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低。每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有更新过这个数据。采取在写时先读出当前版本号,然后加锁操作。跟上一次的版本号进行比较,如果一样则进行写操作,如果不一样则要重复 读~ 比较~ 写 操作。Java 中的乐观锁基本都是通过 CAS 操作实现,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
2. 悲观锁
 悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高。每次去拿数据的时候都认为别人会修改。每次在读写数据的时候都会上锁,这样其它线程想读写这个数据就会阻塞直到拿到锁。
3. 自旋锁
 如果持有锁的线程能在很短时间内释放锁资源,那么等待竞争锁的线程就不需要做内核态与用户态之间的切换进入阻塞挂起状态。它们只需要等一等(自旋,也就是不释放 CPU),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核之间的切换消耗。

D. JVM 锁实现原理

1. 重量级 Synchronized 锁简介
Synchronized 锁是存在 Java 对象头里。
2. Synchronized 锁的作用域
Synchronized 可以把任意一个非 NULL 对象当作锁。Synchronized 作用于方法时,锁住的是对象的实例(this)。Synchronized 作用于静态方法时,锁住的是 class 实例。Synchronized 作用于一个对象时,锁住的是所有以该对象为锁的代码块。
3. Synchronized 工作原理
JVM 基于进入和退出 monitor 对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter 和 monitorexit 指令实现的。monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
4. JVM 锁的分类及其解释

无锁状态
 无锁即没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。对象头开辟 25bit 的空间用来存储对象的 hashcode,4bit 用于存放分代年龄。1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为 01。
偏向锁(Biased Locking)
(-XX:-UseBiasedLocking = false)偏向锁会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问.
不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。偏向锁开辟 23bit 的空间用来存放线程 ID,2bit 用来存放 epoch。4bit 用于存放分代年龄,1bit 用于存放是否偏向锁标识(0- 否,1- 是),2bit 用来存放锁标识位为 01。epoch:这里简单理解,就是 epoch 的值可以作为一种检测偏向锁有效性的时间戳。
轻量级锁
 指当前锁是偏向锁的时候,被另外的线程访问,那么偏向锁就会升级为轻量级锁,其它线程会通过自旋尝试获取锁,不会阻塞,从而提高性能。轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标识位,其标识位为 00。
重量级锁
 也就是通常说 synchronized 的对象锁,其中指针指向的是 monitor 对象的起始地址,当一个 monitor 被某个线程持有后,它便处于锁定状态。开辟 30bit 的空间存放执行重量级锁的指针,2bit 存放锁的标识位,其标识位为 10。
GC 标记
GC 标记开辟 30bit 的内存空间却没有占用,2bit 存放锁的标识位,其标识位为 11。
5. synchronized 执行过程
 检测 Mark Word 里面是不是当前线程的 ID。如果是,表示当前线程处于偏向锁。如果不是,则使用 CAS 将当前线程的 ID 替换 Mark Word。如果成功,则表示当前线程获得偏向锁,置偏向标志位为 1。如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。当前线程使用 CAS 将对象头的 Mark Word 替换为锁记录指针。如果成功,当前线程获得锁。如果失败,表示其它线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋成功,则依然处于轻量级状态。如果自旋失败,则升级为重量级锁。
6. 加锁流程
加锁过程由 JVM 自身内部实现。
 当执行 synchronized 同步块的时候,JVM 会根据启用的锁和当前线程的争用情况,决定如何执行同步操作。
在所有线程都启用的情况下。
 线程进入临界区时会先去获取偏向锁。如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁。如果自旋也没有获取到锁,则使用重量级锁。没有获取到锁的线程挂起,直到持有锁的线程执行完同步块唤醒它们。如果线程争用激烈,应该禁用偏向锁 **(-XX:-UseBiasedLocking = false)**。
加锁步骤
 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为 01 状态,是否为偏向锁为 0)。虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 拷贝。拷贝对象头中的 Mark Word 复制到锁记录中(Lock Record)。拷贝成功
虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。并将 Lock Record 里的 owner 指针指向对象的 Mark Word。如果这个动作成功了,那么这个线程就拥有了该对象的锁。并且对象 Mark Word 的锁标志位设置为 00,表示此对象处于轻量级锁状态。拷贝失败
虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧。如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为 10。Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。
7. Synchronized 锁竞争状态
Java 线程在执行到 synchronized 的时候,会形成两个字节码指令,这里相当于是一个监视器 monitor,监控 synchronized 保护的区域,监视器会设置几种状态用来区分请求线程。

ContentionList
 竞争队列,所有请求锁的线程首先被放在这个竞争队列中。
EntryList
ContentionList 中那些有资格成为候选资源的线程被移动到 EntryList 中。
WaitSet
 调用 wait 方法被阻塞的线程被放置在这里。
OnDeck
 任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck。
Owner
 当前已经获取到锁资源的线程被称为 Owner。
!Owner
 当前释放锁的线程。
7.1) JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck)。
 但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问.
为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中的某个线程置为 OnDeck 线程(一般是最先进去的那个线程)。Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM 中,也把这种选择行为称之为“竞争切换”。
7.2) OnDeck 线程获取到锁资源后会变为 Owner 线程。
 没有得到锁资源的仍然停留在 EntryList 中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify 或者 notifyAll 唤醒,会重新进入 EntryList 中。
7.3) 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态。
 该阻塞是由操作系统来完成的(Linux 内核下采用 pthread\_mutex\_lock 内核函数实现的)。
7.4) Synchronized 是非公平锁。
synchronized 在线程进入 ContentionList 时。等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList。这明显对于已经进入队列的线程是不公平的。还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
8. 锁优化
减少锁的时间。
 不需要同步执行的代码,能不放在同步块里面执行就不要放在同步块内执行,可以让锁尽快释放。
减少锁的粒度。
 将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争,它的思想也是用空间来换时间。
ConcurrentHashMap
Java 中的 ConcurrentHashMap 使用一个 Segment 数组。Segment 继承自 ReenTranLock。每个 Segment 就是一个可重入锁。每个 Segment 都有一个 HashEntry<k, v> 数据用来存放数据,put 操作时,先确定往哪个 Segment 放数据,只需要锁定这个 segment 执行 put,其它的 Segment 不会被锁定。数组中有多少个 Segment 就允许同一时刻多少个线程存放数据,这样增加了并发能力。
LongAdder
 实现思路类似 ConcurrentHashMap。LongAdder 有一个根据当前并发状况动态改变的 Cell 数组。Cell 对象里面有一个 long 类型的 value 用来存储值。
LinkedBlockingQueue
 在队列头入队,在队列尾出队,入队和出队使用不同的锁。相对于 LinkedBlockingArray 只有一个锁效率高。
锁粗化
 大部分情况下是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度。例:假如有一个循环,循环内的操作需要加锁,我们应该把锁放在循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的。
使用读写锁
CopyOnWriteArrayList,CopyOnWriteArraySet
使用 CAS
 如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用 CAS 效率会更高。因为加锁会导致线程的上下文切换。如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用 volatiled + CAS 操作会是非常高效的选择。

六、AQS

A. Lock API

1.  void lock():如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放。2.  void lockInterruptibly():和 lock() 方法相似,但阻塞的线程可中断,抛出 java.lang.InterruptedException 异常。3.  boolean tryLock():非阻塞获取锁,尝试获取锁,如果成功返回 true。4.  boolean tryLock(long timeout, TimeUnit timeUnit):带有超时时间的获取锁方法。5.  void unlock():释放锁。

B. 几种常见的 Lock 实现

ReentrantLock
ReentrantLock 是重入锁。是唯一一个实现 Lock 接口的类。重入锁是指线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数。
ReentrantReadWriteLock
ReentrantReadWriteLock 是可重入读写锁。ReentrantReadWriteLock 类中维护了两个锁,ReadLock 和 WriteLock,它们都分别实现了 Lock 接口。读写锁的基本原则:读和读不互斥、读和写互斥、写和写互斥。读写锁适用场景:读多写少。
StampedLock
StampedLock 是 JDK8 引入的新的锁机制。由于读写锁中 读和读可以并发、但读和写是有冲突的,如果大量的读线程存在,可能会引起写线程饥饿。StampedLock 是一种乐观的读写锁策略,使得乐观锁完全不会阻塞写线程。

C. AQS 概念

AQS
AbstractQueuedSynchronized 的简称。AQS 是一个 Java 提供的底层同步工具类,主要作用是为 Java 中的并发同步组件提供统一的底层支持。AQS 用一个 int 类型的变量表示 当前同步状态(volatile int state)。AQS 用一个 FIFO 线程等待队列 来存放多线程争用资源时被阻塞的线程。
state 的访问方式有三种。
getState(),setState(),compareAndSetState()
FIFO 线程等待队列。
 它是一个双端队列,遵循 FIFO 原则。主要作用是用来存放在锁上阻塞的线程。当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会构造成一个 Node 结点。队列的头结点是成功获取锁的结点,当头结点线程释放锁时,会唤醒后面的结点并释放当前头结点的引用。

D. AQS 的两种功能

独占
 独占锁,每次只能有一个线程持有锁。例:ReentrantLock
共享
 共享锁,允许多个线程同时获取锁,并发访问共享资源。例:ReentrantReadWriteLock

E. AQS 源码详解

1. 独占模式添加结点
waitStatus
waitStatus 表示当前 Node 结点的等待状态。CANCELLED(1):表示当前结点已取消调度,进入该状态后的结点将不会再变化。SIGNAL(-1):表示后继结点在等待当前结点唤醒,后继结点入队时,会将前继结点的状态更新为 SIGNAL。CONDITION(-2):表示结点需要等待进入同步队列,等待获取同步锁。PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。0:新结点入队时的默认状态。
acquire(int)
acquire 是一种以独占方式获取资源。如果获取到资源,线程直接返回。否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。该方法是独占模式下线程获取共享资源的顶层入口。获取到资源后,线程就可以去执行其临界区的代码了。

acquire 执行流程:tryAcquire():尝试直接去获取资源,如果成功则直接返回。addWaiter():将该线程加入等待队列的尾部,并标记为独占模式。acquireQueued():使线程在等待队列中获取资源,一直获取到资源后才返回,如果在整个等待过程中被中断过,则返回 true,否则返回 false。如果线程在等待过程中被中断过,它是不响应的,只是获取资源后才进行自我中断 selfInterrupt(),将中断补上。
tryAcquire(int)
tryAcquire 尝试以独占的方式获取资源,如果获取成功,则直接返回 true,否则返回 false。AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器通过 state 的 get/set/CAS 去实现。至于能不能重入、能不能加塞,那就要看具体的自定义同步器如何设计了。这里之所以没有定义成 abstract。是因为独占模式下只用实现 tryAcquire – tryRelease。而共享模式下只用实现 tryAcquireShared – tryReleaseShared。如果都定义成 abstract,那么每个模式也要去实现另一个模式下的接口。

addWaiter(Node)
addWaiter 以两种不同的模式构造结点,并返回当前线程所在的结点。Node.EXCLUSIVE 互斥模式。Node.SHARED 共享模式。如果队列不为空,则通过 compareAndSetTail 方法以 CAS 的方式将当前线程结点加入到等待队列的末尾。否则,通过 enq(node) 方法初始化一个等待队列,并返回当前结点。

enq(Node)
enq(Node)用于将当前结点插入等待队列。如果队列为空,则初始化当前队列,创建一个空的结点作为 head 结点,并将 tail 也指向它。整个过程以 CAS 自旋的方式进行,直到成功加入队尾为止。

acquireQueued(Node, int)
acquireQueued 用于队列中的线程自旋地以独占且不可中断的方式获取同步状态(acquire),直到拿到锁之后再返回。结点进入队尾后,检查状态,找到安全休息点。如果当前结点已经成为头结点,尝试获取锁(tryAcquire),若获取成功则返回。否则调用 park() 进入 waiting 状态,等待 unpark() 或 interrupt() 唤醒自己。被唤醒后,看自己是否有资格拿到锁。如果有资格,head 指向当前结点,并返回从入队到拿到锁的整个过程中是否被中断过。如果没有资格,则继续 waiting。

shouldParkAfterFailedAcquire(Node, Node)
shouldParkAfterFailedAcquire 方式通过对当前结点的前一个结点的状态进行判断,对当前结点做出不同的操作。若前驱结点的状态已经为 Node.SIGNAL(表示后继结点在等待前驱结点唤醒),当前结点处于等待被唤醒的状态。若前驱结点已取消调度,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后面。若前驱结点非 Node.SIGNAL, 且没有取消调度,将前驱结点的状态置为 Node.SIGNAL。

parkAndCheckInterrupt()
parkAndCheckInterrupt 是让线程去休息,真正进入等待状态。调用 park(),使线程进入 waiting 状态。两种途径可以唤醒该线程:被 unpark(),被 interrupt()。如果被唤醒,查看自己是不是被中断了。需要注意的是:Thread.interrupted() 会清除当前线程的中断标记位。

acquire 流程总结
ReentrantLock.lock() 流程就是 acquire 流程(acquire(1))。

 调用自定义同步器的 tryAcquire() 尝试直接去获取资源,如果获取成功则直接返回。获取失败,则调用 addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式(EXCLUSIVE)。acquireQueued() 使线程在等待队列中休息,当有机会获取锁时(unpark() 时)、会去尝试获取资源。获取到资源后才返回,如果在整个等待过程中被中断过,则返回 true,否则返回 false。如果线程在等待过程中被中断过,它是不响应的,直到获取资源后才进行自我中断 selfInterrupt(),将中断补上。

2. 独占模式释放结点
release(int)
release(int) 方法是独占模式下线程释放共享资源的顶层入口。release(int) 方法会释放指定量的资源,如果彻底释放了(即 state = 0),它会唤醒等待队列里的其它线程来获取资源。

tryRelease(int)
 跟 tryAcquire() 一样,tryRelease() 方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease() 都会成功,因为是独占模式(EXCLUSIVE)。该线程释放资源,那么它肯定已经拿到资源了,直接减去相应量的资源即可(state -= arg)。也不用考虑线程安全的问题,但要注意它的返回值。因为 release() 是根据 tryRelease() 的返回值来判断该线程是否已经完成释放掉资源了。所以自定义同步器在实现时,如果已经彻底释放掉资源(state=0),要返回 true,否则返回 false。

unparkSuccessor(Node)
unparkSuccessor 方法用于唤醒等待队列中下一个线程,下一个线程并不一定是当前结点的 next 结点。而是下一个可以用来唤醒的线程,如果这个结点存在,调用 unpark() 方法唤醒。

3. 共享模式添加结点
acquireShared(int)
acquireShared 方法是共享模式下线程获取资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

tryAcquireShared 需要自定义同步器去实现。AQS 已经把其返回值的语义定义好了。负值代表获取失败。0 代表获取成功,但没有剩余资源。正数表示获取成功,还有剩余资源,其它线程还可以获取。
acquireShared 流程:
tryAcquireShared() 尝试获取资源,成功则直接返回。如果失败则通过 doAcquireShared() 进入等待队列 park(),直到被 unpark()、interrupt() 并成功获取到资源才返回。整个等待过程也是忽略中断的。
doAcquireShared(int)
doAcquireShared() 方法是将当前线程加入等待队列的尾部休息。直到其它线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。跟独占模式相比,共享模式只有线程是 head.next 时(也就是位于头结点后面的一个结点),才会去尝试获取资源。如果资源还有剩余还会唤醒 head.next 之后的线程结点。假如 head.next 所需要的资源量大于其它线程所释放的资源量。则 head.next 线程不会被唤醒、而是继续 park() 等待其它线程释放资源。更不会去唤醒它之后的线程结点。这样做只是 AQS 保证严格按照入队顺序唤醒,保证公平、但降低了并发效率。

setHeadAndPropagate(Node, int)
setHeadAndPropagate 方法跟 setHead() 比较多了一个步骤。就是当自己被唤醒的同时,如果还有剩余资源,还会去唤醒后继结点。

4. 共享模式释放资源
releaseShared()
releaseShared() 方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放则允许唤醒等待线程,它会唤醒等待队列里的其它线程来获取资源。

doReleaseShared()
doReleaseShared 方法主要用于唤醒后继结点。

共享模式释放资源总结:
releaseShared() 方法跟独占模式下的 release() 类似。
不同之处在于:
 独占模式下的 tryRelease() 在完全释放掉资源(state = 0)后,才会返回 true 去唤醒其它线程。共享模式下的 releaseShared() 拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例:资源总量 10;有三个线程 A、B、C;其中 A 需要 5 个资源,B 需要 3 个资源,C 需要 4 个资源。A、B 两个线程并发执行共需要 8 个资源,此时只剩下 2 个资源,C 线程等待。当 A 线程执行过程中释放了 1 个资源后,剩余资源量为 3,C 线程还是继续等待。当 B 线程执行过程中释放了 1 个资源后,剩余资源量为 4,C 线程开始并行执行。

F. 自定义同步器的实现

 自定义同步器在实现时只需要实现共享资源 state 的获取与释放即可,其余功能 AQS 已经在顶层实现好了。**
自定义同步器的主要实现方法。**
isHeldExclusively()
 该线程是否正在独占资源,只有用到 condition 才需要去实现它。
tryAcquire(int)
 独占方式,尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int)
 独占方式,尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int)
 共享方式,尝试获取资源。负数表示失败。0 表示成功,但没有剩余可用资源。正数表示成功,且有剩余资源。
tryReleaseShared(int)
 共享方式,尝试释放资源。如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
ReentrantLock 实现原理。
State 初始化为 0,表示未锁定状态。A 线程 ReentrantLock.lock() 时,调用 tryAcquire() 独占锁并将 state+1。其它线程再 tryAcquire() 时就会失败。直到 A 线程 ReentrantLock.unlock() 到 state= 0 为止(即释放锁),其它线程才有机会获取锁。A 线程释放锁之前,A 线程是可以重复获取此锁的(state++),这就是重入锁的概念。注意:获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

七、CAS

CAS(Compare And Swap),是用于实现多线程同步的原子指令。CAS 将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。原子性保证新值基于最新信息计算,如果该值在同一时间被另一个线程更新,则写入失败。
1. CAS 操作步骤
 当多个资源同时对某个资源进行 CAS 操作,只能有一个线程操作成功。但是并不会阻塞其它线程,其它线程只会收到操作失败的信号,可见 CAS 其实是一个乐观锁。例:假设内存中的原数据是 V,旧的预期值是 A,需要修改的新值是 B。首先将主内存中的数据 V 读取到线程的工作内存中。(读取)比较 A 与 V 是否相等。(比较)如果比较相等,将 B 写入 V。(写回)返回操作是否成功。
2. ABA 问题(线程 1 和线程 2 同时执行 CAS 逻辑)
 时刻 1:线程 1 执行读取操作,获取原值 A,然后线程被切换走。时刻 2:线程 2 执行完成 CAS 操作,将原值由 A 修改为 B。时刻 3:线程 2 再次执行 CAS 操作,并将原值由 B 修改为 A。时刻 4:线程 1 恢复运行,将比较值与原值进行比较,发现两个值相等(其实内存中的值已经被线程 2 修改过了)。
3. 解决 ABA 问题的方法
 在变量前面追加版本号:每次变量更新就把版本号加 1,则 A -B- A 就变成 1A-2B-3A。

八、JAVA 中常用的锁介绍

公平锁
 公平锁是指当多个线程尝试获取锁时,成功获取锁的顺序与请求获取锁的顺序相同。
非公平锁
 非公平锁是指当锁状态可用时,不管在当前锁上是否有其它线程在等待,新进来的线程都有机会抢占锁。
1. Synchronized
 可重入锁,非公平锁,由 JVM 实现。
2. Wait() / notify() / notifyAll()
 用于多线程协调运行。wait():使线程进入等待状态。notify():唤醒正在等待的线程。notifyAll():唤醒所有正在等待的线程。已唤醒的线程还需要重新获得锁后才能继续执行。
3. ReadWriteLock
ReadWriteLock 是一种悲观的读写锁,读读可以同步,读写互斥,写写互斥。适用场景:读多写少。
4. StampedLock
 不可重入锁,StampedLock 是一种乐观的读写锁,读的过程中允许获取写锁后写入。
5. ReentrantLock
 可重入锁。基于 AQS 实现。提供两种模式:(1)公平锁(2)非公平锁。Lock  lock = new ReentrantLock()。lock.lock();加锁。lock.unlock();释放锁。
6. ReentrantReadWriteLock
 可重入锁。基于 AQS 实现。
实现原理:
 将同步变量 state 按照高 16 位和低 16 位进行拆分,高 16 位表示读锁,低 16 表示写锁。
7. Condition
 使用 condition 对象来实现 wait() 和 notify() 功能。使用 condition 时,引用的 condition 必须从 Lock() 实例的 newCondition 返回。private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();condition.await():释放当前锁,进入等待状态。condition.signal():唤醒某个等待线程。condition.signalAll():唤醒所有等待线程。唤醒线程从 await() 返回后需要重新获得锁。
8. Atomic
Atomic 类是通过无锁(lock-free)的方式实现线程的安全(thread-safe)访问。Atomic 主要原理是利用了 CAS 来保证线程安全。
9. Future
 线程是继承自 Runnable 接口,Runnable 接口没有返回值。如果需要返回值,需实现 Callable 接口,Callable 是一个泛型接口,可以返回指定类型的结果。对线程池提交一个 Callable 任务,可以获得一个 Future 对象。可以用 Future 在将来某个时刻获取结果。
10. CompletableFuture
CompletableFuture 是针对 Future 做的改进,可以传入回调对象。当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
11. ForkJoin
ForkJoin 是一种基于“分治”的算法,通过分解任务,并行执行,最后合并结果得到最终结果。
12. ThreadLocal
ThreadLocal 表示线程的“局部变量”,它确保每个线程的 ThreadLocal 变量都是各自独立的。ThreadLocal 适合在一个线程的处理流程中保持上下文(避免了同一个参数在所有方法中传递)。使用 ThreadLocal 要用 try … finally 结构,并在 finally 中清除。

九、JAVA 中 Concurrent 集合线程安全类

A. 线程安全类

CopyOnWriteArrayList
 写入时复制。简单来说,就是平时查询的时候,都不需要加锁,随便访问。只有在写入 / 删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会阻塞,而是继续读取旧的数据。
ConcurrentHashMap
 设计原理跟 HashMap 差不多,只是引入了 segment 的概念。目的是将 map 拆分成多个 Segment(默认 16 个)。操作 ConcurrentHashMap 细化到操作某一个 segment。在多线程环境下,不同线程操作不同的 segment,他们互不影响,便可实现并发操作。
CopyOnWriteArraySet
CopyOnWriteArraySet 底层存储结构是 **CopyOnWriteArrayList**,是一个线程安全的无序集合。
ArrayBlockingQueue / LinkedBlockingQueue
ArrayBlockingQueue / LinkedBlockingQueue 都是一个阻塞式的队列。ArrayBlockingQueue 底层是一个数组,ArrayBlockingQueue 读写共享一个锁,使用的是 ReentrantLock。LinkedBlockingQueue 底层是是链表,LinkedBlockingQueue 读写各有一个锁,使用的也是 ReentrantLock。
LinkedBlockingDeque
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,可以从队列的两端插入和移除元素。使用全局独占锁保证线程安全,使用的是 ReentrantLock 和 两个 Condition 对象(用来阻塞和唤醒线程)。

正文完
 0