面试笔记——线程安全

ops/2024/11/9 5:13:47/

sychronized的底层原理

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
对象锁的互斥功能是由jvm提供的Monitor(由C++语言实现)实现的,通过 javap -v xx.class 查看class字节码信息,如图:
在这里插入图片描述
第二个monitorexit是为了防止代码抛了异常之后能同样释放对象锁(在底层隐式地使用了try-finally,正常代码抛了异常,通过第二个monitorexit释放锁)。

Monitor的内部属性和功能:
在这里插入图片描述
线程获得锁需要对象(锁)(如上图中的lock)关联monitor。

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

在HotSpot虚拟机中,对象在内存中存储可分为:对象头(Header)、实例数据(Instance Data)和对齐填充,如图:
在这里插入图片描述
32位的对象头信息包括:
在这里插入图片描述

  • hashcode:25位的对象标识Hash码
  • age:对象分代年龄占4位
  • biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
  • thread:持有偏向锁的线程ID,占23位
  • epoch:偏向时间戳,占2位
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

对象和Monitor的关联: 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针 (记录在ptr_to_lock_record中)。

在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁,其流程为:

  • 加锁流程
    1. 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
    2. 通过CAS指令(原子操作)将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
    3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
    4. 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
  • 解锁过程
    1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
    2. 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
    3. 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

如,对同一个对象锁进行重入时,就采取的是轻量锁,代码如下:
在这里插入图片描述
加锁过程如下:
在未进入对象锁前,object的存储信息(此时是无锁状态)为:
在这里插入图片描述
当执行到method1时,就会创建一个锁记录——Lock Record,每个线程的栈帧都包含着一个锁记录结构,在锁记录中会存储锁定对象的Mark Word,由Object reference指向对象:
在这里插入图片描述

当前线程持有锁之后,会用CAS交换Mark Word和Lock Record的数据,表示该线程拥有了该对象锁:
在这里插入图片描述
当method2执行时,会发生锁重入,则直接在线程中添加一个Lock Record就可以了,但是第一次已经把Mark Word记录到了Lock Record中,所以在锁重入时,只需要添加记录就可以(但还是进行CAS操作,每加一个锁记录都会进行CAS操作),然后指向对象:
在这里插入图片描述
解锁时:
遍历线程栈的Lock Record,若Lock Record的Mark Word为null,则代表这是一次重入,删除掉Lock Record就可以了;若Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。

轻量级锁的特点: 在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

总结:
在这里插入图片描述
PS:只要锁发生了竞争,都会升级为重量级锁。

Java 内存模型

JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)。线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存。
在这里插入图片描述

CAS

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作,如 AbstractQueuedSynchronizer(AQS框架)和AtomicXXX类。
CAS数据交换流程:一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋(即再从共享内存中去拿值V)的方式等待并再次尝试,直到成功。

CAS的优缺点:

  • 因为没有加锁,所以线程不会陷入阻塞,效率较高
  • 如果竞争激烈,重试频繁发生,效率会受影响

补充——乐观锁和悲观锁:

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗(即,自旋)。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

CAS 底层实现:CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,由C++实现的。
ReentrantLock中的一段CAS代码:
在这里插入图片描述

volatile

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证线程间的可见性
  • 禁止进行指令重排序

线程间的可见性: 用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。

举例

public class ForeverLoop {static boolean stop = false;public static void main(String[] args) {new Thread(() -> {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}stop = true;System.out.println(Thread.currentThread().getName()+":modify stop to true...");},"t1").start();new Thread(() -> {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+stop);},"t2").start();new Thread(() -> {int i = 0;while (!stop) {i++;}System.out.println("stopped... c:"+ i);},"t3").start();}
}

在默认情况下,执行上面的代码时的输出为:

t1:modify stop to true...
t2:true

此时的第三个线程依然在执行
疑问:根据第一个和第二个线程的执行结果看,不同线程可以访问由volatile关键字修饰的变量,但是为什么第三个线程还在继续执行呢?
答案如下:在JVM虚拟机中有一个JIT(即时编译器)给代码做了优化(while这个代码块执行的次数太多,直接把条件改成固定的条件):
在这里插入图片描述
解决方案:

  • 在程序运行的时候加入vm参数 -Xint 表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)
    在这里插入图片描述
  • 在修饰stop变量的时候加上volatile,当前告诉JIT,不要对 volatile 修饰的变量做优化
