重入锁ReentrantLock
背景
在Java中,我们常用 synchronized 关键字来实现线程同步。虽然它提供了一定的线程同步功能,但是也有一些问题。
synchronized 的同步块不提供 线程等待队列 的任何机制,并且在任何一个线程退出后,任何线程都有可以获得锁,这就有可能会导致一些线程长时间获取不到锁,无法获取资源。
另外,下面的情况也实现不了:
无法中断等待获取锁的线程
不能尝试获取锁
无法实现非块结构锁定规则,因为必须在获取它们的同一块中释放内部锁。
ReentrantLock
Reentrantlock是在Java 5.0中加入的,用来增强内部锁的功能。
ReentrantLock类实现了Lock接口,并提供了在访问共享资源时的同步方法。
ReentrantLock类通过 lock 和 unlock方法来访问共享资源。
当调用 lock 时,当前线程就会获取到锁,其它访问此共享资源的线程就会阻塞。
当调用 unlock 时,就会释放资源。
顾名思义,ReentrantLock允许线程不止一次地锁定资源。 当线程首次进入锁定状态时,计数器设置为1。
在解锁之前,线程可以再次重新进入锁,并且每次计数器增加1。 对于每个解锁请求,计数器减1,当计数器为0时,资源解锁。
ReentrantLock可以指定是否是公平锁,当是公平锁时,在线程解锁资源之后,锁将转到已经等待最长时间的线程。
但公平锁会降低程序吞吐量。
ReentrantLock的方法
lock():
调用lock方法,当前线程会获得锁,并且计数器加1。
unlock():
调用unlock方法,计数器减1,当计数器为0时,资源释放。
tryLock():
如果资源未被锁定,调用tryLock方法会返回true,并且计数器加1。如果资源已被锁定,那么方法会返回false,但线程还会被阻止,但会退出。
tryLock(long timeout, TimeUnit unit):
试图获取锁,并会在退出前等待一段时间。
lockInterruptibly():
如果资源是空闲的,则此方法会获取到锁,同时允许线程在获取资源时被某个其他线程打断。 这意味着如果当前线程正在等待锁但其他一些线程请求锁,则当前线程将会被中断并立即返回而不获取锁定
getHoldCount():
返回资源上锁的数量
isHeldByCurrentThread():
如果资源是当前线程拥有,则返回true
ReentrantLock的使用
下面时 ReentrantLock 的一般使用方法。
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock();
}
}
}
重要: unlock 操作一定要放在 finally 里执行,保证不会死锁。
举例子
下面我们写一个有并发问题的例子。
有一个 Account 类,只有一个 num 字段。
public class Account {
private int num = 0;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
有一个 AccountService 类。有一个add方法,是对account中的num进行了加1操作。
public class AccountService {
public void add(Account account) {
//当前线程停顿一下,保证有多个线程可以同时运行下面的加1操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setNum(account.getNum() + 1);
}
}
还有一个 Program 类,创建了多个线程来执行上面的add方法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Program {
public static void main(String[] args) {
int initCount = Thread.activeCount();
System.out.println("当前活动的线程数:" + initCount);
AccountService accountService = new AccountService();
Account account = new Account();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
accountService.add(account);
});
}
executorService.shutdown();
//保证上面的线程全部执行完毕
while(Thread.activeCount() > initCount){
Thread.yield();
}
System.out.println("当前活动的线程数:" + Thread.activeCount());
System.out.println(account.getNum());
}
}
上面的三个类写完后,运行。
预期情况下,最后的结果应该是10,但由于并发问题的存在,结果可能是9或者8之类的,总之,结果不确定。
如果用 ReentrantLock 怎么改呢?
错误写法
ReentrantLock写出了局部对象(不对的)
将AccountService改成如下代码
public class AccountService {
public void add(Account account) {
//sleep一下,保证多个线程同时运行下面的方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
gl>ReentrantLock lock = new ReentrantLock();
gl>lock.lock();
gl>try {
account.setNum(account.getNum() + 1);
gl>} catch (Exception e) {
gl>e.printStackTrace();
gl>} finally {
gl>lock.unlock();
gl>}
}
}
改好之后,多运行几次,发现有时候结果还是9或者8。所以这种写法是错误的。
正确写法
正确写法怎么写呢,我们将 ReentrantLock 提升为 AccountService 的成员变量
import java.util.concurrent.locks.ReentrantLock;
public class AccountService {
ReentrantLock lock = new ReentrantLock();
public void add(Account account) {
//sleep一下,保证多个线程同时运行下面的方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
account.setNum(account.getNum() + 1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
改好之后,多运行几次,结果总是10,这次保证了正确性。
再变动
我们再次改动一下代码,在 Program 中,有这样一段代码
AccountService accountService = new AccountService();
Account account = new Account();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
accountService.add(account);
});
}
也就是说,每个线程操作的都是同一个 AccountService 的add方法。
如果我们把 AccountService 的创建放在每个线程中会怎么样呢?
我们修改成如下的样子:
public class Program {
public static void main(String[] args) {
int initCount = Thread.activeCount();
System.out.println("当前活动的线程数:" + initCount);
Account account = new Account();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
AccountService accountService = new AccountService();
accountService.add(account);
});
}
executorService.shutdown();
//保证上面的线程全部执行完毕
while(Thread.activeCount() > initCount){
Thread.yield();
}
System.out.println("当前活动的线程数:" + Thread.activeCount());
System.out.println(account.getNum());
}
}
修改完毕,我们再次运行,运行几次后,发现结果又出现了9或8之类的结果,有不对了。
那么问题出现了
ReentrantLock的lock到底是锁住了谁?
ReentrantLock的lock到底是锁住了谁,或者说这个锁到底是谁给的。
保护资源的本质其实是:当前线程获取到锁,然后执行某段代码,保证了在此期间其他线程不会执行相同的代码,从而保护了共享资源。
所以从哪里拿这个锁也很重要
ReentrantLock的锁是自己给的,或者说锁定的是自己。
回顾一下上面的改动
第一次我们在 add 方法内新建的 ReentrantLock,那每个线程过来都新建一个 ReentrantLock,获取的是自己的锁,
而其他线程过来之后,也会新建一个 ReentrantLock,每一个lock都和其他线程没有关系,所以不能保证只有一个线程执行此段代码。
第二次我们将 ReentrantLock 升级为成员变量后,ReentrantLock 属于了 AccountService,又由于 AccountService 只有一个,
每次线程都是操作的同一个 AccountService,所以每个线程都是从 这一个 AccountService 的 ReentrantLock 来获取锁。
每个线程拿锁的地方都一样,所以保证了只有一个线程执行此段代码。
第三次修改我们是每个线程都创建一个 AccountService,所以相当于每个线程都是从
自己的 AccountService 里面的 ReentrantLock 拿锁,都不是同一个把锁,所以,最后这个也是有问题的。
ReentrantLock与synchronized
synchronized 使用的是内部锁或监视器来实现同步的。
Java中的每个对象都有一个与之关联的内部锁。 每当线程尝试访问同步块或方法时,它就会获取该对象上的内部锁或监视器。在静态方法的情况下,线程获取的是对类对象的锁定。
因此,在编码方面,内部锁机制是一个不错的选择,适合大部分情况。
因此如果不是需要 ReentrantLock 的特有的优势的话,没有必要使用 ReentrantLock 。
*昵称:
*邮箱:
个人站点:
*想说的话: