java   ReentrantLock   分析

并发编程中经常用到的莫非是这个ReentrantLock这个类,线程获取锁和释放锁。还有一个则是synchronized,常用来多线程控制获取锁机制。

先写一个简单的例子。

package com.multi.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {

	public static void main(String[] args) {
		Lock lock = new ReentrantLock(true);

		MyThread t1 = new MyThread("t1", lock);
		MyThread t2 = new MyThread("t2", lock);
		MyThread t3 = new MyThread("t3", lock);
		t1.start();
		t2.start();
		t3.start();
	}

}

class MyThread extends Thread {

	private Lock lock;

	public MyThread(String name, Lock lock) {
		super(name);
		this.lock = lock;
	}

	@Override
	public void run() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread() + "  is running ");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} finally {
			lock.unlock();
		}
	}

}

这是个简单的用ReentrantLock的代码。

知识点理解:

ReentranctLock:

1) 可重入性:大致意思就是如果一个函数能被安全的重复执行,那么这个函数是可重复的。听起来很绕口。

2)可重入锁:一个线程可以重复的获取它已经拥有的锁。

特性:

1)ReentrantLock可以在不同的方法中使用。

2)支持公平锁和非公平锁概念

static final class NonfairSync extends Sync;(非公平锁)

static final class FairSync extends Sync;(公平锁)

3)支持中断锁,收到中断信号可以释放其拥有的锁。

4)支持超时获取锁:tryLock方法是尝试获取锁,支持获取锁的是带上时间限制,等待一定时间就会返回。

ReentrantLock就先简单说一下AQS(AbstractQueuedSynchronizer)。java.util.concurrent包下很多类都是基于AQS作为基础开发的,Condition,BlockingQueue以及线程池使用的worker都是基于起实现的,其实就是将负杂的繁琐的并发过程封装起来,以便其他的开发工具更容易的开发。其主要通过volatile和Unsafe类的原子操作,来实现阻塞和同步。


AQS是一个抽象类,其他类主要通过重载其tryAcquire(int arg)来获取锁,和tryRelease来释放锁。

AQS不在这里做分析,会有单独的一篇文章来学习AQS。

ReentrantLock类里面主要有三个类,Sync,NonfairSync,FairSync这三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。

Sync是ReentrantLock实现公平锁和非公平锁的主要实现,默认情况下ReentrantLock是非公平锁。

Lock lock = new ReentrantLock(true);   :true则是公平锁,false就是非公平锁,什么都不传也是非公平锁默认的。

非公平锁:

lock.lock();点进去代码会进入到,ReentranctLock内部类Sync。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();
        
        ......省略。
      }

这个抽象类Sync的里有一个抽象方法,lock(),供给NonfairSync,FairSync这两个实现类来实现的。这个是一个模板方法设计模式,具体的逻辑供给子类来实现。

非公平锁的lock的方法,虽然都可以自己看,但是还是粘贴出来,说一下。

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        ......省略
    }

其实重点看这个compareAndSetState(0,1),这个其实一个原子操作,是cas操作来获取线程的资源的。其代表的是如果原来的值是0就将其设为1,并且返回true。其实这段代码就是设置private volatile int state;,这个状态的。

其实现原理就是通过Unsafe直接得到state的内存地址然后直接操作内存的。设置成功,就说明已经获取到了锁,如果失败的,则会进入:

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

这个方法里,这个过程是先去判断锁的状态是否为可用,如果锁已被持有,则再判断持有锁的线程是否未当前线程,如果是则将锁的持有递增,这也是java层实现可重入性的原理。如果再次失败,则进入等待队列。就是要进去等待队列了AQS有一个内部类,是Node就是用来存放获取锁的线程信息。

AQS的线程阻塞队列是一个双向队列,提供了FiFO的特性,Head节点表示头部,tail表示尾部。

1)节点node,维护一个volatile状态,维护一个prev指向向前一个队列节点,根据前一个节点的状态来判断是否获取锁。

2)当前线程释放的时候,只需要修改自身的状态即可,后续节点会观察到这个volatile状态而改变获取锁。volatile是放在内存中的,共享的,所以前一个节点改变状态后,后续节点会看到这个状态信息。

