多线程---进阶

本文详细介绍了多线程中的锁策略,包括乐观锁、悲观锁、读写锁、重量级锁与轻量级锁、自旋锁与挂起等待锁、公平锁与非公平锁以及可重入锁与不可重入锁。接着讨论了CAS、synchronized以及Callable接口在多线程中的应用,并详细剖析了Java并发工具JUC中的ReentrantLock、原子类、Semaphore和CountDownLatch的使用。最后,文章探讨了线程安全的集合类ConcurrentHashMap以及死锁的概念和避免策略。通过对这些概念和技术的掌握,读者可以更好地理解和应对多线程中的并发问题。
摘要由CSDN通过智能技术生成


目录

🥬常见的锁策略

🌵乐观锁和悲观锁

🌵读写锁

🌵重量级锁和轻量级锁

🌵自旋锁和挂起等待锁

🌵公平锁和非公平锁

🌵可重入锁和不可重入锁

🥬CAS

🥬synchronized

🥬Callable接口

🥬JUC(java.util.concurrent) 的常见类

🌵ReentrantLock

🌵Semaphore

🌵CountDownLatch

🥬线程安全的集合类

🌵ConcurrentHashMap

🥬死锁

🥬小结


🥬常见的锁策略

因为加锁是一个开销比较大的操作,所以我们希望在特定的场景下,针对场景做一些取舍,让锁更加高效一些。

🌵乐观锁和悲观锁

乐观锁:假设一般情况下都不会产生锁冲突,因此就尝试直接访问数据,发现了锁冲突,再去处理。

悲观锁:假设一般情况下都会产生冲突,因此先进行处理,再去尝试访问数据。

🌵读写锁

当多个线程尝试修改同一个变量时,线程不安全。当多个线程同时读一个变量时,线程安全。两个读线程之间,不存在线程不安全问题,不必互斥;两个写线程之间,存在线程不安全问题,就需要互斥;一个读线程与一个写线程之间存在线程不安全,需要互斥。所以我们可以根据读写的不同场景,给读和写分别加上锁。

synchronized没有对读写进行区分,只要使用了就一定互斥。

这里的互斥是从CPU来的,CPU提供了一些特殊指令,通过这些指令来完成互斥,操作内核对这些指令进行了封装,并实现了阻塞等待。CPU提供一些特殊指令,操作系统对这些指令进行封装,提供了一个mutex(互斥量),JVM相当于对操作系统的mutex再封装一层,实现了synchronized这样的锁。如果当前的锁,是通过内核的mutex来完成的,开销往往比较大,如果当前的锁是在用户态通过一些其他的操作来完成的,开销往往比较小。

🌵重量级锁和轻量级锁

重量级锁:加锁解锁开销很大,往往是通过内核来完成。

轻量级锁:加锁开锁开销更小,往往是通过用户态来完成。

悲观锁做的工作更多,开销比较大,很大可能性是重量级锁;乐观锁做的工作更少,开销更小,很大可能性是轻量级锁。

🌵自旋锁和挂起等待锁

挂起等待锁:如果线程获取不到锁,就会阻塞等待,什么时候结束阻塞,取决于操作系统的具体调度,当线程挂起的时候不占用CPU。

自旋锁:如果线程获取不到锁,不阻塞等待,而是循环快速的再试一次。(如果线程1得到了锁,线程2就会快速循环的尝试获取锁,一旦线程1把锁释放,线程2就能第一时间获取到锁)。

自旋锁跟挂起等待锁相比,更节省操作系统调度线程的开销,但是更浪费CPU资源。

🌵公平锁和非公平锁

公平锁:遵守先来后到的规则。(如果线程1比线程2更先来,那么线程1会比线程2更先获取到锁)

非公平锁:不遵守先来后到的规则,获取锁的概率取决于操作系统的调度。(synchronized是非公平锁)

