前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 多线程系列Ⅵ

Java 多线程系列Ⅵ

作者头像
终有救赎
发布2024-02-25 08:55:45
880
发布2024-02-25 08:55:45
举报
文章被收录于专栏:多线程多线程

一、CAS

1、CAS特点

CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:三个操作数——内存位置、预期原值及新值

需要读写内存值V;进行比较的值A;准备写入的值B

当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试

特别注意

  1. CAS 是一个原子的硬件指令完成的。CAS 的读内存,比较,写内存操作是一条硬件指令,是原子的。
  2. CAS 是直接读写内存的, 而不是操作寄存器。
  3. 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。CAS 可以视为是一种乐观锁,或者可以理解成 CAS 是乐观锁的一种实现方式。

2、CAS的应用

标准库 java.util.concurrent.atomic 中的类,它们都是使用 CAS(Compare-And-Swap)技术实现的:

例如 AtomicInteger 类,这些类本身就是原子的,因此相关操作即使在多线程下也是安全的:

  • num.getAndIncrement();// 此操作相当于num++
  • num.incrementAndGet();// 此操作相当于++num
  • num.getAndDecrement();// 此操作相当于num–
  • num.decrementAndGet();// 此操作相当于–num

测试原子类:

代码语言:javascript
复制
public class CASTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建原子类,初始化值为0
        AtomicInteger num = new AtomicInteger(0);

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                // num++ 操作
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(num);

    }
}

num.getAndIncrement();操作伪代码:

代码语言:javascript
复制
import java.util.concurrent.atomic.AtomicInteger;  
  
public class Main {  
    static AtomicInteger num = new AtomicInteger(0);  
  
    public static void main(String[] args) {  
        int currentValue = num.get();  
        int incrementedValue = num.incrementAndGet();  
  
        System.out.println("Current value before increment: " + currentValue);  
        System.out.println("Incremented value: " + incrementedValue);  
    }  
}

num.get()用于获取当前值,num.incrementAndGet()用于增加当前值并获取增加后的值。这两个操作是原子操作,也就是说,它们不会被线程调度机制打断。因此,num.getAndIncrement()操作可以在多线程环境中安全使用。

3、CAS 实现自旋锁

代码实现

代码语言:javascript
复制
import java.util.concurrent.atomic.AtomicBoolean;  
  
public class SpinLock {  
    private AtomicBoolean locked = new AtomicBoolean(false);  
  
    public void lock() {  
        // 自旋锁的核心部分:一直循环尝试获取锁  
        while (!locked.compareAndSet(false, true)) {  
            // 等待,直到可以获取锁  
            Thread.yield();  
        }  
    }  
  
    public void unlock() {  
        locked.set(false);  
    }  
}

原理说明lock方法尝试将locked变量的值从false更改为true。如果成功,那么线程就获得了锁,可以继续执行。如果失败(也就是说,locked变量的值已经是true),那么线程就会在循环中等待,直到它可以获得锁。unlock方法将locked变量的值设回false,释放锁。

4、CAS的ABA问题

CAS操作存在一个称为ABA问题。这个问题源于CAS操作的特性:在执行CAS操作时,如果内存位置V的值被其他线程更改,然后再次更改回原始值A,那么对于执行CAS操作的线程来说,内存位置V的值仍然与预期值A匹配,因此它会错误地认为没有其他线程修改过该值。

假设有一个共享变量count,初始值为0。有两个线程A和B,它们都执行以下操作:

  1. 读取count的值;
  2. 增加count的值;
  3. 尝试使用CAS操作将count的新值写入内存。

假设线程A先读取count的值为0,然后增加它的值为1,并尝试使用CAS操作将新值1写入内存。此时,如果线程B先读取count的值为0,增加它的值为1,然后再次将count的值更改回0,并尝试使用CAS操作将新值0写入内存。由于线程B的CAS操作成功,count的值变为0,但是实际上count的值应该为1。