static volatile boolean stop = false;

禁止指令重排序: 用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。
举例:
在这里插入图片描述

在这里插入图片描述volatile使volatile使用技巧:

  • 写变量让volatile修饰的变量的在代码最后位置
  • 读变量让volatile修饰的变量的在代码最开始位置

AQS

AbstractQueuedSynchronizer,即抽象队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的。
AQS与Synchronized的区别:

synchronizedAQS
关键字,c++ 语言实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类:

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

AQS基本工作机制:
在这里插入图片描述
在AQS中有一个由volatile修饰的变量state来表示状态,若线程0想要持有锁,则需要将state的状态由0改为1,表示该线程持有锁;若此时线程1想要获得锁,但此时state状态为1,会请求失败,则会将线程1加入到FIFO队列中,进行等待;线程2同理;若线程0释放锁后,则会把锁分配给FIFO中的head所指向的线程——线程1。

若多个线程同时抢资源时的情况: 如当state的状态还是0,此时多个线程请求该资源,则通过CAS设置state状态,保证操作的原子性,没有抢到资源的线程从尾部添加到FIFO队列中。

关于AQS是公平锁还是非公平锁:
根据AQS不同的实现方式,AQS既可以实现非公平锁也可以实现公平锁:

  • 新的线程与队列中的线程共同来抢资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

ReentrantLock实现原理

ReentrantLock是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
构造方法接受一个可选的公平参数**(默认非公平锁)** ,当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock的构造方法:

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

NonfairSync 继承自 AQS:

abstract static class Sync extends AbstractQueuedSynchronizer {}

在这里插入图片描述

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

综上:

  • ReentrantLock表示支持重新进入的锁,调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞
  • ReentrantLock主要利用CAS+AQS队列来实现
  • 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

ReentrantLock的功能测试:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockTest {//创建锁对象static ReentrantLock lock = new ReentrantLock();//条件1static Condition c1 = lock.newCondition();//条件2static Condition c2 = lock.newCondition();public static void main(String[] args) throws InterruptedException {//可打断
//        lockInterrupt();//可超时
//        timeOutLock();//多条件变量conditionTest();}/*** 多条件变量*/public static void conditionTest(){new Thread(() -> {lock.lock();try {//进入c1条件的等待c1.await();System.out.println(Thread.currentThread().getName()+",acquire lock...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}, "t1").start();new Thread(() -> {lock.lock();try {//进入c2条件的等待c2.await();System.out.println(Thread.currentThread().getName()+",acquire lock...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}, "t2").start();new Thread(() -> {lock.lock();try {//唤醒因等待c1条件的所有线程//c1.signalAll();//唤醒c1条件的一个线程c1.signal();//唤醒c2条件的线程c2.signal();System.out.println(Thread.currentThread().getName()+",acquire lock...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}, "t3").start();}/*** 锁超时* @throws InterruptedException*/public static void timeOutLock() throws InterruptedException {Thread t1 = new Thread(() -> {//尝试获取锁,如果获取锁成功,返回true,否则返回falsetry {if (!lock.tryLock(2, TimeUnit.SECONDS)) {System.out.println("t1-获取锁失败");return;}} catch (InterruptedException e) {e.printStackTrace();}try {System.out.println("t1线程-获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();System.out.println("主线程获得了锁");t1.start();try {Thread.sleep(3000);} finally {lock.unlock();}}/*** 可打断* @throws InterruptedException*/public static void lockInterrupt() throws InterruptedException {Thread t1 = new Thread(() -> {try {//开启可中断的锁lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();System.out.println("等待的过程中被打断");return;}try {System.out.println(Thread.currentThread().getName() + ",获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();System.out.println("主线程获得了锁");t1.start();try {Thread.sleep(1000);t1.interrupt();System.out.println("执行打断");} finally {lock.unlock();}}
}

synchronized和Lock的区别

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁产生的条件

死锁是指在多线程或多进程的系统中,每个进程或线程都在等待其他进程或线程释放资源,导致它们都无法继续执行的状态。简而言之,它是一种资源竞争的情况,其中每个进程或线程都在等待其他资源的释放,而同时也不释放自己的资源,从而导致所有的进程或线程都陷入了僵局,无法继续执行下去。

