Java并发编程系列学习_Lock锁

Lock锁和synchronized关键字锁的对比

在Java并发编程中,锁可以防止多个线程同时访问共享资源,也可以使用synchronized关键字实现锁的控制。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性。Lock和Synchronized的具体的区别如下表格所示:

    类别synchronized锁Lock锁
存在层次在Jvm的层面一个类
锁的释放

1、获取了锁的线程执行完毕后,主动释放锁;

2、线程在执行时发生异常,Jvm强迫线程释放锁;

必须在finally块中释放锁,不然会造成死锁
锁的获取一个线程获取了锁,另外一个线程必须等待锁被释放才能继续获取支持多种锁的获取的方式,比如超时获取锁,在指定时间内没获取到锁,则不再继续阻塞等待
锁的状态不能判断锁的状态可以判断锁的状态
锁的类型可重入 不可中断 非公平可重入 可中断 可公平 可不公平
适合场景少量同步大量同步

Lock锁的基本使用

Lock lock = new ReentrantLock();
lock.lock();
try{
	.......
}finally{
	lock.unlock();
}

在lock接口下面定义了很多常用的API方法:

  • void lock(); //获取锁
  • void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断
  • boolean tryLock(); //非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁
  • Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
  • getHoldCount(); //查询当前的线程保持此锁定的个数 也就是调用Lock方法的个数
  • getQueueLength(); //获取正等待获取此锁定的线程的估计数目
  • getWaitQueueLength(); //返回与此等待相关的给定条件的Condition的线程估计数
  •  hasQueuedThread(); //查询指定的线程(需要传入线程名称)是否等待获取此锁定
  •  hasQueuedThreads(); //查询是否有线程正在等待获取此锁
  •  hasWaiters();  //查询是否有线程正在等待与此锁定有关的condition条件
  •  isFair();  //判断锁是否为公平锁
  •  isLocked();  //是否由任意的线程保持

下面通过一个实例来学习下lock锁的基本使用,看下如何进行同步控制:

package lock.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-22 15:13
 * @Description
 */
public class MyServiceLock {
    /**
     * 实例化锁对象
     */
    private Lock lock = new ReentrantLock();

    private final static int count = 5;

    /**
     * 测试方法
     */
    public void testMethod() {
        try {
            //获取锁
            //调用lock的代码的线程持有了对象的监视器 其他线程只有等到锁被释放再重新抢占
            lock.lock();
            for (int i = 0; i < count; i++) {
                System.out.println("The name of thread:" + Thread.currentThread().getName() + " i:" + i);
            }
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

package lock.lock;

/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-22 15:18
 * @Description
 */
public class TestMainLock {
    public static void main(String[] args) {
        MyServiceLock myService = new MyServiceLock();
        //1.继承Thread
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread1");
            myService.testMethod();
        });
        //2.实现runnable接口
        Thread thread2 = new Thread(() -> {
            System.out.println("Thread2");
            myService.testMethod();
        });
        thread1.start();
        thread2.start();
    }
}

程序运行结果:

锁的底层同步原理

那么我们不经会问了,lock是如何实现线程的同步访问的呢?其底层是一种怎样的实现机制?

实际上,ReentrantLock本身没什么代码,实际上的所有的方法调用的是静态的内部类Sync,Sync继承了AbstractQueuedSynchronizer抽象队列同步器。

关于AQS在源码的解释:

