Offer

尚硅谷-JUC篇

狂神说-JUC:https://www.cnblogs.com/meditation5201314/p/14940976.html

尚硅谷-JUC:https://www.bilibili.com/video/BV1ar4y1x727?p=166&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

狂神的内容偏基础,属于抛砖引玉的那种

尚硅谷偏向更多原理和能力提升。(但是周阳老师对一些名词讲得好晦涩,明明可以用简单几句话讲明白的,愣是能凑成一节课)

以后学完一门课花个脑图,便于记忆回顾总结

csdn csdn csdn csdn csdn


	线程流程控制:
		0. 线程串行化:CompletableFutre.thenapply获取其他线程返回值
		1. 中断线程:循环+interrupted控制线程中断跳出
		2. 取消线程:futuretask.cacel,或者直接抛出异常,处理
		3. 全部完成再返回:CompletableFuture的allof
		4. 线程交互:不同线程对同一个同步对象进行this.wait, this.notify(属于Object上方法)
		5. 线程变量的感知:volatile,synchronized, atomic
		6. 线程池执行返回值:CompletableFuture放入池中即可

🔥1. 基本概念

1.1. 基本概念

1.1.1 线程Start

内部是调用C++源码,C++底层是操作系统分配一个线程启动

private native void start0();
1.1.2 基本名词
1.1.2.1 一锁/二并/三程

​ 一锁、二并、三程

1. synchronized锁
1. 并发和并行
3. 进程、线程、管程
 	1. 管程:就是motitor锁,就是进入、退出一个对象或代码收到保护,保证同步。
1.1.2.2 用户线程/守护线程

​ 用户线程:系统工作的线程,类似于主人

​ 守护线程:为其他线程服务,类似于影子,用户线程都没了,守护线程也没

​ main方法启动时,就有一个主线程在执行(也是用户线程)

1.1.3 CompletableFuture
1.1.3.1 Future接口

​ Future可以fork另一个子线程,获取子线程结果,取消子线程执行,判断任务是否被取消,判断任务是否执行完毕等。

1.1.3.2 Get源码

​ get会循环阻塞,查看某个变量,属于在自旋,需要配合FutureTask类和run()方法一起看,当run结束后,会改变里面的一个变量,然后get就能收到变更了。

1.1.3.3 底层源码

​ 是实现了Future异步和CompletionStage(分多个阶段触发)

1.1.3.4 基本使用

​ 感觉周阳老师这里讲的不是很系统,推荐雷神-谷粒商城讲的CompletableFuture

https://www.bilibili.com/video/BV1np4y1C7Yf?p=195&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

1.1.4 锁
1.1.4.1 乐观锁/悲观锁

​ 乐观锁:默认不添加锁,只有在更新的时候判断一下,适合读操作多。判断规则:

	1. 利用version判断。update XXX set version = version + 1 where version = X
	1. CAS算法。Atomic原子类递增操作就是CAS自旋实现

​ 悲观锁:获取数据就加锁,防止其他线程修改,适合写多读少操作。synchronized,Lock都是悲观锁

1.1.4.2 八锁

推荐狂神讲的八锁问题,感觉都差不多,就是锁对象、锁类、普通方法的区别。

https://www.cnblogs.com/meditation5201314/p/14940976.html#2八锁现象synchronizedstatic

1.1.4.3 synchronized锁--持有方式
1.  static synchronize Method(){}:锁类
  	1.  由ACC_STATIC,ACC_SYNCHRONIZED判断,然后进去monitirEnter,exit逻辑
2.  synchronized  Method(){}:锁对象
  	1.  先由ACC_SYNCHRONIZED判断,然后再进入monitorEnter,monitorExit逻辑
3.  synchronized(this.Obj){}:同步代码块
  	1.  底层就是monitorEnter和monitoerexit构成
1.1.4.4 synchronized锁--底层原理

​ 一个对象Object创建就附带了objectMonitor对象监视器,用来记录哪个线程哪个锁、锁了几次等相关信息。synchronized锁对象、锁类的时候是管程monitor就变更了对象ObjectMonitor信息。

1.1.4.5 公平/非公平锁

​ 公平锁:尽量按照顺序来申请(会有线程切换的开销)

​ 非公平:让线程争抢

1.1.4.6 可重入锁

​ 一个线程内部多个流程可重复获取同一把锁,不会产生死锁。synchronized(隐式可重入锁,内部有objectMonitor锁计数器进行加一减一),reentreenLock(显示可重入锁,lock一次,就必须unlock一次)都是可重入锁。

