一、进程 与 线程
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微秒 / 1000MICROSECONDS: 1微秒 = 1毫秒 / 1000MILLISECONDS: 1毫秒 = 1秒 / 1000SECONDS:秒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对象(用来阻塞和唤醒线程)。