天天看点

Java多线程(六) 解决多线程安全——ReentrantLock及源码解析Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

  • Java多线程(六) 解决多线程安全——ReentrantLock及源码解析
    • ReentrantLock的使用
    • ReentrantLock与synchronized区别
    • ReentrantLock的特性
    • ReentrantLock 源码解析
    • ReentrantReadWriteLock锁
      • ReentrantLock 缺点
      • ReentrantReadWriteLock 读读共享
      • ReentrantReadWriteLock 写写互斥

在之前的文章《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();
        }
}

           

上面的代码输出结果如下

Java多线程(六) 解决多线程安全——ReentrantLock及源码解析Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

ReentrantLock与synchronized区别

  1. synchronized是java语言的关键字,而ReentrantLock是是属于java的一个类,需要lock()和unlock()方法配合try/finally语句块来完成
  2. synchronized无法判断锁的状态,而ReentrantLock能够判断是否获得锁
  3. synchronized会自动释放锁,而ReentrantLock必须手动释放锁,如果他不释放锁就会发生死锁
  4. 对于synchronized来说如果不释放锁其他线程就需要等待,但是ReentrantLock可以尝试解锁
  5. 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();
    }

}
           
Java多线程(六) 解决多线程安全——ReentrantLock及源码解析Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

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();
    }

}
           
Java多线程(六) 解决多线程安全——ReentrantLock及源码解析Java多线程(六) 解决多线程安全——ReentrantLock及源码解析

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();
    }
}
           
Java多线程(六) 解决多线程安全——ReentrantLock及源码解析Java多线程(六) 解决多线程安全——ReentrantLock及源码解析