学习笔记 | 多线程和并发

本贴最后更新于 1741 天前,其中的信息可能已经时移世易

并发和并行

并发:交替着做事
并行:一同做事

进程与线程

进程:一个程序就是一个进程,进程是独立的
线程:线程为进程提供服务,多个线程可以共享数据空间,线程也有私有的空间

线程的 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 种创建方法

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口

死锁

多个线程都在争夺同一个资源导致阻塞形成死锁,假如要避免死锁

  1. 当线程请求不到资源的时候将线程之前的资源都释放掉
  2. 让线程对资源进行顺序申请
  3. 一次性申请所有资源

处理线程返回值

  1. 主线程等待着值的返回(循环查看变量的值并 sleep)
  2. thread 的 join
  3. 实现 Callable 接口
    1. 使用 Thread 运行 FutureTake 类对象,调用 get 方法得到返回值
    2. 使用线程池 submit 实现 Callable 的类得到 Future 对象

yield 与 interrupt

调用 yield 方法建议线程放弃调用时间片,但可能会被忽视,不会影响锁的状态
interrupt 会设置线程的中断的状态,假如线程是阻塞的,就会退出阻塞状态并抛出异常,假如是正常运行的状态会设置线程的中断表示然后继续运行
中断的处理是程序自己处理的

指令重排序

JVM 会对指令进行重新排序,确保和未排序的运行结果一样
在不改变程序的执行结果的前提下,尽可能的为编译器和处理器优化

顺序一致性模型:按照代码的执行顺序进行执行

新建对象的一般步骤:

  1. 分配对象的空间
  2. 初始化对象
  3. 给引用赋值

当重排序出现的时候,可能会将给引用赋值放在初始化对象前面,所以在多线程访问的时候会出现错误

volatile 关键字

所有对 volatile 变量的操作都实在内存中进行的,不会产生副本,保证了共享变量的可见性

线程在对数据操作的时候,先将数据同步到自己的本地内存中,执行结束后再同步回去,这里有一个时间差,在这段时间内,线程对副本的操作对于其他线程来说是不可见的
volatile 没有 Synchronized 的互斥性,所以 i++ 操作并不是原子性操作
适合一写多读的场景

Synchronized 关键字

前期,Synchronized 属于重量级锁,JDK1.6 之后进行了优化,减少了开销
Synchronized 可以对类对象(静态方法和代码锁)和实例对象(实例方法)进行加锁,确保只有一个线程对对象进行操作,保证多线程对资源访问的同步

  1. 通过 monitorenter 指令和 monitorexit 指令(修饰对象)
    monitorenter:获取对象头里的 monitor 对象,使对象的锁计数器加 1
    monitorexit:将锁的计数器设置为 0
  2. ACC_SYNCHRONIZED:标识此方法是同步方法

Sychronized 在获得锁的时候会将该线程的本地内存置为无效,并从主内存读取.释放时会将本地内存刷新到主内存中

Sychronized 和 ReentrantLock

Sychronized 是关键字,ReentrantLock 是类,可以将锁对象化
两者都是可以重入的
Sychronized 是非公平锁,ReentrantLock 可以实现公平和非公平两种模式
Sychronized 操作的是对象的 markword,ReentrantLock 操作的是 park 方法
Sychronized 依赖 JVM,ReentrantLock 调用 JDK 的方法

Java 内存模型

每个线程有自己的工作内存,保存从主内存中复制的副本,属于私有空间
主内存存放共享变量

锁的两种实现形式

  1. 通过 JUC 包中的类
  2. 使用同步代码块(Sychronized)

锁的分类与状态

自旋锁:采用循环的方式获得锁
适应性自旋锁:根据上一次获得锁的时间调整时间进行循环获得锁
锁消除:当方法加了同步锁之后只有一个线程在使用,锁会被 Java 虚拟机自动消除
锁粗化:假如同一个锁被同一个线程重复申请释放,可以将请求锁的范围扩大
偏向锁:对象头的 markword 中存放线程的 ID,减少重复申请消耗
轻量级锁:第二个线程竞争锁的时候偏向锁升级为轻量级锁
重量级锁:同一时间多个线程竞争资源
乐观锁:只是简单的检查一下和期望值是否一致,不加锁
悲观锁:对每次操作都加锁进行操作

Sychronized 状态转换

得到对象锁的过程:
线程的私有栈帧中存储对象的 markword,对象的 markword 更新为指向线程栈帧 markword 的指针,然后线程栈帧中的 owner 指针指向对象的 markword
无锁:没有加锁
偏向锁:假如对象头 MarkWord 中 ThreadId 字段为空,则将 ThreadId 设为改线程的 Id,表示该线程持有偏向锁,下一次该线程不会重复获得锁,假如出现锁的竞争,则偏向锁会撤销升级为轻量级锁
轻量级锁:第二个线程得到对象锁时,对象锁会变成轻量级锁,线程竞争使用 CAS 获得锁
重量级锁:当线程(多个)访问对象时发现对象的 markword 不是自己的线程栈帧,锁会膨胀为重量级锁,之后访问的线程会被堵塞,而最开始的线程会用 cas 自旋获得锁

可以自动的升级和降级

happens-before 的八大规则

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作;
  3. volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;
  5. 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始;
    happen-befores 并不能保证线程交互的可见性

可见性:线程修改公共变量指令对其他线程来说是可见的

线程池

核心线程池,线程池队列,线程池
提交线程步骤:判断核心线程池的线程是否都在工作,如果否,就创建一个线程,否则就判断工作队列是否满,如果是,就存储在队列中,否则判断线程池的线程是否都在工作,如果否,就创建一个线程执行,否则调用饱和策略(对错误的处理,有 4 种)

线程池创建线程的时候会将工作线程封装成 worker,worker 会循环去执行工作队列的任务
任务的阻塞队列可以选择无限的,有限的,基于数组的,基于链表的(建议使用有界的队列,能增强系统的稳定性和预警性 )
调用 execute 执行实现 Runnable 接口的类,submit 提交需要返回值的任务
线程池的 shutdown 和 shutdownnow 都是遍历线程池线程,调用中断方法中断线程,shutdownnow 将线程池状态设置为 stop,然后尝试停止所有正在执行的和暂停任务的线程,返回等待执行任务的列表;shutdown 将线程池状态设置为 shutdown,然后中断所有没有执行任务的线程
线程池的创建:

  1. 通过 ThreadPoolExecutor 类的构造函数
  2. 使用 Executors 类中的三种对象池构造函数创建
    1. FixedThreadPool
    2. SingleThreadExecutor
    3. 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 唤醒的情况

  1. 获得锁
  2. 中断
  3. 前面的情况

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 类原子类:

  1. 基础类:AtomicInteger,AtomicLong,AtomicBoolean
  2. 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicRefenceArray
  3. 引用类型:AtomicReference,AtomicStampedReference,AtomicMarkableReference
  4. 对象的属性修改类型:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference

使用 CAS 和 volatile 变量和 native 方法实现原子操作,避免使用 Sychronized

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖
  • 线程
    120 引用 • 111 回帖 • 3 关注
  • 并发
    75 引用 • 73 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...