并发和并行
并发:交替着做事
并行:一同做事
进程与线程
进程:一个程序就是一个进程,进程是独立的
线程:线程为进程提供服务,多个线程可以共享数据空间,线程也有私有的空间
线程的 6 种状态
new:线程的新建
runable:分为两种 runable 和 running runable 是没有时间片的
waiting:无限的等待状态,通过 notify()或者 notifyall()进行唤醒
time_waiting:定时的等待状态,时间结束后成为 runable
blocked:线程被堵塞,线程没有得到对象的锁
terminated:线程终止
线程的 start 和 run
调用 start 方法可以使线程就绪并切换到新的线程中运行(自动调用 run 方法),而调用 run 方法还是在当前线程里
sleep 和 wait 的区别
sleep 方法没有释放锁,而 wait 方法释放了锁
sleep 可以被自动唤醒,wait 不加时间就一直等待
wiat()的时候调用 notify()或者 notifyall()会让对象从等待池中转移到锁的竞争池中
notify 会唤醒随机一个等待池的线程,而 notifyall 会唤醒全部线程
Thread 和 Runable
新建 Thread 重写 run 方法就可以新建线程
通过 Runable 接口,可以调用 Thread 的构造函数新建线程
线程的 3 种创建方法
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
死锁
多个线程都在争夺同一个资源导致阻塞形成死锁,假如要避免死锁
- 当线程请求不到资源的时候将线程之前的资源都释放掉
- 让线程对资源进行顺序申请
- 一次性申请所有资源
处理线程返回值
- 主线程等待着值的返回(循环查看变量的值并 sleep)
- thread 的 join
- 实现 Callable 接口
- 使用 Thread 运行 FutureTake 类对象,调用 get 方法得到返回值
- 使用线程池 submit 实现 Callable 的类得到 Future 对象
yield 与 interrupt
调用 yield 方法建议线程放弃调用时间片,但可能会被忽视,不会影响锁的状态
interrupt 会设置线程的中断的状态,假如线程是阻塞的,就会退出阻塞状态并抛出异常,假如是正常运行的状态会设置线程的中断表示然后继续运行
中断的处理是程序自己处理的
指令重排序
JVM 会对指令进行重新排序,确保和未排序的运行结果一样
在不改变程序的执行结果的前提下,尽可能的为编译器和处理器优化
顺序一致性模型:按照代码的执行顺序进行执行
新建对象的一般步骤:
- 分配对象的空间
- 初始化对象
- 给引用赋值
当重排序出现的时候,可能会将给引用赋值放在初始化对象前面,所以在多线程访问的时候会出现错误
volatile 关键字
所有对 volatile 变量的操作都实在内存中进行的,不会产生副本,保证了共享变量的可见性
线程在对数据操作的时候,先将数据同步到自己的本地内存中,执行结束后再同步回去,这里有一个时间差,在这段时间内,线程对副本的操作对于其他线程来说是不可见的
volatile 没有 Synchronized 的互斥性,所以 i++ 操作并不是原子性操作
适合一写多读的场景
Synchronized 关键字
前期,Synchronized 属于重量级锁,JDK1.6 之后进行了优化,减少了开销
Synchronized 可以对类对象(静态方法和代码锁)和实例对象(实例方法)进行加锁,确保只有一个线程对对象进行操作,保证多线程对资源访问的同步
- 通过 monitorenter 指令和 monitorexit 指令(修饰对象)
monitorenter:获取对象头里的 monitor 对象,使对象的锁计数器加 1
monitorexit:将锁的计数器设置为 0 - ACC_SYNCHRONIZED:标识此方法是同步方法
Sychronized 在获得锁的时候会将该线程的本地内存置为无效,并从主内存读取.释放时会将本地内存刷新到主内存中
Sychronized 和 ReentrantLock
Sychronized 是关键字,ReentrantLock 是类,可以将锁对象化
两者都是可以重入的
Sychronized 是非公平锁,ReentrantLock 可以实现公平和非公平两种模式
Sychronized 操作的是对象的 markword,ReentrantLock 操作的是 park 方法
Sychronized 依赖 JVM,ReentrantLock 调用 JDK 的方法
Java 内存模型
每个线程有自己的工作内存,保存从主内存中复制的副本,属于私有空间
主内存存放共享变量
锁的两种实现形式
- 通过 JUC 包中的类
- 使用同步代码块(Sychronized)
锁的分类与状态
自旋锁:采用循环的方式获得锁
适应性自旋锁:根据上一次获得锁的时间调整时间进行循环获得锁
锁消除:当方法加了同步锁之后只有一个线程在使用,锁会被 Java 虚拟机自动消除
锁粗化:假如同一个锁被同一个线程重复申请释放,可以将请求锁的范围扩大
偏向锁:对象头的 markword 中存放线程的 ID,减少重复申请消耗
轻量级锁:第二个线程竞争锁的时候偏向锁升级为轻量级锁
重量级锁:同一时间多个线程竞争资源
乐观锁:只是简单的检查一下和期望值是否一致,不加锁
悲观锁:对每次操作都加锁进行操作
Sychronized 状态转换
得到对象锁的过程:
线程的私有栈帧中存储对象的 markword,对象的 markword 更新为指向线程栈帧 markword 的指针,然后线程栈帧中的 owner 指针指向对象的 markword
无锁:没有加锁
偏向锁:假如对象头 MarkWord 中 ThreadId 字段为空,则将 ThreadId 设为改线程的 Id,表示该线程持有偏向锁,下一次该线程不会重复获得锁,假如出现锁的竞争,则偏向锁会撤销升级为轻量级锁
轻量级锁:第二个线程得到对象锁时,对象锁会变成轻量级锁,线程竞争使用 CAS 获得锁
重量级锁:当线程(多个)访问对象时发现对象的 markword 不是自己的线程栈帧,锁会膨胀为重量级锁,之后访问的线程会被堵塞,而最开始的线程会用 cas 自旋获得锁
可以自动的升级和降级
happens-before 的八大规则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作;
- volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;
- 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始;
happen-befores 并不能保证线程交互的可见性
可见性:线程修改公共变量指令对其他线程来说是可见的
线程池
核心线程池,线程池队列,线程池
提交线程步骤:判断核心线程池的线程是否都在工作,如果否,就创建一个线程,否则就判断工作队列是否满,如果是,就存储在队列中,否则判断线程池的线程是否都在工作,如果否,就创建一个线程执行,否则调用饱和策略(对错误的处理,有 4 种)
线程池创建线程的时候会将工作线程封装成 worker,worker 会循环去执行工作队列的任务
任务的阻塞队列可以选择无限的,有限的,基于数组的,基于链表的(建议使用有界的队列,能增强系统的稳定性和预警性 )
调用 execute 执行实现 Runnable 接口的类,submit 提交需要返回值的任务
线程池的 shutdown 和 shutdownnow 都是遍历线程池线程,调用中断方法中断线程,shutdownnow 将线程池状态设置为 stop,然后尝试停止所有正在执行的和暂停任务的线程,返回等待执行任务的列表;shutdown 将线程池状态设置为 shutdown,然后中断所有没有执行任务的线程
线程池的创建:
- 通过 ThreadPoolExecutor 类的构造函数
- 使用 Executors 类中的三种对象池构造函数创建
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
CAS
获取内存中的值,和期望的值作比较是否一致,假如一致就将新值赋值,不一致就重试
CAS 会出现 ABA 的问题,无法知道中途的值(解决 AtomicStampedRefence)
CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)
队列同步器(AQS)
Java并发之AQS详解 - waterystone - 博客园
Java并发包基石-AQS详解 - dreamcatcher-cx - 博客园
https://javadoop.com/2017/06/16/AbstractQueuedSynchronizer/
一行一行源码分析清楚 AbstractQueuedSynchronizer (二)_Javadoop
一行一行源码分析清楚 AbstractQueuedSynchronizer (三)_Javadoop
AQS 主要是更改 state 的数值
有两种资源的共享模式 exclusive(Reentrantlock)和 share(Countdownlatch/Semaphore)
tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
Reentrantlock
acquire -> if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// tryAcquire(arg)没有成功,这个时候需要把当前线程挂起,放到阻塞队列中。
tryacquire(arg) (获得锁 1.无人持有直接 cas 2.有人拿了,看是否是同一个线程)
addWaiter(Node.EXCLUSIVE) 先是直接添加到阻塞队列的尾部(尾部不是空)使用 cas
如果尾部为空或者 cas 出错,就用 enq(node)添加(会先创建一个初始化的 head(假如空的话),然后添加到他的后面)
acquireQueued(进入阻塞队列,判断是第一个还是其他位置)如果是第一个,若前驱是 head,就使用 tryacquire 竞争一下锁
如果是其他位置,将前驱节点的 waitstate 设置为-1,如果大于 0,就向前找小于等于 0 的 waitstate,然后挂起
release-> 获得 state,将 state-1 如果 state 等于 0 就释放(将 head 的 waitstate=0,从后向前找 waitestate<=0 的点(一直向前找),找到后唤醒(unpark(node)唤醒后的 node 从 park 的地方起来),再 set 一下 state
Reentranlock 分为公平锁和非公平锁,非公平锁的当前线程调用 lock 的时候直接用 cas 抢锁,抢不到后和公平锁一样请求锁 acquire
在调用 tryacquire 的时候又会使用 cas 抢,然后再和之前一样去阻塞队列排队
Condition
condition 基于 reentranlock 实现,await 和 signal 都要获得锁
conditon 的 await()响应中断->addcondition()将节点添加到条件队列,插入到尾部(先得到 tail 的 node,清除非 condition 的状态节点(unlinkcancelledwaiter(普通单链表的操作)),然后在初始化 node 并添加到条件队列(入队了)
之后完全释放锁(直接把 state=0,返回赋值前的值)->(isOnSyncQueue(判断条件是否是 CONDITION&&node.next!=null&&并在阻塞队列中找是否有这个元素))如果都没有,等待进入阻塞队列(挂起)(其他线程可以来获得锁)
signal(要获得当前线程的独占锁)将要转移的 node(一般是第一个,也有可能第一个取消了(通过 cas 判断 waitstate))与条件队列断绝关系,使用 enq 加入阻塞队列(state 会变成 0),如果阻塞队列中 node 前驱节点放弃了锁或者 cas 前驱节点的 waitstate(cas signal)失败了就唤醒(unpark)节点
node 唤醒的情况
- 获得锁
- 中断
- 前面的情况
node 被唤醒之后,会 checkinterruptwhilewaiting 判断是否发生了中断而且如果发生中断是发生在 signal 前或者 signal 后(transferAfterCancelledwait 通过 cas 设置 state 判断),中断唤醒后如果发现不是被 signal 的(被 signal 的 node waitestate 会变成 0,node 通过 cas 检查)会主动加入到阻塞队列中 enq
加入阻塞队列中,准备获得锁,acquirequeue(node,savedstate)并判断是否发生中断,有中断就进行处理(程序处理,例如抛出或者不抛)
Countdownlatch
将 state 设为 n,使用 countdown() 减少 1,state=0 的时候就唤醒调用 await()的方法
await()->tryacquire(判断 state 是否=0 来返回 1,-1)
返回-1 ->doacquiresharedinterruptibly 先入队,将前驱节点 waitstate 设置为-1,挂起(可以重复多次入队挂起 数量等于 n)
countdown()->tryreleaseshared(不断减少 state)如果=0,就调用 doreleaseshared 方法(state=0)
第一个线程被 doreleaseshared()里面的 unparksuccess 唤醒之后,由于 state=0,循环在第二轮的时候 >tryacquire 返回 1,线程被调用 setheadandProgate(node,r),设置为头结点(state 为 0)并唤醒下一节点调用 doreleaseshared()
cyclicbarrier
基于 condition 实现,只有当每个线程 await()了之后,使用 generation 开始下一次的新生,另外在执行过程中有中断 超时或者要做的 action 出现异常 栅栏就会 break,所有 await 的线程会 notifyall 然后抛出异常
semephroe
分公平策略和非公平策略,state 表示资源的数量,state=0 表示需要没有资源了,要等 release,其他的和共享的 aqs 差不多,通过判断资源的数量(cas)来阻塞线程
ThreadLocal
假如一个变量被 ThreadLocal 修饰,那么每一个线程在访问这个变量的时候会保存到本地副本中,并且会存放在线程的 ThreadLocalMap 中,key 为 ThreadLocal 对象
Atomic 原子类
用原子的方式更新数据
有 4 类原子类:
- 基础类:AtomicInteger,AtomicLong,AtomicBoolean
- 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicRefenceArray
- 引用类型:AtomicReference,AtomicStampedReference,AtomicMarkableReference
- 对象的属性修改类型:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference
使用 CAS 和 volatile 变量和 native 方法实现原子操作,避免使用 Sychronized
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于