 Provides a framework for implementing blocking locks and related
 synchronizers (semaphores, events, etc) that rely on
 first-in-first-out (FIFO) wait queues.  This class is designed to
 be a useful basis for most kinds of synchronizers that rely on a
 single atomic {@code int} value to represent state. Subclasses
 must define the protected methods that change this state, and which
 define what that state means in terms of this object being acquired
 or released.  Given these, the other methods in this class carry
 out all queuing and blocking mechanics. Subclasses can maintain
 other state fields, but only the atomically updated {@code int}
 value manipulated using methods {@link #getState}, {@link
 #setState} and {@link #compareAndSetState} is tracked with respect
 to synchronization.
 
 <p>Subclasses should be defined as non-public internal helper
 classes that are used to implement the synchronization properties
 of their enclosing class.  Class
 {@code AbstractQueuedSynchronizer} does not implement any
 synchronization interface.  Instead it defines methods such as
 {@link #acquireInterruptibly} that can be invoked as
 appropriate by concrete locks and related synchronizers to
 implement their public methods.

其含义为:

       同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法
      子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。
      同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。

AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire:

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

ReentrantLock中NonfairSync(继承AQS)会重写该方法为:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

而AQS中的模板方法acquire()会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式。

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }

AQS同步器小结

ReentrantLock底层实现上会基于AQS提供的模板方法,维护一个同步队列,实现多个线程多共享资源的互斥访问。

  1. 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
  2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
  3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
  4. 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态;

ReentrantLock实现可重入的原理

在上面的例子中,我们讨论都都是ReentrantLock而这个锁是一个重入锁,是实现lock接口的实现类,它支持可重入、支持公平锁和非公平锁。

所谓可重入,就是可以对共享的资源进行重复加锁,即当前的线程获取锁后再去获取锁后不会被阻塞。相对而言,synchronized也是支持可重入的,但是它是隐式的,通过获取自增、释放自减的方式实现可重入。

要想支持重入性,就要解决两个问题:**1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//2.若被占有,检查占有线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
		// 3. 再次获取,计数加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

这段代码的逻辑也很简单,具体请看注释。为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?(依然还是以非公平锁为例)核心方法为tryRelease

protected final boolean tryRelease(int releases) {
	//1. 同步状态减1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//2. 只有当同步状态为0时,锁成功被释放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
	// 3. 锁未被完全释放,返回false
    setState(c);
    return free;
}

代码的逻辑请看注释,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。到现在我们可以理清ReentrantLock重入性的实现了,也就是理解了同步语义的第一条。

ReentrantLock实现公平锁和非公平锁的原理

ReentrantLock支持两种锁:公平锁非公平锁何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的构造方法无参时是构造非公平锁,源码为:

public ReentrantLock() {
    sync = new NonfairSync();
}

另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

在上面非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。我们来看看公平锁的处理逻辑是怎样的,核心方法为:

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

这段代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁

读写锁ReentrantReadWriteLock的介绍

上文介绍的ReentrantLock以及synchronized关键字都是属于独占式的获取锁,也就是同一个时刻只有一个线程能获取到锁。但是针对大部分业务场景是读,少量写入的情景下,还是使用独占式的锁的话,就会大大降低系统的吞吐量,而都是读的情况,并不会出现数据的脏读,那就完全没必要使用独占式的锁。这时就衍生出一个概念,基于lock的读写锁:ReentrantReadWriteLock。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

  1. 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
  2. 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
  3. 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

接下来我们看个例子,学习下读写锁是怎么用的。

package lock.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-25 16:45
 * @Description
 */
public class ReadService {
    /**
     * 读写锁
     */
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 读读不互斥
     */
    public void read() {
        try {
            //获取读锁
            lock.readLock().lock();
            System.out.println("获取读锁" + Thread.currentThread().getName() + " "+System.currentTimeMillis());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            lock.readLock().unlock();
        }
    }

    /**
     * 读写互斥
     */
    public void write() {
        try {
            //获取写锁
            lock.writeLock().lock();
            System.out.println("获取写锁" + Thread.currentThread().getName() + " "+System.currentTimeMillis());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            lock.writeLock().unlock();
        }
    }
}

创建5个线程并发读(写入)的结果:

package lock.readwrite;

/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-25 16:49
 * @Description
 */
public class ReadServiceMain {
    public static void main(String[] args) {
        ReadService readService = new ReadService();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                readService.read();
                //readService.write();
            });
            thread.start();
        }

    }
}

await()以及signal()实现生产者消费者模式

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同。

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

wait方法:

  1. void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
  2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
  3. boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
  4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

signal方法:

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回
  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
package lock.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-25 09:59
 * @Description
 */
public class MyPrintService {
    /**
     * 可重试锁
     */
    private ReentrantLock lock = new ReentrantLock();
    /**
     * 条件
     */
    private Condition condition = lock.newCondition();
    /**
     * 是否有值
     */
    private boolean hasValue = false;
    /**
     * set方法
     */
    public void set() {
        try {
            //获取锁
            lock.lock();
            //生产者:有值就进行等待
            while (hasValue == true) {
                condition.await();
            }
            //没值就放值
            System.out.println("生产一个apple");
            hasValue = true;
            //通知-消费者来取值
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    /**
     * get方法
     */
    public void get() {
        try {
            //获取锁
            lock.lock();
            //消费者:没值就等待
            while (hasValue == false) {
                condition.await();
            }
            //有值就进行获取
            System.out.println("消费一个apple");
            hasValue = false;
            //通知生产者再放去值
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
package lock.pc;
/**
 * @Author: jiaqing.xu@hand-china.com
 * @Date: 2019-03-25 10:08
 * @Description
 */
public class TestSetMain {
    public static void main(String[] args) {
        MyPrintService myPrintService = new MyPrintService();
        //1.继承Thread
        Thread threadSet = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                myPrintService.set();
            }
        });
        //2.继承Thread
        Thread threadGet = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                myPrintService.get();
            }
        });
        //启动线程
        threadSet.start();
        threadGet.start();
    }
}

等待/通知机制,通过使用condition提供的await和signal/signalAll方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”,await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切:

如图,线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值