1.1.4.7 Synchronized锁--执行原理
/*
	1. synchronized的字节码指令monitorEnter对对象里面的objectMonitor字段修改。判断是锁对象是否被调用,加锁几次,然后进行加锁
	2. 加完锁后处理正常业务,其他线程获取锁对象时失败会进去阻塞队列
	3. 执行完后monitorExit进行数据-1,解锁
*/
synchronized(this){
    
}
1.1.5 线程中断
1.1.5.1 实现方式

线程中断应有由线程自己决定中断

  1. thread自带中断api实现
  2. volatile 全局变量,对各个线程可见
  3. atomicBoolean 全局变量,对各个线程可见
1.1.5.2 源码分析
  1. 正常活动线程:interrupted会标志为中断,中断后并不会结束运行,过一段时间后才能结束。中断已经结束的线程,不会产生任何影响,isTreeupted还是false
  2. 阻塞状态线程(sleep,wait.join):当其他线程或本线程调用该线程的interrupted,中断状态将清除,变成false,并报错interruptedException

用户查看线程是否中断

1.1.5.3 静态方法VS实例方法

​ Thread.interrupted():静态方法,返回线程中断状态并置为false

​ Thread.currentThread().isInterrupted():实例方法,返回线程中断状态

​ Thread.currentThread().interrupt():中断某个线程。不会让线程结束,可以配合循环使用

1.1.5 LockSupport
1.1.5.1 等待唤醒--实现方式(生产者-消费者)

​ 用来优化之前的线程唤醒和阻塞,比其他唤醒方法功能更强(下面3个都只能有一个,多次的话会失效)

1. Object->wait(), notify, notifyAll
 	1. wait,notify必须放在同步对象里面
 	2. wait,notify先后顺序不能错,否则先唤醒后wait无法唤醒
2. Condition-> await(), signal()
 	1. await,signal必须放在同一个lock,unlock里面
 	2. await,signal先后顺序也不能乱
3. LockSupport-> park阻塞、unpark唤醒线程
 	1. park阻塞线程,unpark(线程)。就类似给了一个许可证,以后永久可用。先后顺序可以不一致,都能唤醒。许可证只有一个,即一个park,一个unpark
1.1.6 Java内存模型--JMM
1.1.6.1 基本概念

​ Java Memory Mode:用来实现操作系统和硬件之间内存访问的差异,实现在各个平台内存访问速度一致的效果。

​ 原子性:操作不可打断

​ 可见性:多个线程对主内容中数据进行拷贝一份变量副本,对主内存中共享变量的修改对其他线程可见。(每个线程自己有工作内存存变量副本,不同线程之间不可互相访问)

​ 有序性:指令会重排序,但执行结果一定一致

1.1.6.2 JMM规范--先行发生原则

正是因为有了先行发生原则,所以实际代码才少了很多volatile, synchronized定义

  1. 线程A操作一个变量后对变量变更需要B操作可见,
  2. A操作里面的各种代码子操作顺序可以随着编译器优化而有所不同,只要最终结果不变就行
  3. 顺序性:代码先写的比代码后写的快、对象创建后才能finalize、lock后才能unlock,start()先于线程操作前、线程修改变量后对后续线程获取变量可见(不安全的情况就是普通变量两个线程同时获取在修改)

在这里插入图片描述

1.2 代码名词

1.2.1 Volatile
1.2.1.1 基本特点
  1. 可见性:修改某个变量对其他线程都要可见

  2. 有序性:禁止指令重排

  3. 没有原子性:volatile i++不能保证原子性

1.2.1.2 内存屏障
1. 对volatile写操作都立刻刷到主内存中
1. 对volatile读操作都先把线程内部的内存变量定义无效,然后从主内存中读取
1. 在读写操作前后加入了组织指令重排的约束

​ 注:感觉老师这里对内存交互讲的不是很好,还是狂神讲的简单易懂,学习链接:https://www.bilibili.com/video/BV1B7411L7tE?p=30&vd_source=510ec700814c4e5dc4c4fda8f06c10e8

1.2.1.3 存在问题

​ DCL:doucle check lock双端检测锁:

//如果不加volatile,那么当new对象的时候,由于内部是先创建对象位置,然后指令重排,然后user指向空对象,然后在给对象放入对象位置。当user指向空对象的时候,多线程下第一次if判断就会发生问题
volatile User user = null;

if (user ==null){
    synzhronized(UserTest.class){
        if(user == null){
            user = new User;
        }
    }
}
1.2.2 CAS
1.2.2.1 基本概念

​ Compare and swap:内部原理是乐观锁,如果共享内存值不一样,就放弃,或者自旋重试。

1.2.2.2 Atomic自增--线程安全

通过Unsafe类(内部都是native方法)操作硬件保证非阻塞的原子性操作,属于CPU指令原语。

​ 内部AtomicInteger是利用CAS自旋 + volatile + native方法来保证原子性

