Java 并发 - Lock 显示锁解析

一、前言


前面《Java 并发 - synchronize 解析》我们详谈过解决多线程同步问题的关键字 synchronized,synchronized 属于隐式锁,即锁的持有与释放都是隐式的,我们无需干预,而本篇我们要讲解的是显式锁,即锁的持有和释放都必须由我们手动编写。

二、Lock 接口


2.1 Lock接口简介

锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如 ReadWriteLock 的读写锁。

在 Lock 接口出现之前,Java 程序是靠 synchronized 关键字实现锁功能的。JDK1.5 之后并发包中新增了 Lock 接口以及相关实现类来实现锁功能。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中 synchronized 关键字就不那么容易实现了,使用 Lock 接口容易很多。Lock 是 synchronized 关键字的进阶,掌握 Lock 有助于学习并发包中的源代码,在并发包中大量的类使用了 Lock 接口作为同步的处理方式。

Lock接口的实现类:

  • ReentrantLock 
  • ReentrantReadWriteLock.ReadLock 
  • ReentrantReadWriteLock.WriteLock

2.2 Lock的简单使用

Lock lock = new ReentrantLock();
lock.lock();
try{
    //临界区......
}finally{
    lock.unlock();
}

当前线程使用 lock() 方法与 unlock() 对临界区进行包围,其他线程由于无法持有锁将无法进入临界区直到当前线程释放锁。因为 Lock 是接口所以使用时要结合它的实现类,另外在 finall 语句块中释放锁的目的是保证获取到锁之后,最终能够被释放。注意: 最好不要把获取锁的过程写在 try 语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁无法被释放。

2.3 Lock接口的特性和常见方法

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

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回

Lock 接口基本的方法:

方法名称 描述
void lock() 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
void lockInterruptibly() 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
boolean tryLock() 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。
boolean tryLock(long time, TimeUnit unit) 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.
void unlock() 释放锁。

 

三、Lock接口的实现类:ReentrantLock


3.1 ReentrantLock 使用

ReentrantLock 和 synchronized 关键字一样可以用来实现线程之间的同步互斥,但是在功能是比 synchronized 关键字更强大而且更灵活。

构造方法:

方法名称 描述
ReentrantLock() 创建一个 ReentrantLock的实例。
ReentrantLock(boolean fair) 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例

ReentrantLock 类常见方法(Lock 接口已有方法这里没加上):

方法名称 描述
int getHoldCount() 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
protected Thread getOwner() 返回当前拥有此锁的线程,如果不拥有,则返回 null
protected Collection getQueuedThreads() 返回包含可能正在等待获取此锁的线程的集合
int getQueueLength() 返回等待获取此锁的线程数的估计。
protected Collection getWaitingThreads(Condition condition) 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。
int getWaitQueueLength(Condition condition) 返回与此锁相关联的给定条件等待的线程数的估计。
boolean hasQueuedThread(Thread thread) 查询给定线程是否等待获取此锁。
boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁。
boolean hasWaiters(Condition condition) 查询任何线程是否等待与此锁相关联的给定条件
boolean isFair() 如果此锁的公平设置为true,则返回 true 。
boolean isHeldByCurrentThread() 查询此锁是否由当前线程持有。
boolean isLocked() 查询此锁是否由任何线程持有。

 ReentrantLock 简单使用:

public class ReentrantLockTest {
    public static void main(String[] args) {
        MyService service = new MyService();
        MyThread a1 = new MyThread(service);
        MyThread a2 = new MyThread(service);
        MyThread a3 = new MyThread(service);
        MyThread a4 = new MyThread(service);
        MyThread a5 = new MyThread(service);
        a1.start();
        a2.start();
        a3.start();
        a4.start();
        a5.start();
    }

    static public class MyService {
        private Lock lock = new ReentrantLock();
        public void testMethod() {
            lock.lock();
            try {
                for (int i = 0; i < 5; i++) {
                    System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
                }
            } finally {
                lock.unlock();
            }
        }
    }

    static public class MyThread extends Thread {
        private MyService service;
        public MyThread(MyService service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            service.testMethod();
        }
    }
}

运行结果:

从运行结果可以看出,当一个线程运行完毕后才把锁释放,其他线程才能执行,其他线程的执行顺序是不确定的。

3.2 Condition 接口简介

我们通过之前的学习知道了:synchronized 关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用 notify/notifyAll() 方法进行通知时,被通知的线程是有 JVM 选择的,使用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”,这个功能非常重要,而且是 Condition 接口默认提供的。

而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll() 方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll() 方法只会唤醒注册在该 Condition 实例中的所有等待线程。

Condition 接口的常见方法:

方法名称 描述
void await() 相当于Object类的wait方法
boolean await(long time, TimeUnit unit) 相当于Object类的wait(long timeout)方法
signal() 相当于Object类的notify方法
signalAll() 相当于Object类的notifyAll方法

