Java并发编程(三)

并发编程的三个特性

并发编程的三个重要特性是原子性、可见性和有序性。

  1. 原子性:原子性指的是一个操作是不可中断的,要么全部执行成功,要么全部不执行,是不可再分割的最小操作单位。保证原子性可以避免多个线程同时对共享数据进行修改出现的并发安全问题。

  2. 可见性:可见性指的是当一个线程对共享变量进行修改后,其他线程能够立即看到这个修改。可见性问题主要源于线程间的缓存一致性,即每个线程都有自己的工作内存,共享变量存在于主内存中。为了保证可见性,需要使用同步机制(如锁、volatile关键字等)来确保共享变量的修改对其他线程可见。

  3. 有序性:有序性指的是程序的执行结果需要按照一定的执行顺序来保证。在多线程环境下,由于线程的执行是并发的,不同的线程之间的指令可能会以不同的顺序执行,导致程序的执行结果与预期不符。为了保证有序性,需要使用同步机制和指令重排序禁止等手段来控制指令的执行顺序。

volatile 关键字

1.如何保证变量的可见性?

在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 关键字能保证数据的可见性,但不能保证原子性。

2.如何禁止指令重排序?

可以使用 volatile 关键字修饰变量来禁用变量的指令重排序, volatile 是通过插入特定的内存屏障的方式来禁止指令重排序。正是因为 volatile 关键字禁用了变量的指令重排序,才保证了变量的可见性。在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:

public native void loadFence();
public native void storeFence();
public native void fullFence();

乐观锁和悲观锁

1.什么是悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被并发访问的时候都会出现问题,所以每次在操作资源之前都会上锁,这样其它线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。像 Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

适用场景:悲观锁通常多用于写操作比较多的情况,这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。

2.什么是乐观锁?

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证资源是否被其它线程修改了。乐观锁的实现有版本号机制或 CAS 算法。

适用场景:乐观锁通常多用于读操作较多、写操作比较少的情况,这样可以避免频繁加锁影响性能。

3.如何实现乐观锁?

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。

版本号机制

一般是在数据表中加上一个版本号字段 version,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

CAS 算法

CAS 的思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)

  • E:预期值(Expected)

  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

举一个简单的例子:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

  • i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。

  • i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的,也就是JNI 调用。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。

sun.misc 包下的 Unsafe 类提供了 compareAndSwapObject、compareAndSwapInt、compareAndSwapLong 等方法来实现对 Object、int、long 类型的 CAS 操作。

