JAVA中锁的概念
-
自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。
-
悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
-
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
-
独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
-
共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)
- 可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
- 公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。
几种重要的锁实现方式: synchronized、 ReentrantLock、 ReentrantReadWriteLock
同步关键字 synchronized
概念
属于最基本的线程通信机制,基于对象监视器实现的。
Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。
一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。
持性:
- 可重入
- 独享
- 悲观锁
锁的范围:
- 类锁
- 对象锁
- 锁消除
- 锁粗化
可见性
同步关鍵字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)
代码示例
synchronized代码示例
// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {
//普通方法+同步关键字,不能同步,因为锁的是对象,main里两个线程创建了两个对象
public synchronized void test1(){
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
//static方法+同步关键字,能同步,因为锁的是类
public synchronized static void test1(){
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
//synchronized (this),不能同步
static Object temp = new Object();
public void test1() {
synchronized (this) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
//synchronized(static变量),synchronized(ObjectSyncDemo1.class),能同步,两个线程争抢的是同一把锁
static Object temp = new Object();
public void test1() {
//synchronized (ObjectSyncDemo1.class) {
synchronized (temp) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
可重入的代码示例
// 可重入
public class ObjectSyncDemo2 {
public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我执行结束" + arg);
}
public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
锁粗化的概念
https://www.oracle.com/java/technologies/javase/6performance.html#2.1.2
锁粗化是发生在编译器级别(JIT)的一种锁优化方式。
JVM会对热点代码(不间断、高频地)中的多个锁合并成一个锁,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看,修改优化级 别为C2
public class ObjectSyncDemo3 {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
//jvm会对热点代码进行优化,变成下面这样
/*
synchronized (this) {
i++;
i++;
}*/
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}
锁消除的概念
锁消除是发生在编译器级别(JIT)的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
比如StringBuffer类的append操作,方法内部是加锁的,所以在执行一系列的append操作时JVM就会在保证线程安全的情况下消除锁
// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}
public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}
public void test1(Object arg) {
// jit 优化, 消除了锁
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}
同步关键字加锁原理
HotSpot中,对象前面会有一个类指针和标题,储标识哈希码的标题字以及用于分代垃圾收集的年龄和标记位
参考来源:
https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
CAS轻量级加锁其实就是替换mark word 的内容
使用CAS修改mark word完毕,加锁成功。则mark word中的tag进入00
状态(01=unlocked,00=Light-Weight locked,10=Heavy-weight locked)。
解锁的过程,则是一个逆向恢复 mark word的过程
偏向锁到轻量级锁
默认情况下JVM锁会经历:偏向锁-->轻量级锁-->重量级锁这四个状态
偏向标记第一次有用,出现过争用后就没用了。-XX: -UseBiasedLocking
禁用使用偏置锁定,-XX:+UseBiasedLocking
设置启用偏向锁。
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步
JVM为了少千活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作
重量级锁 - 监视器(monitor)
修改mark word如果失败,会自旋CAS一定次数,该次数可以通过参数配置
超过次数,仍未抢到锁,则锁升级为重量级锁进入阻塞。
monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。
在虚拟机的源码中可以看到监视器的内容
简单过一下Lock锁相关
方法 | 描述 |
---|---|
lock | 获取锁的方法,若锁被其他线程获取,则等待(阻塞) |
lockInterruptibly | 在锁的获取过程中可以中断当前线程 |
tryLock | 尝试非阻塞地获取锁,立即返回 |
unlock | 释放锁 |
根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。
ReentrantLock
独享锁;支持公平锁、非公平锁两种模式;可重入锁;
ReentrantLock
在上锁时,会根据实例化时指定的策略去获取锁,默认为非公平锁。如果上锁成功,锁状态值+1(重入,最大次数为 Integer.MAX_VALUE
),并将锁持有者设置为当前线程实例。在 Sync(AQS)
内部维护了一个队列,存放了所有上锁失败的线程。公平锁在上锁前,会检查在自己前面是否还有其他线程等待,如果有就放弃竞争,继续等待。而非公平锁会抓住每个机会,不管是否前面是否还有其它线程等待,只顾上锁
ReetrantLock
在释放锁时,将状态计数器减一(重入),当状态计数器为0时,锁可用。此时再从等待队列中寻找合适的线程唤醒,默认从队首开始,如果队列正在更新中,且未找到合适的线程,那么从队尾开始寻找。
ReadWriteLock
维护一对关联锁,一个用于只读操作,一个用于写入;读锁可以由多个读线程同时持有,写锁是排他的。
适合读取线程比写入线程多的场景,改进互斥锁的性能,
示例场景:缓存组件、集合的并发线程安全性改造。
写锁是线程独占,读锁是共享,所以写->读是升级。(读->写,是不能实现的)
关于锁降级
锁降级
指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 缓存示例
public class CacheDataDemo {
// 创建一个map用于缓存
private Map<String, Object> map = new HashMap<>();
private static ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
// 1 读取缓存里面的数据
// cache.query()
// 2 如果缓存没数据,则取数据库里面查询 database.query()
// 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
}
public Object get(String id) {
Object value = null;
// 首先开启读锁,从缓存中去取
rwl.readLock().lock();
try {
if (map.get(id) == null) {
// TODO database.query(); 全部查询数据库 ,缓存雪崩
// 必须释放读锁
rwl.readLock().unlock();
// 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
rwl.writeLock().lock(); // 所有线程在此处等待 1000 1 999 (在同步代码里面再次检查是否缓存)
try {
// 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
if (map.get(id) == null) {
// TODO value = ...如果缓存没有,就去数据库里面读取
}
rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性
} finally {
rwl.writeLock().unlock(); // 释放写锁@
}
}
//TODO cache.query();锁降级就是为了在这里保持数据可见性,避免在前面释放写锁后数据被别的线程修改
} finally {
rwl.readLock().unlock();
}
return value;
}
}
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程T获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
如果遵循锁降级的步骤,线程C在释放写锁之前获取读锁,那么线程T在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。
Condition
用于替代wait/notify
Object中的 wait(), notify(), notifyAll()方法是和 synchronized配合使用的,可以唤醒一个或者全部(单个等待集)
Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/ unpark机制);