--unsafe类中方法
    public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //重新获取volatile变量
        var5 = this.getIntVolatile(var1, var2);
        //比较与预期值,不同就自旋
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
1.2.2.3 存在问题
1. 自旋开销大
1. ABA问题: compareStampReference解决(可以包装基本类型和对象)
1.2.3 原子类
1.2.3.1 基本分类
1. 基本类型、数组类型
1. 引用类型:AtomicReference(普通对象包装) AtomicStampedReference(版本标记), atomicMarkableReference(一次性标记)
1. 线程安全--类属性修改:AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater
1.2.3.2 LongAdder--基本概念

​ 内置一个base变量和cell[]数组

​ Atomic:底层是原语和自旋,一次只允许一个线程操作,高并发下性能下降

​ LongAdder:就是采用cell[]数组存,然后获取的时候把base变量和cell[]数组值加起来

1.2.3.3 LongAdder--源码分析(略)

​ 总的来说,LongAdder可用,get并发下获取数据性能好,但是sum精度不准确

1.2.4 ThreadLocal
1.2.4.1 基本概念

​ 给每个线程一个变量副本。对应JMM中的线程变量副本。

之前synchronized,volatile, cas都是对共享变量的操作,现在ThreadLocal是对线程私有变量的操作

1.2.4.2 基本使用
// 1.初始化
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

// 2. 使用后remove

// 3.线程池开启后要shutdown,否则会一直开启

// 4.使用static修饰,每次new对象都只会有一份
1.2.4.3 底层原理

​ Thread 内置ThreadLocal,ThreadLocal内置ThreadLocalMap。

​ 当赋值的时候,不存在就创建(和hashmap类似),赋值就是给map<threadLocal实例,值>这么赋值

1.2.4.4 强/软/弱/虚引用

这几个引用, finalize都是Object下面的,

  1. 强引用:普通的引用对象
  2. 软引用:内存不足会回收
  3. 弱引用:GC发现就会回收
  4. 虚引用:必须被回收,get是null,就是对象回收后的一种通知机制,配合引用队列使用,和finallize类似
1.2.4.5 key-弱引用
1. 回收key: GC发现线程死亡了,threadLocal的key就被回收,避免内存泄漏(类似人和身份证)
1. 回收value: 当GC回收了threadLocal的key后,任意每次调用ThreadLocal的get,set,remove都会回收key=null的value值
1. 所以全靠调用get,set,remove也会存在内存泄漏,每次用完都要remove
1.2.5 内存布局

​ 一个对象object在内存中存下来分为

  1. 对象头
    1. 运行时元数据(markword):记录了锁的状态(无锁、偏向、轻量、重量)
    2. 类型指针
  2. 实例数据
  3. 填充数据
1.2.6 锁升级
1.2.6.1 基本概念

​ 锁升级:无锁-> 偏向锁->轻量->重量。 (写锁叫独占锁,读锁叫共享锁)

​ 对于每一个对象object,内部都有monitor,当线程用synchronized修饰成重量级锁时,线程需要用户态到内核态的切换,非常耗时。(线程阻塞、唤醒也会有用户态、内核态的切换)

img

1.2.6.2 锁升级
  1. 无锁(标记位001):对象默认无锁

  2. 偏向锁(默认开启 标记位101 需要项目启动后等一会对象创建就是偏向锁了):对象的对象头中markWord存储的是偏向线程id。

    1. 概念:单线程竞争下获取锁时,同一个线程频繁获取一个锁,后续会自动获得该锁(类似熟人一直来某个地方)
    2. 实现:对象头记录了对象是否被某个线程抢到,抢到了就标记线程id,后续就能标记该线程和其他线程,若其他线程来竞争成功,就利用CAS替换新线程id,若其他线程竞争失败,就需要进行锁升级
      1. 对于锁升级还是竞争成功替换线程id,这是由原线程是否执行完synchronized,如果执行完同步代码块,就能替换,刚进入,就要锁升级)
  3. 轻量级锁:markWord存线程栈中Lock Record指针

    1. 概念:2个(多个)线程交替获取对象的锁
    2. 实现:A线程获得,B线程竞争,竞争成功,对象偏向锁改为B,B竞争失败,对象偏向锁升级为轻量级锁,B自旋等待获取A持有的轻量级锁。(之所以存在线程栈中,就是把对象markword复制到线程栈帧中了,释放轻量级锁也是把栈帧中写回到markword中)。B线程如果自旋到一定次数(自适应次数,因为更多线程来了)后会升级成重量级锁
  4. 重量级锁:存堆中monitor指针

    1. 概念:synchronized重量级锁,基于Monitor存储线程id来保证其他线程无法获取
轻量级锁和偏向锁

​	轻量级锁竞争失败就会自旋竞争,偏向只有一个线程