/**
	*  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

4.乐观锁存在哪些问题?

ABA 问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,是不能说明它的值没有被其他线程修改过的,因为在检查之后到赋值之前的这段时间,它的值可能被改为其他值,然后又被改回了 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

解决方法:ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

引入版本号或者时间戳是指在编程语言层面进行处理,当从数据库查出来数据时,通过编写代码给他一个版本号或者时间戳,再更新的时候同时检查版本号和时间戳。并非在数据库中引入版本号,在数据库中引入通常是为了实现乐观锁控制。

循环时间长开销大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

解决方法:可以设置一个合理的自旋次数限制,超过这个限制后放弃自旋并采取其他策略,如线程挂起或阻塞等,这样可以避免无限自旋导致长时间占用CPU资源

只能保证一个共享变量的原子操作:CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

解决方法:从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

共享锁和独占锁

1.是什么是共享锁和独占锁?

  • 共享锁:又叫读锁,线程在读取数据的时候可以获取读锁,一个线程获取了数据的读锁之后,其他线程只能获取该数据的读锁,无法获取写锁。

  • 独占锁:又称写锁、线程在修改数据的时候可以获取写锁,一个线程获取了某个数据的写锁之后,其它线程无法再获取这个数据的任何锁。

2.线程持有读锁还能获取写锁吗?

  • 在线程持有读锁的情况下,该线程不能取得写锁。

  • 在线程持有写锁的情况下,该线程可以继续获取读锁,读锁获取成功之后,原先的写锁会被是释放。

3.读锁为什么不能升级为写锁?

写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。

另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。

锁降级是针对同一个锁对象进行的

synchronized

1.介绍一下synchronized关键字

synchronized 是 Java 中的一个的关键字,用来修饰的方法和代码块进行加锁。synchronized 所添加的锁有以下几个特点。

2.如何使用synchronized?

synchronized 关键字的使用方式主要有下面 3 种:

1、修饰实例方法

给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 。

synchronized void method() {
    //业务代码
}

2、修饰静态方法

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁,也就是类对象的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?

不互斥!不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类对象的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

3.修饰代码块

对synchronized后面括号里指定的对象或者类加锁:

  • synchronized(object) 表示进入同步代码库前要获得给定对象的锁。

  • synchronized(类.class) 表示进入同步代码前要获得给定类对象的锁

synchronized(this) {
    //业务代码
}

4.构造方法可以用synchronized修饰么?

构造方法不能使用 synchronized 关键字修饰。因为构造方法本身就属于线程安全的

5.synchronized底层原理了解吗?

synchronized 关键字底层原理属于 JVM 层面的东西

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。synchronized 修饰的方法使用的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。两者本质上都是对对象监视器 monitor 的获取。

synchronized 同步语句块

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class。

从下面可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。上面的字节码中包含一个 monitorenter 指令以及两个 monitorexit 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

synchronized 修饰方法的的情况

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

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

  • 如果是实例方法,JVM 会尝试获取实例对象的锁。

  • 如果是静态方法,JVM 会尝试获取当前 class 的锁。

6.synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字只能用于修饰变量,synchronized 关键字用来修饰方法以及代码块 。

  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

ReentrantLock

1.ReentrantLock是什么?

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大。

public class ReentrantLock implements Lock, java.io.Serializable

ReentrantLock 里面有一个内部类 Sync,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。

// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2.公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

3.可中断锁和不可中断锁有什么区别?

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。

  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

4.synchronized和ReentrantLock有什么区别?

  • 两者都是可重入锁。可重入锁指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。synchronized 是依赖于 JVM 实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的,也就是 API 层面。

  • ReentrantLock 比 synchronized 增加了等待可中断、可实现公平锁等高级功能。

AQS

1.AQS介绍

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。AQS 就是一个抽象类,主要用来构建锁和同步器。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。

2.AQS 原理

自旋锁是指在获取锁失败时,通过不断的循环重试来避免线程的阻塞和切换开销。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《深入理解计算机系统》学习笔记 - 第七课 - 机器级别的程序三

Lecture 07 Machine Level Programming III Procedures 机器级别的程序三 文章目录 Lecture 07 Machine Level Programming III Procedures 机器级别的程序三概述程序机制 栈结构栈说明栈定义推入数据弹出数据 调用控制代码示例程序控制流程%rip 传递数据ABI 标准示例 管理局部…

WPF Button使用漂亮 控件模板ControlTemplate 按钮使用控制模板实例及源代码 设计一个具有圆角边框、鼠标悬停时颜色变化的按钮模板

续前两篇模板文章 模板介绍1 模板介绍2 WPF中的Button控件默认样式简洁&#xff0c;但可以通过设置模板来实现更丰富的视觉效果和交互体验。按钮模板主要包括背景、边框、内容&#xff08;通常为文本或图像&#xff09;等元素。通过自定义模板&#xff0c;我们可以改…

JVM篇:JVM的简介

JVM简介 JVM全称为Java Virtual Machine&#xff0c;翻译过来就是java虚拟机&#xff0c;Java程序&#xff08;Java二进制字节码&#xff09;的运行环境 JVM的优点&#xff1a; Java最大的一个优点是&#xff0c;一次编写&#xff0c;到处运行。之所以能够实现这个功能就是依…

Docker自建私人云盘系统

Docker自建私人云盘系统。 有个人云盘需求的人&#xff0c;主要需求有这几类&#xff1a; 文件同步、分享需要。 照片、视频同步需要&#xff0c;尤其是全家人都是用的同步。 影视观看需要&#xff08;分为家庭内部、家庭外部&#xff09; 搭建个人网站/博客 云端OFFICE需…

TiDB在WMS物流系统的应用与实践 (转)

业务背景 北京科捷物流有限公司于2003年在北京正式成立,是ISO质量管理体系认证企业、国家AAAAA级物流企业、海关AEO高级认证企业,注册资金1亿元,是中国领先的大数据科技公司——神州控股的全资子公司。科捷物流融合B2B和B2C的客户需求,基于遍布全国的物流网络与自主知识产…

电话号码的字母组合[中等]

一、题目 给定一个仅包含数字2-9的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意1不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "23" 输出&am…

【MySQL学习笔记008】多表查询及案例实战

1、多表关系 概述&#xff1a;项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构&#xff0c;由于业务之间相互关联&#xff0c;所以各个表结构之间也存在着各种联系&#xff0c;基本上可分为三种&a…

什么是 NLP (自然语言处理)

NLP&#xff08;自然语言处理&#xff09;到底是做什么&#xff1f; NLP 的全称是 Natural Language Processing&#xff0c;翻译成中文称作&#xff1a;自然语言处理。它是计算机和人工智能的一个重要领域。顾名思义&#xff0c;该领域研究如何处理自然语言。 自然语言就是我…

小狐狸ChatGPT付费创作系统小程序端开发工具提示打开显示无法打开页面解决办法

最新版2.6.7版下载&#xff1a;https://download.csdn.net/download/mo3408/88656497 很多会员在上传小程序前端时经常出现首页无法打开的情况&#xff0c;错误提示无法打开该页面&#xff0c;不支持打开&#xff0c;这种问题其实就是权限问题&#xff0c;页面是通过调用web-v…

实习知识整理13:在购物车界面点击提交订单进入订单信息界面

在这块主要就是对前端传到后端的数据的处理&#xff0c;然后由后端再返还到新的前端界面 首先点击下单按钮后&#xff0c; 提交购物车中所选中的信息 因为前端是将name定义为 cartList[0].cartId &#xff0c;cartList[1].cartId 形式的 所以后端需要重新定义一个类来进行封装…

C语言中宏定义的一种妙用

1.前言 最近分析了一个宏定义的妙用方法&#xff0c;利用宏定义来构建一个枚举类型&#xff0c;通过自己代码测试验证&#xff0c;方法可行&#xff0c;分享给大家。 2.源码 实验源码如下所示&#xff1a; head1.h DEF_TEST(name1) DEF_TEST(name2) DEF_TEST(name3) #unde…

Redis哨兵sentinel

是什么&#xff1f; 哨兵巡查监控后台master主机是否故障&#xff0c;如果故障根据投票数自动将某一个slave库变为master&#xff0c;就行对外服务&#xff0c;称为无人值守运维 能干嘛&#xff1f; 主从监控&#xff1a;监控主从redis库是否正常工作 消息通知&#xff1a;…

带大家做一个,易上手的家常红烧茄子

我们先准备茄子 我这里用的一个大茄子 建议大茄子两个 一个做出来 还是看着有点少 茄子切成滚刀块 茄子倒入 小半勺盐 然后用手抓拌均匀 腌制十分钟 准备一根半小葱 三瓣蒜 蒜切成 蒜末 葱切碎 调一个料汁 两勺生抽 半勺老抽 半勺白砂糖 半勺盐 倒一点蚝油 半勺淀粉 小半…

在用Vite开发时静态图片放哪里,才能保证显示,不出现找不到资源

在用Vite开发时静态图片放哪里 在用Vite开发时静态图片&#xff08;资源&#xff09;放哪里呢 &#xff1f; 如果你想直接全部显示的那么请你把静态资源放到public目录下面&#xff0c;这样你一打包所有的静态资源都会放到打包根目录下。但是此时你在项目中引用的地址一定要是…

github登录需要双因素认证(Two-factor authentication)

前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 github登录需要双因素认证&#xff08;Two-factor authentication&#xff09; 今天登录github发现需要绑定双因素才能够登录 我们…

【开源】基于Vue+SpringBoot的实验室耗材管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 耗材档案模块2.2 耗材入库模块2.3 耗材出库模块2.4 耗材申请模块2.5 耗材审核模块 三、系统展示四、核心代码4.1 查询耗材品类4.2 查询资产出库清单4.3 资产出库4.4 查询入库单4.5 资产入库 五、免责说明 一、摘要 1.1…

单文件超过4GB就无法拷贝到U盘?这个你一定要知道

前言 随着现在科技发展&#xff0c;小伙伴们所使用的数据也越变越大。还记得WindowsXP流行的时候&#xff0c;XP的镜像文件仅为几百MB大小。 但是现在随便一个系统就有可能超过4GB。 如果单个文件超过4GB就有可能没办法拷贝进U盘&#xff0c;在这里就需要给小伙伴们普及一下U…

不再悲观,投行发表2024年股市展望

KlipC报道&#xff1a;2023已经步入尾声&#xff0c;尽管地缘政治冲突下&#xff0c;高利率和高通胀仍扰动着各国经济前景&#xff0c;但对于2024年&#xff0c;投行们已不似展望2023年时那样悲观。 那么展望2024年&#xff0c;世界经济会是什么样的&#xff1f; 摩根大通在接受…

k8s-cni网络 10

Flannel vxlan模式跨主机通信原理 在同一个节点上的pod 流量通过cni网桥可以直接进行转发&#xff1b; 在需要跨主机访问时&#xff0c;数据包通过flannel(隧道) 知道另一边的mac地址&#xff0c;就可以拿到另一边的ip地址&#xff0c;然后构建常规的以太网数据包&#xff0c;…

Java的maven

一.概念&#xff1a; 是一款用于管理和构建java项目的工具 作用: 方便项目的依赖管理 统一项目的结构,方便程序员开发及维护 提供了一套标准的项目构建流程,方便编译和构建 二.仓库类型: 本地仓库>自己计算机上的一个目录 中央仓库>由Maven团队维护的全球唯一的。…