显示锁--LOCK

387 阅读5分钟

基础介绍

在程序中可以通过synchronized实现锁功能,对于它可称之为内置锁,是由Java语言层面直接为我们提供使用的,可以在程序中隐式的获取锁。但是对于它的使用方式是固化的,只能先获取在释放。而且在使用的过程中,当一个线程获取到某个资源的锁后,其他线程再要获取锁资源则必须进行等待,synchronized并没有提供中断或超时获取的操作。

为了解决这些问题,所以才出现了显示锁。在显示锁中提供了三个很常见的方法:lock()、unLock()、try Lock().

image.png

lock的标准用法

//加锁
lock.lock();
//业务逻辑
try{
    i++;
}finally{
    //解锁
    lock.unLock();
}

不要将获取的过程写在try中,因为如果在获取锁时发生异常,异常抛出的同时会导致锁的释放。

在finally块释放锁,目的是保证获取到锁之后,最终能够将锁释放

何时使用Synchronized还是Lock

如果在使用锁的过程中,不需要考虑尝试获取锁或锁中断的这些特性的话。尽量使用synchronized。因为synchronized在现在的JDK中优化还是很多的,如锁优化升级。

同时synchronized要不显示锁消耗的内存要少,为什么呢?因为synchronized是一个语言层面上的内容,而lock是一个接口,在使用lock时需要获取其对象实例化后才能进行操作,特别在锁很多的情况下,如果没有特殊要求,建议使用synchronized。

ReentrantLock

标准使用方式

根据源码可知Lock本身是一个接口,那么对于其实现类来说,最常用的就是ReentrantLock。

image.png

那么ReentarntLock应该如何使用呢?其实很简单,只要遵循其规范即可:

//通过两个线程对value进行count次自增
public class LockTest extends Thread{

    private static int count = 100000;
    private static int value = 0;

    private static Lock lock = new ReentrantLock();

    @Override
    public void run() {

        for (int i = 0; i < count; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+" : "+value);
                value++;
            }finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        LockTest l1 = new LockTest();
        LockTest l2 = new LockTest();

        l1.start();
        l2.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println(value);
    }
}

在加锁时务必注意,对于解锁需要在finally进行,因为在执行业务逻辑时,有可能会发生异常导致锁无法被释放。

而synchronized的使用要么作用在方法上,要么作用在语句块。当出现异常后,代表脱离了执行的代码块,锁自然就会释放。而显示锁本身就是一个对象的实例,如果加锁之后,没有进行释放操作的话,那么锁就会一直存在。

可重入

ReentrantLock一般会把它称之为可重入锁,其是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。

简单来说就是:同一个线程对于已经获取的锁,可以多次继续申请到该锁的使用权。而synchronized关键字隐式支持重进入,比如一个synchronized修饰的递归方法,在方法执行的时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

其内部的实现流程为:

  1. 每个锁关联一个线程持有者和计数器,当计数器为0时,表示该锁没有被任何线程持有,那么线程都会可能获得该锁而调用对应的方法。
  2. 当某个线程请求成功后,JVM会记录锁的持有线程,并将计数器置为1,此时其他线程请求获取锁,则必须等待。
  3. 当持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器递增。
  4. 当持有锁的线程退出同步代码块时,计数器递减,当计数器为0时,则释放锁

synchronized可重入

public class SynDemo {

    public static synchronized void lock1(){
        System.out.println("lock1");
        lock2();
    }

    public static synchronized void lock2(){
        System.out.println("lock2");
    }
    
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                lock1();
            }
        }.start();
    }
}

执行结果

lock1
lock2

根据结果可以看到,当同一个线程调用多个同步方法时,当其第一次获取锁成功时,接着带哦用其它同步方法时,仍然可以继续向下调用,不会发生阻塞。实现了锁的可重入。

ReentrantLock可重入

public class ReentrantTest {

    private static Lock lock = new ReentrantLock();
    private static int count = 0;

    public static int getCount() {
        return count;
    }

    public void test1(){
        lock.lock();
        try {
            count++;
            test2();
        }finally {
            lock.unlock();
        }
    }

    public void test2(){
        lock.lock();
        try {
            count++;
        }finally {
            lock.unlock();
        }
    }

    static class MyThread implements Runnable{

