并发编程
2021-06-29 11:59:24 23 举报
AI智能生成
JAVA并发编程流程图
作者其他创作
大纲/内容
ThreadLocal
提供线程类的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
引用类型
强引用
Object o = new Object();
在栈桢有效的作用域内,是用于不会被回收的
软引用
是指被SoftReference类实现的引用
SoftReference<Object> soft = new SoftReference<>(new Object());
当系统有足够的内存的时候,它能够存活。当系统内存不足,垃圾回收动作到来时,它会被回收释放内存。
可以当作缓存,内存充足不会被回收,内存不充足会被回收
弱引用
被WeakReference实现的引用
只能活到下一次垃圾回收发生之前。如果进行垃圾回收,一定会被回收
也可以当作缓存。相对于SoftReference时间更短一点
虚引用
被PhantomReference类实现的引用,无法通过虚引用来获取到一个对象实例。
Reference<Person> queue = new ReferenceQueue<>();
PhantomReference<Person> reference = new PhantomReference<>(new Person(),queue);
PhantomReference<Person> reference = new PhantomReference<>(new Person(),queue);
它被用来跟踪对象引用被加入到队列的时刻,所以它使用是需要和队列一起使用的。
before gc获取不到对象。afterGC之后会把对象放到队列里面,可以从队列里面拿到
三个关键词
线程并发
在多线程并发的场景下
传递数据
我们可以通过threadlocal在同一线程,不同组件中传递公共变量
线程隔离
每个线程的变量都是独立的,不会相互影响
使用
set():将变量绑定到当前线程
get():获取当前线程绑定的变量
sychronized与threadlocal区别
sychronized
以时间换空间的方式,只提供一份变量,让不同的线程排队访问
侧重点:多个线程之间访问资源的同步
threadlocal
以空间换时间的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而想不干扰
侧重点:多线程中让每个线程之间相互隔离
原理
jdk8中,每个Thread维护一个ThreadLocalMap,这个map的key是ThreadLocal本身,value才是真正要存储的Object
好处:
1.每个Map存储的Entry数量变少,hash冲突问题也会减少
在thread销毁时,threadlocalmap也会随着线程销毁,减少内存的使用
1.每个Map存储的Entry数量变少,hash冲突问题也会减少
在thread销毁时,threadlocalmap也会随着线程销毁,减少内存的使用
ThreadLcoalMap
是ThreadLocal的静态内部类
entry继承了WeakReference
key是threadLocal value就是变量值
内存泄漏问题
Entry继承了弱引用
帮助程序更好的GC
但是只对key做了弱引用,它的value还是强引用的一个关系
如果一个threadLocal没有外部强引用,那么下次GC,threadlocal弱引用会被回收
这个时候threadlocalmap就会出现key为null的Entry,也就无法根据key获取到value了,value又是强引用的,线程一直存在的话,就会一直存在这个value,就不会被垃圾回收到。
调用get,set,remove的方法时,会把key为null的entry value置为空。下次垃圾回收就会回收掉。
如果使用ThreadLocal没有调用方法,且线程一直存在,就会造成内存泄漏。 thread→threadmap→entry→value
解决:在使用完ThreadLocal后调用一下它的remove方法就可以了。
线程
如何创建一个线程
1.继承Thread类
2.实现Runnable接口
3.线程池
newCachedThreadPool可缓存的
newFixedThreadpool 可指定大小的
new Scu
线程池
原理
核心线程
队列
参数
corePoolSize
maximumPoolSize
keepAliveTime
queue
拒绝策略
AbortPolicy:抛异常。 DiscardPolicy直接扔掉。等
核心线程,额外线程
创建方式
newFixedThreadPool
ExecutorService threadPool = Executors.newFixedThreadPool(3); //3-->corePoolSize
子主题
如果在线程池中使用无界阻塞队列会发生什么
在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存飙升
调用超时,队列变得越来越大,此时内存会飙升,而且还可能会导致OOM,内存溢出
如果线程池队列满了,会发生什么事
corePoolSize:10
maximumPoolSize:Integer.MAX_VALUE
ArrayBlockQueue(200)
maximumPoolSize:Integer.MAX_VALUE
ArrayBlockQueue(200)
你可以无限的不停创建额外的线程出来,一台机器上,有几千个线程,甚至是几万个线程,每个线程都有自己的栈内存,占用一定的内存资源,会导致内存资源耗尽,系统也会崩溃。
即使内存没有崩溃,也会导致你的机器cpu load 负载特别的高。
即使内存没有崩溃,也会导致你的机器cpu load 负载特别的高。
到底如何创建要看具体情况
瞬时有很多任务过来,用无界队列也未尝不可
只是一瞬间过来,不是一直很多
控制额外线程,可以创建一些额外线程去处理瞬时访问的情况
建议自定义reject策略,如果线程池无法执行更多的任务,此时你可以把这个任务信息写入到磁盘里去,后台专门启动一个线程,后续等待你的线程池工作负载降低了,可以慢慢从磁盘里读取之前持久化的任务,重新提交到线程池里去。
如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
必然会导致线程池里积压的任务都是会丢失的。
解决
如果说要提交一个任务到线程池里去,在提交之前,可以在数据库里插入这个任务的信息,更新他的状态:未提交,已提交,已完成
提交成功后,更新他的状态为已提交
系统重启,后台线程去扫描数据库里的未提交和已提交状态的任务,可以把没执行的任务重新拿出来,重新执行。
锁
锁机制
对象
对象头,实例数据,对齐填充气节
对齐填充字节是为了满足java对象必须是8byte的倍数
对象头
存放了对象本身的运行时信息
mark word
32/64bit
对象存在一把锁,这个锁的信息就存放在对象头的markword当中
最后两位
锁标志位
class point
sychronized
悲观锁/互斥锁
悲观锁/互斥锁
sychronized被编译后会生成monitorenter和monitorexit这两个字节码指令来进行线程同步(重量级锁)
类锁/对象锁
对象锁
sychronized(对象)
修饰非静态方法
类锁
sychronized(类.class)
修饰静态变量
原子性,可见性,有序性,指令重排
从java6开始,引入了偏向锁,轻量级锁
锁的四种状态
(锁只能升级,不能降级)
(锁只能升级,不能降级)
无锁
没有对线程进行锁定,所有线程都能访问到同一资源
情况
1.某个对象不会出现在多线程环境下,即使出现在多线程环境下也不会出现竞争的情况
2.资源会被竞争,但是使用非锁的方式同步线程,比如cas
偏向锁
在实际运行时只有一个线程会获取这个对象的锁,那么就不用通过mutex lock或者cas浪费资源,设想最好对象能够认识这个线程,也就是偏向锁
在mark word中,当锁标志位为01,倒数第三个bit为1,则为偏向锁。于是再去读markword前23个bit也就是线程ID。
如果是老顾客,则直接调用对象。但是如果发生了变化,对象发现目前不只有一个线程来竞争锁,那么就会升级为轻量级锁
轻量级锁
锁标志位为 00
把mark word中的前30位变为指向栈中锁记录的指针
流程
当一个线程想要获得某个对象的锁时,假如看到锁标志位为00,那么就知道它是轻量级锁。
这时线程会在自己的虚拟机栈(线程私有)中开辟一块被称为Lock Record的空间。存放对象头中mark word的副本以及owner指针。
线程会通过cas尝试获取锁,一旦获取到锁,就会复制对象头中的MarkWord到lock record中,并且将lock record中的owner指针指向该对象。
另一方面,对象头中的markword前30个bit会生成一个指针指向线程栈中的lock record。这样一来,就实现了线程和对象锁的绑定。
对象被锁定,其他线程想要获取这个对象,会不断自旋尝试获取这个对象的锁。 自旋~CPU空转
优化:适应性自旋
自旋时间不再固定了,而是由上一次在同一个锁上的自旋时间和锁状态来决定
比如当前正在自旋等待的线程刚刚已经成功获得过锁,那么虚拟机会认为这次自旋很有可能再次成功。
一旦自旋等待的线程超过一个,那么轻量级锁就会升级为重量级锁。
重量级锁
monitor是依赖于操作系统的mutex lock来实现的,java线程实际上是对于操作系统线程的映射
所以每当挂起或者唤醒一个线程都要切换操作系统的内核态
在某些情况下甚至切换时间要超过执行任务的时间。
monitor:监视器/管程
只能容纳一名线程的房间,一个线程进入了房间,其他线程只能等待
一个线程进入monitor,就会变成Active状态
此时A线程执行途中遇到一个判断条件,需要暂时等待,就进入 Wait Set。此时Entry的线程就有机会进入mionitor。
假设B线程进入了monitor,并且顺利完成任务。那么它可以通过notify唤醒线程A让A再进入monitor继续执行任务,执行完后A便可以退出。
是可重入锁
可重入锁实现每一个锁都关联一个线程和一个计数器,累加锁的数量,只有为0的时候才能被别的线程持有,每完成一个同步代码块,计数器会 -1
为什么sychronized无法禁止指令重排,却能保证有序性?
1.Sycnronized是排他锁,可重入锁,它能够确保被它修饰的代码块能够单线程执行。
2.所以就满足了as-if-serial语义:单线程下,不管怎么排序,单线程的执行结果不能被改变!
CAS无锁
(乐观锁)
(乐观锁)
大部分情况下都是读操作,或者同步代码块执行时间远远小于线程切换的耗时。能不能不锁定资源,也能同步线程。
比较然后交换
当资源状态值为0的一瞬间,AB线程都读到了,那么这两个线程的值为0。
Old Value:代表之前读到的资源对象的状态值
New Value:代表想要将资源状态更新后的值
A线程快一步,B线程就得一直自旋,但是自旋也有次数限制,到达了次数就会放弃自旋
如何实现CAS的原子性?
不同架构的CPU都提供了指令级别的CAS
ARM架构:LL/SC
X86架构下:cmpxchg
并没有用到锁
AtomicInteger
主要成员变量
Unsafe实例
使用Unsafe的cas操作来进行对值的更新
循环就是自旋,自旋可以在启动参数配置,如果不配置默认是10,所以不会死循环
调用的是native本地方法,和具体的平台实现相关
long类型的offset
private volatile int value;
volatile修饰的value,这个也很关键,多个线程能够拿到同一个值保证值并发安全
Unsafe
Cas的底层实现核心就是Unsafe
JVM开的一个后门,提供了硬件级别的原子操作
JUC
如何设计一个同步管理框架
1.通用性,在实现底层必要同步机制的同时,开放一定的空间,给上层进行业务逻辑的编写
2.利用CAS,原子地修改共享标记位。
3.阻碍其他线程的调用
有的线程可能只想快速尝试获取一下资源,如果获取不到也没关系,进行其他处理
返回true/false
有的线程可能一定要获取到资源,获取不到可以等待
也可以返回truefalse,让业务轮询
会不断占用cpu性能
加大上层开发的复杂度
可以设计一个队列,让想要获取共享资源的线程进行排队
AQS
(AbstractQueuedSychronized)
Java.util.concurrent.locks
(AbstractQueuedSychronized)
Java.util.concurrent.locks
成员属性
private volatile int state;
用来判断资源是否被占用的标记位
volatile保证线程之间的可见性
为什么不用boolean,用int
谈到线程获取锁的两种模式
独占锁
一旦被占用,其他线程都不能占用
共享锁
一旦被占用,其他共享模式下的线程也能占用
在共享模式下,可能有多个线程正在共享资源,所以state需要表示线程占用的数量。
两个节点 头节点和尾节点
如果一个线程在当前时刻没有获取到共享资源,那么它要进行排队,队列的数据结构:FIFO先进先出的双向链表
head 和tail就表示双向链表的头和尾
队列里Node
waitStatus等待状态
prev next前后指针
thread线程对象
获取锁两种情况
1.尝试获取锁(修改标记位),立即返回
tryAcquire
被protected修饰的方法
参数是int值,代表了对state的修改
返回值是boolean值,代表是否成功获取锁
只有一行实现,就是抛出了一个异常
意图:AQS需要继承类必须Override这个tryAquire方法,否则就直接抛出不支持该操作的异常
给上层调用开放了空间,上层可以Override这个方法,自由编写业务逻辑
上层调用tryAquire成功则代表获得锁,此时可以对相应的共享资源进行操作,获取锁失败则上层业务可以直接进行相应业务的处理。
2.获取锁(修改标记位),愿意进入队列等待,直到获取
acquire
如果选择等待锁那么就可以直接调用acquire操作,而不用自己进行复杂的aquire处理
final修饰,继承的子类不允许修改acquire方法,直接使用acquire方法
先 !tryAcquire(arg)判断,如果获取到了锁,直接跳出这个过程
如果没获取到锁,则进入&&下一个 acquireQueued方法里的addWaiter(Node.EXCLUSIVE)
addWaiter(Node.EXCLUSIVE)
将当前线程封装成一个Node,加入等待队列,返回值为当前节点
如果等待队列尾节点不为空,则把尾节点设置为node的前节点,然后通过Cas操作把node设置为尾节点,然后把前节点的next指针指向当前node。
如果尾节点为空或者第一次尝试cas失败,那么会进行完整的入队方法
enq()
对当前队列进行初始化,并自旋的通过CAS将当前节点插入,直到入队成功为止。
acquireQueued(node,arg)
主体
一个自旋操作,如果当前节点的前置节点是头节点,而且当前线程尝试获取锁成功了,那么已经达到了目的,直接返回
FIFO的头节点是一个虚节点,是当前占位的一个摆设,而第二个节点才是真正想要去拿锁的节点,第二个节点拿到锁之后,它就会变成头节点,然后头节点就会出队。
当前节点如果不是头节点,或者是头节点,并且尝试获取失败。
那么就进入下一个判断,判断当前线程是否需要挂起,因为自旋消耗CPU性能,所以正常情况下选择将还没有轮到出队的线程挂起,再在适合的时间把它唤醒。
JAVA中断
interrupt
interrupt
作用于线程对象,并不会直接促成该对象挂起,而是根据当前thread的活动状态来产生不同的效果
如果线程处于运行状态,那么interrept只会改变thread对象的一个中断状态值,并不会影响线程继续运行。
判断是否需要挂起当前线程
如果当前节点的前置节点状态为SIGNAL,说明前置节点也在等待拿锁,所以当前节点可以挂起休息
如果waitStatus>0,那么说明状态只可能是Cancel,所以可以将其从队列中删除
如果前置节点是其他状态,既然当前节点已经加入,那么前置节点就应该做好准备来等待锁,所以通过CAS将前置节点的状态设置为SIGNAL
如果shouldPark方法返回True,代表当前节点需要被挂起,则执行真正的挂起操作
LockSupport.park(this);是通过native方法来调用操作系统原语,来将当前线程挂起
Thread.interrupted();是返回当前线程的中断标识位,并将其复位为false
总结
如果当前节点处于队列的头节点,那么就不断尝试拿锁,直到拿锁成功
其他节点都正在被挂起或者挂起
release方法
什么时候挂起的线程被唤醒?
一个线程使用完资源在释放锁的时候,去唤醒其他正在等待锁的线程,让他们一起去拿锁
tryRelease
尝试释放锁,需要Override
假如尝试释放锁成功,唤醒等待队列里的其他节点
首先将head节点的waitStatus置为0
从FIFO的尾节点开始搜索,找到除了头节点之外一个最靠前的节点,并且waitStatus<=0的节点
对其进行LockSupport.unpark操作,唤醒挂起的线程
被唤醒的线程将会继续执行AquireQueue方法,进行自旋尝试获取锁
ReentrantLock
ReentrantLock基于AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步,同时,和sychronized一样,支持可重入。除此之外,ReentrantLock在调度上更灵活,支持更多丰富的功能。
源码
实现了Lock接口
Lock的意义在于提供了区别Sychronized的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构,并且可以关联多个Condition对象。
六个方法
lock
用于获取锁,如果当前锁被其他线程占用,那么就等待,直到获取锁
lockInterruptibly
加入当前线程在等待锁的时候被中断,那么就退出等待,并且抛出中断异常
tryLock 无参
尝试获取锁,并立即返回
tryLock 有参
在一段时间内尝试获取锁,假如被中断,那么就抛出中断异常
unLock
释放锁
newCondition
Condition代表一个等待状态,获得锁的线程可能在某一时刻需要等待一些条件的完成才能继续执行,那么它可以通过await方法注册到Condition对象上进行等待,然后通过condition对象的signal方法将其唤醒 类似于Object的wait和notify
一个lock可以关联多个Condition,多个线程可以被绑定在不同的Condition对象上,这样就可以分组唤醒
Condition还提供了和限时,中断相关的功能。丰富了线程的调度策略
只有一个属性Sync
被final修饰,一旦初始化就不可修改引用了。
三个内部类
Sync/NofairSync/FairSync
Sync/NofairSync/FairSync
Sync
继承了AQS,可以用AQS中所有预设的机制
被Abstract修饰的,需要通过子类来进行实例化,无法直接被实例话
lock方式是一个空实现,等着子类来重写,说明公平锁和非公平锁的lock实现是不一样的。
其余方法都被final修饰,意味着不可被子类修改
nonfairTryAcquire非公平的尝试获取锁
先获取state,锁状态是否为空闲
如果空闲就可以通过CAS来原子的修改一次state,如果state修改成功,则代表获取了锁。将当前线程设置为独占线程。
当state不为0,先判断当前线程是否已经是独占线程
可重入性的实现
一个线程不用释放,就可以重复获取这个锁N次,但是也要释放相同的次数。
tryRelease
返回值代表是否完全被释放
NonfairSync非公平锁
非公平锁的效率更高,非公平锁意味着后请求锁的线程可能在线程被唤醒之前拿到锁
lock
尝试获取锁,如果获取不到则等待直到获取为止
上来就通过cas获取锁,非公平锁,获取不到则进行排队
tryAcquire
无论是否获取锁都立即返回
直接调用父类的nonfairTryAcquire
fairSync公平锁
lock直接调用父类的acquire,acqurie首次会调用tryAcquire
tryAcquire
如果锁空闲,且FIFO中没有排在当前线程之前的线程,则进行获取锁,如果返回失败就返回false。然后在acquire中进入排队流程
如果锁不是空闲的,为了满足可重入,也要进行相应的判断
如果当前线程不是持有锁的独占线程,那么也是返回false
创建选择公平锁或非公平锁
属性sync是被final修饰的,一旦初始化时选择好实现类型,就无法修改了
lock和interruptibly的区别
java中断机制
interrupt:如果thread正在RUNNABLE状态,此时interrupt不会抛出异常,且不会中断,只会修改thread内部中断状态值 true代表被中断,faluse代表未被调用中断
isInterrupted:返回线程中的中断标志位
interrupted:返回当前线程的中断标志位,同时会重置中断标志位
lock:实际上是对AQS中acquire方法的调用
如果线程在等待锁的过程中,被调用了中断LockSupport.park,该线程不会立即抛出异常,而是存储该中断的状态值,直到获取锁之后才抛出
CountDownLatch
CountDownLatch这个同步工具允许一条或多条线程等待其他线程中的一组操作完成后,再继续执行。
主线程等待子线程完成之后再继续执行
主线程等待子线程完成之后再继续执行
await设置超时时间
源码
Sync对象
tryAcquireShared:获取AQS内部锁状态,如果未被占用则返回1,如果被占用返回-1
tryReleaseShared:每次通过AQS自旋的CAS操作来对state自减1,若不需要释放锁,或未完全释放锁,则返回false,若锁已完全释放,则返回true
await:设置超时时间
countDown:用于子任务告知当前事务的完成
getCount:获取还未完成的任务数
StampedLock
在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强
提供了一些功能,优化了读锁,写锁的访问,同时使读写锁之间可以相互转换,更细粒度控制并发。
CurrentHashMap
分段锁
java7
源码
继承AbstractMap,实现ConcurrentMap
静态变量
1.所有hashEntry数目的初始值
2.加载因子,扩容因子
3.并发等级
4.所有HashEntry数目的最大值
5.Segment数组最小长度
6.重试次数
2.加载因子,扩容因子
3.并发等级
4.所有HashEntry数目的最大值
5.Segment数组最小长度
6.重试次数
成员变量
segementShift:转换,数值的转换变换有关系
segementMask:mask主要就是一串0和1的序列,进行位与操作。
核心内部类
HashEntry
UNSAFE.objectFieldOffset:成员属性在内存地址相对于此对象的内存地址的偏移量,可能是通过CAS来对成员变量Next进行操作
Segement
继承自ReentrantLock
1.MAX_SCAN_RETRIES就是指定的重试次数
多线程进行put操作,只有一个线程可以获取锁,其他线程不必死等,可以通过TryLock的方式进行重试,并做其他操作。
2.count指HashEntry数组的元素个数
3.modCount指HashEntry数组的修改次数
4.threshould指下一次需要扩容的阈值
5.loadFactor指负载因子
6.scanAndLockForPut,scanAndLock
用于在尝试获取锁期间来进行一些准备工作来提升效率
lock
ReentrantLock 可重入锁
trylock
AQS
Abstract Queue Sychronzier 抽象队列同步器
加锁步骤
- 多个线程,加锁时,通过CAS更新state = 1,只有一个线程能成功
- 线程2加锁失败,就进入等待队列
线程1执行完使用unlock解锁后,唤醒等待队列排在最头的线程,然后线程2就把state改成1,然后把加锁线程改成它自己
默认情况下,锁的策略是非公平锁
非公平锁
线程1执行完,线程2还没来及获取锁,线程3来了先拿到锁
公平锁
线程3过来先发现队列里面有线程2,就先进入等待队列排队。公平锁所有人都得判断一下。
ReentrantLock lock = new ReentrantLock(true); =>公平锁
0 条评论
下一页