11-1. Java中的重入锁、读写锁、自定义锁实现

前言:上一节讲述了锁的原理,这节先讲解锁的应用,再通过上节的原理来实现一个自定义的锁。

1 从锁开始讲起

1.1 lock

在java.util.concurrent.locks.Lock.java中的源码解释:

根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。

lock的常用API
lock.lock(); // 如果一个线程拿到锁,其他线程会等待
lock.tryLock(); // 尝试获取锁,获取不到立即返回
lock.tryLock(1000L); // 尝试获取锁1秒,获取不到也立即返回

1.1.1 可重入锁ReentrantLock

package szu.vander.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author : Vander
 * @date :   2019/12/7
 * @description : 可重入锁
 */
public class ReentrantDemo {

    private final static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args){
        reentrantLock.lock();
        try {
            System.out.println("第1次获取锁");
            System.out.println("this thread lock hold count : " + reentrantLock.getHoldCount());
            reentrantLock.lock();
            System.out.println("第2次获取锁");
            System.out.println("this thread lock hold count : " + reentrantLock.getHoldCount());
        } finally {
            reentrantLock.unlock();
            reentrantLock.unlock();
        }
        System.out.println("this thread lock hold count : " + reentrantLock.getHoldCount());

        new Thread(() -> {
            System.out.println(Thread.currentThread() + ": expect to get the lock!");
            reentrantLock.lock();
            System.out.println(Thread.currentThread() + ": had get the lock!");
        }).start();

    }

}

运行结果

这里说明一下ReentrantLock有公平和非公平之分。(公平锁就是哪个线程等得久就让哪个线程先执行)

1.1.2 可响应中断的ReentrantLock

package concurrent.lock;

import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author : Vander
 * @date :   2019/12/8
 * @description : 获取锁后可被中断
 */
public class LockInterruptiblyDemo {

    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
        Runnable runnable = () -> {
            try {
                lockInterruptiblyDemo.getLockAndInterrupt();
            } catch (InterruptedException e) {
                Instant now = Instant.now();
                System.out.println(String.format("%s - %s : in the main runnable func, being interrupt, %s"
                        , now
                        , Thread.currentThread()
                        , e.toString()));
            }
        };
        Thread thread0 = new Thread(runnable);
        Thread thread1 = new Thread(runnable);

        try {
            thread0.start();
            Thread.sleep(500); // 等待0.5秒,让thread0先执行

            thread1.start();
            Thread.sleep(2000); // 两秒后,中断thread1

            thread1.interrupt(); // 处于Sleep状态或处于lockInterruptibly状态能被中断
        } catch (InterruptedException e) {
            Instant now = Instant.now();
            System.out.println(String.format("%s - %s : in the main func, being interrupt, %s"
                    , now
                    , Thread.currentThread()
                    , e.toString()));
        }

    }

    public void getLockAndInterrupt() throws InterruptedException {
        Instant now = Instant.now();
        System.out.println(String.format("%s - %s : expect the this lock", now, Thread.currentThread()));
        lock.lockInterruptibly();// 阻塞,能立即响应中断,lock.lock(),阻塞且不立即响应中断
        try {
            now = Instant.now();
            System.out.println(String.format("%s - %s : get the this lock" +
                    ", and start sleep 10s", now, Thread.currentThread()));
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            now = Instant.now();
            System.out.println(String.format("%s - %s : in the getLockAndInterrupt func, being interrupt, %s"
                    , now
                    , Thread.currentThread()
                    , e.toString()));
        } finally {
            now = Instant.now();
            System.out.println(String.format("%s - %s : run into finally", now, Thread.currentThread()));
            lock.unlock();
            System.out.println(String.format("%s - %s : release the this lock", now, Thread.currentThread()));
        }

    }

}

运行结果:线程0获得了锁,并处于Sleep状态,线程1进入阻塞并且能响应中断的状态,将线程1中断,线程1可以响应中断。

2)改为lock.lock()

运行结果:改成lock()之后,线程1就不立即响应中断了,它会一直等锁,然后进入Sleep的时候才发现中断状态为已经被改变了,才去响应中断

1.1.3 读写锁(ReadWriteLock)

为了提高读操作比写操作多的场景的性能,设计出了读写锁的思路。当某个线程进行写操作时,所以进行读操作的线程都不能来读,因为这样有可能会读取到脏数据;另一种情况是,如果某个线程仅仅是来读数据的,总不能让其它读数据的线程不能来读吧,所以这种情况下的读锁是可以共享的,属于共享锁。而第一种情况的写数据时,锁是互斥的,属于互斥锁,也称为独享锁

设计思路:维护一对关联锁,一个用于只读操作,一个用于写入操作;
读锁可以由多个读线程同时持有,而写锁是排他的,互斥的。

示例场景
缓存组件、集合的并发线程安全性改造。

