【JavaEE】多线程(6)

一、用户态与内核态

【概念】

用户态是指用户程序运行时的状态,在这种状态下,CPU只能执行用户态下的指令,并且只能访问受限的内存空间

内核态是操作系统内核运行时的状态,内核是计算机系统的核心部分,CPU可以执行所有指令,可以访问所有内存空间

【 两者切换原因】

当用户程序需要执行一些需要操作系统支持的操作时,需要将用户态切换到内核态

【举例】

线程的阻塞与唤醒就需要用户态与内核态切换

当线程被阻塞时,线程会从用户态切换到内核态,操作系统内核会处理阻塞请求将线程的状态设置为阻塞,并将其添加到等待队列中

等线程被唤醒时,操作系统会在内核态中将线程的状态改为就绪并将其从等待队列中移除,切换到用户态后,继续执行用户态下的指令

【注意】

用户态与内核态之间切换的开销非常大,因此,减少不必要的用户态与内核态之间的切换对于系统性能和效率提高很重要

二、锁策略

2.1 什么是锁策略

锁策略是指在多线程编程中,这把锁在加锁、解锁、锁冲突时都会怎么做

2.2 乐观锁 vs 悲观锁

悲观锁认为多个线程访问同⼀个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁

乐观锁认为多个线程访问同⼀个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据

在访问的同时识别当前的数据是否出现访问冲突.

【Java中synchronized是哪种锁】

synchronized既是乐观锁也是悲观锁,因为它支持自适应

synchronized在开始的时候会使用乐观锁,当发现锁竞争的次数增加时会切换为悲观锁

2.3 重量级锁 vs 轻量级锁

一般认为悲观锁就是重量级锁,乐观锁就是轻量级锁

重量级锁加锁的过程做的事情多------重量;轻量级锁加锁的过程做的事情少------轻量

synchronized是一个轻量级锁,如果锁冲突比较严重就会变成重量级锁

2.4 自旋锁 vs 挂起等待锁

自旋锁是轻量级锁的一种典型实现方式,下面是自旋锁一段伪码:

java 复制代码
while (true) {
    if (锁是否被占用) {
        continue;
    }
    获取到锁
    break;
}

CPU在忙等、空转,如果获取锁失败,就立即再尝试获取锁,无限循环,直到获取到锁为止;消耗了更多的CPU资源,但是锁一旦被释放,就会第一时间拿到锁

自旋锁轻量的原因:一方面自旋锁避免了线程的阻塞与唤醒的开销,减少了性能的消耗;另一方面自旋锁一般适用于线程占用锁时间较少的场景,不会造成过多CPU资源

拿到锁的速度更快,但消耗CPU

挂起等待锁是重量级锁的一种典型实现方式,借助系统中的线程调度,如果当前锁被占用,该线程尝试获取锁,就会挂起(阻塞状态),直到这个锁被释放,系统调度到这个线程,该线程才会尝试获取这个锁

挂起等待锁重量的原因:需要进行线程的阻塞与唤醒,有较多的用户态与内核态之间的切换,重量

拿到锁的速度更慢,节省CPU

synchronized 轻量级锁部分是基于自旋锁实现的,重量级锁部分是基于挂起等待锁实现的

2.5 可重入锁 vs 不可重入锁

可重入锁:同一个线程,针对同一把锁,连续加锁两次,不会死锁

不可重入锁:同一个线程,针对同一把锁,连续加锁两次,会死锁

synchronized是可重入锁

2.6 公平锁 vs 非公平锁

公平锁: 严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就先拿到锁

非公平锁: 多个线程随机获取到锁,和线程等待时间无关

synchronized属于非公平锁

2.7 互斥锁 vs 读写锁

synchronized是互斥锁

读写锁是一个比较特殊的锁,先来看下面几个有关线程安全场景:

  • 两个线程只读一个共享数据,不会发生线程安全问题
  • 两个线程写一个共享数据,会发生线程安全问题
  • 两个线程一个都一个写,会发生线程安全问题

读写锁拥有一下功能:

  • 读锁和读锁之间不会发生互斥------有利于降低锁冲突的概率
  • 写锁和写锁之间会发生互斥
  • 读锁和写锁之间会发生互斥

synchronized不是读写锁,因为加上synchronized后,即使是两个都只读共享变量也会产生互斥

三、synchronized 实现原理

3.1 特点

  • 既是悲观锁,也是乐观锁
  • 既是轻量级锁,也是重量级锁;轻量级锁基于自旋锁实现,重量级锁基于挂起等待所实现
  • 可重入锁
  • 非公平锁
  • 是互斥锁,不是读写锁

3.2 synchronized 自适应

什么是偏向锁:

代码首次执行synchronized对对象加锁时并不是真正加锁 ,而是作一个标记,如果后续没有其他线程针对这个对象加锁的话,就一直保持这种状态,直到解锁,这样就减少了系统开销

当后续有其他线程占用同一个锁对象加锁时,才会真正加锁,此时就已升级成了轻量级锁

3.3 锁消除

锁消除是一种锁优化策略

当在代码中写了加锁的操作,编译器&JVM会对你当前的代码进行检查,看这个锁加的是否合适,如果完全没必要加锁,就会把加锁操作优化掉

比如在单线程的环境下进行加锁操作,该操作就会被编译器优化掉

3.4 锁粗化

锁的粒度:当加锁的范围内,进行的操作越多,锁的粒度越粗,反之,锁的粒度越细

在保证逻辑等价的情况下,为了避免频繁加锁解锁,编译器会将多次细粒度的锁,合并成一次粗粒度的锁