获取锁失败后就会加入到队列里,但是有一点,不公平锁就是,每个新来的线程来获取所得时候,不是直接放入到队列尾部,而是也去cas修改state状态,看看是否获取锁成功。

总结非公平锁:

首先会尝试改变AQS的状态,改变成功了就获取锁,否则失败后再次通过判断当前的state的状态是否为0,如果为0,就再次尝试获取锁。如果state不为0,该锁已经被其他线程持有了,但是其它线程也可能也是自己啊,所以也要判断一下是否是自己获取线程,如果是则是获取成功,且锁的次数要加1,这是可重入锁,不是则加入到node阻塞队列里。加入到队列后则在for循环中通过判断当前线程状态来决定是否哟啊阻塞。可以看出在加入队列前及阻塞前多次尝试去获取锁,而避免进入线程阻塞,这是因为阻塞、唤醒都需要cpu的调度,以及上下文切换,这是个重量级的操作,应尽量避免

公平锁:

FairSync类:
final void lock() {
   //先去判断锁的状态,而不是直接去获取
	acquire(1);
}
AQS类:
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
FairSync类:
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    
    //hasQueuedPredecessors判断是否有前节点,如果有就不会尝试去获取锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平锁,主要区别是:什么事都要有个先来后到,先来的有先。获取锁的时候是先看锁是否可用并且是否有节点,就是是否有阻塞队列。有的话,就是直接放入到队列尾部,而不是获取锁。

释放锁:

