Java并发之 Lock 锁

news2024/11/26 11:48:07

一、Lock接口

1 Lock简介&地位&作用

  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和synchronized是最常见的两个锁,他们都能够达到线程安全的目录,但是使用和功能上又有较大的不同
  • Lock接口最常见的实现类就是ReentrantLock
  • 通常情况下Lock只允许一个线程访问共享资源,特殊情况也允许多个线程并发访问,如ReadWriteLock的ReadLock

2 为什么需要Lock?

(1)因为sychronized在某些场合不适用,存在如下缺点:

  • 效率低

锁的释放情况少(只有执行结束或者抛异常才能释放锁)、试图获取锁时不能设定超时、不能中断一个正在试图获取锁的线程

  • 不够灵活

加锁和释放的时机单一(不像读写锁那样针对不同场景而选择使用读锁或写锁),每个锁仅有单一的条件(某个对象),适用场景可能是不够的

  • 无法知道是否成功获取到锁

(2)Lock接口拥有而synchronized关键字不具备的主要特性:

特性描述
尝试非阻塞地获取锁:tryLock()当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁,否则立即返回false
能被中断地获取锁:lockInterruptibly()在等待获取锁的线程能够响应中断,等待锁的线程被中断时,中断异常将会被抛出,不再等待
超时获取锁:tryLock(long time, TimeUnit unit)在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回

3 Lock接口基本的方法:

方法名称描述
void lock()获得锁。如果锁已经被其他线程获取,则进行等待
boolean tryLock()只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。
boolean tryLock(long time, TimeUnit unit)超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.
void lockInterruptibly()获取锁,如果可用并立即返回。如果锁不可用,那么等待,和 tryLock(long time, TimeUnit unit) 方法不同的是等待时间无限长,但是在等待中可以中断当前线程(响应中断)。
Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
void unlock()释放锁。

下面对以上接口的使用进行演示:

二、Lock 接口方法演示

1. lock()

  • 最普通的获取锁,如果所被其他线程获得了,进行等待
  • Lock不会像synchronized一样在异常时自动释放锁
  • 使用时,一定要在finally中释放锁
  • lock()方法 不能被中断,一旦死锁,lock() 就会永久等待

代码演示:

/**
 * 描述:     Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
 */
public class MustUnlock {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
}

2. tryLock()

  • tryLock()用来尝试获取锁,如果当前线程没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相比上面的lock(),他可以返回一个值,让我们知道是否成功获取到锁;进而决定后续程序的行为
  • 它会立刻返回,即便在拿不到锁时,不会一直等待

3. tryLock(long time,TimeUnit unit)

可以设定超时时间的尝试获取锁,一段时间内等待锁,超时就放弃。

使用 tryLock(long time,TimeUnit unit) 来避免死锁的代码演示:

/**
 * 〈用trylock避免死锁〉
 *
 * @author Chkl
 * @create 2020/3/11
 * @since 1.0.0
 */
