4.1.4 Lock锁-JDK1.5(JUC)-java语言层锁
代码如下:
package www.wl.java;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyThread implements Runnable { // 线程主体
private int ticket = 100;
private Lock lock = new ReentrantLock ();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
lock.lock ();
if (ticket > 0) {
try {
Thread.sleep (200);
} catch (InterruptedException e) {
e.printStackTrace ();
}
System.out.println (Thread.currentThread ().getName () + "还剩下" + this.ticket-- + "票");
}
}catch(Exception e){
e.printStackTrace ();
}finally{
lock.unlock ();
}
}
}
}
public class 多线程 {
public static void main(String[] args) {
MyThread mt = new MyThread() ;
new Thread(mt,"黄牛A").start();
new Thread(mt,"黄牛B").start();
new Thread(mt,"黄牛C").start();
}
}
4.1.5 CAS(Compare and Swap)--乐观锁
--基于synchronized的优化(等待时间的优化)
悲观锁:线程获取锁(JDK1.6之前建锁,内建锁(synchronized))是一种悲观锁的策略
假设每一次执行临界区代码(访问共享资源)都会产生冲突,所以当线程获取到锁的同时也会阻塞其他尝试获取该锁的线程
乐观锁:(CAS操作,无锁操作(lock体系)):假设所有线程访问共享资源不会出现冲突,由于不会出现冲突自然就不会阻塞其他线程,因此就不会出现阻塞停顿的状态
出现冲突时,无锁操作使用CAS来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止
4.1.5.1 CAS操作过程:
CAS可以理解为CAS(V,O,N):
V:当前内存地址实际存放的值
O:预期值(旧值)
N:更新的新值
当执行CAS后,如果V==0时,期望值与内存实际值相等,该值没有被任何其他线程修改过,因此可以将N替换到内存中
当V=!0.表明该值已经被其他线程修改过,所以无法将N替换,返回最新的值V
当多个线程使用CAS操作同一个变量时,只有一个线程会成功,并成功更新变量值,其余线程均会失败,失败线程会重新尝试或将线程挂起(阻塞)
原老级内建锁:
最主要的问题:当存在线程竞争的情况下会出现线程阻塞以及唤醒带来的性能问题,对应互斥同步(阻塞同步),效率很低
而CAS并不是武断的将线程挂起,会尝试若干次CAS操作,并非进行耗时的挂起与唤醒操作,因此非阻塞式同步
4.1.5.2 CAS问题
1.ABA问题:
解决思路:沿用数据库的乐观锁机制,添加版本号1A-2B-3A
JDK1.5提供atomic包下AtomicStamoedReference类来解决CAS的ABA问题
2.自旋(CAS)会浪费大量的处理器资源(CPU)---踩刹车
与线程阻塞相比,自旋会浪费大量的处理器资源,因为此时线程仍处于运行状态,只不过跑的是无用指令,期望在无用指令时,锁能被释放出来
解决思路:自适应自旋(重量级锁的优先)
根据以往自旋等待时能否获取到锁,来动态调整自旋的时间(循环尝试的数量)
如果在上一次自旋时获取到锁,则此次自旋时间稍微变长一点,如果在上一次自旋结束还没有获取到锁,此次自旋时间稍微短一点
3.公平性
处于阻塞状态的线程无法立即竞争被释放的锁,而处于自旋状态的线程很有可能先获取到锁
内建锁无法实现公平性
LOCK体系可以实现公平锁
4.1.5.3 对象头
1.JDK1.6之后对内建锁做了优化(新增偏向锁,轻量级锁)
无锁状态0 01
偏向锁1 01
轻量级锁00
重量级锁(JDK1.6之前)-(内建锁) 10
这四种状态随着竞争情况逐渐升级,锁可以升级不能降级,为了提高获得锁与释放锁的效率
- 偏向锁
偏向锁:最乐观的锁,从始至终只有一个线程请求一把锁
偏向锁的获取:
当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里记录存储偏向锁的线程ID,以后该线程在进入和退 出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word中偏向锁线程ID是否是当前线程ID。
如果测试成功,表示线程已经获得了锁直接进入代码块运行
如果测试失败,检查当前偏向锁字段是否为0.如果为0,采用CAS操作将偏向锁字段设置为1,并且更新自己的线程ID到Mark Word字段中
如果为1,表示此时偏向锁已经被别的线程获取,则此线程不断尝试使用CAS获取偏性锁或者将偏向锁撤销,升级为轻量级锁(升级概率较大)
偏向锁的撤销:
偏向锁使用一种等待竞争出现才释放锁的机制,当有其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁。
小tip:偏向锁的撤销开销较大,需要等到线程进入全局安全点safepoint(当前线程在CPU上没有任何有用字节码)
偏向锁头部Epoch字段值:表示此对象偏向锁的撤销次数,默认撤销40次以上,表示此对象不再适用于偏向锁,当下次线程再次获取此对象时,直接变为轻量级锁
如何关闭偏向锁:
偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,-XX:BiasedLockingStartupDelay=0。将延迟关闭,JVM一启动就激活偏向锁
关闭偏向锁:-XX:-UseBiasedLocking=false,程序默认会进入轻量级锁状态
只有一次CAS过程,出现在第一次加锁时
2.轻量级锁
轻量级锁:多个线程在不同时间段请求同一把锁,也就是基本不存在锁竞争,针对此种情况,JVM采取轻量级锁来避免线程的阻塞式以及唤醒
加锁:
线程在执行同步代码块之前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的mark word 字段直接复制到此空间中
然后线程尝试使用CAS将对象头的mark word替换为指向锁记录的指针(指向当前线程)
如果成功表示获取到轻量级锁,如果失败,表示其他线程竞争轻量级锁,当前线程便使用自旋来不断尝试
解锁
轻量级解锁时,会使用CAS将复制的 Mark Word替换回到对象头,如果成功,则表示没有竞争发生,正常解锁。 如果失败,表示当前锁存在竞争,锁就会进一步膨胀成重量级锁
总结:
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。
1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适 应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对 象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过 程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。