继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

juc-05-Lock与锁分类

黑桃SEVEN_PIG
关注TA
已关注
手记 46
粉丝 10
获赞 8

这篇文章,聊聊 Lock 接口以及它的核心方法,以及锁的分类,各种锁是什么,什么特点,怎么玩。

1 Locksynchronized 的比较

锁是一种工具,用于控制对共享资源的访问。Locksynchronized,这两个是最常见的锁,都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。

在前面文章 juc-03-synchronized、notify、notifyAll、wait、volatile 中,我们学习了 synchronized 关键字创建一把内置锁。

synchronized 内置锁分两种

  • 对象锁:锁的是类的对象实例。作用在实例方法,或者实例代码块上。
  • 类锁:锁的是类的Class对象,每个类的的Class对象在一个JVM中只有一个,所以类锁也只有一个。作用在 static 方法,或者 static 代码块上。

Lock 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或者不足以满足要求的时候,来提供高级功能的。

为什么 synchronized 不够用?
1)效率低:锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
2) 不够灵活(读写锁更灵活) : 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
3)无法知道是否成功获取到锁

Lock 特点:
1)通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现可以允许并发访问,比如 ReadWriteLock 里面的ReadLock
2)获取锁可以被中断超时获取锁尝试获取锁,读多写少用 ReadWriteLock 读写锁

2、Lock 接口和核心方法

2.1 Lock 接口源码

public interface Lock {

    /**
     * 获取锁,如果锁已经被其他线程获取,则进行等待
     */
    void lock();

    /**
     * 尝试获取锁,直到线程被中断
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
     */
    boolean tryLock();

    /**
     * 给定等待时间内尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁
     */
    void unlock();

    /**
     * 返回一个绑定当前 Lock 实例的新的 Condition 实例
     */
    Condition newCondition();
}

2.2、Lock 主要方法介绍

在Lock中声明了四个方法来获取锁和一个释放锁的方法:

  • lock() :获取锁,如果锁已经被其他线程获取,则进行等待
  • tryLock() :尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
  • tryLock(long time, TimeUnit unit) :给定等待时间内尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
  • lockInterruptibly() :尝试获取锁,直到线程被中断
  • unlock() : 释放锁,一般在finally中执行

Lock 最常见的实现类是 ReenTrantLock ,我们使用 ReenTrantLock 了解这四个获取锁的方法有何区别。

2.2.1、lock()

  • lock() 就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
  • Lock不会像 synchronized 一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁 lock.unlock(),以保证发生异常时,锁一定被释放
  • lock()弊端 lock() 方法不能被中断,这会带来很大隐患:一旦陷入死锁lock() 就会陷入永久等待

finally中释放锁

public class LockTest {

    // 显式锁 Lock
    private static Lock lock = new ReentrantLock();

    @Test
    public void testLock(){
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            // 很重要,必须记得在 finally unlock()
            // Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
            lock.unlock();
        }
    }
}

2.2.2、tryLock()

  • tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
  • 功能比 lock() 更强大了,可以根据是否获取到锁,决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时,不会一直在那等

2.2.2、tryLock(long time, TimeUnit unit) 超时就放弃,