锁降级(解决读取两次数据的数据不一致问题)
写锁是线程独占,读锁是共享,所以写->读是降级。(读->写,是不能实现的,因为持有读锁的情况下,是不能再持有写锁的)
在读写锁中还会提到的概念是锁降级指的是写锁降级成为读锁。持有当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author : Vander
 * @date :   2019/12/8
 * @description : 这里既是读写锁的一种常见应用,也是锁降级的常用之处
 */
public class CacheDataDemo {

    /**
     * hashMap不是线程安全的
     */
    private Map cache = new HashMap<>();

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public T get(String key) {
        // 获取读锁,保证读期间,数据没有被修改
        readWriteLock.readLock().lock();
        if(cache.get(key) == null) {
            // 从数据库中读取,并写入缓存
            try {
                T value = getDataFromDB(key);
                // 先释放读锁
                readWriteLock.readLock().unlock();
                // 获取写锁
                readWriteLock.writeLock().lock();
                // 再次判断是否对应的Key的缓存为空,这是为了防止获取写锁的过程中,已经有其它线程写入了缓存
                // 往缓存中写数据
                if(cache.get(key) == null) {
                    cache.put(key, value);
                }
                // 锁降级:持有写锁过程中,再获取读锁,再释放写锁:写锁->读锁
                readWriteLock.readLock().lock();
            } finally {
                // 进行后续操作,可能需要继续进行读取数据的操作
                readWriteLock.writeLock().unlock();
            }

        }
        T value = cache.get(key);
        readWriteLock.readLock().unlock();
        return value;
    }

    private T getDataFromDB(String key) {
        // 模拟从数据库中读取数据
        return (T)new Object();
    }

}

2.1 实现一个自定义的锁

2.1.1 从原理开始讲解

上一节讲解了synchronized同步关键字,从偏向锁->轻量级锁->重量级锁,自己实现的Lock,先不实现这么复杂的东西,从简单的思路出发,我们直接来实现一个简单的重量级锁,重量级锁中有两个重要的属性:锁池(等待池,即多个线程争抢这个锁,记录下这些线程争抢这个锁的Pool)、锁的拥有者,这里我们先实现一个不可重入的锁,就先不记录锁的状态了。

基本思路:


package szu.vander.lock;

import szu.vander.log.LogLevel;
import szu.vander.log.Logger;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;

/**
 * @author : Vander
 * @date :   2019/12/8
 * @description : 自定义Lock实现
 */
public class UserDefinedLock implements Lock {

    private final static Logger log = new Logger(false, LogLevel.INFO.getLevel());

    /**
     * 存放争抢锁的线程信息
     */
    private volatile LinkedBlockingQueue waitLockPool = new LinkedBlockingQueue<>();
    /**
     * 当前锁的拥有者,存放线程信息
     */
    private volatile AtomicReference owner = new AtomicReference<>();

    @Override
    public boolean tryLock() {
        if (owner.get() == null) {
            return setOwner(Thread.currentThread());
        }
        return false;
    }

    @Override
    public void lock() {
        // 锁池中的每个线程都要来尝试争抢锁
        while (!tryLock()) {
            // 将等待锁的线程放入等待池中
            waitLockPool.add(Thread.currentThread());
            // 对于没有抢到锁的线程全都阻塞
            log.info("begin to park");
            LockSupport.park(Thread.currentThread());
        }
        waitLockPool.remove(Thread.currentThread());
    }

    @Override
    public void unlock() {
        // 当前线程执行了解锁
        if (owner.compareAndSet(Thread.currentThread(), null)) {
            // 通知等待池中的所有线程去争抢锁
            for (Thread thread: waitLockPool) {
                // 此处如果使用waitLockPool.poll去遍历,速度将会非常慢
                LockSupport.unpark(thread);
            }
        }
    }

    /**
     * 使用CAS机制来修改owner的值
     *
     * @param owner
     */
    private boolean setOwner(Thread owner) {
        return this.owner.compareAndSet(null, owner);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }

    public static void main(String[] args) {
        Lock lock = new UserDefinedLock();
        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                lock.lock();
                log.info("get the this lock, and start sleep");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("release the this lock");
                lock.unlock();
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(runnable1).start();
        }

    }

}

测试代码:

package szu.vander.test.lock;

import szu.vander.lock.UserDefinedLock;

import java.util.concurrent.TimeUnit;

/**
 * @author : Vander
 * @date :   2019/12/8
 * @description :
 */
public class UserDefinedLockTest {

    private static int sum = 0;

    private static UserDefinedLock lock = new UserDefinedLock();

    private static int add() {
        lock.lock();
        sum++;
        lock.unlock();
        return sum;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    add();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println("Result : " + sum);
    }


}

运行结果:使用10个线程同时进行累加,不会出现加不满10w的情况,说明自定义锁也不会存在并发问题

你可能感兴趣的:(11-1. Java中的重入锁、读写锁、自定义锁实现)