​	轻量级锁每次退出同步代码块就要释放锁,而偏向锁只有多线程竞争才会释放锁,因为默认是一个线程

img

1.2.6.3 锁--HashCode存储

​ 无锁有Hashcode,计算了之后就无法进入偏向锁,进行锁升级成轻量级锁

​ 处于偏向锁还要计算HashCode,就会变成重量级锁获取Hashcode

​ 轻量、重量锁都有HashCode

1.2.6.4 Synchronized锁总结

​ 1. Synchronized本质就是先自旋,再阻塞,内部实现用对象头的标记实现无锁、偏向、轻量、重量的升级。(改变了jdk1.6之前都弄成重量级锁的压力)

1.2.6.5 锁消除/锁粗化

​ 锁消除:多线程获取同步代码块的时候,里面每次重新new对象,synchronized(对象)就会有JIT优化成直接new对象。对应JVM里面JIT逃逸分析-同步省略

​ 锁粗化:多个synchronized包同一个对象,就会优化成一个synchronized包这个对象

        //锁粗化
		Object o = new Object();
        //优化成synchronized(o){sout(1), sout(2)}
        synchronized (o){
            System.out.println("1");
        }
        synchronized (o){
            System.out.println("2");
        }

​ 无锁:自由自在

​ 偏向:唯我独尊

​ 轻量:楚汉争霸

​ 重量:群雄逐鹿


1.2.7 AQS
1.2.7.1 基本概念

​ AbstractQueuedSynchronizer:抽象队列同步器

1. 底层是有一个FIFO的CLH等待队列(类似候客厅)和int型state来标记资源是否被获得
2. 由AQS实现的上层:reentranlock, countdownlatch, semaphore
1.2.7.2 内部实现

AQS = state + CLH队列

Node = waitStatud + Thread

  1. CLH是由一个Node结点来实现队列
    1. Node结点内部有标记线程是共享/独享状态,等待状态
  2. 有个头尾指针指向上面Node结点
  3. volatile int state由于标记资源状态
    class Node{}
	private transient volatile Node head;

    private transient volatile Node tail;
    private volatile int state;

image-20220722150943139

Node结点内部属性说明

image-20220722162130795

1.2.7.3 源码剖析

ReentranLock源码

  1. 非公平就先抢占state独占,然后调用nonfairTryAcquire到CLH队列,后续循环
  2. 公平一来就调用公平锁的队列,先进先出

非公平锁和公平锁内部acquire方法

1. tryAcquire(): 公平锁有个排队函数,其他都和非公平锁一样,尝试CAS自旋获取state状态
1. acquireQueued,addwaiter: addwaiter根据当前线程创建结点,acquireQueued:  根据上一个Node结点中volatile waitStatus判断线程是否需要LockSupport.park()暂停,就类似于阻塞在等待队列中

		3. unlock解锁:
     			1. 把类似于办理好业务的线程窗口置为null, state置为0,waitStatus修改,并唤醒unpark正在等待的线程
     			2. unpark的线程要调用各自的tryAcquire(),用于区分公平锁和非公平锁

image-20220723105812016

1.2.8 读写锁
1.2.8.1 基本概念

​ 读写锁:适合读多写少的情景,运行同时多个读,但是读写,写写互斥。底层也是AQS的Sync

演变流程就是从无锁->独占锁->读写锁->邮戳锁

image-20220723160240915

1.2.8.2 锁降级

​ readWriteLock锁降级:由写锁降级为读锁(类似linux中写权限比读权限高)。就是先加写锁,在加读锁,再释放写锁,再释放读锁。

不同线程操作同一对象时读写是互斥的,但是同一个线程内容是可以锁降级。

​ 作用:当程序中想实现写后读的话,就用到这种锁降级方法,否则写了释放写锁再获取读锁就可能被其他写线程给获取到修改了。

1.2.8.3 StampedLock邮戳锁

​ 之前读锁未释放前都不允许插入写锁,所以可能存在多个读锁,一个写锁情况下,发生写锁无法插入,这就是锁饥饿。所以为了保证读没有完成的时候写锁可以介入,从而引入了stampedLock

​ 实现方式:

1. 读模式:和之前readwritelock一样
1. 写模式:和之前readwritelock一样
1. 乐观模式:就是在读的时候用乐观锁判断,如果发现被改过,就悲观读

1.3 个人总结--脑图

image-20220731123106243


	以前一直想不通为什么线程要区分这么细,后来才发现,实际开发中,不同用户就是一个线程请求,如何把各个请求处理好,保证系统性能和安全性,才是程序员需要考虑的问题
posted @ 2022-07-31 12:32  Empirefree  阅读(152)  评论(0编辑  收藏  举报