[Java EE] 多线程进阶 (1)

一.常见的锁策略

1.乐观锁和悲观锁

描述的是加锁时遇到的场景

悲观锁 : 加锁的时候 , 预测接下来的锁竞争会非常激烈 , 就需要针对这种情况进行一些额外的工作

乐观锁 : 加锁的时候 , 预测接下来的锁竞争情况不激烈 , 就不需要额外的工作

注意 : Synchronized 初始使用乐观锁策略 , 但发现竞争比较频繁的时候 , 就会自动切换成悲观锁策略

2.重量级锁和轻量级锁

遇到加锁之后的解决方案

重量级锁 : 加锁机制重度依赖于 OS 的 mutex ; 会有大量的内核态用户切换 , 容易引发线程的调度(一般是用来应对悲观场景下的锁 , 需要付出更多的代价)

轻量级锁 : 加锁机制尽量不再使用 mutex , 而是在用户态代码完成 , 实在搞不定了 , 再使用 mutex ; 会有少量的内核态用户切换 , 不太容易引发线程调度

用户态和内核态 : 是操作系统为权限隔离 , 保障安全而划分的两种 CPU 运行级别, 核心区别在于对硬件/系统资源的访问权限 , 所有进程运行都会在这两种状态之间切换

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

3.挂起等待锁和自旋锁

挂起等待锁 : 获取锁失败后 , 线程放弃 CPU资源 并进入阻塞队列挂起 , 只有锁被释放后才被唤醒(重量级锁的典型实现)

自旋锁 : 获取锁失败后 , 线程不放弃 CPU 并 进入自旋(循环)检测锁是否被释放(轻量级锁的典型实现) (自旋锁的特点是不阻塞线程 , 而是通过循环消耗 CPU 资源来等待锁释放 , 适用于锁竞争不激烈 , 持有时间短的场景)

注意 : synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的

总结 :

悲观锁=>重量级锁=>挂起等待锁

乐观锁=>轻量级锁=>自旋锁

synchronzied 针对上述的锁策略是自适应的

4.互斥锁和读写锁

互斥锁 : 任意时刻仅允许一个线程访问临界区(读/写均互斥)

读写锁 : ① 读锁 : 多线程可同时持有(读并发) ; ② 写锁 : 仅一个线程持有(写互斥) ; 可以提高效率 , 减少互斥的机会

Java 标准库中提供了 ReentrantReadWriteLock 类 , 实现了读写锁

  • ReenTrantReadWriteLock.ReadLock 类表示一个读锁 , 这个对象提供了 lock/unlock 方法
  • ReenTrantReadWriteLock.WriteLock 类表示一个写锁 , 实现了lock/unlock 方法
  • 其中 : ① 读加锁和读加锁之间 , 不互斥 ; ② 写加锁和写加锁之间 , 互斥 ; ③ 读加锁和写加锁之间 , 互斥

注意 : synchronized 不是读写锁

5.可重入锁和不可重入锁

可重入锁 : 允许同一线程多次获取同一把锁(递归锁) ; 内部机制 : 维护 [ 线程归属标记+锁计数器 ] , 解锁需要计数器归 0

不可重入锁 : 同一线程持有锁时 , 再次请求锁会阻塞/死锁

在 Java 中只要以 Reentrant 开头命名的锁都是可重入锁 , 而且 JDK 提高的所有现成的 Lock 实现类 , 包括 synchronized 关键字锁都是可重入的 ; 而 Linux 系统中提供的 mutex 时不可重入的

注意 : synchronized 是可重入锁

6.公平锁和非公平锁(针对插队现象的公平和非公平)

公平锁 : 按照线程等待的先后顺序获取锁 , 先等待的线程先执行

非公平锁 : 线程请求锁时直接尝试抢锁 , 抢不到再进入等待队列(机会均等)

Java 标准库中

公平锁 : ReentrantLock(true) ,

非公平锁 : ReentrantLock(false)/synchronized

注意 :

synchronized 是非公平锁

操作系统内部的线程调度就可以视为随机的 , 如果不做任何额外的限制 , 锁就是非公平锁 , 如果想要实现公平锁 , 就需要依赖额外的数据结构 , 来记录线程的先后顺序