public class TryLockDeadLock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadLock r1 = new TryLockDeadLock();
        TryLockDeadLock r2 = new TryLockDeadLock();

        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {

                        try {
                            System.out.println("线程1获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));

                            if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
                                try {
                                    System.out.println("线程1获取到了锁2");
                                    System.out.println("线程1获取到了两把锁");
                                    break;
                                }finally {
                                    lock2.unlock();
                                }
                            }else {
                                System.out.println("线程1获取锁2失败");
                            }

                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {

                        try {
                            System.out.println("线程2获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));

                            if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
                                try {
                                    System.out.println("线程2获取到了锁1");
                                    System.out.println("线程2获取到了两把锁");
                                    break;
                                }finally {
                                    lock1.unlock();
                                }
                            }else {
                                System.out.println("线程2获取锁1失败,已重试");
                            }

                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

打印结果、

4. LockInterruptibly

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

public class LockInterruptibly implements Runnable{

    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockInterruptibly l = new LockInterruptibly();
        Thread thread0 = new Thread(l);
        Thread thread1 = new Thread(l);

        thread0.start();
        thread1.start();

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

        thread0.interrupt();
    }

    @Override
    public void run() {
        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() + "等待锁期间被中断了");
        }
    }
}

打印结果:

image-20230610130313756

5. 锁的可见性保证

  • lock符合happens-before规则,具有可见性
  • 当线程解锁,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

三、锁的分类

根据不同的划分标准,常见的锁的划分如思维导图所示

1、乐观锁&悲观锁

(1)为什么会诞生非互斥同步锁(乐观锁)

主要由于互斥同步锁(悲观锁)存在一些劣势,如下:

  • 阻塞和唤醒带来的性能劣势
  • 永久阻塞:如果持有锁的线程被永久阻塞,如无限循环,死锁等活跃性问题,那么等待该线程释放锁的线程永远得不到执行
  • 优先级反转:阻塞的那个线程A优先级高,持有锁的B优先级低,如果B不释放,A就需要一直等待,导致优先级反转

(2)什么是乐观锁&悲观锁

悲观锁:

  • 悲观锁认为:如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以为了结果的正确性,悲观锁会在每次获取并修改结果时把数据锁住,让别人无法访问
  • Java中悲观锁典型的实现就是synchronized和lock相关类

乐观锁:

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过。
    • 如果没有被改变过,就说明只有自己在操作,就正常修改数据
    • 如果数据与最初拿到的不一致,说明其他人在这段时间内修改过数据,就会执行放弃、报错或重试等策略
  • 乐观锁的实现通常是利用CAS算法,典型例子是:原子类,并发容器等

代码演示:实现累加器

public class PessimismOptimismLock {
    int a;

    //悲观锁
    public synchronized void testMethod(){
        a++;
    }
    public static void main(String[] args) {
        //乐观锁
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
        //悲观锁
        new PessimismOptimismLock().testMethod();
    }
}
典型例子
  • Git:Git是乐观锁的典型应用,当我们向远程仓库push的时候,git会检查远程仓库的版本是不是领先我们现在的版本,

    • 如果远端版本和本地版本不一致,表明远端代码被人修改过了,提交就失败
    • 如果版本一直,才能顺利提交到远程仓库
  • 数据库:

    • select for update就是悲观锁
    • 用version控制就是乐观锁
      • 添加一个字段lock_version
      • 更新操作前先查出这条数据的version 记为mversion
      • 进行更新操作时:update set num = 2 , version = vsersion+1 where version = mversion and id = 5
      • 如果version更新了不等于查询出来的值了,更新就无效
开销对比
  • 悲观锁的原始开销要高于乐观锁,但是一劳永逸,临界区(加锁区)持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 乐观锁一开始的开销比悲观锁小,如果自旋时间很长或者不停重试,name消耗的资源也会越来越多
使用场景
  • 悲观锁:适合于并发写入多的情况,适合于临界区(可以理解为加锁区)持锁时间较长的情况,悲观锁可以避免大量的无用自旋锁等消耗
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈
  • 乐观锁:适合并发写入少,大部分都是读取的场景,不加锁能让读取性能大幅提高

2、可重入&非可重入锁

非可重入锁就是最常见的锁,一旦锁被使用,如果没有释放,就不能再使用这个锁了

可重入锁,是指同一线程获取到一把锁之后,可在不释放该锁的条件下再次获取该锁,以ReentrantLock为例进行演示,如下:

(1)重复调用演示:
private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }

