欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java分布式应用学习笔记06浅谈并发加锁机制分析 博客分类: 分布式集群 java分布式集群线程锁并发 

程序员文章站 2024-03-16 15:16:58
...

1.  前言

之前总结的多线程的调度、并发调度、线程加锁安全等等并发包底层大都使用了线程锁机制。咱们通过锁的源码来看看JDK如何将这些资源进行加锁限制的,怎么就能做到线程集中等待后就唤醒主线程的。

2.  一段并发包源码

以下是java.util.concurrent.CyclicBarrier的底层代码片段。

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            ……………………省略
        } finally {
            lock.unlock();
        }
    }

 

在执行等待的时候,里面使用了ReentrantLock对其进行资源加锁,保证在代码块中使用变量、读写变量不会被别的线程打扰。

3.  轻量级锁ReentrantLock

基于以上程序我们就来看看ReentrantLock内部是如何工作的。ReentrantLock内部有个Sync类,继承自AbstractQueuedSynchronizer,基于Sync又有2个子类继承于它,而ReentrantLock就是依靠这2Sync子类为内核实现的。代码大家直接看JDK源程序即可。

ReentrantLocklock()方法,它的加锁方法实际上是使用的内部静态类Sync的方法,至于调用的是公平——FairSync还是不公平的——NonfairSync,这个要看构建ReentrantLock的时候的构造函数了。

NonfairSync的加锁方法实现流程是这样的:首先基于CAS——Compare and Swap原则,先将state0尝试变成1。如果设置成功了,证明了一个事实——此时此刻,没有其他的线程持有该锁。则当前线程设置为ExclusiveOwnerThread(独家拥有线程);那么如果状态变量state设置不成功呢,则又揭示了一个事实,当前线程锁已经被其他线程所持有,那么调用acquire方法,该方法首先先尝试再次获取状态state,如果为0了,那么继续尝试设置状态为1。若成功则与此时无其他线程持有锁操作雷同。如果state依然不为0,则判断当前线程是否为独家拥有线程变量——exclusiveOwnerThread,是的话将state的值+1.如果不是,则将当前线程放入等待队列中挂起,挂起方法

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
    }

 

FairSynclock()方法和NonfairSync大同小异。只不过它没有获取state变量信息的过程,直接是调用acquire(1)方法请求线程锁。

ReentrantLockunlock()方法,解锁的方法比较简单,不区分公平与不公平,都是获取当前state值,之后减去释放锁的个数,减操作后结果如果为0,表示锁可以释放了。通知队列上的那些线程,唤醒队列头线程,进行run操作。

这些加锁、解锁操作很明显都离不开队列——AbstractQueuedSynchronizer的辅助操作,将线程组织成为线程队列形式。

4.  读写锁ReentrantReadWriteLock

了解了ReentrantLock在加锁原理上使用了一把锁,一把钥匙开一把锁嘛~而如果遇到大部分操作是读操作的、而写操作比较少的时候使用ReentrantLock未免有点“奢侈”。使用ReentrantReadWriteLock——读写双锁进行读取和写入,在读多写少的场景下可以提升不少性能。它的基本工作原理是这样的:当使用读锁进行lock的时候,就算是有其他线程也进行读操作,而不是写操作的时候,线程不会阻塞,可以并行执行,和没有加lock几乎是一样的。当调用写锁的lock方法时,无论其他线程是什么性质的(读、写),都阻塞,所以这个双重锁适用于读操作频率较多、写操作频率较少的操作。咱们看个使用例子

	// 读写锁
	static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

			// 获得读锁-加锁
			reentrantReadWriteLock.readLock().lock();
			String str = listString.get(sum);
			reentrantReadWriteLock.readLock().unlock();

			// 获得写锁-加锁
			reentrantReadWriteLock.writeLock().lock();
			String str = Thread.currentThread().getName() + "--write:" + sum;
			listString.add(str);
			reentrantReadWriteLock.writeLock().unlock();

 因为一些特殊原因,不能将源码完整的场景全部给出,只能写出简单的使用关键的片段。

读锁的加锁源代码片段是

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

 

写锁的加锁源码片段是

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

 

相比较而言,就是上面说过的一旦写锁加锁时发现有其他线程进行了操作,则将当前线程放置于线程等待队列中——之后再唤醒。而读操作锁直接进行了共享线程,并发读取。

5.  总结

综合前面几篇线程调度、多线程并发计算等等,底层都是基于加锁、抽象线程等待队列AbstractQueuedSynchronizer及其2个具体子类、CAS算法的综合体现。使用多线程并发包后我们可以构建高可用和高计算能力的分布式系统。

注意:如果没有将解锁代码写到finally块中,是有问题的!!如果发生了任何的运行时异常,会向上抛,那么锁会永远不会解除,那么造成的后果大家一定知道了。