Java的Lock接口与读写锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fuzhongmin05/article/details/81052715

一、Lock接口与synchronized关键字

       锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性

      使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显式的锁获取和释放来的好。

synchronized关键字在使用的过程中会有如下几个问题:
1. 不可控性,无法做到随心的加锁和释放锁;
2. 效率比较低下,比如我们现在并发的读两个文件,读与读之间是互不影响的,但如果给这个读的对象使用synchronized来实现同步的话,那么只要有一个线程进入了,那么其他的线程都要等待;
3. 无法知道线程是否获取到了锁;

Lock是一个上层的接口,其原型如下,总共提供了6个方法:

public interface Lock {
    // 用来获取锁,如果锁已经被其他线程获取,则一直等待,直到获取到锁
    void lock();
    // 该方法获取锁时,可以响应中断,比如现在有两个线程,一个已经获取到了锁,另一个线程调用这个方法正在等待锁
    //但是此刻又不想让这个线程一直在这死等,可以通过调用线程的Thread.interrupted()方法,来中断线程的等待过程
    void lockInterruptibly() throws InterruptedException;
    // tryLock方法会返回bool值,该方法会尝试着获取锁,如果获取到锁,就返回true,如果没有获取到锁,就返回false,
    //但是该方法会立刻返回,而不会一直等待
    boolean tryLock();
    // 这个方法和上面的tryLock差不多是一样的,只是会尝试指定的时间,如果在指定的时间内拿到了锁,则会返回true,
    //如果在指定的时间内没有拿到锁,则会返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    // 实现线程通信,相当于wait和notify,后面会单独讲解
    Condition newCondition();
}

使用Lock是需要手动释放锁的,但是如果程序中抛出了异常,那么就无法做到释放锁,有可能引起死锁,所以我们在使用Lock的时候,有一种固定的格式,如下:

Lock l = ...;
l.lock();
try {
 // access the resource protected by this lock
} finally {// 必须使用try,最后在finally里面释放锁
 l.unlock();
}

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放

Lock接口提供的synchronized关键字所不具备的主要特性有:

  1. 尝试非阻塞地获取锁,当前线程获取锁时,如果锁没有被其他线程获取到,则成功获取并持有锁;
  2. 被中断地获取锁,与syncronized不同,获取到锁的线程能响应中断,当获取到锁的线程被中断时,会抛出中断异常,并释放锁;
  3. 超时获取锁,在指定的截止时间前获取锁,如果时间到了仍未获取到锁,则返回;

以多线程读取文件来示例:

public class LockDemo {
    // new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类
    Lock lock = new ReentrantLock();
    public void readFile(String fileMessage){
        lock.lock();// 上锁
        try {
            System.out.println(Thread.currentThread().getName()+"得到了锁,正在读取文件……");
            for (int i = 0; i< fileMessage.length(); i++){
                System.out.print(fileMessage.charAt(i));
            }
            System.out.println();
            System.out.println("文件读取完毕!");
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放了锁!");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockDemo demo = new LockDemo();
        String fileName = "H:/Java_Workspace_Console/test.txt";
        // 创建若干个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.readFile(fileName);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.readFile(fileName);
            }
        }).start();
    }

}

如果先把锁的那两行代码注释掉,看下效果如何:
这里写图片描述
多个线程读取到的内容错乱。
然后我们把锁的代码加上,看下效果如何:
这里写图片描述
如果我们把上面的readFile方法前面加上synchronized关键字,然后把锁去掉,效果是一样的。
tryLock方法的使用和Lock方法的使用类似,不做过多的说明了,代码如下:

public class TryLockDemo {
    // new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类
    Lock lock = new ReentrantLock();
    public void readFile(String fileMessage){
        // 上锁
        if (lock.tryLock()) {
            try {
                System.out.println(Thread.currentThread().getName()+"得到了锁,正在读取文件……");
                for (int i = 0; i< fileMessage.length(); i++){
                    System.out.print(fileMessage.charAt(i));
                }
                System.out.println();
                System.out.println("文件读取完毕!");
            } finally {
                System.out.println(Thread.currentThread().getName()+"释放了锁!");
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TryLockDemo demo = new TryLockDemo();
        String fileName = "H:/Java_Workspace_Console/test.txt";
        // 创建若干个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.readFile(fileName);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.readFile(fileName);
            }
        }).start();
    }
}

二、读写锁

      ReentrantLock是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

      除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

扫描二维码关注公众号,回复: 4791106 查看本文章

      一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的接口是ReadWriteLock:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

该接口也有一个实现类ReentrantReadWriteLock,下面我们先看一下,多线程同时读取文件时,用synchronized实现的效果,代码如下:

public class SyncReadDemo {
    public synchronized void get(Thread thread) {
        System.out.println("start time:"+System.currentTimeMillis());
        for(int i=0; i<5; i++){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在进行读操作……");
        }
        System.out.println(thread.getName() + ":读操作完毕!");
        System.out.println("end time:"+System.currentTimeMillis());
    }

    public static void main(String[] args) {
        final SyncReadDemo lock = new SyncReadDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}

测试结果如下:
这里写图片描述
整个过程耗时200ms

在加了synchronized关键字之后,读与读之间,也是互斥的,也就是说,必须等待Thread-0读完之后,才会轮到Thread-1线程读,而无法做到同时读文件,这种情况在大量线程同时都需要读文件的时候,效率低下。
下面我们来测试ReadAndWriteLock的性能,代码如下:

public class ReadAndWriteLockDemo {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void get(Thread thread) {
        lock.readLock().lock();
        try{
            System.out.println("start time:"+System.currentTimeMillis());
            for(int i=0; i<5; i++){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
            System.out.println("end time:"+System.currentTimeMillis());
        }finally{
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        final ReadAndWriteLockDemo lock = new ReadAndWriteLockDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}
结果如下: ![这里写图片描述](https://img-blog.csdn.net/20180715140733562?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Z1emhvbmdtaW4wNQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 整个过程耗时:100ms Thread-0和Thread-1是在同时读取文件。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。读锁和写锁是互斥的。

可重入锁

      如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一 个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

可中断锁

  可中断锁:顾名思义,就是可以相应中断的锁。
  在Java中,synchronized就不是可中断锁,而Lock是可中断锁
  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁
  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以在构造函数中指定其为公平锁
  公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?如果把每次不同线程获取到锁定义为1次切换,公平性锁为了保证锁的获取按照FIFO原则,代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但会有更少的线程切换,保证了其更大的吞吐量



参考资料:《Java并发编程的艺术》第5章

猜你喜欢

转载自blog.csdn.net/fuzhongmin05/article/details/81052715