(2)递归调用演示:
public class RecursionDemo {
    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource(){
        lock.lock();
        try {
            System.out.println("已经对资源进行处理");
            if (lock.getHoldCount()<5){
                //递归调用
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        }finally {
            lock.unlock();
        }
    }

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

从结果可以看出获得锁之后是可以重复获得锁再最后释放的,这就是可重入锁

  • 可重入锁的好处
    • 避免了死锁
    • 使用灵活,提高了封装性
(3)源码分析

可重入锁 & 非可重入锁的获取锁和释放锁的方法源码对比:

3、公平锁&非公平锁

(1) 什么是公平和非公平

  • 公平:指按照线程请求的顺序来分配锁
  • 非公平:不完全按照请求的顺序,在合适的时机下,可以插队
    • 非公平也同样不提倡"插队"行为,这里的非公平,指的是在合适的时机插队,而不是盲目插队
    • 什么是合适的时机?
      • 比如你排队买票,现在在第二位,当你成为第一位的时候,你脑子懵了2秒;此时有个人,突然插在你前面,问售票员:XXX点的车几点发车,问完就走,只花了3秒,此时你也刚清醒,也就没太在乎。这个例子主要是说,插队的时机,就算他没插队,中间懵逼的2秒就白白浪费了,为了更高效的运作,就可以把这个线程唤醒时的空窗期也能利用到,提高吞吐量。

(2)为什么要有非公平锁

  • 为了提高效率(大多数都默认采用非公平锁)
  • 避免唤醒带来的空档期

(3)公平的情况(以ReentrantLock 为例)

  • 如果创建ReentrantLock 对象时,参数填写为true,那么这个锁就是公平锁

演示案例:模拟打印机打印任务,有两个类,一个是打印作业Job类,一个是打印队列PrintQueeue 类,一个打印任务包含两次打印,两次获得锁。在main方法中创建10个线程执行Job,当锁使用公平锁时:

/**
 * 〈演示公平锁和不公平锁〉
 *
 */
public class FairLock {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue), "线程"+i);
        }
        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }
}

class Job implements Runnable {
    PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.println(
                Thread.currentThread().getName() + "开始打印");
        printQueue.printJob(new Object());
        System.out.println(
                Thread.currentThread().getName() + "打印结束");

    }
}


class PrintQueue {
    //公平锁
    private Lock queueLock = new ReentrantLock(true);
    //非公平锁
//    private Lock queueLock = new ReentrantLock();

    public void printJob(Object document) {
        queueLock.lock();

        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
        } finally {
            queueLock.unlock();
        }
    }
}

使用公平锁进行打印操作,会按照请求锁的顺序依次执行,按照请求顺序,不会出现插队。一次运行结果如下,因为每次打印后需要休眠n秒模拟打印耗时,休眠时间足够所有的线程依次启动,所以执行顺序一定是线程0-9按顺序请求第一把锁,之后线程0-9再按顺序请求第二把锁,顺序一定不会变

线程0开始打印
线程0正在打印,需要8
线程1开始打印
线程2开始打印
线程3开始打印
线程4开始打印
线程5开始打印
线程6开始打印
线程7开始打印
线程8开始打印
线程9开始打印
线程1正在打印,需要3
线程2正在打印,需要3
线程3正在打印,需要5
线程4正在打印,需要4
线程5正在打印,需要2
线程6正在打印,需要2
线程7正在打印,需要2
线程8正在打印,需要10
线程9正在打印,需要7
线程0正在打印,需要9秒
线程1正在打印,需要5秒
线程0打印完毕
线程2正在打印,需要8秒
线程1打印完毕
线程2打印完毕
线程3正在打印,需要2秒
线程3打印完毕
线程4正在打印,需要8秒
线程4打印完毕
线程5正在打印,需要5秒
线程5打印完毕
线程6正在打印,需要9秒
线程7正在打印,需要1秒
线程6打印完毕
线程7打印完毕
线程8正在打印,需要10秒
线程8打印完毕
线程9正在打印,需要10秒
线程9打印完毕

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

修改PrintQueue 中的锁为非公平锁

	//非公平锁
    private Lock queueLock = new ReentrantLock(); // 或者 new ReentrantLock(false)

打印结果:

运行结果如上,从结果可以看到,打印顺序并没有再按照0-9、0-9执行了,线程2的第一次打印结束后马上又开始了第二次打印,这就是非公平锁的好处了,线程2执行完第一个打印之后,线程3准备打印,但是在准备的空窗期,线程2干脆一次性把第二次打印也完成了,不影响线程3打印的正常运行,同理下面的线程56789都是这种情况,提高了效率,充分利用了空窗期

(5)特例

  • trylock()方法不准守公平规则,自带插队属性
  • 当trylock()执行时,一旦有线程释放了锁,就一定被使用trylock()的线程获得,即使现在这个锁的等待队列里有线程在等待

(6)对比非公平和公平的优缺点

(7)公平锁和非公平锁的获取锁的方法源码分析

4、共享锁&排它锁

以ReetrantReadWriteLock读写锁为例

