Java并发编程的艺术笔记-线程中的锁

1.Lock接口

  • 使用时需要显式地获取和释放锁
  • 缺少了隐式获取释放锁的便捷性,但拥有了锁获取与释放的可操作性、可中断的获取锁以
    及超时获取锁等多种synchronized关键字所不具备的同步特性
  • Lock接口的实现基本都是通过聚合一个同步器的子类来完成线程访问控制的(可以参考队列同步器中的自定义同步组件——TwinsLock部分)

在这里插入图片描述

不要将获取锁的过程写在try块中(如果在获取自定义锁时发生了异常,异常抛出的同时会导致锁无故释放)


2.队列同步器

  • 用来构建锁或者其他同步组件的基础框架

  • 使用int成员变量表示同步状态(如ReentrantLock锁的对应自定义同步器的实现中,同步状态表示锁被一个线程重复获取的次数),通过内置的FIFO队列来完成资源获取线程的排队工作

  • 既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态

  • 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作

  • 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态

  • 同步器提供的模板方法基本上分为3类:

    • 独占式获取与释放同步状态
    • 共享式获取与释放同步状态
    • 查询同步队列中的等待线程情况
2.1 队列同步器的接口与示例
class Mutex implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
	}
    ... 同步器其他方法的实现
    // 仅需要将操作代理到Sync上即可
	private final Sync sync = new Sync();
    // 下面方法是Mutex的方法,但实际调用的是静态内部类Sync的方法
	public boolean isLocked() { return sync.isHeldExclusively(); }
    ...其他调用了Sync方法的方法
}

2.2 队列同步器的实现分析
2.2.1 .同步队列
  • 同步队列中的节点用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点

  • 同步器拥有首节点和尾节点,没有成功获取同步状态的线程将会成为节点加入该队列的尾部

  • 同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点

  • 设置首节点是通过获取同步状态成功的线程来完成,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证

2.2.2 独占式同步状态获取与释放

同步状态获取:

public final void acquire(int arg) {
    // tryAcquire:保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点
    // addWaiter:将Node.EXCLUSIVE节点(该节点保证同一时刻只能有一个线程成功获取同步状态)加入到同步队列的尾部
    // acquireQueued:使得该节点以“死循环”的方式获取同步状态(未成功获取的节点中的线程被阻塞,节点依旧在自旋。阻塞线程需要通过前驱节点(需要是头节点)的出队或被中断来唤醒)
    // 注意:节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    	selfInterrupt();
}

在这里插入图片描述

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
    	Node h = head;
        if (h != null && h.waitStatus != 0)
        	unparkSuccessor(h);  // 唤醒处于等待状态的线程
        return true;
    }
    return false;
}

综上所述:

  • 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋
  • 移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态
  • 在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点
2.2.3 共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态

  • 在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0

  • 在释放同步状态时,和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态线程安全释放(一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程)

2.2.4 独占式超时获取同步状态
  • 调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状
    态(即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false)
  • 独占式超时获取同步状态和独占式获取同步状态主要区别在于未获取到同步状态时的处理逻辑:
    • acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态
    • doAcquireNanos(int arg,long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回

在这里插入图片描述


3.重入锁

  • 重入锁ReentrantLock是支持重进入的锁,该锁支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择

  • synchronized关键字隐式的支持重进入

  • 如果在绝对时间上,先对锁进行获取的请求一定先被满足,则该锁是公平的(FIFO),反之是不公平的

3.1 实现重进入

实现重进入需要解决以下两个问题:

  • 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
  • 锁的最终释放:锁对于获取进行计数自增(计数表示当前锁被重复获取的次数);锁被释放时,计数自减,当计数等于0时表示锁已经成功释放
3.2 公平与非公平获取锁的区别
  • 公平性与否是针对获取锁而言的,如果一个锁是公平的,则锁的获取顺序就应该符合请求的绝对时间顺序(即FIFO)
  • ReentrantLock中获取两种锁的实现不同:
// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // *只要CAS设置同步状态成功,则表示当前线程获取了锁
        if (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);  // 将同步状态值进行增加并返回true
        return true;
    }
    return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // *相较于非公平锁多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁
        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;
}
  • 公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换;非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量

4.读写锁

  • 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞

  • 要实现对一个共享缓存的写操作后的更新对后续的读操作可见:

    • 没有读写锁:写操作开始则所有晚于写操作的读操作进入等待状态。写操作完成进行通知,等待的读操作开始执行(等待通知机制)
    • 使用读写锁:在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行
  • 读写锁的性能都会比排它锁好(大多数场景读是多于写的),它能够提供比排它锁更好的并发性和吞吐量

  • JUC中提供读写锁的实现是ReentrantReadWriteLock,该锁特性如下:

在这里插入图片描述

4.1 读写锁的实现分析
4.1.1 读写状态的设计
  • 读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态(ReentrantLock中自定义同步器的同步状态表示锁被一个线程重复获取的次数)

  • 读写锁将表示同步状态的整型变量按位切分为两个部分:

在这里插入图片描述

4.1.2 写锁的获取与释放
  • 写锁的获取:
    • 如果当前线程已经获取了写锁,则增加写状态;如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态
    • 存在读锁,则写锁不能被获取:如果允许读锁在已被获取的情况下对写锁的获取,则正在运行的其他读线程就无法感知到当前写线程的操作
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    // c!=0表示32位整型变量不为0
    if (c != 0) {
        // w==0表示写状态为0,则读状态r一定不为0(因为c=w+r)
        // 存在读锁或者当前获取线程不是已经获取写锁的线程
        if (w == 0 || current != getExclusiveOwnerThread())
        	return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
        	throw new Error("Maximum lock count exceeded");
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
    	return false;
    }
    setExclusiveOwnerThread(current);
    return true;
}

  • 写锁的释放:每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁
4.1.3 读锁的获取与释放
  • 读锁的获取:
    • 如果当前线程已经获取了读锁,则增加读状态;如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态
    • 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程增加读状态,成功获取读锁
protected final int tryAcquireShared(int unused) {
    for (;;) {
        int c = getState();
        int nextc = c + (1 << 16);  // 增加读状态(需要向左移动16位)
        if (nextc < c)
        	throw new Error("Maximum lock count exceeded");
        if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
        	return -1;
        // CAS保证增加读状态时的线程安全
        if (compareAndSetState(c, nextc))
        	return 1;
    }
}
  • 读锁的释放:读锁的每次释放均减少读状态,减少的值是(1<<16)
4.1.4 锁降级
  • 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程

注意:当前线程有写锁,然后释放,再获取读锁,这不能成为锁降级

  • RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程),目的也是保证数据可见性(如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、、同步等。学习Java并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少竞争、避免死等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值