最近看了寒食君的java多线程的教程,感觉深受启发,做个笔记以后方便回忆。
寒食君的个人空间-寒食君个人主页-哔哩哔哩视频
java多线程
什么是线程模型?
JVM线程与操作系统线程之间存在着某种映射关系,这两种不同维度的线程之间的规范和协议,就是线程模型。
有一对一,一对多,多对多模型
什么是锁
在并发的环境下,当有多个线程去争抢同一个资源的时候,多个线程同时对资源进行了更改就会发生数据不一致的问题。为解决这个问题,通过一种抽象的锁去对资源进行绑定,即当一个线程持有锁的时候,其他线程必须等待。
悲观锁与乐观锁
乐观锁认为别人每次拿数据的时候不会修改数据,所以没有上锁,所以乐观锁也称为无锁。但是在更新的时候会判断别人有没有修改数据,比如CAS就是java中可观锁的一种实现方式。
悲观锁认为别人每次拿数据的时候都会修改数据,所以每次在读写数据的时候都会加上锁,比如Synchronized就是java中悲观锁的实现方式。
CAS
一句话:先比较再交换。比较什么呢:比较之前读到的值和当前时刻的读到的值发生变化没。发生变化了就不更改。不发生变化了就更改。
由于各个操作系统下的CAS操作都是原子的,比较和交换可以要么全部完成,要么全部不完成,这就使得CAS是安全的。
比如:Atomiclnteger,AQS就是通过CAS操作实现了无锁同步,仅仅在外层CAS加入一个循环实现自旋,进行多次的CAS操作来实现无锁同步。
Synchronized
Synchronized是一种对象锁,他是通过对象头中的Mark Word来区分锁的类型和锁与线程的对应关系。
在java6之前,Synchronized只有重量级锁,也就是通过Monitor来进行加锁操作,但是Monitor底层依赖操作系统的Mutex Lock来实现,通过操作系统实现线程的切换需要将用户态(java线程就是用户线程)切换到操作系统的内核态。这是非常费时间的。所以依赖Monitor实现的锁也叫叫重量级锁。
在java6以后,java引入了“无锁”、“偏向锁”,“轻量级锁”,“重量级锁”的概念。
无锁:就是在多线程的环境下没有竞争,所以不需要进行同步保护,或者不适用操作系统级别的语言进行锁定,而是通过其他机制,比如CAS
偏向锁:就是说一个对象认识一个线程,只要这个线程过来,就直接把锁交过去。当有多个线程同时来争取对象,那么将变成轻量型锁。
轻量型锁(自旋锁):当一个对象被某个线程锁定,通过线程就通过 自选来等待,当其他线程释放锁了之后,当前线程就可以获取改对象了。自旋的数量不能超过一个,超过一个变为重量级锁。
重量型锁:如上。
Monitor
可以把他想象成一个只能容纳一个人的房间,当一个线程进入moitor,其他线程都只有等待
-
Entry Set中聚集了一些想要进入Monitor的线程,它们处于waiting状态。
-
假设某个名为A线程成功进入了Monitor,那么它就处于active状态。
-
如果A线程执行途中,遇到一个判断条件,需要它暂时让出房间使用权,那么它将进入wait set,状态也 被标记为waiting。
-
这时entry set中的其他线程就有机会进入Monitor,假设一个线程B成功进入并且顺利完成,那么 它可以通过notify的形式来唤醒wait set中的线程A,让线程A再次进入Monitor,执行完成后便退出。
Mark Word
就是一个表示锁状态和指针
AQS
一句话:即抽象队列同步器。核心思想我认为就是:当资源被某个线程占用的时候,将其他需要访问某个资源的线程加入到队列中等待。
为什么会出现AQS呢?就是当一个资源被占用的时候,其他线程需要访问这个资源,就需要通过不断的自旋来等待资源被释放,多个线程同时自旋是非常消耗cpu的资源的,如果把多个等待线程放入到一个队列之中,仅仅让头部的线程自旋,其他线程挂起,当头部的线程占到了资源去通知后续的节点,这样就可以大大的降低资源的利用率。
一个重要属性:state属性表示共享线程被多少个线程占有。因为AQS有两种模式,共享模式(e.g. CountDownLatch)和独占模式(e.g. ReentrantLock)。共享模式下某个资源可能被多个资源同时占有。
一个重要内部类:AQS通过Node类去封装线程。储存了线程对象、等待状态waitStatus前后节点的指针信息。因为AQS用到的是双向队列。
两个重要方法:tryAcquire(尝试获取锁),acquire(先尝试获取锁,获取不到就利用尾插法加入到等待队列之中)
addWaiter就是将node插入到队列之中,插入尾节点的时候用到了CAS操作。
acquireQueue就是去拿锁,如果失败,再去去判断线程是不是要被挂起。
注:通过waitStatus进行判断。只有队列的头节点释放了资源,头节点的next节点才有机会去拿锁。
waitStatus
0,节点初始化默认值或节点已经释放锁
cancelled为1,表示当前节点获取锁的请求已经被取消了
signal为-1,表示当前节点的后续节点需要被被唤醒
condition为-2,表示当前节点正在等待某一个Condition对象
propagate为-3,传递共享模式下锁释放状态
ReentrantLock
一句话:即可重入锁,ReentrantLock基于AQS实现,和他的名字一样,他的一个主要的特点就是可重入性,并实现了公平锁和非公平锁。
可重入性是指一个线程可以不用释放而重复获取一个锁n次,但是在释放的时候也要释放n次。
公平与非公平是指获取锁的时候有没有机会插队
ReentrantLock使用了setExclusiveOwnerThread,这个方法是将某一个线程设置为独占线程,所以他是悲观锁
CountDownLatch
一句话:CountDownLatch是基于AQS实现的,就是去帮助主线程再等待其他线程多个线程任务完成之后能继续执行。
我的理解是:子线程完成一个,倒计时减一,当倒计时变为0,阀门打开,主线程开始继续运行。倒计时指的是AQS的state属性。
ConcurrentHashMap
一句话:为了解决HashMap中线程不安全的问题,提出了ConcurrentHashMap,使用分段锁来锁定数据,也就是说一把锁只锁定一部分数据,大大减少了HashTable中锁的竞争问题。
ConcurrentHashMap可以认为是对HashTable的一种改进,因为HashTable只用了一把锁,锁住了所有数据,导致线程竞争资源特别严重。
Segment数组中的每个元素都持有一把锁,且指向一个哈希表即HashEntry数组。
segment数组和HashEntry数组的长度一定是2的整数次,这是如果数组的长度是2的整数,取模操作和移位操作是相同的,这样就大大减少了运行成本。而且在数组长度扩大的时候也不会出现排序变乱的问题。
构造函数:就是构建一个ConcurrentHashMap的数据结构,先确定Segment数组的长度,然后将每个Segment指向一个HashEntry数组,然后初始化HashEntry数组,感觉一个HashEntry数组类似一个HashMap。
put操作:就是通过hash运算找到Segment,然后通过Segment指针找到HashEntry数组,然后再进行hash运算,找到对应的HashEntry,然后查找HashEntry里面是不是存在该值,存在替换(这里用到了锁),不存在不利用头插法插入该元素
put操作中用到了ReentrantLock锁(调用了tryLock方法),每个HashEntrty数组都有一把锁,只有获得了这个锁才能插入值进去,不然就会有线程不安全的问题。
这里有个改进就是,如果没有获取到锁会先建立节点,再进行等待,这相当于是个预加载的机制。
ThreadPoolExecutor
一句话:为了让一个线程不反复的回收和新建,利用一个容器来管理线程。使一个线程在运行结束后不销毁去运行其他程序。这样做即减少开销又便于管理。
他的内部类Worker(Worker就是一个线程的包装类)继承于AQS,这是因为线程空闲的时候应该把他加入到一个AQS的队列之中等待,也就是将其中断,利用AQS机制减少自旋带来的性能减少。
threadPoolExecutor的执行流程
提交一个任务,如果池子存在空闲线程就直接拿去用
如果没有,就看核心线程是不是满了,没有满就创建线程
如果核心线程池满了就加入等待队列
如果等待队列满了,那就继续创建线程
如果线程的数量达到最大线程的数量,那就启用拒绝策略,不再接收