        private ReentrantTest reentrantTest;

        public MyThread(ReentrantTest reentrantTest) {
            this.reentrantTest = reentrantTest;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                reentrantTest.test1();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        ReentrantTest reentrantTest = new ReentrantTest();
        new Thread(new MyThread(reentrantTest)).start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println(count);
    }
}

运行可以发现,虽然进行了多次加锁,但是并没有阻塞,代表其也是支持可重入的。

公平锁与非公平锁

原理

在多线程执行并发中,当有多个线程同时来获取同一把锁,如果是按照谁等待时间最长,谁先获取锁,则代表是一把公平锁。反之如果是随机获取的,CPU时间片轮询到哪个线程,哪个线程就获取锁,则代表是一把非公平锁。

那么公平锁和非公平锁哪个性能好呢?答案是非公平锁性能好,因为充分利用了CPU,减少线程唤醒的上下文切换的时间。

image.png

公平锁

image.png

非公平锁

image.png

代码实现

在ReentarntLock和synchroized中,默认都是非公平锁,ReentrantLock可以通过参数将其开启使用公平锁。

image.png

1)ReentrantLock公平锁

public class FairLockTest {

    //开启公平锁
    private static Lock lock = new ReentrantLock(true);

    public static void test(){

        for (int i = 0; i < 2; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        new Thread("线程A"){
            @Override
            public void run() {
                test();
            }
        }.start();

        new Thread("线程B"){
            @Override
            public void run() {
                test();
            }
        }.start();
    }
}

根据结果可以看到,其获取锁的过程是按照公平策略来进行。

2)ReentrantLock非公平锁

只需要在实例化ReentrantLock时,不传入参数即为非公平锁。 根据执行结果可以看到,是按照非公平策略来进行锁的获取。

ReentrantLock与synchronized的比较

相似点:

都是以阻塞性方式进行加锁同步的,也就是说如果一个线程获取了对象锁,执行同步代码块,则其它线程要访问都要阻塞等待,直到获取锁的线程释放了锁才能进行访问。

不同点:

  1. 对于synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm的实现,而ReentrantLock它是JDK1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来实现完成。
  2. synchronized的使用比较方便简洁,并且由编译器保证锁的释放和加锁,而ReentrantLock需要手工声明来加锁和释放锁,为了避免手工释放锁造成死锁,所以最好在finally中声明释放锁。
  3. ReentrantLock的锁粒度和灵活度要优于synchronized。

ReentrantReadWriteLock

对于之前学习的ReentrantLock或者synchronized都可以称之为独占锁、排它锁,可以理解为是悲观锁,这些锁在同一时刻只允许一个线程进行访问。但是对于互联网项目来说,绝大多数场景都是读多写少,比例大概在10:1。按照数据库的场景来说,对于读多写少的处理,就会进行读写分离。

在读多写少的场景下,对业务代码的处理,此时也可以考虑进行读写分别加锁的操作,此时就可以使用ReentrantReadWriteLock。其对ReadWriteLock接口进行实现,内部会维护一对锁,分别为读锁和写锁。

image.png

读写锁特性

读操作不互斥、写操作互斥、读写互斥

公平性:支持公平性和非公平性

重入性:支持锁重入

锁降级:写锁能降级为读锁,遵循获取写锁、获取读锁在写锁的次序。读锁不能升级为写锁。

读写锁实现原理

ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于读操作,一个用于写操作。

image.png

ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁,ReentrantReadWriteLock定义如下:

image.png

其内部的writeLock()用于获取写锁,readLock()用于读锁

读写锁演示

读写锁的特点在于写互斥、读不互斥、读写互斥。下面就通过例子来演示具体效果:

public class ReentrantReadWriteLockDemo {

    private static int count = 0;

    private static class WriteDemo implements Runnable{

        ReentrantReadWriteLock lock ;

