一、相关概念
- 自旋锁
对某数据的修改如果失败,不会放弃当前CPU执行权,而是循环使用CAS进行尝试修改直到成功。 - 悲观锁
假设会发生并发操作,因此对数据的相关操作从读开始就上锁 - 乐观锁
假设不会有并发操作,在修改数据时如果发现数据不一致了,则读取最新数据再尝试进行修改。自旋锁就是一种乐观锁 - 独享锁
即排它锁,同时只有一个线程可持有 - 共享锁
多个线程也可加持同一把共享锁,如读锁,多线程可读 - 可重入锁/不可重入锁
线程拿到一把锁后,是否可以自由进入由同一把锁同步的其他代码中 - 公平锁/非公平锁
线程争抢锁资源是否按先到先得原则,如果是则是公平锁,否则是非公平锁
二、锁的实现
1、同步关键字synchronized
基于对象监视器,每个对象都对应一个监视器,同时只有一个线程可以锁定这个监视器,其他尝试锁定的线程都会被阻塞。synchronized上锁与解锁都是jvm层面的,会自动释放锁。下面看下代码实现:
- 对象锁
public class SynTest1 {
public static void main(String[] args) {
SynTest1 obj = new SynTest1();
new Thread(new Runnable() {
@Override
public void run() {
obj.method();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj.method();
}
}).start();
}
private synchronized void method() {
System.out.println("method() run with...." + Thread.currentThread());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method() end with...." + Thread.currentThread());
}
}
输出结果:
method() run with…Thread[Thread-0,5,main]
method() end with…Thread[Thread-0,5,main]
method() run with…Thread[Thread-1,5,main]
method() end with…Thread[Thread-1,5,main]
- 类锁
public class SynTest1 {
public static void main(String[] args) {
// SynTest1 obj = new SynTest1();
new Thread(new Runnable() {
@Override
public void run() {
SynTest1.method();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SynTest1.method();
}
}).start();
}
private static synchronized void method() {
System.out.println("method() run with...." + Thread.currentThread());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method() end with...." + Thread.currentThread());
}
}
输出结果:
method() run with…Thread[Thread-0,5,main]
method() end with…Thread[Thread-0,5,main]
method() run with…Thread[Thread-1,5,main]
method() end with…Thread[Thread-1,5,main]
同时,synchronized 还可以用在代码块上:
public class SynTest1 {
public static void main(String[] args) {
SynTest1 obj = new SynTest1();
new Thread(new Runnable() {
@Override
public void run() {
obj.method();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj.method();
}
}).start();
}
private void method() {
synchronized (this) {//这里的this指代调用该方法的对象
System.out.println("method() run with...." + Thread.currentThread());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method() end with...." + Thread.currentThread());
}
}
}
synchronized实现的是一种悲观锁、独享锁,被synchronized加持的一段操作都是需要获得锁才能进行操作的,且同时只有一个线程可进行操作。同时synchronized也是一种重入锁,结合代码我们理解下什么是可重入锁:
public class SynTest2 {
static SynTest2 obj = new SynTest2();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
obj.method(1);
}
}).start();
}
private synchronized void method(int leval) {
System.out.println("in door-" + leval);
try {
Thread.sleep(3000);
if (leval == 1) {
obj.method(2);
} else {
System.out.println("get it in door-" + leval);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("out door-" + leval);
}
}
输出结果:
in door-1
in door-2
get it in door-2
out door-2
out door-1
就像我们拿一把钥匙回家一样,先打开入户门,还可以拿这把钥匙打开卧室门然后躺在柔软的床上。
2、synchronized实现原理
对象在内存中存储的的形式中有一部分为头信息(Mark Word),标记了对象和锁相关的信息,synchronized实现就是基于其中锁的标志位实现的。简化存储内容为:
是否偏向锁 | 锁标志位 | 锁状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 无锁 |
00 | CAS轻量级锁 | |
10 | 重量级锁 |
偏向锁状态是jvm底层的一种优化,默认是1开启状态,jvm会先“偷懒”认为是无多线程操作的,即无锁状态。当线程去获取锁是首先看偏向锁中是否存储有线程id,没有则存储当前线程id,即首次有线程访问,当单线程情况下回去比对该线程id,相同则认为是单线程操作,即不需要加锁。一旦有其他线程访问,查看到的线程id与自己不符,则会升级为多线程操作,去CAS操作设置锁标志位为加锁状态,释放锁则是恢复锁标志位操作。但CAS自旋是很消耗CPU的,在自旋一定时间后就会转为同步等待状态,标志位为重量级锁。
3、Lock锁
通过Lock接口实现类API来获取锁释放锁,功能更加强大灵活,但不会主动释放锁,需要编程实现。常用实现有ReentrantLock、ReentrantReadWriteLock。核心实现类:
方法 | 描述 |
---|---|
lock | 获取锁,如果有其他锁占用则阻塞等待 |
lockInterruptibly | 获取锁过程中可中断 |
tryLock | 非阻塞式获取,立即返回或者指定尝试一定时间 |
unlock | 释放锁 |
- ReentrantLock:可重入锁,独享锁、支持公平与非公平锁
public class LockTest1 {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
test1();
}
private static void test1() {
lock.lock();
try {
System.out.println("do something...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- ReentrantReadWriteLock:读写锁,用于读多写少场景优化
public class LockTest2 {
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
LockTest2 lt = new LockTest2();
new Thread(new Runnable() {
@Override
public void run() {
lt.read(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lt.read(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lt.write(Thread.currentThread());
}
}).start();
}
private static void read(Thread t) {
rwLock.readLock().lock();// 获取读锁并上锁,共享锁,多个线程可获取
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1) {
System.out.println(t.getName() + "正在读取数据...");
}
rwLock.readLock().unlock();
}
private static void write(Thread t) {
rwLock.writeLock().lock();//独享锁,只有一个线程能同时访问
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1) {
System.out.println(t.getName() + "正在写数据...");
}
rwLock.writeLock().unlock();
}
}
输出结果较长,可自行运行,可以看到线程0和线程1会交叉读取数据,而线程2写数据操作只有当读操作全部完毕才会执行。
4、锁降级理解
持有写锁过程中在获取到读锁,再将写锁释放的过程,多应用于缓存处理中。
public class LockTest3 {
private static Map<String, Object> map = new HashMap<String, Object>();
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private static Object get(String id) {
Object value = null;
// 开启读锁,先从缓存中读取
rwLock.readLock().lock();
try {
if ((value = map.get(id)) == null) {// 缓存失效
// 查询db读取数据,但访问量过大,这里db压力扛不住就会发生缓存雪崩
// 解决方法:读锁-》写锁,使得只有一个线程去db读取数据,其他线程坐等结果
rwLock.readLock().unlock();// 先释放之前的读锁才能加写锁
rwLock.writeLock().lock();// 此时大量访问线程会在此处阻塞
try {// 再次检查缓存,会有一个线程去库里读取回来,设置到缓存中
if ((value = map.get(id)) == null) {
// select value from...
}
map.put(id, value);
// 在释放写锁之前考虑下,如果有其他线程修改这个值,则会到时数据不一致,此时需要加读锁降级写锁,保证数据不可修改
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();// 释放写锁
}
}
} finally {
rwLock.readLock().unlock();// 最后释放读锁
}
return value;
}
}