Java并发调优-Synchronized与Lock原理解析
2020-11-20 13:58:01 0 举报
AI智能生成
Java并发调优-Synchronized与Lock原理解析
作者其他创作
大纲/内容
锁的类型
公平/非公平
公平锁
非公平锁
悲观/乐观
悲观锁
乐观锁
锁资源消耗维度
重量级锁(传统锁)
依赖于系统的同步函数,在linux上使用mutex互斥锁,最底层实现依赖于futex
这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高
对于加了synchronized关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的
偏向锁
轻量级锁
自旋锁
Synchronized
Synchronized用法
Synchronized块
对于同步代码块,编译成字节码时,会在同步代码块的前后加上 monitorenter 和 monitorexit
Synchronized方法
对于同步方法,编译成字节码时,会给方法加上ACC_SYNCHRONIZED,当调用改方法时,会先尝试获取锁
Synchronized原理
monitor实现原理
Java实例对象
图解
Markword
图解
Monitor对象
图解
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
Monitor对象临界资源对象一起创建,销毁
偏向锁
锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中
这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,
不同线程过来,CAS会失败,也就意味着获取锁失败。
不同线程过来,CAS会失败,也就意味着获取锁失败。
应用场景
偏向锁主要用来优化同一线程多次申请同一个锁的竞争
用于解决同一个线程反问执行同步代码时,重量级锁存在反问获取Monitor对象带来的不必要的性能开销
当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标
志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。
志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。
在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生
stop the word 后
stop the word 后
流程
轻量级锁
应用场景
轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时
间的竞争。
间的竞争。
当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word
中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换
Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当
前锁有一定的竞争,偏向锁将升级为轻量级锁。
中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换
Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当
前锁有一定的竞争,偏向锁将升级为轻量级锁。
流程
自旋锁
JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。
这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。
这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。
自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁
流程
重量级锁(传统锁)
依赖于系统的同步函数,在linux上使用mutex互斥锁,最底层实现依赖于futex
这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高
对于加了synchronized关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的
锁升级优化
流程
动态编译实现锁消除 / 锁粗化
Java 还使用了编译器对锁进行优化。JIT 编译器在动态编译同步块的时
候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程
访问,而没有被发布到其它线程
候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程
访问,而没有被发布到其它线程
减小锁粒度
我们可以考虑将一个数组和队列对象拆成多个小对象,来降低锁竞争,提升并行
度。
度。
Lock
Lock用法
ReentrantLock
是一个独占
锁,同一时间只允许一个线程访问
锁,同一时间只允许一个线程访问
ReentrantReadWriteLock
允许多个读线
程同时访问,但不允许写线程和读线程、写线程和写线程同
时访问。
程同时访问,但不允许写线程和读线程、写线程和写线程同
时访问。
读写锁内部维护了两个锁,一个是用于读操作的
ReadLock,一个是用于写操作的 WriteLock
ReadLock,一个是用于写操作的 WriteLock
它的自定义同步器(继承
AQS)需要在同步状态 state 上维护多个读线程和一个写线
程的状态,该状态的设计成为实现读写锁的关键
AQS)需要在同步状态 state 上维护多个读线程和一个写线
程的状态,该状态的设计成为实现读写锁的关键
缺点
在读取很多、写入很少的情况
下,RRW 会使写入线程遭遇饥饿(Starvation)问题,也
就是说写入线程会因迟迟无法竞争到锁而一直处于等待状
态
下,RRW 会使写入线程遭遇饥饿(Starvation)问题,也
就是说写入线程会因迟迟无法竞争到锁而一直处于等待状
态
StampedLock
StampedLock改进ReentrantReadWriteLock缺点提出
StampedLock 不是基于 AQS 实现的,但实现的原理
和 AQS 是一样的,都是基于队列和锁状态实现的
和 AQS 是一样的,都是基于队列和锁状态实现的
使用乐观锁的机制
乐观锁
Atomic下的类
AtomicInteger
Unsafe 的 getAndAddInt 方法
实现原理
保证CPU的L1/L2/L3缓存与主内存间的数据同步
总线锁定
当处理器要操作一个共享变量的时候,其在总线上会发出一
个 Lock 信号,这时其它处理器就不能操作共享变量了,该
处理器会独享此共享内存中的变量。
个 Lock 信号,这时其它处理器就不能操作共享变量了,该
处理器会独享此共享内存中的变量。
但总线锁定在阻塞其它
处理器获取该共享变量的操作请求时,也可能会导致大量阻
塞,从而增加系统的性能开销
处理器获取该共享变量的操作请求时,也可能会导致大量阻
塞,从而增加系统的性能开销
缓存锁定
当某个
处理器对缓存中的共享变量进行了操作,就会通知其它处理
器放弃存储该共享资源或者重新读取该共享资源。目前最新
的处理器都支持缓存锁定机制
处理器对缓存中的共享变量进行了操作,就会通知其它处理
器放弃存储该共享资源或者重新读取该共享资源。目前最新
的处理器都支持缓存锁定机制
缺点
使用不断重试CAS操作,如果长时间不成功,就会给 CPU 带来非常大的执行开销
Adder结尾的类
LongAdder
原理
降低操作共享变量的并发数,也就
是将对单一共享变量的操作压力分散到多个变量值上
是将对单一共享变量的操作压力分散到多个变量值上
将竞
争的每个写线程的 value 值分散到一个数组中,不同线程会
命中到数组的不同槽中,各个线程只对自己槽中的 value 值
进行 CAS 操作
争的每个写线程的 value 值分散到一个数组中,不同线程会
命中到数组的不同槽中,各个线程只对自己槽中的 value 值
进行 CAS 操作
最后在读取值的时候会将原子操作的共享
变量与各个分散在数组的 value 值相加,返回一个近似准确
的数值
变量与各个分散在数组的 value 值相加,返回一个近似准确
的数值
缺点
代价就是会消耗更多的内存空间
Lock原理
基于 AQS (AbstractQueuedSynchronizer)实现
volatile state
加锁状态
getState()
setState()
compareAndSetState()
setState()
compareAndSetState()
CLH
Node
双向链表
CAS
底层使用unsafe实现
子主题
LockSupport.park()/unpark()
Unsafe
Condition
使用await()、signal()这种方式实现线程间协作更加安全和高效
同一个锁可以创建多个条件变量,每个条件变量对应一个等待队列
在Condition上调用await线程会加入等待队列
在Condition上调用signal等待队列中的头结点会被移入到AQS的阻塞队列中
Condition实现原理图解
AQS资源共享方式
1.独占锁Exclusive
只有一个线程能执行,如ReentrantLock采用独占模式。
2.共享锁shared
多个线程获取某个锁可能会获得成功,多个线程可同时执行,如:Semaphore、CountDownLatch。
流程
1.线程获取锁流程:
线程A获取锁,state将0置为1,线程A占用
在A没有释放锁期间,线程B也来获取锁,线程B获取state为1,表示线程被占用,线程B创建Node节点放入队尾(tail),并且阻塞线程B
同理线程C获取state为1,表示线程被占用,线程C创建Node节点,放入队尾,且阻塞线程
在A没有释放锁期间,线程B也来获取锁,线程B获取state为1,表示线程被占用,线程B创建Node节点放入队尾(tail),并且阻塞线程B
同理线程C获取state为1,表示线程被占用,线程C创建Node节点,放入队尾,且阻塞线程
2.线程释放锁流程:
乐观锁
Lock性能优化
多线程调优
多线程性能损耗分析
上下文切换
导致上下文切换的原因
一种是程序本身触发的切换
sleep
wait
yield
join
park
synchronized
lock
另一种是由系统或者虚拟
机诱发的非自发性上下文切换
机诱发的非自发性上下文切换
线程被分配的时间片用完
虚拟机垃圾回收导致
垃圾回收机
制的使用有可能会导致 stop-the-world 事件的发生
制的使用有可能会导致 stop-the-world 事件的发生
执行优先级的线程导致
如何发现上下文切换
上下文切换导致性能损耗的原因
操作系统保存和恢复上下文;
调度器进行线程调度;
处理器高速缓存重新加载;
上下文切换也可能导致整个高速缓存区被冲刷
优化策略
竞争锁优化
锁的优化归根结底就是减少竞争
1. 减少锁的持有时间
尽量减少同步块代码的范围
2. 降低锁的粒度
锁分离
读写锁实现了锁分离
锁分段
我们在使用锁来保证集合或者大对象原子性时,可以考虑将锁对象进一步分解
3. 非阻塞乐观锁替代竞争锁
volatile 关键字的作用是保障可见性及有序性,volatile 的读写操作不会导致上下文切换,
因此开销比较小。
因此开销比较小。
CAS 是一个无锁算法实现,保障了对一个共享变
量读写操作的一致性。
量读写操作的一致性。
wait/notify 优化
wait/notify 的使用导致了较多的上下文切换
建议使用 Lock 锁结合 Condition 接口替代 Synchronized 内部锁中的 wait /
notify,实现等待/通知。
notify,实现等待/通知。
合理地设置线程池大小,避免创建过多线程
使用协程实现非阻塞等待
协程
协程是一种比线程更加轻量级的东西,相比于由操作系统内核来管理的进程和线程
协程则完全由程序本身所控制,也就是在用户态执行。
协程避免了像线程切换那样产生的上下文切
换,在性能方面得到了很大的提升
换,在性能方面得到了很大的提升
减少 Java 虚拟机的垃圾回收
并发容器调优
List
数组型
Vector
Vector 也是基于 Synchronized 同步锁实现的线程安全
Synchronized 关键字几乎修饰了所有对外暴露的方法,所以在读远大于写的操作场景中,Vector 将会发生大量锁竞争,
从而给系统带来性能开销。
从而给系统带来性能开销。
CopyOnWriteArrayList
实现了读操
作无锁
作无锁
写操作则通过操作底层数组的新副本来实现,是一种读写分离的并发策略
Map
Hashtable
ConcurrentHashMap
ConcurrentSkipListMap
总结
0 条评论
下一页