        public WriteDemo(ReentrantReadWriteLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.writeLock().lock();
                count++;
                System.out.println("写锁: "+count);
                lock.writeLock().unlock();
            }
        }
    }

    private static class ReadDemo implements Runnable{
        ReentrantReadWriteLock lock ;

        public ReadDemo(ReentrantReadWriteLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {

            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.readLock().lock();
            count++;
            System.out.println("读锁: "+count);
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        WriteDemo writeDemo = new WriteDemo(lock);
        ReadDemo readDemo = new ReadDemo(lock);
        //运行多个写线程,不会重复,证明写互斥
        //运行多个读线程,可能重复,证明读不互斥
        //同时运行,读锁和写锁后面不会出现重复的数字,证明读写互斥
        for (int i = 0; i < 3; i++) {
            new Thread(writeDemo).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(readDemo).start();
        }
    }
}

锁降级

读写锁是支持锁降级的,但不支持锁升级。写锁可以被降级为读锁,但读锁不能被升级写锁。什么意思呢?简单来说就是获取到了写锁的线程能够再次获取到同一把锁的读锁,因为支持提到过ReentrantReadWriteLock这把锁内部是维护了两个锁的。 而获取到了读锁的线程不能再次获取同一把锁的写锁

1)写锁降级读锁

public class LockDegradeDemo1 {

    private static class Demo{

        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        public void fun1(){
            //获取写锁
            lock.writeLock().lock();
            System.out.println("fun1");
            fun2();
            lock.writeLock().unlock();
        }

        public void fun2(){
            //获取读锁
            lock.readLock().lock();
            System.out.println("fun2");
            lock.readLock().unlock();

        }
    }

    public static void main(String[] args) {

        new Demo().fun1();
    }
}

根据执行结果可知,当一个线程获取到了写锁后,其可以继续向下来获取同一把锁的读锁。

2)读锁升级写锁

public class LockDegradeDemo2 {

    private static class Demo{

        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        public void fun1(){
            //获取写锁
            lock.writeLock().lock();
            System.out.println("fun1");
            //fun2();
            lock.writeLock().unlock();
        }

        public void fun2(){
            //获取读锁
            lock.readLock().lock();
            System.out.println("fun2");
            fun1();
            lock.readLock().unlock();

        }
    }

    public static void main(String[] args) {

        new Demo().fun2();
    }
}

根据执行结果可知。当线程获取到读锁,不能继续获取写锁。

性能优化演示

在读多写少的情况下,通过读写锁可以优化原有的synchronized对于程序执行的性能。

public class Sku {

    private String name;
    private double totalMoney;//总销售额
    private int storeNumber;//库存数

    public Sku(String name, double totalMoney, int storeNumber) {
        this.name = name;
        this.totalMoney = totalMoney;
        this.storeNumber = storeNumber;
    }

    public double getTotalMoney() {
        return totalMoney;
    }

    public int getStoreNumber() {
        return storeNumber;
    }

    public void changeNumber(int sellNumber){
        this.totalMoney += sellNumber*25;
        this.storeNumber -= sellNumber;
    }
}
public interface SkuService {

    //获得商品的信息
    Sku getSkuInfo();

    //设置商品的数量
    void setNum(int number);
}

以synchronized形式运行

public class SkuServiceImplSync implements SkuService{

    private Sku sku;

    public SkuServiceImplSync(Sku sku) {
        this.sku = sku;
    }

    @Override
    public synchronized Sku getSkuInfo() {
        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return this.sku;
    }

    @Override
    public synchronized void setNum(int number) {
        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sku.changeNumber(number);
    }
}
public class SkuExec {

    //读写线程的比例
    static final int readWriteRatio = 10;

    //最少线程数
    static final int minthreadCount = 3;

    //读操作
    private static class ReadThread implements Runnable{

        private SkuService skuService;