(1)什么是共享锁和排它锁

  • 排它锁:又称独占锁,独享锁
  • 共享锁:又称为读锁,获得共享锁后,可以查看但是无法修改和删除数据,其他线程此时也可以蝴蝶共享锁,同样无法修改和删除数据
  • 共享锁和排它锁的典型就是读写锁ReetrantReadWriteLock,其中读锁是共享锁,写锁是排它锁

(2)读写锁的作用

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

(3)读写锁的规则

  • 当一个线程占用读锁时,其他线程可以申请读锁,不能申请写锁
  • 当一个线程占用写锁时,其他线程读锁写锁都不可以申请
  • 总结:要么多读,要么一写

(4)ReetrantReadWriteLock的具体使用

创建4个线程,前两个获取读锁,后两个获取写锁,运行后可以看到读锁可以同时获取,写锁必须等前面的线程释放了才能再获取,写锁获取期间,不允许其它线程的读写操作。

public class CinemaReadWrite {
    private static ReentrantReadWriteLock
            reentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock
            readLock = reentrantReadWriteLock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock
            writeLock = reentrantReadWriteLock.writeLock();


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

    }
    
    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + "得到了读锁,正在读取ing");
            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()
                    + "得到了写锁,正在读取ing");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()
                    +"释放读锁");
            writeLock.unlock();
        }
    }
}

运行结果:

(5) 读锁插队策略

  • 公平锁:不允许插队
  • 非公平锁:
    • 写锁可以随时插队
    • 读锁仅在等待队列头节点不是想要获取写锁的线程的时候可以插队(即头结点是写锁不可以插队,是读锁就可以插队)
源码分析
  • 公平读写锁

  • 非公平读写锁

(6)演示头结点是读操作,后面的读操作插队的情况

public class NonfairBargeDemo {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//非公平读写锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//读锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//写锁

    //读操作
    private static void read(){
        System.out.println(Thread.currentThread().getName()+" : 开始尝试获取读锁");
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在读取");
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()+" : 释放读锁");
            readLock.unlock();
        }
    }

    //写操作
    private static void write(){
        System.out.println(Thread.currentThread().getName()+" : 开始尝试获取写锁");
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在写入");
            Thread.sleep(40);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()+" : 释放写锁");
            writeLock.unlock();
        }
    }

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

        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < thread.length; i++) {
                    thread[i] = new Thread(()->read(),"子线程创建的Thread"+i);
                    thread[i].start();
                }
            }
        }).start();

    }
    
}

线程2刚拿到锁时,此时等待队列中的头结点是线程3,是读操作,所以后面的读操作可以进行插队,Thread330就没有排队,直接去抢到了锁;当线程3也获取到锁之后,由于队列的头节点是线程4,写操作,所以后面的读操作都不再插队,按部就班的排队。

(7)升降级策略

为什么需要升降级?
  • 提高效率:

    某个线程执行过程中不同时间段的操作不同,一开始执行写操作,之后都进行读;一直使用写锁的话,后面的读操作不能和其他线程进行共享,就会浪费资源;如果将写锁释放掉然后去抢占读锁,不一定能抢到。所有就有了写锁降级,然后让其他线程也能获取到读锁。

支持锁的降级,不支持升级
  • 为什么不支持读锁的升级?

    因为读锁升级需要等所有的读锁都释放了才能升级,容易造成死锁,比如两个线程都在等待升级的话,就会互相等待对方释放读锁,就成了死锁

public class NonfairBargeDemo {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//公平读写锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//读锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//写锁

    //读升级
    private static void readUpgrading(){
        System.out.println(Thread.currentThread().getName()+" : 开始尝试获取读锁");
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在读取");
            Thread.sleep(1000);
            System.out.println("升级会带来阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName()+"升级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName()+" : 释放读锁");
            readLock.unlock();
        }
    }

    //写降级
    private static void writeDownGrading(){
        System.out.println(Thread.currentThread().getName()+" : 开始尝试获取写锁");
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在写入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+":在不释放写锁的情况下,直接获取读锁,成功降级");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName()+" : 释放写锁");
            readLock.unlock();
        }
    }

    //主函数
    public static void main(String[] args) throws InterruptedException {
        System.out.println("降级是可以的");
        Thread thread1 =new Thread(()->writeDownGrading(),"thread1");
        thread1.start();
        thread1.join();
        System.out.println("=========================");
        System.out.println("升级是不行的");
        Thread thread2 = new Thread(() -> readUpgrading(), "thread2");
        thread2.start();

    }

}