对于操作系统的调度器来说,默认是不公平的,默认获取锁的概率是均等的(不考虑线程的优先级情况下),要想实现公平锁,需要额外的数据结构(就比如说队列,用队列来记录线程的先来后到的过程)。大部分情况下,使用非公平锁就够用了,如果有些场景下,我们期望对于线程的调度的时间成本是可控的,这个时候就更需要公平锁了(比如我们需要通过10个线程并发执行10个任务,如果现在是非公平锁,此时若遇到前9个线程一直霸占着锁,则第10个线程始终拿不到锁,这时候用公平锁能更好的保证这10个任务能均衡的实行)。

🌵可重入锁和不可重入锁

可重入锁:之前有说过synchronized是可重入锁,就是针对同一把锁,连续加锁两次,此时不会死锁。为什么不会"死锁"呢?此时加锁的时候会让当前的锁记录一下这个锁是谁持有的,如果发现现在有同一个线程再次尝试获取锁,此时不会阻塞等待,代码会继续运行,此时这个锁里面会维护一个计算器,这个计算器就计算了当前这个线程针对这把锁尝试加锁了几次,每次解锁,计数器--,直到计数器为0,此时才真正的释放锁。

不可重入锁:如果针对同一把锁,连续加锁两次,会造成"死锁"。

 //以下就是一个可重入锁的例子
synchronized void func1(){
       func2();
}

synchronized void func2(){
    
}

//func1加锁操作,可以成功。接下来执行func2,再次尝试加锁,如果是不可重入锁,那么当前锁已经被占用
//了,此时func2这个加锁就应该阻塞等待,等待func1锁释放,func2才能获取得到锁,但是由于func2在func1
//内部,因此一旦func2阻塞,就会导致func1页阻塞,于是func1就无法执行到释放锁,这个时候代码就僵住了,
//此时就会造成"死锁"现象

🥬CAS

CAS:compare and swap,比较和交换,拿两个内存进行比较,或者是拿寄存器的值和内存的值进行比较,然后交换比较的这两个东西。这个操作是一个"原子的",CPU提供了一组CAS相关的指令,使用一条指令就可以完成上面的比较和交换过程。

例如:

int num=1;
num++;
//线程不安全,如果此时多个线程并发执行i++,就需要加锁,但是加锁是低效的,此时就可以CSA
//即可以保证高效,又能保证线程安全

基于CSA实现了一些"原子类",这些原子类也相当于一个整数,就可以实现一些++、--操作运算

都是不需要加锁就能保证线程安全。(java标准库中,也提供了原子类)

标准库中:

public static void main(String[] args) {
        AtomicInteger num = new AtomicInteger(10);
        // 这样的方法能够保证原子性. 内部就是通过 CAS 来实现的.
        // 这个操作就相当于 num++
        num.getAndIncrement();
        // 这个操作就相当于 ++num
        num.incrementAndGet();

    }

模拟实现getAndIncrement

    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }

如果有两个线程同时调用getAndIncrement,那它是怎么不用加锁就能保证线程安全的呢?

CAS其实就是在感知这两个操作之间是否夹杂了其他线程的操作,是否有其他线程在这个过程中偷偷的修改了数据 ,如果没有,此时就能直接把数据给改了,如果其他线程已经修改了,那么就重新读取旧值,交给下次循环来判定。

CAS中的ABA问题(经典面试题):

ABA问题就是说我们在使用CAS的时候也无法区分这个数据A是始终没变,还是从A变成了B,然后又变回了A,大部分情况下这种问题影响不大,但是有时候却会引入bug。

比如说我们去取钱,如果此时机子突然卡了,我们按了两下取钱,此时线程1和线程2都尝试进行-50操作

1、线程1获取到当前值为100,线程2也获取到当前值为100

2、线程1执行-50操作,比较当前值和oldval值是否一致,一致就扣钱。线程1执行完毕后,账户余额就变成了50

3、在线程2执行-50操作之前,如果突然有人往账户里面又打了

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值