        public ReadThread(SkuService skuService) {
            this.skuService = skuService;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {
                skuService.getSkuInfo();
            }
            System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
                               +(System.currentTimeMillis()-start)+"ms");
        }
    }

    //写操作
    private static class WriteThread implements Runnable{

        private SkuService skuService;

        public WriteThread(SkuService skuService) {
            this.skuService = skuService;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            Random r = new Random();
            for(int i=0;i<10;i++){//操作10次
                skuService.setNum(r.nextInt(10));
            }
            System.out.println(Thread.currentThread().getName()
                               +"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Sku sku = new Sku("computer",10000,10000);
        SkuService skuService = new SkuServiceImplSync(sku);

        for(int i = 0;i<minthreadCount;i++){

            Thread setT = new Thread(new WriteThread(skuService));

            for(int j=0;j<readWriteRatio;j++) {
                Thread getT = new Thread(new ReadThread(skuService));
                getT.start();
            }

            TimeUnit.MILLISECONDS.sleep(100);
            setT.start();
        }
    }
}

执行结果

Thread-0写商品数据耗时:58ms---------
Thread-11写商品数据耗时:58ms---------
Thread-22写商品数据耗时:58ms---------
Thread-4读取商品数据耗时:2577ms
Thread-5读取商品数据耗时:3029ms
Thread-6读取商品数据耗时:3040ms
Thread-9读取商品数据耗时:4016ms
Thread-29读取商品数据耗时:4980ms
Thread-15读取商品数据耗时:8971ms
Thread-13读取商品数据耗时:9742ms
Thread-18读取商品数据耗时:10257ms
Thread-19读取商品数据耗时:10417ms
Thread-25读取商品数据耗时:10805ms
Thread-26读取商品数据耗时:11250ms
Thread-27读取商品数据耗时:11645ms
Thread-31读取商品数据耗时:12137ms
Thread-3读取商品数据耗时:13257ms
Thread-7读取商品数据耗时:13714ms
Thread-10读取商品数据耗时:13911ms
Thread-32读取商品数据耗时:13730ms
Thread-28读取商品数据耗时:14101ms
Thread-23读取商品数据耗时:14409ms
Thread-1读取商品数据耗时:14808ms
Thread-20读取商品数据耗时:14986ms
Thread-17读取商品数据耗时:15150ms
Thread-14读取商品数据耗时:15691ms
Thread-16读取商品数据耗时:16312ms
Thread-21读取商品数据耗时:16494ms
Thread-24读取商品数据耗时:16514ms
Thread-30读取商品数据耗时:16637ms
Thread-8读取商品数据耗时:16867ms
Thread-2读取商品数据耗时:16982ms
Thread-12读取商品数据耗时:16986ms

以ReentrantReadWriteLock形式执行

public class SkuServiceImplReen implements SkuService{

    private Sku sku;

    public SkuServiceImplReen(Sku sku) {
        this.sku = sku;
    }

    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    @Override
    public Sku getSkuInfo() {
        readLock.lock();
        try {
            TimeUnit.MILLISECONDS.sleep(5);
            return this.sku;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            readLock.unlock();
        }

    }

    @Override
    public  void setNum(int number) {
        writeLock.lock();
        try {
            TimeUnit.MILLISECONDS.sleep(5);
            sku.changeNumber(number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }
}

修改启动类

SkuService skuService = new SkuServiceImplReen(sku);

执行结果

Thread-0写商品数据耗时:66ms---------
Thread-11写商品数据耗时:68ms---------
Thread-22写商品数据耗时:62ms---------
Thread-2读取商品数据耗时:765ms
Thread-8读取商品数据耗时:764ms
Thread-10读取商品数据耗时:764ms
Thread-5读取商品数据耗时:765ms
Thread-1读取商品数据耗时:765ms
Thread-4读取商品数据耗时:765ms
Thread-7读取商品数据耗时:764ms
Thread-6读取商品数据耗时:764ms
Thread-3读取商品数据耗时:765ms
Thread-9读取商品数据耗时:770ms
Thread-15读取商品数据耗时:760ms
Thread-17读取商品数据耗时:760ms
Thread-12读取商品数据耗时:760ms
Thread-13读取商品数据耗时:760ms
Thread-14读取商品数据耗时:760ms
Thread-18读取商品数据耗时:759ms
Thread-16读取商品数据耗时:760ms
Thread-20读取商品数据耗时:765ms
Thread-19读取商品数据耗时:765ms
Thread-21读取商品数据耗时:765ms
Thread-31读取商品数据耗时:704ms
Thread-28读取商品数据耗时:705ms
Thread-25读取商品数据耗时:705ms
Thread-30读取商品数据耗时:704ms
Thread-29读取商品数据耗时:705ms
Thread-27读取商品数据耗时:705ms
Thread-26读取商品数据耗时:705ms
Thread-23读取商品数据耗时:705ms
Thread-24读取商品数据耗时:705ms
Thread-32读取商品数据耗时:704ms

根据最终结果可以看出,其性能的提升是非常巨大的。

StamptedLock

stamptedLock类是在JDK8引入的一把新锁,其是对原有的ReentrantReadWriteLock读写锁的增强,增加了一个乐观读模式,内部提供了相关API不仅优化了读锁、写锁的访问,也可以让读锁和写锁间相互转换,从而更细粒度的控制并发。

ReentrantReadWriteLock存在的问题

在使用读写锁时,还容易出现写线程饥饿的问题。主要是因为读锁和写锁互斥。比方说:当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。

对于写线程饥饿问题,可以通过公平锁进行一定程度的解决,但是它是以牺牲系统吞吐量为代价的。

StampedLock特点

1)获取锁的方法,会返回一个票据(stamp),当该值为0代表获取锁失败,其他值都代表成功。

2)释放锁的方法,都需要传递获取锁时返回的票据,从而控制是同一把锁。

3)StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁就会造成死锁。