public void unlock() {
	sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放锁是很简单的,就是先去改变state这个状态的值,改变后如果状态为0,则说明释放成功了,如果直接可重入了多次,也要释放很多次的锁。

释放过程:

Head节点就是当前持有锁的线程节点,当释放锁时,从头结点的next来看,头结点的下一个节点如果不为null,且waitStatus不大于0,则跳过判断,否则从队尾向前找到最前的一个waitStatus的节点,然后通过LockSupport.unpark(s.thread)唤醒该节点线程。可以看出ReentrantLock的非公平锁只是在获取锁的时候是非公平的,如果进入到等待队列后,在head节点的线程unlock()时,会按照进入的顺序来得到唤醒,保证了队列的FIFO的特性。

参考文章:

http://silencedut.com/2017/01/09/%E7%94%B1ReentrantLock%E5%88%86%E6%9E%90JUC%E7%9A%84%E6%A0%B8%E5%BF%83AQS/

http://www.cnblogs.com/leesf456/p/5383609.html

https://github.com/pzxwhc/MineKnowContainer/issues/16

时间: 2024-10-26 08:56:45

java   ReentrantLock   分析的相关文章

java代码分析及分析工具

java代码分析及分析工具 一个项目从搭建开始,开发的初期往往思路比较清晰,代码也比较清晰.随着时间的推移,业务越来越复杂.代码也就面临着耦合,冗余,甚至杂乱,到最后谁都不敢碰. 作为一个互联网电子商务网站的业务支撑系统,业务复杂不言而喻.从09年开始一直沿用到现在,中间代码经过了多少人的手,留下了多少的坑,已经记不清楚了,谁也说不清了. 代码的维护成本越来越高.代码已经急需做调整和改善.最近项目组专门设立了一个小组,利用业余时间做代码分析的工作,目标对核心代码进行分析并进行设计重构. 代码分析

HDFS API的java代码分析与实例

HDFS API的java代码分析与实例 1.HDFS常用的方法,我已经写好,我们看一下 // Create()方法,直接在HDFS中写入一个新的文件,path为写入路径,text为写入的文本内容 public static void  Create(String path,String text) throws IOException {             Configuration conf=new Configuration();                  conf.set(

J2SE快速进阶——Java内存分析

程序的执行过程 要在Java中分析内存,我们先来了解一下程序的执行过程: 正如上图所示,大致分为3个步骤: 1.最开始,我们的程序是存在于硬盘中的,当启动运行时,程序会被加载(load)到内存中去,这里的内存可以看做我们的内存条: 2.此时,内存中除了存在刚加载的程序的代码,还存在操作系统本身的代码(好吧,此句可以当做废话→_→),操作系统会找到程序中的Main方法开始执行程序: 3.第三步就是本文的重点,系统在程序执行过程中对内存的管理.在Java中,内存大致会被分为四块--heap(栈).s

学java教程之java内存分析

学编程吧学java教程之java内存分析发布了,欢迎大家通过xuebiancheng8.com来访问 java的内存模型是java中非常重要的知识,也是面试的时候重点. java虚拟机的内存模型中和我们打交道多的分为这么几个区域 堆区,栈区,方法区. 其中方法区又分为常量池,静态区和方法区. 这几部分分别是干嘛的呢,堆区是用来存放new出来的对象的,堆区是应用程序共享的区域. 栈区又叫方法栈,程序在运行的时候,代码要在方法栈中运行,运行的代码需要放在方法栈中来执行,然后寄存器一行一行加载执行.

Java AsyncTask 分析内部实现

sdk3.0前,使用内部的线程池,多线程并发执行.线程池大小等于5,最大达128 sdk3.0后,使用默认的serial线程池,执行完一个线程,再顺序执行下一个线程.sdk4.3时 线程池大小等于5,最大达128 sdk4.4后线程池大小等于 cpu count + 1,最大值为cpu count * 2 + 1 sdk3.0后有两种线程池的实现,默认为 Serial 线程池 public static final Executor SERIAL_EXECUTOR = new SerialExe

Java性能优化指南系列(二):Java 性能分析工具

进行JAVA程序性能分析的时候,我们一般都会使用各种不同的工具.它们大部分都是可视化的,使得我们可以直观地看到应用程序的内部和运行环境到底执行了什么操作,所以性能分析(性能调优)是依赖于工具的.在第2章,我强调了基于数据驱动的性能测试是非常重要的,我们必须测试应用的性能并理解每个指标的含义.性能分析和数据驱动非常类似,为了提升应用程序的性能,我们必须获取应用运行的相关数据.如何获取这些数据并理解它们是本章的主题.[本章重点介绍JDK中提供的性能分析工具] 操作系统工具及其分析 程序分析的起点并不

java内存分析总结

1.自带的jconsole工具. (1)如果是从命令行启动,使 JDK 在 PATH 上,运行 jconsole 即可. (2)如果从 GUI shell 启动,找到 JDK 安装路径,打开 bin 文件夹,双击 jconsole . (3)当分析工具弹出时(取决于正在运行的 Java 版本以及正在运行的 Java 程序数量),可能会出现一个对话框,要求输入一个进程的 URL 来连接, 也可能列出许多不同     的本地 Java 进程(有时包含 JConsole 进程本身)来连接. 参照htt

Java 性能分析工具 , 第 2 部分:Java 内置监控工具

引言 本文为 Java 性能分析工具系列文章第二篇,第一篇:操作系统工具.在本文中将介绍如何使用 Java 内置监控工具更加深入的了解 Java 应用程序和 JVM 本身.在 JDK 中有许多内置的工具,其中包括: jcmd:打印一个 Java 进程的类,线程以及虚拟机信息.适合用在脚本中.使用 jcmd - h 来查看使用方法. jconsole:提供 JVM 活动的图形化展示,包括线程使用,类使用以及垃圾回收(GC)信息. jhat:帮助分析内存堆存储. jmap:提供 JVM 内存使用信息

关于java内存分析的探讨

这些天一直都想找个机会把Java内存方面的知识整理整理,毕竟任何知识都涉及到这方面.Java内存分析是java学习的一大重点. 下面我们进入正式话题讨论: 我们知道Java内存大致分为三块:如下图 我们先大致了解下java各分区的数据存放内容: 栈区:主要为方法服务,存在许许多多的方法栈帧,在方法栈帧里开辟局部变量开辟空间,基本类型按基本数据类型数据大小开辟空间,而引用类型则开辟四个字节大小. 堆区:主要存放创建的对象数据. 方法区:主要存放加载的类,静态变量,静态初始化块,常量,以及程序运行的