3.3 使用 Condition 实现等待/通知机制

public class UseSingleConditionWaitNotify {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        ThreadA a = new ThreadA(service);
        a.start();
        Thread.sleep(3000);
        service.signal();
	}

    static public class MyService {
        private Lock lock = new ReentrantLock();
        public Condition condition = lock.newCondition();
		
        public void await() {
            lock.lock();
            try {
                System.out.println(" await时间为" + System.currentTimeMillis());
                condition.await();
                System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        public void signal() throws InterruptedException {
            lock.lock();
            try {				
                System.out.println("signal时间为" + System.currentTimeMillis());
                condition.signal();
                Thread.sleep(3000);
                System.out.println("这是condition.signal()方法之后的语句");
            } finally {
                lock.unlock();
            }
        }
    }

    static public class ThreadA extends Thread {
        private MyService service;
        public ThreadA(MyService service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            service.await();
        }
    }
}

运行结果:

在使用 wait/notify 实现等待通知机制的时候我们知道必须执行完 notify() 方法所在的 synchronized 代码块后才释放锁。在这里也差不多,必须执行完 signal 所在的 try 语句块之后才释放锁,condition.await() 后的语句才能被执行。注意: 必须在 condition.await() 方法调用之前调用 lock.lock() 代码获得同步监视器,不然会报错。

3.4 公平锁与非公平锁

Lock 锁分为:公平锁 和 非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的 FIFO 先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。

public class FairorNofairLock {
    public static void main(String[] args) throws InterruptedException {
        final Service service = new Service(true);//true为公平锁,false为非公平锁

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("★线程" + Thread.currentThread().getName() + "运行了");
                service.serviceMethod();
            }
        };

        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }

    }

    static public class Service {
        private ReentrantLock lock;

        public Service(boolean isFair) {
            super();
            lock = new ReentrantLock(isFair);
        }

        public void serviceMethod() {
            lock.lock();
            try {
                System.out.println("ThreadName=" + Thread.currentThread().getName()
                        + "获得锁定");
            } finally {
                lock.unlock();
            }
        }

    }
}

运行结果:

可以看到公平锁的运行结果是有序的。现在把Service的参数修改为false则为非公平锁:

final Service service = new Service(false);//true为公平锁,false为非公平锁

可以看到非公平锁的运行结果是无序的。

四、ReadWriteLock 接口的实现类:ReentrantReadWriteLock


4.1 简介

我们刚刚接触到的 ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock 接口的实现类 ReentrantReadWriteLock 读写锁就是为了解决这个问题。读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的)。在没有线程 Thread 进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread 可以同时进行读取操作,但是同一时刻只允许一个 Thread 进行写入操作。

4.2 ReentrantReadWriteLock的特性与常见方法

ReentrantReadWriteLock的特性:

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量上来看还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁也能够同时获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级称为读锁

ReentrantReadWriteLock常见方法:

 构造方法:

方法名称 描述
ReentrantReadWriteLock() 创建一个 ReentrantReadWriteLock()的实例
ReentrantReadWriteLock(boolean fair) 创建一个特定锁类型(公平锁/非公平锁)的ReentrantReadWriteLock的实例

常见方法: 和 ReentrantLock 类 类似这里就不列举了。

4.3 ReentrantReadWriteLock的使用

4.3.1 读读共享

两个线程同时运行 read 方法,你会发现两个线程可以同时或者说是几乎同时运行 lock() 方法后面的代码,输出的两句话显示的时间一样。这样提高了程序的运行效率。

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName()
                        + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

4.3.2 写写互斥

把上面的代码的

lock.readLock().lock();

改为:

lock.writeLock().lock();

两个线程同时运行read方法,你会发现同一时间只允许一个线程执行 lock() 方法后面的代码。

4.3.3 读写互斥

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName()
                        + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获得写锁" + Thread.currentThread().getName()
                        + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

测试代码:

		Service service = new Service();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		Thread.sleep(1000);

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

运行两个使用同一个 Service 对象实例的线程 a,b,线程 a 执行上面的 read 方法,线程 b 执行上面的 write 方法。你会发现同一时间只允许一个线程执行 lock() 方法后面的代码。记住:只要出现写操作的过程就是互斥的。

4.3.4 写读互斥

和读写互斥类似,这里不用代码演示了。记住:只要出现写操作的过程就是互斥的。

 

五、关于 synchronized 与 ReentrantLock


在JDK 1.6之后,虚拟机对于 synchronized 关键字进行整体优化后,在性能上 synchronized 与 ReentrantLock 已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是 synchronized 关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而 ReentrantLock 提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized 的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择 ReentrantLock。

发布了66 篇原创文章 · 获赞 2 · 访问量 8023

猜你喜欢

转载自blog.csdn.net/u010289802/article/details/104234201