比如,进程1持有资源A,等待资源B,而进程2持有资源B,等待资源A。这时候,如果1和2都不释放自己的资源,它们就会陷入死锁状态,无法继续向下执行。

Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {synchronized (A) {System.out.println(Thread.currentThread().getName()+"-lock A");try {  sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println(Thread.currentThread().getName()+"-lock B");System.out.println(Thread.currentThread().getName()+"-操作...");}}
}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {System.out.println(Thread.currentThread().getName()+"-lock B");try {sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (A) {System.out.println(Thread.currentThread().getName()+"-lock A");System.out.println(Thread.currentThread().getName()+"-操作...");}}
}, "t2");
t1.start();
t2.start();

判断死锁的工具:

  • 可以通过jdk自带的jps和jstack来判断是否发生死锁:
    • jps:输出JVM中运行的进程状态信息
    • jstack:查看java进程内线程的堆栈信息
  • jconsole
    • 用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
    • 打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
  • VisualVM:故障处理工具
    • 能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
    • 打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

ConcurrentHashMap

ConcurrentHashMap 是一种线程安全的Map。
底层数据结构:

  • JDK1.7底层采用分段的数组+链表实现
  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

JDK1.7中ConcurrentHashMap:
在这里插入图片描述
添加数据的过程:
在这里插入图片描述
JDK1.8中ConcurrentHashMap:
数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表

采用 CAS + Synchronized来保证并发安全进行实现:

  • CAS控制数组节点的添加
  • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

在这里插入图片描述
加锁的方式

  • JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
  • JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

Java程序如何保证多线程安全

避免并发程序出现问题,从Java并发编程三大特性下手:

  • 原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行,通过synchronized、lock保证;
  • 可内存见性:让一个线程对共享变量的修改对另一个线程可见, 通过 volatile(首选,用来修饰共享变量)、synchronized、lock实现;
  • 有序性:指令重排——处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,通过volatile修饰共享变量,禁止指令重排。

http://www.ppmy.cn/ops/24734.html

相关文章

Ubuntu/Linux Anaconda 命令行终端安装

参考:https://blog.csdn.net/qq_39407949/article/details/136296504 1 下载安装包 1.1 寻找适配版本安装包 需要在官网上查找自己需要的版本,地址链接在下面: https://repo.anaconda.com/archive/1.2 下载 这里以我自己安装的版本为例: https://repo.…

粘合聚酰亚胺PI塑料材料使用UV胶的优势有哪些? (三十四)

聚酰亚胺PI难于粘接,在PI粘接方法中使用UV胶粘剂粘接PI的优势有哪些? 聚酰亚胺(PI)是一种具有耐高低温性能、高绝缘性、耐化性、低热膨胀系数的材料,广泛用于FPC基材和各种耐高温电机电器的绝缘材料。然而,…

App在某个页面静止造成卡死假象

今天遇到一个bug记录下来,供后续积累经验。 预期结果:App在冷启动时会加载一个“广告页”,“广告页”有个定时器,定时器结束后会自动跳转到App首页;然而当用户开启指纹或人脸识别登录之后,App直接进入验证界面,验证通过后直接进入App首页。 bug场景:进入App需要指纹识别…

HarmonyOS ArkUI实战开发—状态管理

一、状态管理 在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染&…

教程推荐:手机应用自动化

手机应用程序的自动化通常涉及使用专门设计的自动化框架和工具。对于Android和iOS平台,以下是一些常用的自动化工具: Android: Espresso: Espresso是谷歌官方支持的自动化测试框架。它适用于写UI测试来模拟用户对Android应用的交云。Espresso工作在应用…

力扣---二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出: [1,3,4]示例 2: 输入: [1,null,3] 输出: [1,3]示例 3: 输入: [] 输出: []实现方法&…

2024年第十五届蓝桥杯江苏省赛回顾

呜呜呜~~~ 我在考完了后感觉自己直接炸了:好多学到的算法都没有用上,几乎所有的题目都是暴力的。。。 最后十几分钟对于一道dp算法终于有思路了,但是。。匆匆忙忙之间就是没有调试出来。(还是交了一道暴力[旋风狗头]直接哭死~~&…

OFDM802.11a的FPGA实现(七)一级交织:分组交织器(含verilog和matlab代码)

1.前言 在前面的文章中讲解了卷积编码和删余,实现了1/2、2/3、3/4编码速率的输出。数据域在编码之后,下一个部分就是交织。今天对交织进行具体实现。 交织是为了在时域或频域或者同时在时域、频域上分布传输的信息比特,使信道的突发错误在时间上得以扩散…