(三)JDK并发包——锁

synchronized可以用于控制一个线程是否可以访问临界区资源,Object.wait()Object.notify()方法可以实现线程等待和通知。这些工具都很简单可靠,但是想要实现更复杂和高级的功能,就要用到Java中的锁。

1.重入锁(ReentrantLock)

  • lock.lock()简单的上锁
    重入锁完全可以替代synchronized关键字,在Java早期版本中重入锁的性能远远优于synchronized,而从JDK1.6开始,synchronized进行了大量的优化,两者性能不相上下。
public class Main {
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args){
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lock.lock();
                lock.lock();
                try {
                    System.out.println(LocalDateTime.now());
                } finally {
                    lock.unlock();
                    lock.unlock();
                }
            }).start();
        }
    }
}

重入锁如它的名字一样,一个线程可以连续两次获得同一把锁,(否则线程会在第二次请求锁时和自己产生死锁),同样,多次获得锁之后也要多次释放锁,否则其他线程将无法获取锁。

  • lock.lockInterruptibly()响应中断
public class Main {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args){
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            t1.start();
            t2.start();
            t2.interrupt();
    }
    
    static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    lock.lockInterruptibly();
                    System.out.println(LocalDateTime.now());
                } catch (InterruptedException e) {
                    System.out.println("break");
                    break;
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

使用lock.lockInterruptibly()可以使线程在等待锁时响应中断,此时线程会抛出一个InterruptedException异常并放弃锁的竞争。

  • lock.tryLock()超时结束
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException 

无参数的lock.tryLock()方法会在调用后尝试获得锁,如果成功立刻返回true,失败立刻返回false。而有参数的方法可以持续请求一段时间后自动退出请求并返回false,同时有参数的lock.tryLock()同样可以响应中断。

  • new ReentrantLock(true)公平锁
    使用synchronized关键字进行锁控制,产生的锁就是非公平锁,即在分配锁时不会管线程请求锁的时间先后,所有线程都有可能分配到锁。而公平锁则按照请求锁的时间先后分配锁,这保证了不会出现饥饿现象,但公平锁需要维护一个有序的请求队列,因此开销更大,性能较低。
public ReentrantLock(boolean fair)
  • 重入锁的实现
    第一,原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被其他线程持有。
    第二,等待队列。所有没有请求到锁的线程,会进入等待队列中进行等待。待有线程释放锁之后,系统就能从等待队列中唤醒一个线程,继续工作。
    第三,阻塞原语(park)和(unpark),用来挂起和恢复线程。没有得到锁的线程将被挂起。

2.Condition接口

Condition的作用类似于Object.wait()Object.notify(),不过Condition是用于和ReentrantLock合作。它有以下方法。

// await()方法会使当前线程等待,同时释放当前锁,当其他线程使用
// signal()或signalAll()方法时,线程会重新获得锁并继续执行。或者
// 当线程被中断时,也能跳出等待。功能上类似于Object.wait()方法。
void await() throws InterruptedException;
// 与await()方法类似,但是并不响应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 用于唤醒一个在等待中的线程,类似于Object.notify()
void signal();
void signalAll();

类似于Object.wait()Object.notify(),执行前必须用synchronized获得Object对象的锁。Condition对象由锁的newCondition()方法生成,使用await()signal()方法前线程必须获得锁对象,而使用后要释放锁对象。

public Condition newCondition() { return sync.newCondition(); }

3.信号量(Semaphore)

信号量是对锁的拓展,可以允许多个线程同时访问临界区资源。

public Semaphore(int permits)
public Semaphore(int permits, boolean fair)

构造信号量时,必须要指定信号量的准入数,还可以指定是否公平分配信号量。

// 尝试获取信号量,获取时可以响应中断
public void acquire() throws InterruptedException 
// 尝试获取信号量,获取时不响应中断
public void acquireUninterruptibly()
// 尝试获取信号量,立刻返回结果,成功为true,失败为false
public boolean tryAcquire()
// 尝试获取信号量一段时间,可以响应中断
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException 
// 释放占有的信号量
public void release()

信号量的方法与重入锁基本类似,区别只有同时进入临界区的线程数量。

4. 读写锁(ReadWriteLock)

如果使用synchronized关键字或者重入锁,则所有读与读之间、读与写之间、写与写之间都是串行操作,而读写锁允许多个线程同时读,写写和读写之间依然相互排斥。如果系统中的读远大于写,则读写锁可以很好的提升系统性能。

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

使用时,从ReadWriteLock上分别获得读锁和写锁,读写操作时分别请求对应的锁。

5.倒计时器(CountDownLatch)

倒计时器可以让某个线程等待直到计数器归零。


public class Main {
    private static CountDownLatch count = new CountDownLatch(10);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(new Random().nextInt());
                    count.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        try {
            count.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // todo
    }
}

countDownLatch.await()方法同样可以响应中断。

6.循环栅栏(CyclicBarrier)

循环栅栏类似于倒计时器但功能更多一些,首先循环栅栏可以重复触发,另外可以接受一个Runnable对象,作为一次计数完成后系统会触发的动作。

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction) 
public int await() throws InterruptedException, BrokenBarrierException 

在使用时,每有一个线程执行CyclicBarrier.await(),程序计数加一,到达指定数后就会触发barrierActionCyclicBarrier.await()会返回线程到达的名次,最后一个到达的将会返回0。CyclicBarrier.await()可以响应中断,而当线程已经不可能满足程序计数时(比如某个线程被中断),则其余线程会抛出BrokenBarrierException异常。

7.线程阻塞工具类(LockSupport)

LockSupport可以在线程中的任意位置让线程阻塞,与Thread.suspend()方法相比,弥补了由于resume()方法发生导致线程无法继续执行的情况;与Object.wait()方法相比,不需要先获得某个对象的锁,也不会抛出InterruptedException异常。

public static void park(Object blocker) 
public static void unpark(Thread thread) 

即使unpark()方法发生在park()方法之前,它也能使下一次的park()方法立即返回,同时,处于park()方法挂起状态的线程不会像Thread.suspend()方法一样显示Runnable状态,而是明确的Waiting状态,并且还会标注是由park()方法引起的。

8.限流(RateLimiter)

限流算法的思路:

  • 最简单的限流算法就是给出一个单位时间,然后使用一个计数器count统计单位时间内收到的请求数量,当请求数量超过门限时,余下的请求丢弃或等待。
  • 漏桶算法:利用一个缓冲区,无论请求的速率如何,都先进入缓冲区等待,然后以固定的流速离开缓冲区。
  • 令牌桶算法:系统以一定的速率生成令牌并存入令牌桶中,桶中只能存放一定时限内的令牌。当有请求到来时,拿走桶中的一个令牌,如果桶中没有令牌,则等待或丢弃请求。

RateLimiter就是是Google旗下一个库Guava中的一个工具,采用了令牌桶算法来控制流量。可以防止过量的请求创建的线程压垮服务器。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容