二.synchronized 原理

1.核心特性

可重入 , 非公平 , 隐式加锁/解锁(JVM 自动处理)

2.锁升级

无锁 => 偏向锁 => 轻量级锁=> 重量级锁

注意 : 锁升级是单项的(只能从低到高) , 无法降级(不会从重量级到轻量级)

① 无锁

此时没有锁竞争 , 无需加锁

② 偏向锁(Biased Lock) -- 单线程无竞争场景

目标 : 消除单线程重复加锁的开销(仅第一次加锁有少量开销)

  • 加锁逻辑 :

  • 解锁逻辑 :

③ 轻量级锁(Lightweight Lock) -- 多线程交替竞争场景

目标 : 用 CAS 自旋代替内核态阻塞 , 减少上下文切换开销

触发条件 : 有多个线程竞争偏向锁 , JVM 撤销偏向锁 , 升级为轻量级锁

  • 加锁逻辑 :

  • 解锁逻辑 :

④ 重量级锁(Heavuweight Lock) -- 多线程持续竞争场景

目标 : 通过操作系统内核态的监视器锁保证互斥 , 牺牲性能换取稳定性

触发条件 : 轻量级锁自旋次数达到阈值 , 或多个线程同时自旋 , JVM 升级为重量级锁

  • 加锁逻辑 :

  • 监视器锁 :

3.其他锁优化

1.锁消除(Lock Elimination) - 移除"无竞争的锁"

JVM 的 JIT 编译器通过逃逸分析判断 : 若锁对象是线程私有的 , 则该线程不存在线程并发竞争 , JIT 会在编译阶段自动移除该锁的加锁操作

代码示例 :

① 局部变量作为锁对象
public void lockEliminationDemo() {
    // 锁对象:局部变量 lockObj,仅当前线程可见(无逃逸)
    Object lockObj = new Object();
    
    synchronized (lockObj) { // JIT 会消除此锁
        System.out.println("无竞争的同步代码块");
    }
}

优化后 :

public void lockEliminationDemo() {
    Object lockObj = new Object();
    System.out.println("无竞争的同步代码块"); // 锁被消除
}

②JDK 内置类的隐式锁消除

StringBufferappend() 方法是同步方法(加了 synchronized),但如果 StringBuffer 是局部变量(无逃逸),JIT 会消除锁:

public String stringBufferDemo() {
    // sb 是局部变量,无逃逸
    StringBuffer sb = new StringBuffer();
    sb.append("Java"); // 同步方法,锁被消除
    sb.append("Lock"); // 同步方法,锁被消除
    return sb.toString();
}

sb 仅在当前方法内使用,无其他线程竞争,JIT 消除 append() 方法的 synchronized 锁,性能等同于 StringBuilder

2. 锁粗化(Lock Coarsening) - 合并细粒度锁

当 JIT 检测到同一个锁对象被频繁,连续地加锁解锁 (如循环内加锁 , 连续调用同步方法) , 会见多次加解锁操作合并为一次 , 减少锁操作对底层的开销

代码示例 :

① 循环内细粒度的锁          
'public class demo38 {
    private static int count = 0;
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker){
                    count++;
                }synchronized (locker){
                    count++;
                }synchronized (locker){
                    count++;
                }
            }
        });
    }
}

优化后

public class demo38 {
    private static int count = 0;
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker){
                    count++;
                    count++;
                    count++;
                }
            }
        });
    }
}

注意 : JIT 不会无限制的粗化锁 : 若合并后的锁持有时间过长 , 可能导致其他线程长期阻塞 , JIT 会平衡 加解锁次数 和 持有锁时间 , 仅对短时间内连续的锁操作进行粗优化

维度

synchronized

ReentrantLock

锁类型

隐式锁(JVM 自动管理)

显式锁(手动 lock ()/unlock ())

公平性

仅非公平锁

可配置公平 / 非公平锁

锁升级

自动(无锁→偏向→轻量→重量)

无锁升级,默认非公平,底层依赖 CAS+AQS

功能扩展

无(仅基础加锁 / 解锁)

支持可中断锁、超时锁、条件变量(Condition)

