Lock、synchronized和ReadWriteLock,StampedLock戳锁的区别和联系以及Condition

https://www.cnblogs.com/RunForLove/p/5543545.html

先来看一段代码,实现如下打印效果:

1 2 A 3 4 B 5 6 C 7 8 D 9 10 E 11 12 F 13 14 G 15 16 H 17 18 I 19 20 J 21 22 K 23 24 L 25 26 M 27 28 N 29 30 O 31 32 P 33 34 Q 35 36 R 37 38 S 39 40 T 41 42 U 43 44 V 45 46 W 47 48 X 49 50 Y 51 52 Z 

复制代码
package test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        ThreadA a = new ThreadA(lock, condition1, condition2);
        ThreadB b = new ThreadB(lock, condition1, condition2);
        a.start();
        b.start();
    }
}

class ThreadA extends Thread {

    Lock      lock;
    Condition condition1;
    Condition condition2;

    public ThreadA(Lock lock, Condition condition1, Condition condition2){
        super();
        this.lock = lock;
        this.condition1 = condition1;
        this.condition2 = condition2;
    }

    @Override
    public void run() {
        try {
            lock.lock();
            for (int i = 1; i <= 52; i++) {
                if (i % 2 != 0) {
                    System.out.print(i + " " + (i + 1));
                    condition2.signal();
                } else {
                    condition1.await();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

class ThreadB extends Thread {

    Lock      lock;
    Condition condition1;
    Condition condition2;

    public ThreadB(Lock lock, Condition condition1, Condition condition2){
        super();
        this.lock = lock;
        this.condition1 = condition1;
        this.condition2 = condition2;
    }

    @Override
    public void run() {
        try {
            lock.lock();
            for (int i = (int) 'A'; i < (int) ('A' + 52); i++) {
                System.out.print(" " + (char) i + " ");
                condition1.signal();
                condition2.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
复制代码

上面的代码是我们在项目中使用类Condition和类ReentrantLock的示例,接下来对这几个概念阐述。

Synchronized和Lock的简单对比:

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

  我们知道如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法),获取锁的线程被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这非常影响程序的执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

  再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

  总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个接口,通过这个接口可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

复制代码
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
复制代码

在Lock接口中,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

  首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

复制代码
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
复制代码

  其次tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。通常这样使用:

复制代码
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
复制代码

  lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此lockInterruptibly()一般的使用形式如下:

复制代码
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
复制代码

当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

  总结:lock( )方法,如果锁已被其他线程获取,则会等待。tryLock( )方法,拿不到锁时不会一直在那等待。lockInterruptibly( )方法能够响应中断。ReentrantLock,意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。常用方法Demo如下:

复制代码
package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestLockMethod {

    private List<Object> list = new ArrayList<Object>();
    Lock                 lock = new ReentrantLock();

    public static void main(String[] args) {
        final TestLockMethod test = new TestLockMethod();

        new Thread() {

            @Override
            public void run() {
                test.insert(Thread.currentThread());
            }
        }.start();

        new Thread() {

            @Override
            public void run() {
                test.insert(Thread.currentThread());
            }
        }.start();
    }

    public void insert(Thread thread) {
        System.out.println("线程" + thread.getName() + "进来了");
        lock.lock();
        try {
            System.out.println(thread.getName() + "得到了锁");
            for (int i = 0; i < 100; i++) {
                Thread.sleep(9000);
                list.add(Integer.valueOf(i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName() + "释放了锁");
            try {
                Thread.sleep(9000);
                lock.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序运行结果:
线程Thread-0进来了
Thread-0得到了锁
线程Thread-1进来了
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁
复制代码

上述运行结果中Thread-1等待线程Thread-0释放锁后才执行的。lock( )方法中,如果锁被其他线程获取,则会等待。接下来,再看tryLock( )方法的使用,如下:

复制代码
package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestTryLock {

    private List<Object> list = new ArrayList<Object>();
    private Lock         lock = new ReentrantLock();

    public static void main(String[] args) {
        final TestTryLock test = new TestTryLock();
        new Thread("第一个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();

        new Thread("第二个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();
    }

    public void doSomething(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println(thread.getName() + "得到了锁.");
                for (int i = 0; i < 10; i++) {
                    list.add(i);
                    Thread.sleep(10);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + "释放了锁.");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName() + "获取锁失败.");
        }
    }
}

运行结果:
第一个线程  得到了锁.
第二个线程  获取锁失败.
第一个线程  释放了锁.
复制代码

作为对比,我们再看如下代码:

复制代码
package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestTryLock {

    private List<Object> list = new ArrayList<Object>();
    private Lock         lock = new ReentrantLock();

    public static void main(String[] args) {
        final TestTryLock test = new TestTryLock();
        new Thread("第一个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();

        new Thread("第二个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();
    }

    public void doSomething(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println(thread.getName() + "得到了锁.");
                for (int i = 0; i < 10; i++) {
                    list.add(i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + "释放了锁.");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName() + "获取锁失败.");
        }
    }
}

运行结果:
第一个线程  得到了锁.
第一个线程  释放了锁.
第二个线程  得到了锁.
第二个线程  释放了锁.
复制代码

我们可以假想这样一种情况,for循环中是我们要执行的业务逻辑。而执行时间的长短,会引起输出的不可控。所以我们对于tryLock( )的使用,如果获取不到锁"//TODO",如果获取到了锁"//TODO"。

复制代码
package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestLockInterruptibly {

    private Lock         lock = new ReentrantLock();
    private List<Object> list = new ArrayList<Object>();

    public static void main(String[] args) {
        TestLockInterruptibly test = new TestLockInterruptibly();
        MyThread threadA = new MyThread(test);
        MyThread threadB = new MyThread(test);
        threadA.start();
        threadB.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadB.interrupt();
    }

    public void doSomething(Thread thread) throws InterruptedException {
        // 如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        lock.lockInterruptibly();
        try {
            System.out.println(thread.getName() + "得到了锁.");
            long startTm = System.currentTimeMillis();
            int i = 0;
            for (;;) {
                if (System.currentTimeMillis() - startTm >= 10000) {
                    break;
                }
                list.add(i++);
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "执行了finally方法.");
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁.");
        }
    }
}

class MyThread extends Thread {

    private TestLockInterruptibly test = null;

    public MyThread(TestLockInterruptibly test){
        super();
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.doSomething(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "被中断");
        }
    }
}

运行结果:
Thread-0得到了锁.
Thread-1被中断
Thread-0执行了finally方法.
Thread-0释放了锁.
复制代码

  lockInterruptibly()是可响应中断锁的使用实例。当Thread-1没有获取到锁,处于等待状态的时候,可以接受响应中断,而放弃锁等待。这点lock( )方法是做不到响应中断的,lock方法会一直阻塞。[在调用tryLock方法之后会立即返回结果,根据是否获得锁然后做出相应的业务操作]。

  针对前面说的synchronized对于多个读的情况,效率很差劲。JDK中针对读写,提供了接口ReadWriteLock,其定义:

private interface ReadWriteLock{
    Lock readLock();
    Lock writeLock();
}

接口ReadWriteLock中仅定义了两个方法:readLock方法和writeLock方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。类ReentrantReadWriteLock实现了ReadWriteLock接口中的两个方法:读锁和写锁。

复制代码
package concurrent;

public class TestReadWriteLock {

    public static void main(String[] args) {
        final TestReadWriteLock test = new TestReadWriteLock();

        new Thread("第一个线程") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();

        new Thread("第二个线程") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();
    }

    public synchronized void doSomething(Thread thread) {
        long start = System.currentTimeMillis();
        while (System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName() + "  正在进行读操作.");
        }
        System.out.println(thread.getName() + "  完成读操作.");
    }
}

运行结果:
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  正在进行读操作.
第一个线程  完成读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  正在进行读操作.
第二个线程  完成读操作.
复制代码

上面代码中,使用了关键字synchronized来控制并发,我们可以看到,对于读操作,效率很慢。如果改成读写锁的话,测试代码如下所示:

复制代码
package concurrent;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TestReadLock {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final TestReadLock test = new TestReadLock();
        new Thread("第一个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();

        new Thread("第二个线程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();
    }

    private void doSomething(Thread thread) {
        lock.readLock().lock();
        try {
            long startTm = System.currentTimeMillis();
            while (System.currentTimeMillis() - startTm <= Integer.MAX_VALUE) {
                System.out.println(thread.getName() + "正在读操作.");
            }
            System.out.println(thread.getName() + "读操作完毕.");
        } finally {
            lock.readLock().unlock();
        }
    }
}

运行结果如下:
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第二个线程  正在读操作.
第二个线程  正在读操作.
第二个线程  正在读操作.
第二个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第二个线程  正在读操作.
第二个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作.
第一个线程  正在读操作....
复制代码

上述代码中,两个线程可以同时进行读操作,这样就大大的提升了读操作的效率问题。在类ReentrantReadWriteLock中,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

总结来说,Lock和synchronized有以下几点不同:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

锁的相关概念:可重入锁、可中断锁、公平锁、读写锁。

复制代码
class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}
复制代码

如上代码,是可重入锁的示例。如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和lock都具备可重入性,所以不会发生此现象。

  可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

/**
 * 如果参数为true表示为公平锁,为fasle为非公平锁。
 * 默认情况下,如果使用无参构造器,则是非公平锁。
**/

ReentrantLock lock = new ReentrantLock(true);

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

4.StampedLock戳锁

StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问。下面是JDK1.8源码自带的示例:

复制代码
 1 public class StampedLockDemo {
 2     //一个点的x,y坐标
 3     private double x,y;
 4     private final StampedLock sl = new StampedLock();
 5 
 6     //【写锁(排它锁)】
 7     void move(double deltaX,double deltaY) {// an exclusively locked method 
 8         /**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化
 9          * 即每次+1,直到加到最大值,然后从0重新开始 
10          **/
11         long stamp =sl.writeLock(); //写锁
12         try {
13             x +=deltaX;
14             y +=deltaY;
15         } finally {
16             sl.unlockWrite(stamp);//释放写锁
17         }
18     }
19 
20     //【乐观读锁】
21     double distanceFromOrigin() { // A read-only method
22         /**
23          * tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
24          * 每次读的时候得到一个当前的stamp值(类似时间戳的作用)
25          */
26         long stamp = sl.tryOptimisticRead();
27         //这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断
28         double currentX = x, currentY = y;
29         /**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,
30          * 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
31          * 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
32          * 读锁又重新返回一个stampe值*/
33         if (!sl.validate(stamp)) {//如果验证失败(读之前已发生写)
34             stamp = sl.readLock(); //悲观读锁
35             try {
36                 currentX = x;
37                 currentY = y;
38             }finally{
39                 sl.unlockRead(stamp);//释放读锁
40             }
41         }
42         //读锁验证成功后执行计算,即读的时候没有发生写
43         return Math.sqrt(currentX *currentX + currentY *currentY);
44     }
45 
46     //【悲观读锁】
47     void moveIfAtOrigin(double newX, double newY) { // upgrade
48         // 读锁(这里可用乐观锁替代)
49         long stamp = sl.readLock();
50         try {
51             //循环,检查当前状态是否符合
52             while (x == 0.0 && y == 0.0) {
53             /**
54              * 转换当前读戳为写戳,即上写锁
55              * 1.写锁戳,直接返回写锁戳
56              * 2.读锁戳且写锁可获得,则释放读锁,返回写锁戳
57              * 3.乐观读戳,当立即可用时返回写锁戳
58              * 4.其他情况返回0
59              */
60             long ws = sl.tryConvertToWriteLock(stamp);
61             //如果写锁成功
62             if (ws != 0L) {
63               stamp = ws;// 替换票据为写锁
64               x = newX;//修改数据
65               y = newY;
66               break;
67             }
68             //转换为写锁失败
69             else {
70                 //释放读锁
71                 sl.unlockRead(stamp);
72                 //获取写锁(必要情况下阻塞一直到获取写锁成功)
73                 stamp = sl.writeLock();
74             }
75           }
76         } finally {
77             //释放锁(可能是读/写锁)
78             sl.unlock(stamp);
79         }
80     }
81 }
复制代码

原理

StampedLockd的内部实现是基于CLH锁的,一种自旋锁,保证没有饥饿且FIFO。

CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。

如上图所示,StampedLockd源码中的WNote就是等待链表队列,每一个WNode标识一个等待线程,whead为CLH队列头,wtail为CLH队列尾,state为锁的状态。long型即64位,倒数第八位标识写锁状态,如果为1,标识写锁占用!下面围绕这个state来讲述锁操作。

首先是常量标识:

WBIT=1000 0000(即-128)

RBIT =0111 1111(即127) 

SBIT =1000 0000(后7位表示当前正在读取的线程数量,清0)

1.乐观读

tryOptimisticRead():如果当前没有写锁占用,返回state(后7位清0,即清0读线程数),如果有写锁,返回0,即失败。

2.校验stamp

校验这个戳是否有效validate():比较当前stamp和发生乐观锁得到的stamp比较,不一致则失败。

3.悲观读

乐观锁失败后锁升级为readLock():尝试state+1,用于统计读线程的数量,如果失败,进入acquireRead()进行自旋,通过CAS获取锁。如果自旋失败,入CLH队列,然后再自旋,如果成功获得读锁,激活cowait队列中的读线程Unsafe.unpark(),最终依然失败,Unsafe().park()挂起当前线程。

4.排它写

writeLock():典型的cas操作,如果STATE等于s,设置写锁位为1(s+WBIT)。acquireWrite跟acquireRead逻辑类似,先自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程。

5.释放锁

unlockWrite():释放锁与加锁动作相反。将写标记位清零,如果state溢出,则退回到初始值;

性能和建议:JDK8之后才有,当高并发下且读远大于写时,由于可以乐观读,性能极高!

5.总结

4种锁,最稳定是内置synchronized锁(并不是完全被替代),当并发量大且读远大于写的情况下最快的的是StampedLock锁(乐观读。近似于无锁)。建议大家采用。

参考地址:

[1] http://www.cnblogs.com/dolphin0520/p/3923167.html

[2] http://ifeve.com/


参考:http://blog.csdn.net/qq_20641565/article/details/53208909

参考:https://www.cnblogs.com/liang1101/p/6475555.html?utm_source=itdadao&utm_medium=referral

原文地址:https://www.cnblogs.com/yuluoxingkong/p/10304171.html