(8)适用场合

相比于 ReentrantLock 适用于一般场合,ReentrantReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。

5、自旋锁&阻塞锁

(1)为什么需要自旋锁

  • 阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态装换需要耗费处理器时间

  • 如果同步代码块中的内容过于简单,状态转换消耗的时间可能比用户代码执行的时间还长

  • 同步资源锁定时间很短的场景,线程挂起和恢复现场的花费可能会让系统得不偿失

  • 如果物理机有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就释放锁

自旋锁:为了让当前线程“稍微等一下”,需要让当前线程自旋,如果自旋完成后前面锁定同步资源的线程已经释放锁了,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免线程切换的开销,这就是自旋锁

阻塞锁和自旋锁相反,阻塞锁如果没有拿到锁,会直接把线程阻塞,直到被唤醒

(2) 自旋锁缺点

如果锁被占用时间很长,那么自旋的线程只会白白浪费CPU资源

(3)自旋锁的应用

  • 在java1.5版本及以上的并发框架java.util.concurrent 的atmoic包下的类基本都是自旋锁的实现;
  • AtomicInteger 的实现:自旋锁的实现原理是CAS。AtomicInteger 源码中调用 unsafe进行自增操作的的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功.

AtomicInteger的getAndIncrement源码

(4) 自己实现一个简单的自旋锁

/**
 *      自旋锁演示
 */
public class SpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();

    //加锁操作
    public void lock(){
        Thread current = Thread.currentThread();
        //只有在 null 的时候,current 才能执行通过,否则就是循环
        while (!sign.compareAndSet(null,current)){
            System.out.println("获取锁失败,已重试");
        }
    }

    //解锁操作
    public void unlock(){
        Thread current = Thread.currentThread();
        // 将 sign 的线程设为null,就意味着其他线程可以再次进行设值,就相当于解锁了
        sign.compareAndSet(current,null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":开始尝试获取自旋锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + ":获取到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + ":释放了自旋锁");
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
        
    }

}

(5) 自旋锁的使用场景

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

6. 可中断锁和不可中断锁

  • Java中,synchronized 就是不可中断锁,而 Lock 是可中断锁,因为 trylock(time) 和 lockInterruptibly() 都可以响应中断
  • 如果某个线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间太长了,线程B不相等了,可以去处理其他事情,把B中断,这就是可中断锁
public class LockInterruptibly implements Runnable{

    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockInterruptibly l = new LockInterruptibly();
        Thread thread0 = new Thread(l);
        Thread thread1 = new Thread(l);

        thread0.start();
        thread1.start();

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

        thread0.interrupt();
    }

    @Override
    public void run() {
        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("【等锁期间】被中断");
            e.printStackTrace();
        }
    }
}

四、锁优化

1. Java虚拟机对锁的优化

(1) 自旋锁&自适应

  • 自适应是对自旋锁的优化,比如当自旋锁尝试自旋了很多次,都没有获取到锁,那他就会阻塞;
  • 如果这次自旋100次都没拿到锁,并进入了阻塞,那下次就可能没有自旋了,直接进入阻塞

(2)锁消除

在一些不会出现线程不安全的地方,jvm 会对这里的锁消除

(3)锁粗化

理论上来说,让同步代码块的范围越小越好;但是如果一系列的操作都是对一个对象进行反复的加锁解锁操作,那他就会优化,只在这一系列操作的开始加锁,在结束时进行解锁,就减少了锁的数量。避免了反复加锁解锁带来的资源浪费。

2. 程序员对锁的优化

  • 缩小同步代码块

  • 尽量不要锁住方法

  • 减少请求锁的次数

    例如,在日志框架中,多个线程去执行日志记录操作,那可以加一个中间件,将多个操作合成一个操作,然后去用一个线程去执行这合成的一个操作

  • 避免人为制造"热点"

    例如,在使用size()方法获取hashmap的大小时,如果遍历整个hashmap来获取大小时,为了准确性就会加锁,从而使得其他线程进入阻塞;为了避免这里成为加锁"热点",我们可以维护一个计数器,每次put()操作就给计数器加1,每次删除操作就减1,就无需遍历获取hashmap的大小,而是直接去取map中的一个元素,大大减小开销。

  • 锁中尽量不要再包含锁

    因为容易出现死锁

  • 选择合适锁的类型&合适的工具类

