Java多线程(六) 解决多线程安全——ReentrantLock及源码解析
在之前的文章《Java多线程(三) 多线程不安全的典型例子》中我写到了在多线程环境中经常会碰到多线程非安全的情况,并且举出了三种典型例子,之后我在上一篇文章《Java多线程(四) 解决多线程安全——synchronized》中讲解了一种解决多线程非安全的方法,在本篇中介绍一种锁机制ReentrantLock,他也能起到保护多线程安全的作用。
ReentrantLock的使用
下面是ReentrantLock使用的格式,在使用了ReentrantLock加锁以后一定要给它解锁。
ReentrantLocklock = new ReentrantLock();
try{
lock.lock();//加锁操作
}finally{
lock.unlock();
}
下面通过一个简单的例子再来看一下具体怎么使用这个锁。还是买票的例子,三个线程想去买票,票数一共60张,使用ReentrantLock加锁保证线程安全。
class Ticket implements Runnable{
private int alltickets = 60;
private boolean flag = true;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(alltickets>0) {
if (this.flag == true) {
try {
this.lock.lock();
Thread.sleep(300);
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
this.lock.unlock();
}
} else {
break;
}
}
}
public void buy() throws InterruptedException {
if(this.alltickets<=0)
{
System.out.println("没票可买了"+Thread.currentThread().getName());
this.flag = false;
return;
}
else
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"买了第"+this.alltickets--+"张票 ");
}
}
}
public class testThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Ticket t = new Ticket();
new Thread(t, "小华同学").start();
new Thread(t, "小明同学").start();
new Thread(t, "黄牛").start();
}
}
上面的代码输出结果如下
ReentrantLock与synchronized区别
-
synchronized是java语言的关键字,而ReentrantLock是是属于java的一个类,需要lock()和unlock()方法配合try/finally语句块来完成
-
synchronized无法判断锁的状态,而ReentrantLock能够判断是否获得锁
-
synchronized会自动释放锁,而ReentrantLock必须手动释放锁,如果他不释放锁就会发生死锁
-
对于synchronized来说如果不释放锁其他线程就需要等待,但是ReentrantLock可以尝试解锁
-
synchronized是可重入锁、不可以中断,但是ReentrantLock可以中断,它可以是公平锁也可以是非公平锁
ReentrantLock的特性
- 可重入锁
- 公平锁、非公平锁
- 可中断
- 可定时
ReentrantLock 源码解析
在执行lock.lock()函数后,会进入acquire(int arg)函数,在这个函数中,首先会在tryAcquire()函数中尝试加锁,如果尝试加锁失败了,就需要将该线程加入等待队列,并且阻塞住该线程。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//先尝试加锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//加不上锁的话新建节点入队,并且阻塞
selfInterrupt();
}
下面这段代码时tryAcquire(int acquires)函数,首先需要知道的是ReentrantLock可以是公平锁也可以是非公平锁(公平锁就是先进入的进程总比后进入的进程早获得锁),而ReentrantLock在公平锁和非公平锁时区别就是对于公平锁来说,当有线程被锁阻塞时,线程会进行排队,当锁被释放时,会按顺序给把锁给下一个线程。
这里的tryAcquire(int acquires)函数是一个公平锁的例子,如果得到当前没有锁,那么首先要查看队列里有没有线程在排队,如果当前线程是队列里第一个线程,那么要通过CAS乐观锁机制将锁的state值变成1(关于CAS机制可以看这篇博客《Java多线程(五) 乐观锁和CAS机制》),这里使用乐观锁的意义是防止在多线程情况下,有多个线程都探知锁空闲并且队列没有其他线程而发生非安全问题,之后将这把锁给到当前线程。如果发现锁不空闲,那么需要修改锁的state值,并且返回尝试获得锁失败。这里的state值应该就是这把锁的重数,比如对这把锁调用三次lock()函数,state值就为3了。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//锁空闲
if (!hasQueuedPredecessors() &&//因为是公平锁,判断前面有没有线程在排队
compareAndSetState(0, acquires)) {//尝试修改state
setExclusiveOwnerThread(current);//修改当前这把锁属于的线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//锁不空闲,判断获得这把锁的线程是不是当前线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);//改变state的值,和重入锁有关系,1,2,3,4......
return true;//加到锁了
}
return false;
}
}
在公平锁情况下,如果当前线程尝试获取锁失败了,那么这个线程就要被做成等待队列的新节点,并被加入到等待队列中,下面的代码就是做这件事的。代码里的for死循环是为了在多线程情况下,使得每一个进来的线程都成功放入队列。
private Node addWaiter(Node mode) {
Node node = new Node(mode);//新建对象
for (;;) {//保证当前node对象一定要入队成功,否则会一直循环
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();//队列里没东西,就初始化队列
}
}
}
当把新节点放入队列中后,下一步就需要对该线程进行阻塞操作。
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//如果是第一个排队的,就再去尝试获取锁
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node ))//修改前一个结点的waitstate
interrupted |= parkAndCheckInterrupt();//进行park,如果被unpark,就得重新进入循环判断是不是第一个排队的线程
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();//打断
throw t;
}
}
ReentrantReadWriteLock锁
ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率是非常低下的,所以JDK提供了一种读写锁——ReentrantReadWriteLock 类,使用它可以在进行读操作时不需要同步执行,提升运行速度,加快运行效率。读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作相关的锁,也称排他锁。读锁之间不互斥,读锁和写锁互斥,写锁与写锁互斥,因此只要出现写锁,就会出现互斥同步的效果。读操作是指读取实例变量的值,写操作是指向实例变量写入值。
ReentrantLock 缺点
上面说过,ReentrantLock效率较低,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,可以看下下面的代码,可以看到线程2是在线程1执行完毕才进来,但是两个线程仅仅都是读取数据,因此这样的效率很低。
class TestReentrantLock implements Runnable{
private ReentrantLock lock = new ReentrantLock();
private String name = "abc";
@Override
public void run() {
lock.lock();
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程进入时间: " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+" "+name);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程运行时间: " + (endTime - startTime ) + "ms");
lock.unlock();
}
}
public class ReadLock {
public static void main(String[] args) {
TestReentrantLock t = new TestReentrantLock();
new Thread(t,"ReentrantLock线程1").start();
new Thread(t,"ReentrantLock线程2").start();
}
}
ReentrantReadWriteLock 读读共享
上面说了使用ReentrantLock会阻塞其他线程使得效率低下,这里可以使用ReentrantReadWriteLock,他能够做到读读共享,可以看下面这个例子,两个读数据的线程几乎同时进入,极大提升效率。这里因为是进行读操作,所以使用的是ReentrantReadWriteLock的读锁,所以要使readLock.readLock().lock()和readLock.readLock().unlock()进行上锁和解锁。
class TestReentrantReadWriteLock implements Runnable{
private ReentrantReadWriteLock readLock = new ReentrantReadWriteLock();
private String name = "abc";
@Override
public void run() {
readLock.readLock().lock();
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程进入时间: " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+" "+name);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程运行时间: " + (endTime - startTime ) + "ms");
readLock.readLock().unlock();
}
}
public class ReadLock {
public static void main(String[] args) {
TestReentrantReadWriteLock t1 = new TestReentrantReadWriteLock();
new Thread(t1,"ReentrantReadWriteLock线程1").start();
new Thread(t1,"ReentrantReadWriteLock线程2").start();
}
}
ReentrantReadWriteLock 写写互斥
除了读锁,ReentrantReadWriteLock 还具有写锁,使用了写锁的效果就变成同一时间只允许一个线程执行lock()之后的方法的代码。
class TestReentrantReadWriteLock implements Runnable{
private ReentrantReadWriteLock readLock = new ReentrantReadWriteLock();
private int count = 0;
@Override
public void run() {
readLock.writeLock().lock();
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程进入时间: " + System.currentTimeMillis());
count ++;
System.out.println(Thread.currentThread().getName()+" "+count);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程运行时间: " + (endTime - startTime ) + "ms");
readLock.writeLock().unlock();
}
}
public class ReadLock {
public static void main(String[] args) {
TestReentrantReadWriteLock t1 = new TestReentrantReadWriteLock();
new Thread(t1,"ReentrantReadWriteLock线程1").start();
new Thread(t1,"ReentrantReadWriteLock线程2").start();
}
}