concurrency_lock

第二十七章 Java锁(Lock)与信号量(Semaphore)

1 概述

除了关键字synchronized以外,Java还在标准库java.util.concurrent.locks包中包含了锁(Lock)和信号量(Semaphore),为开发人员提供了更加灵活的线程同步方法。

与关键字synchronized的用法相比,java.util.concurrent.locks.Lock有着很多不同之处。

  1. synchronized是关键字,锁monitor的获取(acquire)和释放(release)是由Java编译器自动添加的,开发人员不能改变。而Lock提供了类似的获取和释放的功能,但是使用更加灵活。
  2. 使用了synchronized关键字,锁monitor的获取和释放只能在一个函数内完成。而Lock类并没有这个限制。Lock的获取和释放可以发生在任何地方。
  3. synchronized关键字只提供了自动的获取和释放功能,不能和其他功能相结合使用。而Lock更加灵活,开发人员可以在具体类中添加额外的逻辑:例如考虑公平性(Fairness)等。
  4. 当一个线程无法获得锁monitor时,该线程就会转为等待状态。而Lock类提供了tryLock()方法,该方法允许如果获取锁不成功的话,该线程可以继续执行。

总之,Lock类为开发人员提供了更多的灵活性。但是,总的来说,使用synchronized关键字程序的运行效率比使用Lock类的程序略高。

2 Java锁

2.1 Lock接口

java.concurrent.locks.Lock是一个接口。Lock接口有如下几个成员方法。

方法名称使用方法
void lock();线程尝试获取锁对象。如果锁对象已被占用,该线程会等待锁对象被释放。在等待过程中,该线程不会被调度。
void lockInterruptibly();该方法与lock()方法类似;唯一的区别是当线程在等待时,可以接收中断(Interrupt)。当接收到中断后,会抛出InterruptedException异常。
boolean tryLock();该方法与lock()方法类似;唯一的区别是当成功获取锁对象时,该方法返回true。当锁对象被占用时,该方法立即返回false。当前线程不会处于等待状态。
boolean tryLock(long time, TimeUnit unit);该方法与tryLock()方法类似,唯一的区别是当锁对象被占用时,当前线程会处于等待状态,直到锁对象被释放;或者超时。
void unlock()释放所获得的锁对象。

如下代码是Java标准库文档提供的代码样例。在finally子句中调用unlock()方法是为了保证无论try语句执行成功或者失败,锁对象都能在最后成功释放。

import java.concurrent.locks.Lock;
import java.concurrent.locks.ReentrantLock;

public class LockExample {
    public static void main(String[] args) {
        Lock l = new ReentrantLock();
        l.lock();
        try {
            // 访问被锁对象保护的资源
        } finally {
            l.unlock();
        }
    }
}

2.2 可重入锁ReentrantLock

2.1章介绍的是锁Lock接口,在2.2、2.3章节中,我们将介绍两个常用的锁的实现。本小节先介绍java.util.concurrent.locks.ReentrantLock。

ReentrantLock类是一种可重入的锁,它实现了java.concurrent.locks.Lock接口。可重入锁的意义在于,一个线程可以多次获得同一个锁对象的拥有权,而不被阻塞。这个特性与关键字synchronized是一致的。在某些场景下,可重入锁非常有用。例如,在使用递归算法解决问题时,如果在递归函数中使用了锁,则在递归调用的过程中自己可能被自己锁住,而无法完成递归调用。但是,如果在此时使用可重入锁,则不需要担心这个问题,因为,一个线程可以多次获得同一个锁的拥有权,所以,自己不会把自己锁住。

如下面的代码所示,在方法resetAndIncrement()方法中调用了increment()方法。因为在此调用前,当前线程已获取了锁对象this.lock,如果该锁不是可重入的锁的话,那么,在increment()方法中,当前线程不能再次获得锁对象,因为该锁对象已被自身占用了。但是,在本例中,该锁是可重入的,所以,当前线程能够多次获取同一个锁对象。试想,如果this.lock不是可重入的锁,而且如果需要在resetAndIncrement()方法中调用increment()方法的话,那么,这段代码将会变得非常复杂。

import java.concurrent.locks.Lock;
import java.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private Lock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        this.lock.lock();
        try {
            this.counter++;
        } finally {
            this.lock.unlock();
        }
    }

    public void resetAndIncrement() {
        this.lock.lock();
        try {
            this.counter = 0;
            // 因为这里使用的是可重入的锁,所以在increment()方法中可以成功获取锁对象
            this.increment();
        } finally {
            this.lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        // 使用Lambda表达式创建Runnable对象
        Thread thread1 = new Thread(()-> example.increment());
        Thread thread2 = new Thread(()-> example.resetAndIncrement());

        thread1.start();
        thread2.start();
    }
}

