Volatile、Synchronized、ReentrantLock锁机制使用说明

news2024/11/25 14:53:48

 一、Volatile底层原理

volatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。

  1. 当对volatile变量进行写操作的时候,JVM会向处理器发送一条LOCK前缀的指令,将该变量所在缓存行的数据写回系统内存。
  2. 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

来看看缓存一致性协议是什么。 缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。

volatile关键字的两个作用:

  1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

 volatile变量的使用:

public class Main extends Thread {
  private volatile boolean keepRunning = true;
  public void run() {
    System.out.println("Thread started");
    while (keepRunning) {
      try {
        System.out.println("Going to sleep");
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("Thread stopped");
  }
  public void stopThread() {
    this.keepRunning = false;
  }
  public static void main(String[] args) throws Exception{
    Main v = new Main();
    v.start();
    Thread.sleep(3000);
    System.out.println("Going to set the stop flag to true");
    v.stopThread();
  }
}

二、synchronized的用法有哪些?

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized的作用有哪些?

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。

monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

关键字synchronized示例:

public class Main {
  private static int myValue = 1;

  public static void main(String[] args) {
    Thread t = new Thread(() -> {
      while (true) {
        updateBalance();
      }
    });
    t.start();
    t = new Thread(() -> {
      while (true) {
        monitorBalance();
      }
    });
    t.start();
  }

  public static synchronized void updateBalance() {
    System.out.println("start:" + myValue);
    myValue = myValue + 1;
    myValue = myValue - 1;
    System.out.println("end:" + myValue);
  }

  public static synchronized void monitorBalance() {
    int b = myValue;
    if (b != 1) {
      System.out.println("Balance  changed: " + b);
      System.exit(1); 
    }
  }
}

上面的代码生成以下结果:

volatile和synchronized的区别?

  1. volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。
  2. volatile至保证可见性;synchronized保证原子性与可见性。
  3. volatile禁用指令重排序;synchronized不会。
  4. volatile不会造成阻塞;synchronized会。

Synchronized总共有三种用法

  1. 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this)
  2. 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁
  3. 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例

ReentrantLock和synchronized区别?

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

wait()和sleep()的异同点?

相同点

  1. 使当前线程暂停运行,把机会交给其他线程
  2. 任何线程在等待期间被中断都会抛出InterruptedException

不同点

  1. wait()是Object超类中的方法;而sleep()是线程Thread类中的方法
  2. 对锁的持有不同,wait()会释放锁,而sleep()并不释放锁
  3. 唤醒方法不完全相同,wait()依靠notify或者notifyAll、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒
  4. 调用wait()需要先获取对象的锁,而Thread.sleep()不用

Runnable和Callable有什么区别?

  • Callable接口方法是call(),Runnable的方法是run()
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常。

守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

线程间通信方式

volatile

volatile 使用共享内存实现线程间相互通信。多个线程同时监听一个变量,当这个变量被某一个线程修改的时候,其他线程可以感知到这个变化。

wait和 notify

wait/notify为Object对象的方法,调用wait/notify需要先获得对象的锁。对象调用wait()之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的notify()方法后,等待线程并不会立即从wait()返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从wait()方法返回,即从wait()方法返回前提是线程获得锁。

join

当在一个线程调用另一个线程的join()方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行。join()是基于等待通知机制实现的。

三、AQS原理

AQS,AbstractQueuedSynchronizer,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state加1。如果 state不为0,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享)构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

四、ReentrantLock 是如何实现可重入性的?

ReentrantLock内部自定义了同步器sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,会检查当前占有锁的线程和当前请求锁的线程是否一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。

源码如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

ReentrantLock 示例: 

public class ReentrantLockDemo01 implements Runnable {

    private Lock lock = new ReentrantLock();

    private int tickets = 200;

    @Override
    public void run() {
        while (true) {
            lock.lock(); // 获取锁
            try {
                if (tickets > 0) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " " + tickets--);
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 释放所
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo01 reentrantLockDemo = new ReentrantLockDemo01();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(reentrantLockDemo, "thread" + i);
            thread.start();
        }
    }
}

reentrantlock用于替代synchronized

  • 需要注意的是,必须要必须要必须要手动释放锁(重要的事情说三遍)
  • 使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放
  • 使用reentrantlock可以进行“尝试锁定”tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待
  • 使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应
  • 在一个线程等待锁的过程中,可以被打断

五、锁的分类

公平锁与非公平锁

按照线程访问顺序获取对象锁。synchronized是非公平锁,Lock默认是非公平锁,可以设置为公平锁,公平锁会影响性能。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

共享式与独占式锁

共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

悲观锁与乐观锁

悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,synchronizedReentrantLock属于悲观锁。

乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是CAS

适用场景:

  • 悲观锁适合写操作多的场景。
  • 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。

六、乐观锁有什么问题?

乐观锁避免了悲观锁独占对象的问题,提高了并发性能,但它也有缺点:

  • 乐观锁只能保证一个共享变量的原子操作。
  • 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
  • ABA问题。CAS的原理是通过比对内存值与预期值是否一样而判断内存值是否被改过,但是会有以下问题:假如内存值原来是A, 后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变。可以引入版本号解决这个问题,每次变量更新都把版本号加一。

七、什么是CAS?

CAS全称Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock内部的AQS和原子类内部都使用了CAS。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” 通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。 类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。

CAS的目的

原子操作是利用类似的特性完成的。整个JUC都是建立在CAS之上的,因此对于synchronized阻塞算法JUC在性能上有了很大的提升。

参考:Java并发八股文第二弹 - 腾讯云开发者社区-腾讯云

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/596302.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DJ4-7 请求分页存储管理方式

目录 4.7.1 请求分页中的硬件支持 1、页表机制 2、缺页中断机构 4.7.2 内存分配策略和分配算法 1、最小物理块数的确定 2、物理块的分配策略 3、物理块的分配算法 4.7.3 调页策略 1、系统应当在何时把一个页面装入内存&#xff1f; 2、从何处调入页面&#xff1f;…

机器学习常识 12: SVM

摘要: 支持向量机 (support vector machine, SVM) 有很多闪光点, 理论方面有 VC 维的支撑, 技术上有核函数将线性不可分变成线性可分, 实践上是小样本学习效果最好的算法. 1. 线性分类器 如图 1 所示, 基础的 SVM 仍然是一个线性二分类器, 这一点与 logistic 回归一致. 图 1.…

MATLAB 之 隐函数绘图、图形修饰处理、图像处理与动画制作和交互式绘图工具

这里写目录标题 一、隐函数绘图1. 隐函数二维绘图3. 隐函数三维绘图 二、图形修饰处理1. 视点处理2. 色彩处理2.1 颜色的向量表示2.2 色图2.3 三维曲面图形的着色 3. 图形的裁剪处理 三、图像处理与动画制作1. 图像处理1.1 图像的读/写1.2 图像的显示 2. 动画制作2.1 制作逐帧动…

chatgpt赋能python:Python交流App:提高Python社区交流效率

Python 交流 App: 提高 Python 社区交流效率 Python 是当今流行程度最高的编程语言之一&#xff0c;有着广泛的应用场景和庞大的社区。 作为 Python 工程师&#xff0c;经常有各种问题需要得到解决&#xff0c;同时也希望能与同行进行交流、分享和学习。这时&#xff0c;一款高…

Linux——Centos7进入单用户模式修改密码

本篇文章适用于经常忘记自己root用户密码的初学者&#xff01;&#xff01;&#xff01;&#xff0c;会进入单用户模式修改root密码即可。 系统启动进入到如下界面后输入字母“e”; 2.可以看到进入到如下界面&#xff1b; 3.一直下翻到图中圈起来的这部分&#xff1b; 4.在Lin…

excel 获取指定字符前后的字符串