点我扫码关注微信公众号

文章来源:Java并发之 Lock 锁


个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/643653.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode热题100】打卡第21天:最小路径和爬楼梯

文章目录 【LeetCode热题100】打卡第21天&#xff1a;最小路径和&爬楼梯⛅前言 最小路径和&#x1f512;题目 爬楼梯&#x1f512;题目&#x1f511;题解 【LeetCode热题100】打卡第21天&#xff1a;最小路径和&爬楼梯 ⛅前言 大家好&#xff0c;我是知识汲取者&#…

深入了解RTMP协议:实时传输的利器

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…

【环境配置】MATLAB r2022b+opencv3.4.1+mexopencv3.4.1+Windows 11 配置

参考链接&#xff1a; mexopencv官方文档&#xff1a;介绍了全部流程 补充官方文档Configure OpenCV出现的问题&#xff1a;直到Configure后没有红色&#xff0c;再点击Generate 最新Opencv 与 VC 的版本对应关系&#xff08;2023-04-08&#xff09; MATLAB支持和兼容的编译器 …

大学物理(上)-期末知识点结合习题复习(5)——刚体力学-转动惯量、力矩、线密度 面密度 体密度、平行轴定理和垂直轴定理、角动量定理和角动量守恒定律

目录 刚体的定轴转动 题1 题目描述 题解 题2 题解 题3 题目描述 题解 题4 题目描述 题解 题5 题目描述 题解 角动量定理和角动量守恒定律 刚体的定轴转动 1.转动动能 由&#xff0c;得 表示质量 表示质量分布点 2.转动惯量 为刚体对给定轴的转动惯量&…

原生小程序 微信小程序 使用ucharts

一般是uni-app项目使用ucharts在原生微信小程序也是可以使用。 方法&#xff1a; ## 使用说明 请将项目根目录 微信小程序/uCharts-组件/qiun-wx-ucharts/src 下全部文件复制到指定位置&#xff0c;例如该项目的components/qiun-wx-uchart目录下&#xff0c;然后在页面的json配…

2023互联网寒冬之下Java程序员要怎么应对才能找到工作

在互联网寒冬下&#xff0c;Java程序员需要做好哪些方面的准备&#xff0c;才能够更好地找到工作呢&#xff1f; 1.关注用人单位的招聘需求&#xff1b;Java程序员应该关注用人单位的招聘信息&#xff0c;并针对不同的招聘岗位进行精准的自我定位和筛选。在面试前可以充分了解…

Gin框架原生方式切割日志,Go语言原生日志切割

目录 摘要 痛点 正文 1.分析 io.Writer 接口 2.实现 io.Writer 接口 3.将它作为原生输出 4.将它作为 Gin 框架的输出 摘要 自定义一个日志输出&#xff0c;将go语言和gin框架的日志自动按天拆分。本文通过实现io.Writer接口的方式&#xff0c;替换原生和gin框架的默认W…

DAY21——二叉树part7

1.二叉搜索树最小差值 二叉搜索树中序遍历得到有序的数字序列&#xff0c;记录前一个节点 class Solution {TreeNode pre;int result Integer.MAX_VALUE;public int getMinimumDifference(TreeNode root) {if(rootnull)return 0;traversal(root);return result;}private voi…

【Java算法题】剑指offer_数据结构之03队列栈

前言 刷题链接&#xff1a; https://www.nowcoder.com/exam/oj/ta?page2&tpId13&type265 原定于5.30写完队列&栈&#xff0c;超时了14天&#xff08;2周&#xff09;&#xff0c;于6.13完成。 刷算法题到现在得出一个心得&#xff0c;万事开头难。没刷之前总觉得…

django的项目结构介绍

目录 django的安装django项目创建django项目启动django项目关闭django项目个个文件分析核心文件 manage.py项目内部文件start01/start01setting文件的讲解 django的安装 pip install django检测安装后的版本 python -m django --versiondjango项目创建 django安装以后 会自动…

