Java-Lock之自我学习

Lock 的实现

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意
味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现。
ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是
线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入
次数
ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个
类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock
接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则
是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的
操作都会存在互斥。
StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写
锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全
并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。
stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程

ReentrantLock 重入锁

重入锁的目的

重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方
法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数
就行了。synchronized 和 ReentrantLock 都是可重入锁。
比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用
demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得
demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死
锁。

public class ReentrantDemo{
public synchronized void demo(){
System.out.println("begin:demo");
demo2();
}
public void demo2(){
System.out.println("begin:demo1");
synchronized (this){
} }
public static void main(String[] args) {
ReentrantDemo rd=new ReentrantDemo();
new Thread(rd::demo).start();
} }
ReentrantReadWriteLock

以前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.

public class ReentranLockReadWriteLockDemo {

    private static HashMap<String,String> hashMap = new HashMap(); // 假设要操作的内存
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock read = rwl.readLock();  // 读锁
    static Lock write = rwl.writeLock(); // 写锁

    public String getKey(String key ){
        read.lock();
        System.out.println("读锁: 开始读数据操作");
        try {
            return hashMap.get(key);
        }finally {
            read.unlock();
        }
    }

    public void put(String key ,String value){
        write.lock();
        System.out.println("写锁: 开始写数据操作");
        try {
            hashMap.put(key,value);
        }finally {
            write.unlock();
        }
    }

    public static void main(String[] args) {

    }

}

通过 hashmap 来模拟了一个内存缓存,然后使用读写锁来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。
在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性

  • 读锁与读锁可以共享
  • 读锁与写锁不可以共享(排他)
  • 写锁与写锁不可以共享(排他)
ReentrantLock 实现原理

锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全的目的。 在synchronize中,分析了偏向锁、轻量级锁,乐观锁。基于乐观锁以及自旋锁来优化了synchronize的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
在ReentrantLock中,在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的??

AQS是啥?

全称是AbstractQueuedSynchronizer。它是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那J.U.C中绝大部分的工具基本都能掌握。
AQS的两种功能
分别为: 独占 和 共享
独占锁:每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现互斥锁。
共享锁: 允许多个线程同时获取锁,并发访问共享资源。ReentrantReadWriteLock就运用了共享锁。
AQS的内部实现
AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个Node数据结构都有两个指针,分别指向后续节点和前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后续。
每个Node其实是由线程封装,当线程争抢失败后会封装成Node加入到AQS队列中去;当获取锁的线程释放锁以后,会从AQS对列中唤醒一个阻塞的节点(就是线程封装的)。
在这里插入图片描述
Node 源码:

static final class Node {
      
        static final Node SHARED = new Node();      
        static final Node EXCLUSIVE = null;       
        static final int CANCELLED =  1;        
        static final int SIGNAL    = -1;     
        static final int CONDITION = -2;       
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;       
        volatile Node next;        
        volatile Thread thread;   
        Node nextWaiter;
        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
     
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

释放锁以及添加线程对于AQS队列的变化
当出现锁竞争以及释放锁的时候,AQS同步队列中的节点会发生变化。
在这里插入图片描述
这里添加节点会设计两个变化:

  1. 新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
  2. 通过CAS将tail重新指向新的尾部节点

head节点表示获取锁成功的节点,当头节点在释放同步状态时,会唤醒后续节点,如果后续节点获得锁成功,会把自己设置为头节点,节点的变化过程如下:
在这里插入图片描述
这个释放节点也会有两个变化

  1. 修改head节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将prev的指针指向null
    设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所有不需要CAS保证,只需要把head节点设置为原节点的后续节点,并且断开原head节点的next引用即可。
公平锁 和 非公平锁的区别

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
非公平锁不一样,差异点有两个:
FairSync.tryAcquire

final void lock(){
	acquice(1);
}

非公平锁在获取锁的时候,会先通过CAS进行抢占,而公平锁不会。
FairSync.tryAcquire

protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
	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;
}

这个方法与nofairTryAcquire(int acquires)比较,不同的地方在于判断条件多了hasQueuedPredecessors()方法,也就加入了同步对列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

Condition

Condition 是一个多线程协调通讯工具,可以让某个线程一起等待某个条件(condition),只要满足条件时,线程才会被唤醒。

public class ConditionWaitTest implements Runnable{

    private Lock lock;
    private Condition condition;

    public ConditionWaitTest(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }


    @Override
    public void run() {
        System.out.println("begin - condition wait ");
        try {
            lock.lock();
            condition.await();
            System.out.println("end - condition wait ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }
}
public class ConditionSignalTest implements Runnable {
    private Lock lock;
    private Condition condition;

    public ConditionSignalTest(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println("begin - condition signal");
        try {
            lock.lock();
            condition.signal();
            System.out.println("end - condition signal");
        }catch (Exception e){
            e.printStackTrace();
        }
        lock.unlock();
    }
}

通过这个案例实现了wait和notify的功能,当调用await方法后,当前线程会释放锁并等待,而其他线程调用condition对象的signal或者signalall方法通知并被阻塞的线程,然后自己执行unlock释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。
所以,condition中两个最重要的方法,一个是await,一个是signal方法

阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队
列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入
正常锁的获取流程。signalall()释放全部的,雷同notifyall方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值