目录 excel 获取指定字符前后的字符串 1.截取指定字符前的字符串 2.截取指定字符后的字符串 excel 获取指定字符前后的字符串 1.截取指定字符前的字符串 1.1LEFT FIND find:返回一个字符串在另一个字符串中出现的起始位置。 (区分大小写&#xff0c;且不允许使用通配符) …

综合指挥调度系统行业分类汇总

综合指挥调度系统是将语音、视频、GIS进行高度融合&#xff0c;构建“平战结合”的指挥调度模式&#xff0c;既满足平时的应急培训、日常通信、会议会商等要求&#xff0c;也能够应对战时的应急指挥、应急救援、应急决策等需求&#xff0c;达到统一指挥、联合行动的目的&#x…

ArcGIS中实现土地利用转移矩阵

土地利用转移矩阵&#xff0c;就是根据同一地区不同时相的土地覆盖现状的变化关系&#xff0c;求得一个二维矩阵。通过对得到的转移矩阵进行分析&#xff0c;能够得到&#xff12;个时相&#xff0c;不同的地类之间相互转化的情况&#xff0c;它描述了不同的土地利用的类型在不…

新华三的网络脉动:为AI泵血,向产业奔流

AI大模型作为最新的通用技术&#xff0c;今年以来&#xff0c;发展如火如荼。也有很多从业者和专家注意到&#xff0c;AI模型训练和应用过程中&#xff0c;需要优先考虑网络的升级与适配。 如果说数据中心、算力集群是AI的“心脏”&#xff0c;那么网络就犹如AI的“动脉”&…

《嵌入式存储器架构、电路与应用》----学习记录(一)

前言 本书系统介绍嵌入式存储器在架构、电路和应用方面的技术进展&#xff0c;包括SRAM、eDRAM、eFlash和近几年兴起的阻变型存储器&#xff0c;并着重介绍我国研究人员在嵌入式存储器方面的研究工作。 第1章 绪论 1.1 什么是嵌入式存储器 存储器(Memory)是现代信息技术中用…

C语言 出现 “从属语句不能是声明”的情况和解决办法

C 出现 “从属语句不能是声明”的情况和解决办法 发blog的时候是六一儿童节&#xff01;2023.6.1&#xff0c;过期的小朋友们都节日快乐&#xff01; 笔者在遇到这个bug的时候的情况是这样的 在查阅了网上的一些资料后也没有发现对应的解决办法&#xff0c;最后发现是一个很基…

chatgpt赋能python:Python中交换变量值的几种方法

Python中交换变量值的几种方法 交换变量值是编程语言中的常见操作&#xff0c;也是Python中常见的操作之一。本文将介绍几种不同的方法来在Python中交换变量的值。 方法一&#xff1a;使用中间变量 最简单的交换变量值的方法之一是使用中间变量&#xff0c;如下所示&#xf…

chatgpt赋能python:Python交流群:分享经验、解决问题、结交朋友

Python 交流群&#xff1a;分享经验、解决问题、结交朋友 Python 是一种高级编程语言&#xff0c;被广泛使用于数据科学、机器学习、人工智能、网络开发、游戏开发等众多领域。作为一个有着10年 Python 编程经验的工程师&#xff0c;我深感 Python 社区的活力和创新力。其中&a…

亿发软件:生产制造供应链管理系统,建设中小型制造企业信息化

在中小型制造企业领域&#xff0c;高效的供应链管理是成功的关键。然而&#xff0c;中小企业所面临的有限的资源和市场需求&#xff0c;需要借助信息化建设&#xff0c;可以提升生产流程&#xff0c;优化供应链管理。亿发软件供应链信息化管理解决方案&#xff0c;优化中小企业…

前后端分离开发模式解决之道

你有没有遇到过&#xff1a; 前端代码刚写完&#xff0c;后端的接口又变了。接口文档永远都是不对的。测试工作永远只能临近上线才能开始。 为什么接口会频繁变动&#xff1f; 设计之初没有想好。 这需要提高需求的理解能力和接口设计能力。 变动的成本较低。 德国有句谚语…

【LeetCode】318. 最大单词长度乘积

318. 最大单词长度乘积&#xff08;中等&#xff09; 方法一 思路 这道题有两个要求&#xff1a; length(words[i]) * length(words[j]) 的最大值&#xff1b;这两个单词不含有公共字母。 首先我们考虑第一个条件&#xff1a;长度乘积的最大值。 是否需要将 words 中的单词按…

Zabbix与麒麟软件完成产品兼容互认证!

信创生态 | Zabbix与麒麟兼容性互认证 上海宏时数据系统有限公司作为Zabbix大中华区总代理&#xff0c;其Zabbix技术服务能力得到国内众多客户肯定。 同时&#xff0c;根据国内用户使用场景的需求&#xff0c;宏时数据自主研发的“UMOP统一监控运维平台”&#xff0c;并已成功…

校园导航小程序 开发指引 与 注意事项

&#x1f52c; 注意事项 大部分数据存储在utils.js中的&#xff0c;页面通过引入utils.js方式渲染数据 图标全部存储在项目images文件夹里,均下载自 iconfont网站&#xff08;自行替换&#xff09; 部分图片引用自 免费图床 - CDN加速图床&#xff08;自行替换&#xff09; …

四维轻云是如何实现地理空间数据云管理的?

四维轻云是一款轻量化的地理空间数据网页管理平台&#xff0c;支持倾斜模型(.osgb)、激光点云(.las)、正射影像(dom)和数字高程模型(dem)等多种地理空间数据的在线管理、编辑及分享&#xff0c;其他类型地理空间数据也将陆续上线。 目前&#xff0c;平台具有项目管理、数据上传…

linux大作业

第1题 自学第10章10.1 Java开发的内容&#xff0c;在Ubuntu系统上安装Eclipse&#xff0c;搭建Java开发环境&#xff0c;并进行测试。 1、在终端输⼊以下命令&#xff1a;sudo apt update 2、运行以下命令安装Java开发工具包&#xff08;JDK&#xff09;&#xff1a; sudo apt …