2.2 读写锁ReadWriteLock

java.util.concurrent.locks.ReadWriteLock是一个接口;其内部包含着一个读操作锁(Lock for reads)和一个写操作锁(Lock for writes)。读操作锁允许多个读操作同时执行,而写操作锁只允许在同一时刻,只能有一个写操作执行。读操作和写操作是互斥的,即:读时不能写;写时不能读。ReadWriteLock的一个实现类是ReentrantReadWriteLock类(可重入的读写锁)。

ReadWriteLock接口定义了两个方法。readLock()方法返回一个用于读操作的锁;writeLock()方法返回一个用于写操作的锁。在下面的代码中,ReadWriteLockExample类定义了一个共享的链表sharedList和一个读写锁lock。add()方法和remove()方法在获得写锁后,向共享链表sharedList中添加或删除元素;而contains()方法则在获得读锁后,查询共享链表中是否包含某个元素。当在main()函数中启动四个线程同时运行时,ReentrantReadWriteLock对象会保护共享链表sharedList。

import java.util.List;
import java.util.LinkedList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private List<String> sharedList = new LinkedList<String>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void add (String key) {
        Lock writeLock = this.lock.writeLock();
        try {
            writeLock.lock();
            this.sharedList.add(key);
        } finally {
            writeLock.unlock();
        }
    }

    public void remove() {
        Lock writeLock = this.lock.writeLock();
        try {
            writeLock.lock();
            if (!this.sharedList.isEmpty())
                this.sharedList.remove(0);
        } finally {
            writeLock.unlock();
        }
    }

    public boolean contains(String key) {
        Lock readLock = this.lock.readLock();
        try {
            readLock.lock();
            
            this.sharedList.contains(0);
        } finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        // 使用Lambda表达式创建Runnable对象
        Thread thread1 = new Thread(()-> example.add(10));
        Thread thread2 = new Thread(()-> example.add(10));
        Thread thread3 = new Thread(()-> example.remove());
        Thread thread4 = new Thread(()-> example.contains(10));

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

3 Java信号量(Semaphore)

与锁不同的是,信号量不是互斥锁(Mutual Exclusion)。信号量允许一定数量的线程同时访问共享资源;而在任意时刻,互斥锁只允许一个线程访问共享资源。在java.util.concurrent.Semaphore类内部保存着一组“许可证(permits)”。当线程调用acquire()方法时,会占用一个信号量中的一个许可证。如果许可证都已被占用,则该线程会进入等待状态。当线程调用release()方法时,会释放一个许可证。实际上,在具体的实现中,信号量对象并不需要在内部创建许可证对象,信号量对象只需维护一个计数器即可。

如下面的代码所示,SemaphoreExample模拟了一个用户管理系统。该系统控制同时登陆用户的数量。当登陆用户数多余上限时,随后的登陆请求将会被阻塞。

所以,在SemaphoreExample中定义了一个私有成员变量semaphore,它将在构造函数中初始化,并设置最多可登陆的用户数。在preLogin()登陆预处理方法中,当前线程会尝试获取一个信号量this.semaphore的“许可证”。如果获取成果,当前线程可以继续执行。如果许可证已用完,当前线程则原地等待。在postLogout()退出后处理方法中,每个登陆的用户会退回一个“许可证”,以允许其他等待的用户完成登陆。

public class SemaphoreExample {
    private Semaphore semaphore = null;

    public SemaphoreExample(int limit) {
        this.semaphore = new Semaphore(limit);
    }

    // 登陆操作
    public void login() {
        preLogin();
        // 继续处理登陆过程中的其他逻辑
    }

    // 登陆预处理
    private void preLogin() {
        this.semaphore.acquire(); 
    }

    // 退出操作
    public void logout() {
        // 处理退出过程中的其他逻辑
        postLogout();
    }

    // 退出后处理
    private void postLogout() {
        this.semaphore.release();
    }
}

4 总结

本章节介绍了Java的锁和信号量类。与关键字synchronized相比,锁提供了更加灵活的线程同步机制。锁机制不仅可以在不同方法中调用,还可以通过继承以扩展锁的功能(例如:添加公平性等)。Java信号量则允许一定数量的线程同时访问共享资源。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.