文章目录
13. 线程安全和锁优化
线程安全的简单定义:多个线程同时访问一个对象的时候,如果不考虑线程的交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以是得到正确的结果。
13.1 java语言中的线程安全
按照线程的安全程度进行排列,可以把java语言操作共享数据分为:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
13.1.1 不可变
- 不可变的对象一定是线程安全的(final修饰、String、java.lang.Number、Long、Double)。
13.1.2 绝对线程安全
- 尽管Vector类中的方法是被synchronized修饰,但在多线程的环境下也可能不安全。
13.1.3 相对线程安全
- 需要在调用端使用额外的同步手段来保证调用的正确性(Vector、HashTable)
13.2 线程安全的实现方法
13.2.1 互斥同步
-
同步:多个线程并发访问共享数据的时候,保证同一时刻只有一个线程使用共享数据。
-
synchronized
生成字节码,同步块前后形成monitorenter
和monitorexit
字节码指令,并使用reference参数指定锁定的对象。 -
执行
monitorenter
指令,获取对象锁,成功获取则锁的计数器加一,执行monitorexit
计数器减一,计数器为零时释放锁(体现synchronized
的可重入性,可以多次进入同步块)。如果没有获取成功,当前线程阻塞等待 -
ReentrantLock
锁:- 等待可中断,持有锁的线程长期不释放锁,等待的线程可以放弃等待。
- 公平锁,线程按照申请锁的时间顺序排序来一次获得锁。非公平锁,不排队,任何等待的线程都有机会获得锁。
synchronized
中的锁是非公平锁,ReentrantLock
默认是非公平锁。 - 锁绑定多个条件,
ReentrantLock
可以同时绑定多个Condition
对象。
-
ReentrantLock
不一定比synchronized
性能高,java虚拟机对synchronized
做了很多优化。
13.2.2 非阻塞同步
- 乐观锁的实现,认为不会出现问题,先进行操作,最后再检测修改。
- CAS
- Unsafe类中,
compareAndSwapInt()、compareAndSet()
等方法使用了CAS; - Atomic原子类保证了原子性,调用
incrementAndGet()
方法,内使用无限循环CAS;
- Unsafe类中,
- 问题:ABA,初次读取和赋值之间的过程可能被其他线程修改,然而CAS并不能发觉,
AtomicStampedReference
版本控制保证CAS的正确性。
13.3 锁优化
13.3.1 自旋锁
- 自旋等待避免了线程切换的开销,自旋时间短效果好,自旋时间过长浪费资源。所以自旋次数默认10次,超过未获取锁,挂起线程。
- 自适应自旋锁,自旋时间通过前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程在进行中,可以允许自旋等待更长时间。
13.3.2 锁消除
- 虚拟机及时编译时,对被检测到不可能存在共享数据竞争的锁进行消除;
- 依据:逃逸分析,堆上的数据不会逃逸出被其他线程访问,可栈上分配,线程私有,无须加锁;
- String连接就自动加锁
13.3.3 锁粗化
- 将连续多次对同一对象加锁改变为一次加锁,即将加锁的同步范围扩展(粗化)到整个操作序列的外部。
13.3.4 轻量级锁
-
对象头
- 运行时数据(MarkWord):HashCode、GC分代年龄等;
- 指向方法区对象类型数据的指针,如果是数据还保存数组长度;
-
锁升级
- 进入同步代码块,未锁定状态时,在当前栈帧中建立锁记录(Lock Record),用于存储锁对象目前的MarkWord的拷贝(Displace Mark Word)
- 使用CAS把Mark Word更新为指向Lock Record的指针,更新成功表示该线程获取了改对象的锁,并且Mark Word的锁标志转变为00,轻量级锁状态;
- 更新失败,意味着多条线程竞争锁。先检查对象的Mark Word是否指向当前线程的栈帧,如果是,直接进入,否则说明这个对象已经被其他线程抢占了;
- 两条及以上的线程争同一个锁,轻量级锁膨胀为重量级锁,Mark Word指向重量级锁(互斥量)的指针。
-
轻量级锁,不存在竞争,使用CAS避免了使用互斥量的开销,但存在锁竞争除了互斥量的开销,还有CAS的开销。
-
乐观锁的实现
13.3.5 偏向锁
- 目的:无竞争的情况下把整个同步都消除掉,也不用CAS;
- 偏向第一个获得该对象的线程;
- 锁对象第一次被线程A获取,对象头标志位01,偏向模式1,使用CAS将线程A的ID记录在对象头的Mark Word中,如果CAS成功,线程A可以直接进入同步块;
- 另外的线程竞争该对象的锁,偏向模式立马宣告结束。