解决方案:可以给每个变量加上一个版本号。当变量被修改时,版本号自增。在执行CAS操作时,除了比较变量的值是否与预期值匹配外,还需要比较版本号是否一致。如果版本号不一致,则说明变量已经被其他线程修改过,需要重新读取变量的最新值并重新尝试CAS操作。

二、synchronized 原理

1、synchronized 基本特征

结合以上的锁策略,我们就可以总结出,Synchronized 具有如下特性:

  1. 原子性:synchronized 关键字可以保证被其修饰的方法或代码块具有原子性,即一个或多个操作要么全部执行成功,要么全部执行失败。
  2. 可见性:synchronized 关键字可以保证当一个线程修改共享变量的值后,其他线程可以立即看到修改后的值。
  3. 有序性:synchronized 关键字可以保证程序的执行顺序按照代码的先后顺序执行,即同一线程中的所有操作都是有序执行的。
  4. 可重入性:synchronized 关键字还可以保证同一个线程可以多次获取同一个锁,不会产生死锁。
  5. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
  6. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
  7. 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。

2、synchronized 锁升级策略

从上述synchronized锁具有的策略可知,synchronized锁可根据实际场景进行锁升级,在JVM中对synchronized主要有以下锁升级策略:

synchronized 锁升级策略是Java6之后引入的概念,在Java6之前只有两种锁状态:无锁和重量级锁;当一个线程获得锁时,其他线程需要等待该线程释放锁后才能继续执行。这种策略存在一些问题,例如死锁和性能问题。

为了解决这些问题,Java 6 引入了偏向锁、轻量级锁和重量级锁三种锁升级策略。这些策略的目的是在保证线程安全的前提下,尽可能减少锁的竞争和系统的开销。

  1. 偏向锁:偏向锁是一种优化策略,它偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录再对象Mark Word之中,同时置偏向标志位1。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。
  2. 轻量级锁:轻量级锁是偏向锁失败后的升级策略。当线程A获取轻量级锁时,会将synchronized对象头Mark Word复制一份到线程A在栈帧中创建的存储锁记录空间(即DispalcedMarkWord),然后使用CAS将对象头中的内容替换为线程A存储的所记录(DisplacedMarkWord)地址。如果线程A复制对象头时,线程B也准备获取锁,复制对象头到线程B的锁记录空间,当线程B进行CAS时发现,线程A已经将对象头替换,线程B的CAS失败,线程B尝试使用自旋等待线程A释放锁。如果线程B自旋次数到了上限,线程A仍没有释放锁,线程B仍在自选等待,此时,线程C又来竞争锁对象,轻量级锁会膨胀为重量级锁。
  3. 重量级锁:重量级锁是一种阻塞锁,它将未获得锁的线程阻塞,防止CPU空运行。重量级锁将未获得锁的线程挂起,并记录当前线程占用的资源,以便其他线程可以继续执行。当持有锁的线程释放锁时,被挂起的线程将被唤醒并重新竞争锁。这种策略可以有效地防止死锁和提高系统的性能。

3、synchronized 锁优化操作

  1. 减少锁的时间:不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
  2. 减少锁的粒度:它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率。
  3. 锁消除:编译器和JVM检测到你加锁的某块代码不涉及线程安全问题,没必要加锁,就自动帮你消除了加锁的步骤。例如:单线程情况下,使用多线程安全的类,比如StringBuffer的append等等。
  4. 锁粗化:你一个线程重复多次的获取释放同一把锁,编译器和JVM就认为你可以一个获取到之后,直到全部事情做完再释放,而不再是做一部分就释放,然后再获取。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-02-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、CAS
    • 1、CAS特点
      • 2、CAS的应用
        • 3、CAS 实现自旋锁
          • 4、CAS的ABA问题
          • 二、synchronized 原理
            • 1、synchronized 基本特征
              • 2、synchronized 锁升级策略
                • 3、synchronized 锁优化操作
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档