多线程原理

1、进程 与 线程

A. 进程

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

B. 线程

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

C. 进程 VS 线程

进程和线程是包含关系,可是多任务便可以由多进程实现,也能够由单进程内的多线程实现,还能够混合 多进程 \+ 多线程。

多进程的优势:
稳定性比多线程高,由于在多进程的状况下,一个进程崩溃不会影响其它进程,而在多线程状况下,任何一个线程崩溃会直接致使整个进程崩溃。

多进程的缺点:
(1)建立进程比建立线程开销大,尤为是在Windows系统上。
(2)进程间通讯比线程间通讯要慢,由于线程间通讯就是读写同一个变量,速度很快。

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){…}。

3、线程池

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

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()
等待指定的时间让线程池关闭。

4、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 的读性能消耗与普通变量几乎相同,可是写操做稍慢。
由于它须要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

5、线程同步

多线程协调运行的原则是:当条件不知足时,线程进入等待状态;当条件知足时,线程被唤醒,继续执行任务。**
关键字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操做会是很是高效的选择。

6、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是能回到零态的。

7、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。

8、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中清除。

9、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对象(用来阻塞和唤醒线程)。