面向对象的多态

7. 面向对象特征三&#xff1a;多态性 概念 多态是面向对象程序设计&#xff08;OOP&#xff09;的一个重要特征&#xff0c;指同一个实体同时具有多种形式&#xff0c;即同一个对象&#xff0c;在不同时刻&#xff0c;代表的对象不一样&#xff0c;指的是对象的多种形态。 变…

作为软件工程师对Docker的认知和看法

文章目录 前言一、Docker是什么&#xff1f;二、Docker和Virtual Machine的区别三、Docker架构1. Client2. Docker Host3. Registry 四、Docker在实际应用中的好处配置环境网络和卷更新管理 总结 前言 两年前我还没有接触过Docker&#xff0c;也不理解Docker在自动化工程应用中…

k8s如何使用ceph rbd块存储(静态供给、存储类动态供给)

目录 前言安装ceph集群ceph集群创建rbd块存储rbd块存储不支持ReadWriteManyk8s配置rbd块存储&#xff08;静态供给&#xff09;创建secret创建pv创建pvck8s节点安装客户端依赖包部署pod查看pod验证是否持久化 k8s配置rbd块存储&#xff08;动态供给&#xff09;查看官网ceph集群…

mysql 最常用的一些语句

1 数据库相关操作 CREATE DATABASE IF NOT EXISTS daily-test DEFAULT CHARSET utf8 COLLATE utf8_general_ci&#xff1b; drop database daily_test; use daily_test 具体操作如下图上所示&#xff1a; 2 mysql常用数据类型 MySQL 数据类型 | 菜鸟教程 3 数据库表相关操作…

Stimulsoft Forms.WEB 23.2.6 Crack

Stimulsoft Forms.WEB 发布 创建、编辑、填写、发布和分发交互式表单。 2023 年 6 月 13 日 - 9:34 新产品 特征 您可以为几乎任何目的创建任何类型的文档 - 发票和支票、各种调查问卷和工作表、订单、简历等等。用户收到可编辑的 PDF 格式的完成模板&#xff0c;可以在任何现代…

英伟达开发板学习系列---国产【Jetson Xavier NX】系统安装及基础配置

1. 前言 最近新买了Jetson Xavier NX, 和之前英伟达原厂的NX的区别在于国产Jetson Xavier NX 是核心板使用的是英伟达的&#xff0c;扩展板是国产的。具体详情如下&#xff1a; 官方NX和国产NX详情区别 2. 设置系统从固态硬盘启动 官方NX出厂是直接将SD卡&#xff08;64/12…

Redis GEO地理位置信息的应用

Redis GEO地理位置信息的应用 Redis GEO概述应用场景Redis GEO命令GEO命令演示 Redis GEO实现附近人的功能基础类API接口接口实现执行测试 Redis GEO 概述 Redis的GEO操作是一种基于地理位置信息进行操作的功能。它使用经度和纬度坐标来表示地理位置&#xff0c;支持存储地理位…

湖南大学CS-2020期末考试解析

【特别注意】 答案来源于@wolf 是我在备考时自己做的,仅供参考,若有不同的地方欢迎讨论。 【试卷评析】 有必要一做。 【试卷与答案】 1.简答题(10 分) 假设一个基于 IEEE 浮点格式的 10 位浮点表示,有 1 个符号位,4 个阶码位(k=4)和 5 个 尾数位(n=5)。 (…

湖南大学CS-2018期末考试解析

【特别注意】 答案来源于@wolf 是我在备考时自己做的,仅供参考,若有不同的地方欢迎讨论。 【试卷评析】 有必要一做。 【试卷与答案】 一、选择题(每题 2 分,共 10 分) 1. 0x12345678 存放在采用小端存储的机器上,地址为 0x100 到

湖南大学CS-2017(另一张)期末考试解析

【特别注意】 答案来源于wolf 是我在备考时自己做的&#xff0c;仅供参考&#xff0c;若有不同的地方欢迎讨论。 【试卷评析】 有必要一做。 【试卷与答案】 由于这张试卷没有电子版&#xff0c;我就直接拍我自己的作答了