四、CAS

4.1 什么是CAS

CAS(compare and swap),意为比较和交换,一个CAS设计以下操作

假设内存中的值为V,旧的预期值为A,要修改的值为B

  1. 比较V与A是否相等
  2. 如果相等,则将B写入V(交换)
  3. 返回操作是否成功

下面是一段CAS的伪码:

java 复制代码
boolean CAS (address, exceptValue, swapValue) {
    if (&address == exceptValue) {
        address = swapValue;
        return true;
    }
    return false;
}

注意:上述代码并不是原子的,真实的CAS是一个原子硬件指令,改代码只是辅助理解

当多个线程针对某一资源进行CAS操作,只有一个线程操作成功,但是其他线程并不会阻塞,而是收到操作失败的信号

4.2 CAS是怎么实现的

简而言之,是因为硬件方面提供了支持,软件层面才可以做到,由于CPU提供了CAS对应的硬件指令,因此操作系统内核也能够完成这样的操作,之后OS会提供出CAS的api,JVM对OS提供的api进一步的封装,我们便可以在Java中使用CAS操作了

4.3 CAS 的应用

1)原子类

标准库中提供了java.util.concurrent.atomic 包,里面的类都是基于CAS实现的原子类

我们以 AtomicInteger类为例:

java 复制代码
public class Demo {
    public static void main1(String[] args) {

        AtomicInteger count = new AtomicInteger(1);

        count.getAndIncrement(); // count++

        count.incrementAndGet(); // ++count

        count.getAndDecrement(); // count--

        count.decrementAndGet(); // --count

        count.getAndAdd(100); //count += 100

    }
}

上述代码的加加减减操作都是原子的,没有用到任何加锁操作

接下来以其中一个方法为例进行详细剖析:看getAndIncrement()的伪代码

java 复制代码
class AtomicInteger {

    private int value;

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

假设两个线程同时调用getAndIncrement

  1. 两个线程都读取value的值到oldValue中(oldValue是一个局部变量,每个线程都有自己的栈)
  1. 线程1先执行CAS,发现oldValue 和 value 相同,则直接对value 赋值 oldValue + 1,注意这里是getAndIncrement,所以先获取再加加,所以返回的是oldValue,但其实value已经加1了
  1. 线程2再执行CAS的时候,发现value 和 oldValue不相等,则进入循环,在循环里重新获取value的值并赋值给oldValue
  1. 线程2第二次执行CAS,发现oldValue 和 value相同,于是执行赋值操作
  1. 线程1和线程2针对同一个变量进行加加操作,整个过程线程是安全的并且没有用到锁

2)实现自旋锁

上述线程2在循环中重新将value赋值给oldValue的操作很像自旋锁的实现逻辑,实际上,自旋锁就是基于CAS实现的,来看伪代码:

java 复制代码
public class SpinLock {
    private Thread owner = null; //此时owner处于未加锁状态

    public void lock(){

        while(!CAS(this.owner, null, Thread.currentThread())){
        }

    }

    public void unlock (){
        this.owner = null;
    }
}

代码中owner用来追踪加锁的线程,如果为null,代表代码中没有任何一个线程加锁,接下来有一个线程1调用lock()方法进行加锁,执行CAS,发现owner为空,则直接进行加锁,并owner指向这个加锁的线程,CAS执行成功,返回true,取反后跳出while循环

此时又来一个线程2也要进行加锁(假设这里锁对象和线程1相同),调用lock()方法,发现owner不为空,说明有其他线程进行了加锁,那就进入循环,并不断尝试CAS操作

当线程1解锁后,调用unlock()方法,此时owner为空,线程2执行CAS操作成功,成功加锁并跳出循环

4.4 CAS 的 ABA 问题

CAS的核心是:比较发现相等→交换,CAS希望的是数据从来没改变过(相等)但是某些情况,可能会有其他线程将数据从A→B→A,CAS并不能判断数据中途是否有发生改变,这就是ABA问题

ABA在一些极端情况下可能产生bug,开下面一段取款的伪代码:

java 复制代码
void 取款 () {
    int oldBalance = balance; // balance 为当前账户余额
    // CAS执行成功,取款500
    while (!CAS(balance, oldBalance, balance - 500)) {}

}

假如我的初衷就是取500块钱,取款机创建了两个线程来并发执行-500操作,我们希望一个-500成功,一个-500失败

此时如果加一个转账的操作就会引发bug

如何避免ABA问题:

上述场景中,用余额来判定本身就不太科学,因为余额会发生改变,容易引发ABA问题

引入版本号,约定版本号只能加 不能减,每次操作余额版本号都要+1,如果版本号没有改变,余额就一定没有改变过


🙉本篇文章到此结束

相关推荐
半夏知半秋13 分钟前
lua debug相关方法详解
开发语言·学习·单元测试·lua
Andy01_16 分钟前
Java八股汇总【MySQL】
java·开发语言·mysql
唐 城24 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·spring·log4j
V+zmm1013433 分钟前
社区二手物品交易小程序ssm+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
坊钰35 分钟前
【Java 数据结构】合并两个有序链表
java·开发语言·数据结构·学习·链表
秋天下着雨42 分钟前
apifox调用jar程序
java·python·jar
m0_748251081 小时前
docker安装nginx,docker部署vue前端,以及docker部署java的jar部署
java·前端·docker
A22741 小时前
Redis——缓存雪崩
java·redis·缓存
Mr.朱鹏1 小时前
操作002:HelloWorld
java·后端·spring·rabbitmq·maven·intellij-idea·java-rabbitmq
顽疲1 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app