当前位置:   article > 正文

多线程(进阶) - 2.5w字总结_多线程里边有个w什么的

多线程里边有个w什么的

常见的锁策略

在谈到"锁策略"之前.我们先要先理解线程和锁的关系,例如

你去银行办理业务,进入银行后先去取号,然后在服务区等待,过了段时间,广播加到你的号码,你走到服务窗口,由银行员工为你办理业务,并且在完成业务的过程中,员工只为你一个人服务,当你完成业务后离开,广播呼叫下一个号码.

在上述过程中,每个办理业务的人就是线程,服务窗口就是,服务窗口后的银行员工就是共享资源,你无法直接办理业务,需要先取号后等待的过程为阻塞,当广播叫到你的名字时,你去办理业务的提示信息为唤醒,你在窗口办理业务就是加锁,完成业务后离开为释放锁

悲观锁和乐观锁

悲观锁:

悲观锁自己去拿数据时,认为别人也去拿数据会修改数据,所以在每次拿数据时都会上锁,这样其他线程去拿数据时,会被阻挡到外面,防止数据错误.

乐观锁:

乐观锁认为在一般情况下是会发生访问冲突,所以不会上锁,只有在数据进行更新时,才会对比数据在当前线程的更新期间有没有被修改过,如果修改过,则可以尝试重新读取,选择报错,放弃修改等策略.

总结:

乐观锁和悲观锁共有各有优缺点,在面对不同的应用场景,采取合适的策略,能够大大的提高系统的效率,比如:当系统冲突次数多,适用于悲观锁,虽然由于频繁上锁,系统资源消耗大,但是能够减少冲突,提高系统的稳定性.当冲突次数少,使用乐观锁就可以减少上锁时消耗的资源,所以悲观锁阻塞事务,乐观锁回滚重试.

Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略 ,而乐观锁的在进程数据是否冲突时所采用的一个策略就是使用"版本号".

读写锁

对于Synchronized属于普通的互斥锁,对于不同线程自己对同一个代码块的上锁是互斥的,而读写锁是对其进行细分,分为"读锁"和"写锁".在多线程之间,对于读操作是安全的,但是边读边写,多个线程写这是线程不安全的,使用读写锁就可以提高多个线程读的效率,还可以保证写操作的安全.

读写锁规则:

  • 线程A占用读锁,线程B申请读锁,申请成功
  • 线程A占用读锁,线程B申请写锁,线程B会开始等待,直到线程A释放写锁才能申请成功
  • 线程A占用写锁,线程B申请写锁或者写锁,线程B开始等待,因为读写操作不能同时进行

标准库的ReentrantReadWriteLock类:

Java标准库中使用ReentrantReadWriteLock 类中的readLock对象获取读锁,writeLock对象获取写锁,两个对象都提供lock/unlock来实现加锁/解锁操作.

//定义读写锁
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁对象
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//写锁对象
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

//读操作
public static void read() {
    readLock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取...");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        readLock.unlock();
        System.out.println(Thread.currentThread().getName()+"读取完毕,释放读锁");
    }
}

//写操作
public static void write() {
    writeLock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入...");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        writeLock.unlock();
        System.out.println(Thread.currentThread().getName() + "释放写锁");
    }
}
public static void main(String[] args) {
    new Thread(() -> read(), "读锁 - 1").start();
    new Thread(() -> read(), "读锁 - 2").start();
    new Thread(() -> write(), "写锁 - 1").start();
    new Thread(() -> write(), "写锁 - 2").start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

由显示结果及其执行次序知:读写锁支持并发读,而写操作是单独进行的.

轻量级锁和重量级锁

二者涉及到用户态和内核态的转变,所以先了解锁的核心"原子性",及CPU是如何提供操作指令.

重量级锁:依赖于操作系统提供的锁

  • 大量的用户态内核态的转化,导致所需时间和成本的提高
  • 很容易引发线程的调度

轻量级锁: 避免使用操作系统提供的锁,尽量在用户态完成任务

  • 少量的转化为内核态
  • 不太容易引发线程调度

简而言之就是:轻量级锁任务少,锁开销小,重量级锁任务多,锁开销大.

synchronized是自适应锁,根据锁冲突情况切换,冲突不高:轻量级锁,冲突很高:重量级锁

自旋锁和挂起等待锁

自旋锁: 当发现锁冲突时,对于抢锁失败的线程,立即尝试重新获取锁,并且循环多次(状态不会改变).所以当其他线程释放锁时,就会第一时间获取到锁

挂起等待锁(阻塞锁): 当发现锁冲突时,会挂起等待(进入阻塞队列),所以当锁被释放时,不会第一时间获得锁.

比如去银行取钱,你有两种选择,一个是去柜台取钱,这就是阻塞锁,另一个是去取款机取钱,这就是自旋锁.在柜台取钱时你会先取号,然后开始等待被叫号,这个等待过程就是进入阻塞队列,被叫号就是被唤醒,而在取款机取钱是,没有广播提醒下一个是谁,即没有唤醒操作,你必须一直关注是否到自己取钱了.

二者都是等待获取共享资源,最大的区别是要不要放弃CPU的执行时间.

自旋锁是轻量级锁的实现:

  • 优点:是一种乐观锁,没有放弃CPU,不涉及线程阻塞和调度(因为一直在尝试获取锁),一旦得到的锁被释放,会第一时间获取
  • 缺点:如果等待的锁迟迟没有释放,就会一直占用CPU,消耗系统资源

挂起等待锁是重量级锁的实现:

  • 优点:是一种悲观锁,在挂起等待的时候不会消耗CPU,等待锁释放后被唤醒.
  • 缺点: 由于自身是挂起等待,所以不会第一时间获取到锁

在JVM中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(被称为锁膨胀的过程),不允许降级,在后面会讲解,三者之间的转化过程.

公平锁和非公平锁

假设现在有三个线程A,B,C尝试获得锁,A先尝试获取到锁,获取成功,随后B,C都去尝试获取,如果按照顺序是B,C这个顺序阻塞等待.

在线程A释放锁后,在公平锁和非公平锁的规则下,那个线程能够获得到锁是不同的

公平锁:遵守"先来后到",因为B比C先来,所以B先获取锁.

非公平锁: 不遵守"先来后到",每个锁获取锁的概率是相同的,这TM才是公平

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/142733
推荐阅读
相关标签