轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

      • Java并发-线程基础与synchronized关键字
      • Java并发-重入锁ReetrantLock的使用
        • 一、ReetrantLock简单示例
        • 二、ReetrantLock的中断响应
        • 三、锁申请等待限时
        • 四、公平锁
        • 五、重入锁的好搭档:Condition
      • Java并发-信号量Semaphore
      • Java并发-读写锁ReadWriteLock
      • Java并发-倒计时器CountDownLatch
      • Java并发-栅栏CyclicBarrier
      • Java并发-LockSupport线程阻塞工具类
      • Java并发-线程池ThreadPoolExecutor
      • Java并发-阻塞队列BlockingQueue
      • Java并发-以空间换时间之ThreadLocal
      • Java并发-无锁策略CAS与atomic包
      • Java并发-JDK并发容器
      • Java并发-异步调用结果之Future和CompletableFuture
      • Java并发-Fork Join框架
      • Java并发-调试与诊断
    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 并发
轩辕李
2018-06-02
目录

Java并发-重入锁ReetrantLock的使用

我们开始介绍JDK中的一些好用的并发控制工具。
先来看ReetrantLock类,他可用来替换synchronized关键字,而且比synchronized关键字更为强大和灵活。

# 一、ReetrantLock简单示例

先看代码:

public class ReeterLock implements Runnable {
	static ReentrantLock lock = new ReentrantLock();
	static int i = 0;

	@Override
	public void run() {
		for (int k = 0; k < 1000000; k++) {
			lock.lock();
			try {
				i++;
			} finally {
				lock.unlock();
			}
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReeterLock reeterLock = new ReeterLock();
		Thread t1 = new Thread(reeterLock);
		Thread t2 = new Thread(reeterLock);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}

}

可以看到重入锁有lock()和unlock()方法,使用上比synchronized关键字要灵活很多。
还记得上章节中关于区域控制权的例子么?这里lock就是获得控制权,unlock就是交出控制权,这样的话江湖有了规矩,大家都好办事儿。
这里说一下性能问题,在Java5的早期版本,ReentrantLock的性能会比synchronized强很多;在Java6之后,synchronized关键字获得了优化,性能基本和ReentrantLock相同。

# 二、ReetrantLock的中断响应

第一章中讲到线程中断机制对于同步阻塞的情况并不能做到收放自如,同步阻塞不会收到异常。
对应上一节的例子来看,如果lock.lock();这一行出现了同步等待,即使调用了线程的interrupt()的方法,lock.lock();也收不到异常。为了解决这个问题,ReentrantLock提供了lockInterruptibly()方法。
使用这个方法,就可以响应中断了。

# 三、锁申请等待限时

如果你排队买肯德基,时间超过10分钟,也许你就失去耐心、悻悻而归了。
有时候获得锁也一样,要获得一个锁,如果等待时间过长,那么也可以选择放弃。

public class TimeLock implements Runnable {
	static ReentrantLock lock = new ReentrantLock();

