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