Java多线程(7)——Lock类

synchronized

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)

但是它有一些功能性的限制:

  • 它无法中断一个正在等候获得锁的线程;
  • 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
  • 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

------------------------------------------------------------------------------------------------------------------

Lock

在jdk1.5以后,JAVA提供了Lock类来实现和synchronized一样的功能,并且还提供了Condition来显示线程间通信,丰富的api使得Lock类的同步功能比synchronized的同步更强大。

  • Lock接口有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock
  • 与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)。从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
  • 在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

一、Lock的使用                                                                                                                                             

通过Lock对象lock,用lock.lock来加锁,用lock.unlock来释放锁,在两者中间放置需要同步处理的代码,例子如下:

class MyService{
    private Lock lock=new ReentrantLock();
    public void service(){
        try{
            lock.lock();
            for (int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
class MyThread extends Thread{
    private MyService myService;
    public MyThread(MyService myService){
        this.myService=myService;
    }
    @Override
    public void run() {
        myService.service();
    }
}
public class LockTest {
    public static void main(String[] args){
        MyService myService=new MyService();
        for(int i=0;i<5;i++){
            new MyThread(myService).start();
        }
    }
}
将加锁和释放锁都是在try-finally。这样的好处是在任何异常发生的情况下,都能保障锁的释放。

输出为:每个线程的打印1-5都是同步进行,顺序没有乱

Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-2 0
Thread-2 1
Thread-2 2
Thread-2 3
Thread-2 4
Thread-4 0
Thread-4 1
Thread-4 2
Thread-4 3
Thread-4 4
Thread-3 0
Thread-3 1
Thread-3 2
Thread-3 3
Thread-3 4

加锁的几种方式:

  • lock():如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
  • tryLock():如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false
  • tryLock(long time, TimeUnit unit) throws InterruptedException:如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false(锁等待定时
  • lockInterruptibly() throws InterruptedException:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断(实现加锁,但是当线程被中断的时候,就会加锁失败,进行异常处理阶段

Condition类

Condition是Java提供了来实现等待/通知的接口类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。

class MyService{
    private Lock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();
    public void service(){
        try{
            lock.lock();
            for (int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
                if(i==3){
                    System.out.println(Thread.currentThread().getName()+"等待被signal");
                    condition.await();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void service1(){
        try {
            lock.lock();
            for (int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
                if(i==2){
                    condition.signal();
                    System.out.println(Thread.currentThread().getName()+"唤醒另一个Thread");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
class MyThread1 extends Thread{
    private MyService myService;
    public MyThread1(MyService myService){
        this.myService=myService;
    }
    @Override
    public void run() {
        myService.service1();
    }
}
class MyThread extends Thread{
    private MyService myService;
    public MyThread(MyService myService){
        this.myService=myService;
    }
    @Override
    public void run() {
        myService.service();
    }
}
public class LockTest {
    public static void main(String[] args){
        MyService myService=new MyService();
        new MyThread(myService).start();
        new MyThread1(myService).start();
    }
}

输出为:

Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0等待被signal
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1唤醒另一个Thread
Thread-1 3
Thread-1 4
Thread-0 4

condition对象通过lock.newCondition()来创建,用condition.await()来实现让线程等待,是线程进入阻塞。用condition.signal()来实现唤醒线程。唤醒的线程是用同一个conditon对象调用await()方法而进入阻塞。并且和wait/notify一样,await()和signal()也是在同步代码区内执行。对于wait/notify而言,对象监视器与等待条件结合在一起 即synchronized(对象)利用该对象去调用wait以及notify。但是对于Condition类,是对象监视器与条件分开,Lock类来实现对象监视器,condition对象来负责条件,去调用await以及signal。

另:awaitUntil(Date deadline)在到达指定时间之后,线程会自动唤醒。但是无论是await或者awaitUntil,当线程中断时,进行阻塞的线程会产生中断异常。Java提供了一个awaitUninterruptibly的方法,使即使线程中断时,进行阻塞的线程也不会产生中断异常。

二、ReentrantLock(可重入锁)                                                                                                                   

Lock接口的一个实现类:   public class ReentrantLock implements Lock, java.io.Serializable

其类层次结构如图:

Lock定义了锁的接口规范,包括(lock,trylock, lockInterruptibly,unlock,newCondition方法)
ReentrantLock实现了Lock接口。 
AbstractQueuedSynchronizer中以队列的形式实现线程之间的同步。 
ReentrantLock的方法都依赖于AbstractQueuedSynchronizer的实现。

1、lock()的实现

进入lock()方法,发现其内部调用的是sync.lock();

    public void lock() {
        sync.lock();
    }
sync是在ReentrantLock的 构造函数中实现的。其中fair参数的不同可实现公平锁和非公平锁。由于在锁释放的阶段,锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁,从而出现公平锁和非公平锁的区别。 
非公平锁:当锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁。 

公平锁:当锁处于无线程占有的状态,在其他线程抢占该锁的时候,都需要先进入队列中等待。 

    public ReentrantLock() {                   //非公平锁
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {       //公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }

下面以非公平锁NonfairSync的sync实例进行分析:

NonfairSync继承自Sync,因此也继承了AbstractQueuedSynchronizer中的所有方法实现。接着进入NonfairSync的lock()方法

 final void lock() {
            // 利用cas置状态位,如果成功,则表示占有锁成功
            if (compareAndSetState(0, 1))
                // 记录当前线程为锁拥有者
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

在lock方法中,利用cas实现ReentrantLock的状态置位(cas即compare and swap,它是CPU的指令,因此赋值操作都是原子性的)。如果成功,则表示占有锁成功,并记录当前线程为锁拥有者。当占有锁失败,则调用acquire(1)方法继续处理。

    public final void acquire(int arg) {
        //尝试获得锁,如果失败,则加入到队列中进行等待
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
acquire()是AbstractQueuedSynchronizer的方法。它首先会调用tryAcquire()去尝试获得锁,如果获得锁失败,则将当前线程加入到CLH队列中进行等待。tryAcquire()方法在NonfairSync中有实现,但最终调用的还是Sync中的nonfairTryAcquire()方法。
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获得状态
            int c = getState();
            // 如果状态为0,则表示该锁未被其他线程占有
            if (c == 0) {
                // 此时要再次利用cas去尝试占有锁
                if (compareAndSetState(0, acquires)) {
                    // 标记当前线程为锁拥有者
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程已经占有了,则state + 1,记录占有次数
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 此时无需利用cas去赋值,因为该锁肯定被当前线程占有
                setState(nextc);
                return true;
            }
            return false;
        }
在nonfairTryAcquire()中,首先会去获得 锁的状态,如果为0,则表示锁未被其他线程占有,此时会 利用cas去尝试将锁的状态置位(即置1),并标记当前线程为锁拥有者;如果锁的状态大于0,则会判断锁是否被当前线程占有,如果是,则state + 1,这也是为什么lock()的次数要和unlock()次数对等;如果占有锁失败,则返回false。 
在nonfairTryAcquire()返回false的情况下,会继续调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,将 当前线程加入到队列中继续尝试获得锁。
    private Node addWaiter(Node mode) {
        // 创建当前线程的节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 如果尾节点不为空
        if (pred != null) {
            // 则将当前线程的节点加入到尾节点之后,成为新的尾节点
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }

        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        // CAS方法有可能失败,因此要循环调用,直到当前线程的节点加入到队列中
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                Node h = new Node(); // Dummy header,头节点为虚拟节点
                h.next = node;
                node.prev = h;
                    if (compareAndSetHead(h)) {
                    tail = node;  
                    return h;
                }
            }
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter()是AbstactQueuedSynchronizer的方法,会以节点的形式来标记当前线程,并加入到尾节点中。enq()方法是在节点加入到尾节点失败的情况下,通过for(;;)循环反复调用cas方法,直到节点加入成功。由于enq()方法是非线程安全的,所以在增加节点的时候,需要使用cas设置head节点和tail节点。此时添加成功的结点状态为Node.EXCLUSIVE。 

在节点加入到队列成功之后,会接着调用acquireQueued()方法去尝试获得锁。

    final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                // 获得前一个节点
                final Node p = node.predecessor();
                // 如果前一个节点是头结点,那么直接去尝试获得锁
                // 因为其他线程有可能随时会释放锁,没必要Park等待
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }
在acquireQueued()方法中,会利用for (;;)一直去获得锁, 如果前一个节点为head节点,则表示可以直接尝试去获得锁了,因为占用锁的线程随时都有可能去释放锁并且该线程是被unpark唤醒的CLH队列中的第一个节点,获得锁成功后返回。 
如果该线程的节点在CLH队列中比较靠后或者获得锁失败,即其他线程依然占用着锁,则会接着调用 shouldParkAfterFailedAcquire()方法来阻塞当前线程,以让出CPU资源。在阻塞线程之前,会执行一些额外的操作以提高CLH队列的性能。由于队列中前面的节点有可能在等待过程中被取消掉了,因此当前线程的节点需要提前,并将前一个节点置状态位为SIGNAL,表示可以阻塞当前节点。因此该函数在判断到前一个节点为SIGNAL时,直接返回true即可。此处虽然存在对CLH队列的同步操作,但由于局部变量节点肯定是不一样的,所以对CLH队列操作是线程安全的。由于在compareAndSetWaitStatus(pred, ws, Node.SIGNAL)执行之前可能发生pred节点抢占锁成功或pred节点被取消掉,因此此处需要返回false以允许该节点可以抢占锁。 
当shouldParkAfterFailedAcquire()返回true时,会进入parkAndCheckInterrupt()方法。parkAndCheckInterrupt()方法最终调用safe.park()阻塞该线程,以免该线程在等待过程中无线循环消耗cpu资源。至此,当前线程便被park了。那么线程何时被unpark,这将在unlock()方法中进行。 
这里有一个小细节需要注意,在线程被唤醒之后,会调用Thread.interrupted()将线程中断状态置位为false,然后记录下中断状态并返回上层函数去抛出异常。我想这样设计的目的是为了可以让该线程可以完成抢占锁的操作,从而可以使当前节点称为CLH的虚拟头节点。
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park
             */
            return true;

        if (ws > 0) {
            // 如果前面的节点是CANCELLED状态,则一直提前
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        } 
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
    }

而FairSync的lock()方法为:

        final void lock() {
            acquire(1);
        }
与不公平锁不同的是,在调用acquire时不会先利用cas指令进行线程抢占,并且在tryAcquire中会先对等待的线程队列进行判断,只有在等待队列中没有线程时,才会利用cas进行线程抢占。

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

2、unlock()的实现

    public void unlock() {
        sync.release(1);
    }

对于不公平锁和公平锁,unlock调用的都是sync.release(1)方法:

    public final boolean release(int arg) {
        // 释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 此处有个疑问,为什么需要判断h.waitStatus != 0
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

可以看到,tryRelease()方法实现了锁的释放,逻辑上即是将锁的状态置为0。当释放锁成功之后,通常情况下不需要唤醒队列中线程,因此队列中总是有一个线程处于活跃状态。

ReentrantLock的锁资源以state状态描述,利用CAS则实现对锁资源的抢占,并通过一个CLH队列阻塞所有竞争线程,在后续则逐个唤醒等待中的竞争线程。ReentrantLock继承AQS完全从代码层面实现了java的同步机制,相对于synchronized,更容易实现对各类锁的扩展。

另外,ReentrantLock在加锁和释放锁之外,还提供了其他丰富的功能:

  • 获取当前线程调用lock的次数,也就是获取当前线程锁定的个数
  • 获取等待锁的线程数
  • 查询指定的线程是否等待获取此锁定
  • 查询是否有线程等待获取此锁定
  • 查询当前线程是否持有锁定
  • 判断一个锁是不是被线程持有

三、ReentrantReadWriteLock(读写锁)                                                                                                    

除了提供了ReentrantLock的锁以外,还提供了ReentrantReadWriteLock的锁。读写锁分成两个锁,一个锁是读锁,一个锁是写锁读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的

读读共享:

class ReadReadTest{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    public void read(){
        try{
            try{
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName() +
                        " " + System.currentTimeMillis());
                Thread.sleep(1000 * 10);
            }finally {
                lock.readLock().unlock();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
class MyThread2 extends Thread{
    private ReadReadTest readReadTest;
    public MyThread2(ReadReadTest readReadTest){
        this.readReadTest=readReadTest;
    }
    public void run(){
        readReadTest.read();
    }
}
public class ReadWriteLock {
    public static void main(String[] args){
        ReadReadTest readReadTest=new ReadReadTest();
        new MyThread2(readReadTest).start();
        new MyThread2(readReadTest).start();
    }
}

输出为:两个线程几乎同时执行同步代码。

获得读锁Thread-1 1523972980636
获得读锁Thread-0 1523972980636

写写互斥:

class ReadReadTest{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    public void read(){
        try{
            try{
                lock.writeLock().lock();
                System.out.println("获得写锁" + Thread.currentThread().getName() +
                        " " + System.currentTimeMillis());
                Thread.sleep(1000 * 10);
            }finally {
                lock.writeLock().unlock();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
class MyThread2 extends Thread{
    private ReadReadTest readReadTest;
    public MyThread2(ReadReadTest readReadTest){
        this.readReadTest=readReadTest;
    }
    public void run(){
        readReadTest.read();
    }
}
public class ReadWriteLock {
    public static void main(String[] args){
        ReadReadTest readReadTest=new ReadReadTest();
        new MyThread2(readReadTest).start();
        new MyThread2(readReadTest).start();
    }
}

输出为:因为写写互斥,所以线程会互相阻塞。

获得写锁Thread-1 1523973107287
获得写锁Thread-0 1523973117292

四、Lock与Synchronized区别                                                                                                                       

  • synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
  • 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
  • ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值