Kosmos Kosmos

---我们总得选择一条路去前行---

目录
学习笔记|多线程和并发
/      

学习笔记|多线程和并发

并发和并行

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

进程与线程

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

线程的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)

https://www.cnblogs.com/waterystone/p/4920797.html

https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

https://javadoop.com/2017/06/16/AbstractQueuedSynchronizer/

https://www.javadoop.com/post/AbstractQueuedSynchronizer-2/

https://www.javadoop.com/post/AbstractQueuedSynchronizer-3

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


今日诗词 标题:学习笔记|多线程和并发
作者:ellenbboe
地址:https://ellenbboe.github.io/articles/2019/07/11/1562843459673.html