三.CAS(Compara And Swap)

CAS (比较和交换)是 Java 实现无锁并发编程的核心底层机制 , 属于乐观锁的实现 , 通过 CPU 原子指令保证操作的原子性 , 无需传统锁(如 synchronized)的阻塞/唤醒开销

1.核心操作

  • 一个 CAS 涉及到的操作 : 若内存地址 V 的值 == 预期值 A , 则将 V 更新为 B , 返回 true ; 否则不操作 , 返回 false
  • 硬件层面的 CPU 原子指令 , JVM 通过 Unsafe 类调用底层指令 , 保证"比较 - 交换" , 不可中断 ; 当多个线程同时对某个资源进行 CAS 操作时 , 只能有一个线程操作成功 , 但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号
  1. check and set
  2. read and update

一个伪代码

参数 : 内存地址(要操作变量的内存地址) , 预期值(变量的旧值) , 新值

booleanCAS(address, expectValue, swapValue) {
    if (&address == expectedValue) { 
        &address = swapValue; 
        return true; 
    } 
    return false; 
}

2.应用

① 是java.util.concurrent.atomic

import java.util.concurrent.atomic.AtomicInteger;

public class demo39 {
    private static AtomicInteger count = new AtomicInteger(0);//赋初始值为0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();//count++;
                //count.incrementAndGet();//++count;
                //count.addAndGet(4);//count+=4;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();//count++;
                //count.incrementAndGet();//++count;
                //count.addAndGet(4);//count+=4;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }

}

② 基于 CAS 实现自旋锁

伪代码

public class SpinLock { 
    private Thread owner = null; 
    public void lock(){ 
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
            
        } 
    } 
    public void unlock (){
        this.owner = null; 
    }
}

若 owner 为 null(锁未被持有) , 则将其设为当前线程 ; 若锁已经被其他线程持有 , 则通过 while 循环自旋等待

3.ABA 问题

① 问题描述 :

线程 1 操作是 [初始值A => 经过修改操作值为B] ; 线程 2 的操作是 [数是指为 A => 修改为 C => 又退回 A] ; 此时线程 1 执行 CAS 时 , 检测到变量仍然为 A , 误以为未被修改 , 会再次将其更新为 B

② 问题核心 :

CAS 仅校验最终值 , 忽略中间修改

③ 解决方案 :

引入版本号 , 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期

当真正修改的时候 , 如果当前版本号 与 读到的版本号相同 , 则修改数据 , 并把版本号+1 , 如果当前版本号高于读到的版本号 , 就操作失败(认为数据已经被修改过了)

4.仅支持单个变量原子操作 问题

① 问题描述 :

CAS 只能保证单个变量操作的原子性 , 无法直接实现多变量的原子更新(如同时更新 a 和 b)

② 解决方案 :

        合并变量 : 将多个变量合并为一个对象 , (通过 AtomicReference 操作对象引用);
import java.util.concurrent.atomic.AtomicReference;

class MulNum{
    int a;
    int b;
    public MulNum(int a,int b){
        this.a = a;
        this.b = b;
    }
}
public class demo40 {
    private static AtomicReference<MulNum> count = new AtomicReference<>(new MulNum(1,2));

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread t1 = new Thread(()->{
                while(true){
                    MulNum oldVar = count.get();
                    MulNum newVar = new MulNum(oldVar.a + 1, oldVar.b + 1);
                    if(count.compareAndSet(oldVar,newVar)){
                        System.out.println("更新后:a=" + newVar.a + ", b=" + newVar.b);
                        break;
                    }
                }
            });
            t1.start();
        }
    }
}
        实现互斥锁(ReentrantLock 为例)
import java.util.concurrent.locks.ReentrantLock;

public class demo41 {
    private int a = 1;
    private int b = 2;
    private ReentrantLock lock = new ReentrantLock();

    public void updateMultiVar() {
        lock.lock();
        try {
            a++;
            b++;
            System.out.println("更新后:a=" + a + ", b=" + b);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        demo41 demo = new demo41();
        for (int i = 0; i < 5; i++) {
            new Thread(demo::updateMultiVar).start();
        }
    }
}
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值