02 对于volitale的理解
volitale是JVM提供的轻量级同步机制:
- 保证可见性
- 不保证原子性
- 禁止指令重排序
CAS底层原理
cas的底层调用了很多Unsafe类native方法
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
...
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
Unsafe是cas的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于一个后门,该类可以直接操作特定内存的数据,它在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。
Unsafe类中的所有方法都是native方法,都直接调用操作系统底层资源。
变量valueoffset表示该变量值在内存中的偏移量,因为Unsafe就是根据内存偏移来获取数据,变量value是由volitale来修饰的,保证了多线程之间的内存可见性问题。
CAS 存在的问题
- 循环带来的cpu占用开销
- 只能保证一个变量的原子性(可以用原子引用)
- ABA问题(增加版本号)
阻塞队列
阻塞队列的作用
不得不阻塞的情况,如何来管理阻塞队列中的各个线程
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候去唤醒线程,这一切都由阻塞队列来帮我们来处理。
BlockingQueue接口
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列,长度是Integer的最大值
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,单个元素的队列
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:链表结构组成的双向阻塞队列
三组api:
add remove 抛出异常:非法状态异常,没有这个元素异常
offer poll 插入返回bool值 取出返回对象或者null
put take 阻塞
阻塞队列的用处:生产者消费者模式、线程池、消息中间件
synchronized 和 lock
synchronized、wait、notify
lock await signal
public class TestPack {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
try {
shareData.incrementData();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
try {
shareData.consumerData();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
class ShareData{
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private int num = 0;
void incrementData() throws InterruptedException {
lock.lock();
try {
while (num != 0){
//等待不能生产
condition.await();
}
System.out.println("num = " + num);
num ++;
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
void consumerData() throws InterruptedException {
lock.lock();
try {
while (num != 1){
//等待不能生产
condition.await();
}
System.out.println("num = " + num);
num --;
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
虚假唤醒
为了防止虚假唤醒,多线程条件下wait等要放入到loop循环中
Lock和synchronized区别
- synchronized是关键字,JVM层面依靠monitorenter和monitorexit来实现,wait和notify也依赖于monitor对象,所以只有在同步方法和同步代码块之内才能调用这两个方法,否则抛IllegalMonitorStateException, Lock是JUC包下的一个接口,是api层面的锁
2.synchronized不需要用户去手动释放锁,锁是自动释放的,Lock需要手动释放,否则可能出现死锁
3.synchronized是不可以被中断的,除非抛出异常或者正常运行完成;Lock可以中断:设置超时时间tryLock(long time)
lockIntertuptibly放在代码块中,调用interrupt方法可以中断
4.加锁是否公平:synchronized非公平锁
Lock既可以是公平也可以是非公平锁,默认是非公平(效率较高) - 绑定多个condition条件
synchronized不可以 Lock可以实现分组唤醒需要唤醒的线程,精确唤醒,不同的condition唤醒不同的线程
例如 A -> B -> C ->D ->A轮流唤醒
例题:A打印5遍 AA,B打印10遍BB,C打印15遍CC 循环十次
//资源类
class ShareData{
private int num = 1; // 1a 2b 3c
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void printA(){
lock.lock();
try {
while (num != 1){
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println("AA");
}
num = 2;
c2.signal();//a唤醒b
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num != 2){
c2.await();
}
for (int i = 0; i < 9; i++) {
System.out.println("BB");
}
num = 3;
c3.signal();//a唤醒c
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num != 3){
c3.await();
}
for (int i = 0; i < 14; i++) {
System.out.println("CC");
}
num = 1;
c1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
volitale、CAS、atomicInteger、blockingQueue,线程交互,原子引用
不用锁来实现生产者和消费者
class MySource{
private volatile boolean flag = true;//默认开启生产加上消费
private AtomicInteger atomicInteger = new AtomicInteger();
BlockingQueue<String> blockingQueue = null;
public MySource(BlockingQueue<String> blockingQueue) {//传入接口
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
//生产方法
public void myProduct() throws Exception{
String data = null;
boolean retValue = false;
while (flag){
data = atomicInteger.incrementAndGet() + "";
retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
if(retValue){
System.out.println(Thread.currentThread().getName() + "插入数据" + data + "成功");
}else {
System.out.println(Thread.currentThread().getName() + "插入数据" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "生产结束flag = false");
}
//消费方法
public void myConsumer() throws Exception{
String result = null;
while (flag){
result = blockingQueue.poll(2L,TimeUnit.SECONDS);
if (result == null || result.equalsIgnoreCase("")){
flag = false;
System.out.println("超过两秒没有取到数据");
return;
}
System.out.println(Thread.currentThread().getName() + "消费数据" + result + "成功");
}
}
public void stop() throws Exception{
this.flag = false;
}
}
创建线程的方法
- 继承Thread类
- 实现Runnable接口,实现run方法
- 实现Callable接口 实现call方法(可以有返回值,抛出异常)
- 线程池
futuretask过早去取结果会阻塞主线程,无法达到分支合并的效果
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 1024;
}
}
public static void main(String[] args) throws Exception {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
Thread thread = new Thread(futureTask);
thread.start();
while (!futureTask.isDone()){
}
System.out.println(futureTask.get());
}
多个线程去抢夺一个futureTask时,只会调用一次,结果复用
线程池
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放到队列中,然后在线程创建之后启动这些任务,如果线程数量超过了最大线程数就在队列中排队等候,其他线程执行完毕将任务取出。
主要特点:线程复用,控制最大并发数,管理线程
- 降低资源的消耗,复用线程避免创建和销毁的消耗
- 提高响应的速度,任务不需要等待线程的创建
- 提高程序的可管理性,线程是稀缺资源,如果无限制创建会消耗系统资源,降低系统稳定性,线程池可以对资源进行统一的分配和管理、调优和监控。
线程池底层就是ThreadPoolExecutor
- Executors.newFixedThreadPool 固定数量线程
- Executors.newSingleThreadExecutor 单个线程
- Executros.newCachedThreadPool 多个缓冲线程
execute只能传入runnable的实现类
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
ThreadPoolExecutor
线程池参数介绍
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
七个参数的构造方法
- corePoolSize:常驻核心线程数
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,大于等于1
- keepAliveTime:多余的空闲线程的存活时间,线程数量超过核心线程数时,当空闲时间超过这个值,多余线程被销毁到只剩下corePoolSize个线程为止
- unit :keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory表示生成线程池中工作线程的线程工厂,创建线程一般用默认的工厂即可
- handler:拒绝策略,任务队列已满,并且工作线程数大于最大线程数时会调用拒绝策略。
四种拒绝策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
阿里巴巴开发手册:
new ThreadPoolExecutor(2,
5,
1,TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
如何配置合理线程池数量
首先通过
System.out.println(Runtime.getRuntime().availableProcessors());
获取服务器核心数量
cpu密集型
IO密集型
由于cpu不是一直在执行命令,因此应该配置多个线程,CPU核心数* 2
死锁编码以及定位分析
public static void main(String[] args) throws Exception {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(()->{
synchronized (lock1){
try {
TimeUnit.SECONDS.sleep(1000);
synchronized (lock2){
System.out.println("线程1任务");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
synchronized (lock2){
try {
TimeUnit.SECONDS.sleep(1000);
synchronized (lock1){
System.out.println("线程2任务");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
通过 jsp -l命令得到正在运行的java相关的线程id
然后通过 jstak 线程号 找到堆栈相信息
LockSupport类
对于传统的synchronized和Lock在线程同步时,等待和唤醒的操作一定要在同步代码块内部,即获取锁之后执行,否则会抛出非法的监视器异常。
并且,如果先唤醒再阻塞则会使得唤醒失败。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,它使用了一种名为许可证permit的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可证,许可证只有1和零,默认是0,可以把许可证看做是累加上限为1的信号量。
主要方法:
阻塞
park()/park(Object blocker)
park方法作用:阻塞当前线程或者阻塞传入的线程。
permit默认是0,所以刚开始调用park方法,线程就会阻塞,直到别的线程将许可证设置为1,park方法被唤醒,然后将permit再次设置为0并且返回。
// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
UNSAFE.park(false, 0L);
}
唤醒
unpark(Thread thread)
unpark方法的作用是唤醒处于阻塞状态的指定线程。
调用unpark方法后会将thread线程的permint设置为1,多次调用unpark还是会设置为1,自动唤醒thread线程
// Makes available the permit for the given thread
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
可以精确唤醒指定的线程,并且先唤醒再阻塞也可以起作用
private static void lockSupportParkUnpark() {
Thread a = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().`在这里插入代码片`getName() + "\t" + "------come in" + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
}, "A");
a.start();
new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
}, "B").start();
}
异常情况:没有考虑到permit上限为1
private static void lockSupportParkUnpark() {
Thread a = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis());
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
}, "A");
a.start();
new Thread(() -> {
LockSupport.unpark(a);
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
}, "B").start();
}
运行时会阻塞,由于permit上限值为1,执行两次park将会导致线程阻塞。
LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死
AQS 抽象队列同步器
AQS是用来构建锁或者其它的同步器组件的重量级基础框架以及整个JUC的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量state表示持有锁的状态。
CLH队列是一个双向链表,AQS中的队列是CLH边提的虚拟双向队列FIFO
AQS是JUC的基石,以下是和AQS有关的并发编程类
- CountDownLatch
- ReentrantLock
- ReentrantReadWriteLock
- CyclicBarrier
- Seamaphore
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
进一步理解锁和同步器的关系
锁面向的是锁的使用者,定义了程序员和锁交互的使用层API隐藏了实现细节
同步器,面向锁的管理者,提出同意规范并且简化了的实现,屏蔽了同步状态管理,阻塞线程排队和通知,唤醒机制,简化Java中各种锁的实现。
AQS能干嘛
加锁会导致线程阻塞,有阻塞就需要进行排队,而排队的线程需要以某种方式的队列来进行管理。
抢到资源的线程继续办理业务,而抢占不到资源的线程必然涉及一种排队等待机制,抢占资源失败的线程继续等待,仍然保留获取锁的可能性并且获取锁的流程仍然在继续。
既然有排队机制,那么一定会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到等待队列中去,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点NOde,通过CAS、自旋以及LockSupport.park()方式维护state变量的状态,使得并发达到同步的效果。
- AQS使用一个volitale修饰的int类型变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要抢占资源的线程封装成为一个个Node节点来实现锁 的分配。通过CAS操作来实现对State值的修改。
- Node节点是啥?包含线程属性的一个内部类
- 可以将NOde和Thread类比候客区的椅子和顾客
AQS同步状态的State成员变量,类似于银行办理业务的受理窗口状态:0就是没人,自由状态可以办理,大于等于1,有人占用窗口,排队
内部类Node,Node的等待状态waitState成员变量,勒斯与等候区其他顾客的等待状态,队列中每个排队的个体就是一个Node
static final class Node{
//共享
static final Node SHARED = new Node();
//独占
static final Node EXCLUSIVE = null;
//线程被取消了
static final int CANCELLED = 1;
//后继线程需要唤醒
static final int SIGNAL = -1;
//等待condition唤醒
static final int CONDITION = -2;
//共享式同步状态获取将会无条件地传播下去
static final int PROPAGATE = -3;
// 初始为e,状态是上面的几种
volatile int waitStatus;
// 前置节点
volatile Node prev;
// 后继节点
volatile Node next;
// ...
AQS的底层是如何排队的
通过调用LockSupport.park来实现排队
reentrantLock
ReentrantLock实现了Lock接口,在它的内部聚合了一个AQS的实现类Sync
在ReentrantLock内定义了静态内部类,分别为NoFairSync非公平锁和FairSync公平锁
ReentrantLock默认创建的是非公平锁。
看一下lock方法执行流程:
在ReentrantLock中,NoFairSync和FairSync中的tryAcquire方法的区别,可以明显看出公平锁公平锁和非公平锁lock方法唯一区别在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors
hasQueuedPredecessors方法时公平锁加锁时等待队列中是否存在有效节点的方法
对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
1公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中
2 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁的对象。也就是说队列的第一个排队线层在unpark,之后还是需要竞争锁。
而在acquire方法中最终都会调用tryAcquire方法
在 NonfairSync 和 FairSync 中均重写了其父类 AbstractQueuedSynchronizer 中的 tryAcquire() 方法
lock首先通过cas操作来查看state的状态是否设置为被占用,如果没有被占用,则更新state,然后将锁的持有者设置为当前线程。
如果发现state已经被设置为1,表示锁已经被其他线层持有,则进入acquire方法,而acquire方法又会调用tryAcquire方法,对于公平锁和非公平锁又有不同的tryAcquire方法。
以非公平锁的tryAcquire方法为例,里面调用了nonfairTryAcquire方法,传入的参数是1
nonfairTryAcquire方法的正常执行流程:
在nonfairTryAcquire方法中,大部分情况下的执行流程如下:线程b执行getState方法,获取state变量的值为1,表示lock锁正在被占用,state==0不成立,然后判断当前线程是否是持有锁的线程,发现是线程A,则返回false,表示该线程并没有抢到锁
nonfairTryAcquire(acquires) 比较特殊的执行流程:
第一种情况是,走到 int c = getState() 语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state 变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true,表示抢占锁成功。其实这里还有一种情况,需要留到 unlock() 方法才能说清楚
第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然示例代码里面并没有体现出来),这时 current == getExclusiveOwnerThread() 条件成立,将 state 变量的值加上 acquire,这种情况下也应该 return true,表示线程 A 正在占用 lock 锁。因此,state 变量的值是可以大于 1 的
往下继续执行addWaiter(Node.EXCLUSIVE)方法
在 tryAcquire() 方法返回 false 之后,进行 ! 操作后为 true,那么会继续执行 addWaiter() 方法
addWaiter方法做了什么?
之前讲过Node节点用于封装用户线程,这里将正在执行的线程通过NOde封装起来,判断 tail 尾指针是否为空,双端队列此时还没有元素呢~肯定为空呀,那么执行 enq(node) 方法,将封装了线程 B 的 Node 节点入队
enq(node) 方法:构建双端同步队列
在双端同步队列总,第一个节点是虚节点,也叫哨兵节点,其实并不存储任何信息,只是用来站位,真正封装线程的节点从第二个节点开始。
第一次执行 for 循环:现在解释起来就不费劲了,当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。此时 tail == null,因此进入 if(t == null) { 的分支,头指针指向哨兵节点,此时队列中只有一个节点,尾节点即是头结点,因此尾指针也指向该哨兵节点
第二次执行for循环,将装着线程B的节点放入到双端队列中,此时tail指向了哨兵节点,并不是null,因此进入else分支,以尾插法的方式,现将nodeB的prev指向之前的tail,再