4)StampedLock提供了三种模式控制读写操作:写锁、悲观读锁、乐观读锁

写锁: 使用类似于ReentrantReadWriteLock,是一把独占锁,当一个线程获取该锁后,其他请求线程会阻塞等待。 对于一条数据没有线程持有写锁或悲观读锁时,才可以获取到写锁,获取成功后会返回一个票据,当释放写锁时,需要传递获取锁时得到的票据。

悲观读锁: 使用类似于ReentrantReadWriteLock,是一把共享锁,多个线程可以同时持有该锁。当一个数据没有线程获取写锁的情况下,多个线程可以同时获取到悲观读锁,当获取到后会返回一个票据,并且阻塞线程获取写锁。当释放锁时,需要传递获取锁时得到的票据。

乐观读锁: 这把锁是StampedLock新增加的。可以把它理解为是一个悲观锁的弱化版。当没有线程持有写锁时,可以获取乐观读锁,并且返回一个票据。值得注意的是,它认为在获取到乐观读锁后,数据不会发生修改,获取到乐观读锁后,其并不会阻塞写入的操作。 那这样的话,它是如何保证数据一致性的呢? 乐观读锁在获取票据时,会将需要的数据拷贝一份,在真正读取数据时,会调用StampedLock中的API,验证票据是否有效。如果在获取到票据到使用数据这期间,有线程获取到了写锁并修改数据的话,则票据就会失效。 如果验证票据有效性时,当返回true,代表票据仍有效,数据没有被修改过,则直接读取原有数据。当返回flase,代表票据失效,数据被修改过,则重新拷贝最新数据使用。 乐观读锁使用与一些很短的只读代码,它可以降低线程之间的锁竞争,从而提高系统吞吐量。但对于读锁获取数据结果必须要进行校验。

5)在StampedLock中读锁和写锁可以相互转换,而在ReentrantReadWriteLock中,写锁可以降级为读锁,而读锁不能升级为写锁。

使用示例:

class Point {

    //定义共享数据
    private double x, y;

    //实例化锁
    private final StampedLock sl = new StampedLock();

    //写锁案例
    void move(double deltaX, double deltaY) { 

        //获取写锁
        long stamp = sl.writeLock();

        try {
            x += deltaX;
            y += deltaY;
        } finally {
            //释放写锁
            sl.unlockWrite(stamp);
        }
    }

    //使用乐观读锁案例
    double distanceFromOrigin() { 

        long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁

        double currentX = x, currentY = y; //将两个字段读入本地局部变量

        if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?

            stamp = sl.readLock(); //如果有,我们再次获得一个读悲观锁
            try {
                currentX = x; // 将两个字段读入本地局部变量
                currentY = y; // 将两个字段读入本地局部变量
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    //使用悲观读锁并锁升级案例
    void moveIfAtOrigin(double newX, double newY) {

        // 获取悲观读锁
        long stamp = sl.readLock();

        try {
            while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合

                //锁升级,将读锁转为写锁
                long ws = sl.tryConvertToWriteLock(stamp); 

                //确认转为写锁是否成功
                if (ws != 0L) { 
                    stamp = ws; //如果成功 替换票据
                    x = newX; //进行状态改变
                    y = newY; //进行状态改变
                    break;
                }
                else { //如果不成功
                    sl.unlockRead(stamp); //显式释放读锁
                    stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
                }
            }
        } finally {
            //释放读锁或写锁
            sl.unlock(stamp); 
        }
    }
}