【高并发专题】-java线程安全-原子性-Lock包详解

对于锁,已经是老生常谈了,前面也梳理过很多次了,我甚至都不想再写这篇了,但其在高并发多线程中的重要性还是不言而喻的,所以还是决定再开一篇,从更深层的角度分析JUC提供的lock包.

先来看一下jdk1.8-api,java.util.concurrent.locks包的结构:

【高并发专题】-java线程安全-原子性-Lock包详解_第1张图片

其中红框中勾出来的是比较重要且经常被用到的,必学必会的部分.

提到锁,先来说一下最最常见的锁:

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方法:

【高并发专题】-java线程安全-原子性-Lock包详解_第2张图片

底层还是用了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

 

你可能感兴趣的:(【java进阶】-,多线程,【高并发专题】)