用 tryLock 可以避免死锁


    /**
     * tryLock(long time, TimeUnit unit) 避免死锁演示
     * 
     * @throws InterruptedException
     */
    @Test
    public void testTryLock() throws InterruptedException {
        // 模拟两个线程同时抢占 lock1 和 lock2 ,使用 tryLock(long time, TimeUnit unit) 尝试获取锁,并避免死锁发生
        Lock lock1 = new ReentrantLock();
        Lock lock2 = new ReentrantLock();
        new Thread(() -> {
            // 尝试获取 lock1 , 再获取 lock2,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
            lock1Tolock2(lock1, lock2);
        }).start();
        new Thread(() -> {
            // 尝试获取 lock2 , 再获取 lock1,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
            lock2Tolock1(lock1, lock2);
        }).start();
        Thread.sleep(10000);
    }

    /**
     * 尝试获取 lock1 , 再获取 lock2,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
     *
     * @param lock1
     * @param lock2
     */
    private void lock1Tolock2(Lock lock1, Lock lock2) {
        boolean success = false;
        while (!success) {
            try {
                // 先尝试获取 lock1,100ms 内获取不到,则重试
                if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取到了锁1");
                        Thread.sleep(new Random().nextInt(1000));
                        // 尝试获取 lock2,如果 100ms 内获取不到,则返回false
                        if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                            try {
                                success = true;
                                Thread.sleep(new Random().nextInt(1000));
                                System.out.println(Thread.currentThread().getName() + "获取到了锁2");
                                System.out.println(Thread.currentThread().getName() + "成功获取到了两把锁");
                            } finally {
                                //释放锁 lock2
                                lock2.unlock();
                                System.out.println(Thread.currentThread().getName() + "释放了锁2");
                            }
                        } else {
                            System.out.println(Thread.currentThread().getName() + "获取锁2失败,已重试");
                        }
                    } finally {
                        lock1.unlock();
                        System.out.println(Thread.currentThread().getName() + "释放了锁1");
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + "获取锁1失败,已重试");
                }
                // sleep 一会再重试
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 尝试获取 lock2 , 再获取 lock1,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
     *
     * @param lock1
     * @param lock2
     */
    private void lock2Tolock1(Lock lock1, Lock lock2) {
        boolean success = false;
        while (!success) {
            try {
                // 尝试获取 lock2,如果 100ms 内获取不到,则返回false
                if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取到了锁2");
                        Thread.sleep(new Random().nextInt(1000));
                        // 尝试获取 lock1,如果 100ms 内获取不到,则返回false
                        if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                            try {
                                success = true;
                                Thread.sleep(new Random().nextInt(1000));
                                System.out.println(Thread.currentThread().getName() + "获取到了锁1");
                                System.out.println(Thread.currentThread().getName() + "成功获取到了两把锁");
                            } finally {
                                //释放锁 lock2
                                lock1.unlock();
                                System.out.println(Thread.currentThread().getName() + "释放了锁1");
                            }
                        } else {
                            System.out.println(Thread.currentThread().getName() + "获取锁1失败,已重试");
                        }
                    } finally {
                        lock2.unlock();
                        System.out.println(Thread.currentThread().getName() + "释放了锁2");
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + "获取锁2失败,已重试");
                }
                // sleep 一会再重试
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果:

Thread-1获取到了锁2
Thread-0获取到了锁1
Thread-1获取锁1失败,已重试
Thread-1释放了锁2
Thread-1获取锁2失败,已重试
Thread-0获取到了锁2
Thread-0成功获取到了两把锁
Thread-0释放了锁2
Thread-0释放了锁1
Thread-1获取到了锁2
Thread-1获取到了锁1
Thread-1成功获取到了两把锁
Thread-1释放了锁1
Thread-1释放了锁2

2.2.3、lock.lockInterruptibly() 尝试获取锁,直到线程被中断

相当于tryLock(long time, TimeUnit unit)把时间设置为无限。在等待锁的过程中,线程可以被**中断**。


    /**
     * lock.lockInterruptibly() 尝试获取锁,直到线程被中断
     * @throws InterruptedException
     */
    @Test
    public void testLockInterruptibly() throws InterruptedException {
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -> {
            doRun(lock);
        });
        Thread thread2 = new Thread(() -> {
            doRun(lock);
        });
        thread1.start();
        thread2.start();
        // 标志 thread1 中断
        thread2.interrupt();
        Thread.sleep(10000);
    }

    private void doRun(Lock lock) {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
        }
    }
    

运行结果:

Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-1获得锁期间被中断了
Thread-0释放了锁

3、锁的分类

  1. 线程要不要锁住同步资源
    锁住:悲观锁
    不锁柱:乐观锁
  2. 同一个线程是否可以重复获取同一把锁
    可以:可重入锁
    不可以:不可重入锁
  3. 多线程竞争时,是否排队
    排队:公平锁
    先尝试插队,插队失败再排队:非公平锁
  4. 多线程能否共享一把锁
    可以:共享锁
    不可以:独占锁
  5. 等待锁过程
    自旋:自旋锁
    阻塞:非自旋锁
  6. 是否可中断
    可以:可中断锁
    不可以:非可中断锁
  • 这些分类,是从各种不同的角度出发去看的
  • 这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于多种类型,比如 ReentrantLock既是互斥锁,又是可重入锁

4、乐观锁和悲观锁

4.1 为什么会诞生"非互斥同步锁"(乐观锁)——互斥同步锁(悲观锁)的劣势

>互斥同步锁(悲观锁)的劣势
>- 阻塞唤醒 带来的性能劣势
>- 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行。

4.2 什么是乐观锁和悲观锁

>从是否锁住资源的角度分类

悲观锁

>如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问数据,这样就可以确保数据内容万无一失。

java中悲观锁的实现就是 synchronizedLock 相关类。

场景举例:

  1. 线程1和线程2同时抢悲观锁
  2. 线程1抢到锁,线程2等待
  3. 线程1释放锁后,线程2拿到锁
  4. 线程2释放锁

乐观锁

>认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象

更新的时候,去对比在我修改的时候,数据有没有被其他人改变过:

  • 如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据;
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃,报错,重试等策略

