ReentrantLock初认知

·  阅读 92

原博客地址:blog.csdn.net/weixin_3931…

ReentrantLock使用

public class NonFairReentrantLock {

    public static void main(String[] args) {

        final  ReentrantLock lock = new ReentrantLock();

        for (int i = 0; i < 10; i++) {
            new Thread("线程: "+i){
                @Override
                public void run() {
                    lock.lock();  // block until condition holds
                    try {
                        // ... method body
                        System.out.println(Thread.currentThread().getName()+" 开始执行!");
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName()+" 执行结束!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }.start();
        }
        // 主线程阻塞
        LockSupport.park();
    }
}

上面启动了10个线程分别去打印两句话,两句话中间线程会睡眠100ms。通过ReentrantLock的lock和unlock方法来加锁和释放锁。

执行结果:

线程: 0 开始执行!
线程: 0 执行结束!
线程: 1 开始执行!
线程: 1 执行结束!
线程: 2 开始执行!
线程: 2 执行结束!
线程: 3 开始执行!
线程: 3 执行结束!
线程: 4 开始执行!
线程: 4 执行结束!
线程: 5 开始执行!
线程: 5 执行结束!
线程: 6 开始执行!
线程: 6 执行结束!
线程: 7 开始执行!
线程: 7 执行结束!
线程: 8 开始执行!
线程: 8 执行结束!
线程: 9 开始执行!
线程: 9 执行结束!

每个线程都保证了 lock与nulock之间的方法块的互斥性。

推测自己怎么实现(不要较真代码只是屡思路)

ReentrantLock有哪些特性:

  • 共享、独占
  • 可重入性
  • 可中断
  • 公平/非公平

下面我们自己挨个来实现这些特性

共享、独占(互斥性就是同一时刻只有一个线程执行互斥性的代码)

1、直接通过synchronized关键字肯定可以实现,但是我们肯定不是直接用这个。代码就不演示了

2、CAS 加while循环来实现。

public class NonFairReentrantLock {
    // 标记是否被加锁,一定要加volatile,否则线程CAS成功之后其他线程没法感知到
    private volatile  int status;
    // java 写CAS的 标准代码。拷贝就行 --start
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static  long statusOffset;
    static {
        try {
            statusOffset = unsafe.objectFieldOffset(NonFairReentrantLock.class.getDeclaredField("status"));
        } catch (NoSuchFieldException e) {
            
        }
    }
    public boolean CAS(int expect, int update){
        return unsafe.compareAndSwapInt(this, statusOffset,expect,update);
    }
    //java 写CAS的 标准代码。拷贝就行 --end
    private void lock(){
        while (true){
            if(CAS(0,1)){
                return;
            }
        }
    }
    private void unLock(){
      CAS(1,0)
    }
}

// 使用:
NonFairReentrantLock lock = new NonFairReentrantLock()
lock.lock()
  //do something
lock.unlock()

代码逻辑:

  • 多个线程来加锁的时候共同去对status变量赋值从0-1,只要CAS成功就意味着线程拿到了“锁”,就可以执行它的互斥业务代码。

  • 没有拿到锁的线程会一直空循环去在哪里一直尝试CAS。此时逻辑上是没有问题的。

思考:

没有获取到“锁”的线程在哪里死循环,这是致命的。非常的消耗CPU。

解法:

  1. Thread.sleep();
private void lock(){
        while (true){
            if(CAS(0,1)){
                return;
            }else {
              // 没加到锁我们让其睡眠一会?这里我就很纠结了。我到底该睡眠多久。
                Thread.sleep(?????);
            }
        }
    }
  1. Object.wait();
    private void lock() throws InterruptedException {
        while (true){
            if(CAS(0,1)){
                return;
            }else {
                // 是可以等待,但是由谁来唤醒?
                // 的确是可以通过notify(),或者notifyAll()来唤醒,但是这会唤醒该对象关联的所有线程
                // 我们最好只唤醒一个,这样才能避免多个线程再次无效的竞争
                wait();
            }
        }
    }
  1. UNSAFE.park() + UNSAFE.unPark() + 阻塞容器
    private final Deque<Thread> deque = new LinkedList();
    //java 写CAS的 标准代码。拷贝就行 --end
    private void lock() throws InterruptedException {
        while (true){
            if(CAS(0,1)){
                return;
            }else {
                // 阻塞了就放到 阻塞容器里。等别的线程来唤醒
                deque.push(Thread.currentThread());
                // 调用底层的park方法,直接阻塞线程
                UNSAFE.park(false, 0L);// 此处代码只是表示park意思,代码不全
            }
        }
    }
    // 已经获取到锁的线程来释放锁
    private void unLock(){
      	CAS(1,0);
        // unPark()方法可以指定的唤醒一个指定的线程。但是此处有点疑问,这个指定的thread怎么来?
        // 如果在线程被阻塞的时候直接把阻塞的线程引用放到一个容器中。我们从容器中获取一个被阻塞的线程即可。
        Thread thread = deque.pop();
        UNSAFE.unPark(thread);
    }

至此,我觉得解决 独占这个问题思路基本ok了(手动滑稽)。

但是经不起推敲 LinkedList不是可不是线程安全的,如果很多个阻塞线程一起往里面push元素,也会出现线程安全问题。而且线程永远不会被唤醒,等于被遗弃了。那么该线程就会死在那里。但是这个问题我们先遗忘。关注主题思路

可重入性

先解释一下什么是可重入性,对一个锁可以加锁多次,但是必须释放同样的次数。

public class NonFairReentrantLock {
    public static void main(String[] args) {

        final  ReentrantLock lock = new ReentrantLock();

        for (int i = 0; i < 10; i++) {
            new Thread("线程: "+i){
                @Override
                public void run() {
                    lock.lock();  // block until condition holds
                    lock.lock();
                    lock.lock();
                    lock.lock();
                    try {
                        // ... method body
                        System.out.println(Thread.currentThread().getName()+" 开始执行!");
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName()+" 执行结束!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                        lock.unlock();
                        lock.unlock();
                        lock.unlock();
                    }
                }
            }.start();
        }
        // 主线程阻塞
        LockSupport.park();
    }
}

1、既然支持重入,我们肯定得在lock对象里面记录获取锁的线程,下次该线程再来加锁的时候对比一下。如果还是它自己我们只要将status+1,解锁的时候将status-1就可以了

public class NonFairReentrantLock {
    // 标记是否被加锁
    private volatile int status;
    // java 写CAS的 标准代码。拷贝就行 --start
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static long statusOffset;
    static {
        try {
            statusOffset = unsafe.objectFieldOffset(NonFairReentrantLock.class.getDeclaredField("status"));
        } catch (NoSuchFieldException e) {
        }
    }
    public boolean CAS(int expect, int update) {
        return unsafe.compareAndSwapInt(this, statusOffset, expect, update);
    }
    private final Deque<Thread> deque = new LinkedList();
    private volatile Thread currentThread = null;
    //java 写CAS的 标准代码。拷贝就行 --end
    private void lock() throws InterruptedException {
        while (true) {
            if (CAS(0, 1)) {
                currentThread = Thread.currentThread();
                return;
            } else if (currentThread ==Thread.currentThread()) {
                CAS(status,status+1);
                return;
            } else {
                // 阻塞了就放到 阻塞容器里。等别的线程来唤醒
                deque.push(Thread.currentThread());
                // 调用底层的park方法,直接阻塞线程
                UNSAFE.park(false, 0L);// 此处代码只是表示park意思,代码不全
            }
        }
    }
    // 已经获取到锁的线程来释放锁
    private void unLock() {
        CAS(status,status-1);
        if(status==0){
            // unPark()方法可以指定的唤醒一个指定的线程。但是此处有点疑问,这个指定的thread怎么来?
            // 如果在线程被阻塞的时候直接把阻塞的线程引用放到一个容器中。我们从容器中获取一个被阻塞的线程即可。
            Thread thread = deque.pop();
            UNSAFE.unPark(thread);
        }
    }
}

可中断 :

线程执行的过程中允许被别的线程中断。最流行的是取消线程上的某些任务.

例如:在应用socket编程的时候,需要创建一个serversocket实例,serversocket类的accept方法就是阻塞方法,即accept会一直等在那里,直到有一个连接请求到达,程序才继续执行。这时候问题就来了,如果没有连接请求,程序会一直阻塞在那里,即不会往下执行,这时候我们就需要中断他。例如可以设置一个等待时间,如果超过此时间,就中断accept方法。

JDK1.5的时候如果想停止一个线程的任务,可以用stop()方法,但是这会直接杀死线程,是非常暴力的。如果这个线程在处理比较重要的逻辑直接杀死线程是非常尴尬的。

微服务的应用优雅下线,肯定不是直接杀死线程,而是传递一个中断信号。让业务自己去处理好中断逻辑。

或者线程在批量提交数据,此时直接杀死线程。肯定会出现数据丢失的问题

公平\非公平

何为公平:谁等的时间长,谁就先被唤醒

何为不公平:唤醒的时候不一定就根据等待时间来决定。可能会被后来的线程提前抢到锁。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改