	@Override
	public void run() {
		try {
			if (lock.tryLock(1, TimeUnit.SECONDS)) {
				Thread.sleep(1111);
			} else {
				System.out.println("try lock failed");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			if (lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		TimeLock reeterLock = new TimeLock();
		Thread t1 = new Thread(reeterLock);
		Thread t2 = new Thread(reeterLock);
		t1.start();
		t2.start();
	}

}

t2线程要去获得锁,他的耐心只有1秒钟,如果排队了1秒还获取不到,那么就失败,可以去先做别的事儿。
tryLock()也可以无参,意思就是试着获得锁,如果得不到就返回false。复杂情况下使用tryLock()而不是lock()可以有效避免死锁。

# 四、公平锁

在操作系统中,关于进程该如何竞争CPU的使用时间是个经典问题。其中有一种算法叫抢占式调度算法,他会让进程去抢占CPU,然后占用CPU一段固定时间,当占用时段结束,则该进程被挂起,让其他进程有可运行的时间。有点类似一人一口苹果的感觉。
那么在Java并发中,如果多个线程差之毫厘的去请求锁,默认的策略是什么呢?默认的策略会是系统随机挑选一个来线程来获得锁。
如果现在你想要保证公平性,也就是说规定先到先得,该怎么办?幸好ReentrantLock提供了这种机制,叫做公平锁的机制。
只需要ReentrantLock的构造函数入参是true即可:

public class FairLock implements Runnable {
	static ReentrantLock lock = new ReentrantLock(true);

	@Override
	public void run() {
		while (true) {
			try {
				lock.lock();
				System.out.println(Thread.currentThread().getName() + " 获得锁!");
			} finally {
				lock.unlock();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		FairLock reeterLock = new FairLock();
		Thread t1 = new Thread(reeterLock, "t");
		Thread t2 = new Thread(reeterLock, "---t");
		t1.start();
		t2.start();
	}

}

默认的策略非常高效,但非公平;公平锁的话比较公平,效率稍有降低。

# 五、重入锁的好搭档:Condition

Condition,是条件的意思,可以看做是ReentrantLock的一种扩展,利用好Condition对象,我们可以让线程在合适的时间进行等待,或者在某一个特定的时刻得到通知,取消等待,继续执行。
Condition中的主要方法有:

  • await() 使当前线程等待,同时释放当前锁。当其他线程中使用signal()或signalAll()方法时,线程会重新获得锁并继续执行
  • awaitUninterruptibly() 与await()方法相同,但不会在等待过程中响应中断
  • singal() 用于唤醒一个在等待中的线程

例如警匪片中,警察前往歹徒毒品交易的地点,卧底发来消息:交易时间变更,请等待我的通知(调用了await()方法)。警察原地待命,30分钟后卧底发来通知:出发(调用了singal()方法),警察继续行动。
Condition和ReentrantLock是配合使用的:

public class ReentrantLockCondition implements Runnable {
	static ReentrantLock lock = new ReentrantLock(true);
	static Condition condition = lock.newCondition();

	@Override
	public void run() {
		try {
			lock.lock();
			condition.await();
			System.out.println("going on");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ReentrantLockCondition reeterLock = new ReentrantLockCondition();
		Thread t1 = new Thread(reeterLock);
		t1.start();
		Thread.sleep(2000);
		lock.lock();
		condition.signal();
		lock.unlock();
	}

}

这里要注意一点,condition.signal()调用的时候一定要保证当前线程重新获得锁(也就是最好先调用lock.lock()方法),否则会报IllegalMonitorStateException异常。


JDK中的并发容器ArrayBlockingQueue中就使用到ReentrantLock和Condition的配方,ArrayBlockingQueue在后面我们会讲到。如果你现在就非常感兴趣的话,可以看下ArrayBlockingQueue的源码:

/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

/**
 * Inserts element at current put position, advances, and signals.
 * Call only when holding lock.
 */
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}

先在构造函数中初始化了lock和两个condition,这里我们先拿notEmpty condition来看一下用法。
ArrayBlockingQueue是一个阻塞队列,他的工作方式是:取出元素的时候,如果队列中没有元素,则线程进行等待,直到队列中有元素,才从队列中取值。
我们看到take()方法中,如果队列元素数为0,则notEmpty condition进行等待。直到put进来一个元素,调用enqueue()方法后,会调用notEmpty.signal()方法,这时take()方法才能解除阻塞、继续执行。
这可真是ReentrantLock和Condition的经典配合。

扩展阅读:锁框架核心--AbstractQueuedSynchronizer (opens new window)

编辑 (opens new window)
#ReetrantLock
上次更新: 2023/06/14
Java并发-线程基础与synchronized关键字
Java并发-信号量Semaphore

← Java并发-线程基础与synchronized关键字 Java并发-信号量Semaphore→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式