乐观锁的实现一般都是利用CAS算法来实现的。

乐观锁逻辑举例,类似 CAS 算法:

  1. 线程1和线程2同时获取共享资源并各自计算
  2. 线程1先计算完成,并判断资源是否已被修改,线程1发现计算期间资源没有被修改,于是把自己的计算结果写到资源中
  3. 线程2后计算完成,并判断资源是否已被修改,发现资源已经被修改过,于是报错或者重试

4.3 典型例子

  • 悲观锁:synchronizedlock 接口,数据库 select for update 就是悲观锁
  • 乐观锁的典型例子就是 原子类、并发容器

4.4 开销对比

  • 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

4.5 两种锁各自的使用场景

悲观锁

适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

乐观锁

适合并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高

5、可重入锁与非可重入锁,以 ReentrantLock 为例

>可重入锁也叫递归锁:一个线程可以多次拿到这把锁,无需释放锁,就可以直接获取到。

可重入锁

ReentrantLock 部分源码

public class ReentrantLock implements Lock, java.io.Serializable {
...
        // 获取锁
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 获取锁时,先判断占有锁的线程是不是正在请求锁的线程,如果是,则设置 state 为重复占用锁的数量
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        // 释放锁
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            // 先判断占有锁的线程是不是正在请求锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果剩余占有当前锁的数量 == 0,则完全释放锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
...
}

不可重入锁

        /**
         * 获取锁
         *
         * @param arg
         * @return
         */
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            // 获取锁失败,锁已经被占用,则返回 false
            return false;
        }

        /**
         * 释放锁
         *
         * @param arg
         * @return
         */
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                //锁未被占用时,不支持释放锁操作
                throw new UnsupportedOperationException();
            }
            // 设置没有线程占有锁
            setExclusiveOwnerThread(null);
            //释放锁,只有拥有锁的线程有资格 setState(0);所以这里不需要用原子操作
            setState(0);
            return true;
        }

好处:

  • 避免死锁:如果有两个方法,都被一把锁锁住,运行到第一个方法拿到这把锁,运行到第二个方法,如果不是可重入锁,就需要等锁释放才可以获取,可能会造成死锁。
  • 提升封装性:避免一次次加锁解锁

可重入锁 ReentrantLock

/**
* Description: 可重入锁 ReentrantLock
* @author Xander
* datetime: 2020-11-12 18:26
*/
public class ReentrantLockDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println(lock.getHoldCount());
            if (lock.getHoldCount() < 5) {
                // 如果重复占有锁的数量小于5,重复获取锁
                accessResource();
            }
            System.out.println("已经对资源进行了处理");
        } finally {
            lock.unlock();
            System.out.println("释放锁后:" + lock.getHoldCount());
        }
    }

    public static void main(String[] args) {
        accessResource();
    }
}

运行结果:

1
2
3
4
5
已经对资源进行了处理
释放锁后:4
已经对资源进行了处理
释放锁后:3
已经对资源进行了处理
释放锁后:2
已经对资源进行了处理
释放锁后:1
已经对资源进行了处理
释放锁后:0

ReentrantLock 的其他方法

  • isHeldByCurrentThread():看出锁是否被当前的线程持有
  • getQueueLength() :返回当前正在等待这把锁的队列有多长

一般是开发和调试时候使用这两个方法

6、公平锁与非公平锁

6.1 什么是公平和非公平

>公平指的是按照线程请求的顺序,来分配锁;
>非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
>注意: 非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。

6.2 为什么要有非公平锁

java设计者这样设计的目的,是为了提高效率,避免唤醒带来的空档期,提高吞吐量。

6.3 公平和不公平的情况(以ReentrantLock为例)

公平

线程1、线程2、线程3、线程4 依次排队等待锁,线程1等待时间最长,先获取到锁,在线程1执行 unlock() 释放锁之后,此时,线程2等待时间最久,线程2会获取到锁,然后再依次是线程3,线程4.

不公平

线程1、线程2、线程3、线程4 依次排队等待锁,线程1获取到锁,如果在线程1释放锁的时候,线程5恰好去执行lock(),由于 ReentrantLock 发现此时并没有线程持有 lock 这把锁(线程2还没来得及获取到,因为获取需要时间),线程5可以插队,直接拿到这把锁,这也是 ReentrantLock默认的策略,也就是“不公平”。

6.4 如何创建公平锁和非公平锁

  • ReentrantLock():ReentrantLock无参构造器,创建非公平锁
  • ReentrantLock(boolean fair):如果 fair 等于 true,则是公平锁,否则是非公平锁。

ReentrantLock 构造器源码

public class ReentrantLock implements Lock, java.io.Serializable {
    ...
    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    ...
}

