对于锁,已经是老生常谈了,前面也梳理过很多次了,我甚至都不想再写这篇了,但其在高并发多线程中的重要性还是不言而喻的,所以还是决定再开一篇,从更深层的角度分析JUC提供的lock包.
先来看一下jdk1.8-api,java.util.concurrent.locks包的结构:
其中红框中勾出来的是比较重要且经常被用到的,必学必会的部分.
提到锁,先来说一下最最常见的锁:
synchronized同步锁,它是由jvm实现的锁,用完后无需手动释放锁,用起来也非常方便.在较新的jdk版本中,该锁的性能据说也有很大提升,值得一用.
synchronized修饰代码块,大括号括起来的代码,作用范围是大括号括起来的部分,作用对象是调用这个代码块的对象.括号中为Object对象,也可以用this指代当前对象.
synchronized (this){
//todo
}
synchronized修饰方法,作用范围是整个该方法,作用对象是调用这个方法的对象.
public synchronized void method()
{
// todo
}
synchronized修饰静态方法,作用范围是整个方法,作用对象是这个类所有对象.
public synchronized static void method() {
// todo
}
synchronized修饰类,作用范围是synchronized后面括号包起来的部分,作用对象是这个类所有对象.
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
值得注意的是,被synchronized修饰的方法,如果该类被子类继承,子类调用该方法时,是不会继承synchronized的,所以如果子类也需要该方法同步,要自己显示的加上synchronized,有点绕,说简单点就是synchronized是不能被继承的.
JUC包下的Lock是接口,ReentrantLock,ReentrantReadWriteLock是其已知实现类.
先来分析下ReentrantLock,ReentrantLock是可重入锁,同时也是可中断锁,可以通过调用unlock方法随时释放锁.
我们看下它源码中的lock方法:
底层还是用了CAS自旋锁,由于int的默认值是0,所以第一次加锁是会成功的,成功后把state标志设为1,此时其他线程就不能再获取到该锁了,没有获取到锁的线程只能进入锁池等待.
还是以上讲Atomic中提到的计数问题为例,我们用ReentrantLock来解决.
public class LockTest1 {
private static final int clientTotal = 5000;
private static final int threadTotal = 800;
private static int count = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newCachedThreadPool();
final CountDownLatch ctl = new CountDownLatch(clientTotal);
final Semaphore semaphore = new Semaphore(threadTotal);
for (int i = 0; i < clientTotal; i++) {
es.execute(() -> {
try {
semaphore.acquire();
lock.lock();
add();
lock.unlock();
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
ctl.countDown();
});
}
ctl.await();
es.shutdown();
System.out.println("count:" + count);
}
public static void add() {
count++;
}
}
多次执行后,结果均为:
保证原子性操作.
ReentrantLock还提供了一个非常重要类,就是Condition,该类提供了await/signal方法,功能上类似于Object的wait和notify.不同的是通过Condition的await/signal会更加灵活,可以指定唤醒哪个线程,不像notify的唤醒,唤醒的是谁都不知道.
这里有一道我碰到的经典面试题,挺好玩的,就是在考察condition的用法:
https://blog.csdn.net/lovexiaotaozi/article/details/88638341
再分析下ReentrantReadWriteLock,ReentrantReadWriteLock是一把性能更高的可重入锁,读锁可以允许多个不同线程重入的,但对于写锁,同时只能有一个线程重入,这样就可以把读和写操作分离开来了,粒度更细,所以在性能上有所提高.
当一个线程拥有写锁时,不释放写锁的情况下,再占有读锁,此时写锁会被降级为读锁.
在公平模式下,无论读锁还是写锁的申请都需要按照FIFO先进先出的原则,非公平模式下,写锁无条件插队.
ReentrantReadWriteLock
//使用读写锁写一个缓存系统(伪代码)
public class ReentrantReadWriteLockDemo {
Map cache = new HashMap();
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
}
public Object getVlaue(String key) {
Object obj = null;
try {
rwl.readLock();
obj = cache.get(key);
if (obj == null) {
rwl.readLock().unlock();// 加入读锁,防止在读的时候其他线程去写数据.
rwl.writeLock();
try {
if (obj == null) {
obj = "从数据库里查";// 伪代码部分
}
} finally {
rwl.writeLock().unlock();
}
rwl.readLock();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
rwl.readLock().unlock();
}
return obj;
}
}
虽然ReentrantReadWriteLock通过读锁可以多个线程重入来提高性能,但是在公平模式下,由于读锁和写锁互斥,而用到ReentrantReadWriteLock大部分情况下都是读操作多于写操作的,所以有可能会产生饥饿现象,导致写锁迟迟不能申请到,从而降低了性能.
stampedLock
stampedLock是jdk1.8新增的一把锁,是对ReentrantReadWriteLock的改进版,解决了上面提到的饥饿问题,但在操作上要复杂于ReentrantReadWriteLock,而且在JDK1.8以后的版本才有,所以很多人都没有用过甚至听过这把锁.
ReentrantReadWriteLock是对ReentrantLock和synchronized锁的加强版,stampedLock是对ReentrantReadWriteLock的加强版,其地位和性能可见一斑了,所以是一把必学必会的锁.
Jdk1.8-api中提供了一段该锁的使用范例:
public class Demo {
private int balance;
private StampedLock lock = new StampedLock();
public void conditionReadWrite (int value) {
// 首先判断balance的值是否符合更新的条件
long stamp = lock.readLock();
while (balance > 0) {
long writeStamp = lock.tryConvertToWriteLock(stamp);
if(writeStamp != 0) { // 成功转换成为写锁
stamp = writeStamp;
balance += value;
break;
} else {
// 没有转换成写锁,这里需要首先释放读锁,然后再拿到写锁
lock.unlockRead(stamp);
// 获取写锁
stamp = lock.writeLock();
}
}
lock.unlock(stamp);
}
public void optimisticRead() {
long stamp = lock.tryOptimisticRead();
int c = balance;
// 这里可能会出现了写操作,因此要进行判断
if(!lock.validate(stamp)) {
// 要从新读取
long readStamp = lock.readLock();
c = balance;
stamp = readStamp;
}
///
lock.unlockRead(stamp);
}
public void read () {
long stamp = lock.readLock();
lock.tryOptimisticRead();
int c = balance;
// ...
lock.unlockRead(stamp);
}
public void write(int value) {
long stamp = lock.writeLock();
balance += value;
lock.unlockWrite(stamp);
}
}
至于源码和实现原理,实在是有点深,在下就不班门弄斧了,可以参考这篇,分析的很到位:
https://www.cnblogs.com/huangjuncong/p/9191760.html
除了jdk提供的这些锁,有时候为了保证操作的原子性,你还需要了解更多锁,比如分布式锁,因为在分布式环境下,Jdk提供的锁就未必能派上用场了.
如果你对分布式锁感兴趣,可以参考这篇:
https://blog.csdn.net/lovexiaotaozi/article/details/83825531