特例 tryLock()

针对 tryLock() 方法,它是很猛的,它不遵守设定的公平的规则
例如,当有线程执行 tryLock() 的时候,一旦线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了。

6.5 对比公平和非公平的优缺点

优势 劣势
公平锁 各线程公平平等,每个线程等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内始终,得不到执行

6.6 源码分析

ReentrantLock 类中公平锁源码

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // hasQueuedPredecessors() 查询是否有任何线程等待获取的时间比当前线程长。
                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;
        }
    }

ReentrantLock 类中非公平锁源码

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 如果当前锁未被占用,直接尝试获取锁,不排队
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

7、共享锁与排它锁

7.1 什么是共享锁与排它锁

>排他锁,又称独占锁,独享锁,例如 synchronized
>共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。

共享锁与排它锁的典型就是读写锁 ReentranReadWriteLock ,其中读锁是共享锁,写锁是排他锁。

7.2 读写锁的作用

  • 在没有读写锁之前,我们假设使用 ReentrantLock,那么虽然我们保证了线程安全,但也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题。
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。

7.3 读写锁的规则

    1. 多个线程只申请读锁,都可以申请到
    1. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
    1. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

>总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)

7.4 ReentranReadWriteLock 具体用法

演示 ReentrantReadWriteLock

public class ReadWriteLockDemo {
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> read(), "Thread1").start();
        new Thread(() -> read(), "Thread2").start();
        new Thread(() -> read(), "Thread3").start();
        new Thread(() -> read(), "Thread4").start();
        new Thread(() -> write(), "Thread5").start();
        new Thread(() -> write(), "Thread6").start();
    }
}

运行结果:

Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread3得到了读锁,正在读取
Thread4得到了读锁,正在读取
Thread2释放读锁
Thread3释放读锁
Thread1释放读锁
Thread4释放读锁
Thread5得到了写锁,正在写入
Thread5释放写锁
Thread6得到了写锁,正在写入
Thread6释放写锁

7.5 读锁插队策略

插队抢锁。

>策略的选择取决于具体锁的实现,ReentrantReadWriteLock 的实现是选择了 策略2,是很明智的。

总结:

    1. 公平锁:不允许插队
    1. 非公平锁: 写锁可以随时插队(尝试去抢,抢不到就排队),读锁仅在等待队列头节点不是想获取写锁的线程的时候可以插队。

7.6 锁的升降级

>支持锁的降级,不支持升级,如果已经获取写锁,则可以在不释放写锁的情况下,直接获取读锁,成功降级。
>为什么不支持锁的升级?死锁

7.7 共享锁和排他锁总结

    1. ReentrantReadWriteLock 实现了 ReadWriteLock 接口,最主要的有两个方法:readLock() 和 writeLock() 用来获取读锁和写锁
    1. 锁申请和释放的策略
      1)多个线程只申请读锁,都可以申请到
      2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
      3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
      4)要么是一个或者多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

总结:要么多读,要么一写

    1. 插队策略:为了防止饥饿,读锁不能插队
    1. 升降级策略:只能降级,不能升级
    1. 使用场合:相比于 ReentrantLock 适用于一般场合, ReentrantReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。

8、自旋锁与阻塞锁

8.1 概念

>阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
>在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让系统得不偿失。如果物理机器有多个处理器,能让两个或者以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就释放锁。而为了让当前线程“稍等一下”,我们需要让当前线程进行自旋,如果在自旋完成后,前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁

阻塞锁自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,等待被唤醒。

8.2 自旋锁缺点

  • 如果锁被占用的时间很长,那么自旋锁的线程只会白浪费处理器资源
    在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

8.3 原理和源码分析

在java1.5 版本以上的并发框架 java.util.concurrentatomic 包下的类基本都是自旋锁的实现。

AtomicInteger 的实现:自旋锁的实现原理是 CAS,
AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while 里死循环,直至修改成功

AtomicInteger 中的 getAndAddInt 实现

public class AtomicInteger extends Number implements java.io.Serializable {
...
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

...
}

public final class Unsafe {
...
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
...
}

8.4 适用场景

  • 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
  • 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放,持有锁的时间长),那也是不合适的。

9、可中断锁

synchronized不可中断锁,而 lock可中断锁,因为 trylock(time)lockInterruptibly() 都可以相应中断。

>如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

上面,我们比较了 Locksynchronized 两种锁的用法, synchronized 使用简单,但是 Lock 可控性更强。我们通过源码学习了 Lock 的核心方法,以及通过 Lock 接口的实现类 ReentrantLock 来简单的使用了 Lock 的核心 API。最后,我们详细地介绍了锁的分类,各种锁的特点,怎么新建,如何使用。

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP