《极客时间教程

缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题。

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

对于单核,所有的线程都是在一个CPU上执行,操作同一个CPU的缓存;一个线程对缓存的写,对另外一个线程来说一定是可见的。

例如在下面的图中,线程A和线程B都是操作同一个CPU里面的缓存,所以线程A更新了变量V的值,那么线程B之后再访问变量V,得到的一定是V的最新值(线程A写过的值)。

对于多核,当多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存。这时两个线程对于变量的操作就不具备可见性了。

【示例】计数器的并发安全问题示例

Java的并发也是基于任务切换。Java中,即使是一条语句,也可能需要执行多条CPU指令。一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。

有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。

【示例】双重检查创建单例对象

但是实际上优化后的执行路径却是这样的:

优化后会导致什么问题呢?我们假设线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换到了线程B上;如果此时线程B也执行getInstance()方法,那么线程B在执行第一个判断时会发现instance!=null,所以直接返回instance,而此时的instance是没有初始化过的,如果我们这个时候访问instance的成员变量就可能触发空指针异常。

导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但这种方案性能堪忧。

合理的方案应该是按需禁用缓存以及编译优化。Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile、synchronized和final三个关键字,以及六项Happens-Before规则。

并发原子性问题的源头是线程切换。

解决这个问题的直接方法就是禁止线程切换。操作系统做线程切换是依赖CPU中断的,所以禁止CPU发生中断就能够禁止线程切换。这个方案对于单核场景是可行的,但不适用于多核场景。

举例来说,long型变量是64位,在32位CPU上执行写操作会被拆分成两次写操作(写高32位和写低32位,如下图所示)。

在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在CPU-1上,一个线程执行在CPU-2上,此时禁止CPU中断,只能保证CPU上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写long型变量高32位的话,那就有可能出现我们开头提及的诡异Bug了。

“同一时刻只有一个线程执行”称之为互斥。如果能够保证对共享变量的修改是互斥的,那么,无论是单核CPU还是多核CPU,就都能保证原子性了。

一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时称这个线程持有锁;否则就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁unlock()。

首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源R;其次,我们要保护资源R就得为它创建一把锁LR;最后,针对这把锁LR,我们还需在进出临界区时添上加锁操作和解锁操作。

Java中,synchronized是一种锁的实现方式。

【示例】synchronized使用示例

【示例】synchronized实现并发安全的计数器

受保护资源和锁之间的关联关系是N:1的关系。

【示例】synchronized实现并发安全的计数器错误示例

用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁。

【示例】账户类Account有两个成员变量,分别是账户余额balance和账户密码password。取款withdraw()和查看余额getBalance()操作会访问账户余额balance,创建一个final对象balLock作为锁(类比球赛门票);而更改密码updatePassword()和查看密码getPassword()操作会修改账户密码password,创建一个final对象pwLock作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的。

答:不能用可变对象做锁。

【示例】保护临界区多个资源的错误示例

因此,可以优化为使用Class对象(Account.class)作为共享的锁。Account.class是所有Account对象共享的,而且这个对象是Java虚拟机在加载Account类的时候创建的,所以不用担心它的唯一性。

死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

【示例】存在死锁的示例

只有以下这四个条件都发生时才会出现死锁:

也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。

其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

通过一个Allocator来管理临界区。当账户Account在执行转账操作的时候,首先向Allocator同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,需通知Allocator同时释放转出账户和转入账户这两个资源。

123//一次性申请转出账户和转入账户,直到成功while(!actr.apply(this,target));如果apply()操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的。但如果apply()操作耗时长,或者并发冲突量大的时候,可能遥循环大量次数才能获得锁,太消耗CPU了。

在这种场景下,更好的方案应该是:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗CPU的问题。

核心是要能够主动释放它占有的资源。

synchronized做不到这点,但是可以通过Lock来解决此类问题。

破坏这个条件,需要对资源进行排序,然后按序申请资源。

假设每个账户都有不同的属性id,这个id可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。

wait()、notify()、notifyAll()方法操作的等待队列是互斥锁的等待队列,所以如果synchronized锁定的是this,那么对应的一定是this.wait()、this.notify()、this.notifyAll();如果synchronized锁定的是target,那么对应的一定是target.wait()、target.notify()、target.notifyAll()。而且wait()、notify()、notifyAll()这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现wait()、notify()、notifyAll()都是在synchronized{}内部被调用的。如果在synchronized{}外部调用,或者锁定的this,而用target.wait()调用的话,JVM会抛出一个运行时异常:java.lang.IllegalMonitorStateException。

等待-通知机制中,需要考虑以下四个要素。

并发编程中,需要注意三类问题:安全性问题、活跃性问题和性能问题。

并发安全问题的三个主要源头是:原子性、可见性、有序性。通俗的说,多线程同时读写共享变量。

对于非共享变量(ThreadLocal)或常量(final),不存在并发安全问题。

对于共享变量,在并发环境下,存在竞态条件。

对于这种情况,解决方案就是互斥(锁)。

活跃性问题主要分为:

三个核心性能指标:

由互斥而产生的阻塞会影响性能。要提升性能有以下思路:

synchronized关键字及wait()、notify()、notifyAll()这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以Java选择了管程。

管程,对应的英文是Monitor,很多Java领域的同学都喜欢将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”。

所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为Java领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。

Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。

并发领域两大核心问题,管程都是能够解决的。

一个是互斥,即同一时刻只允许一个线程访问共享资源;

一个是同步,即线程之间如何通信、协作。

管程是如何解决互斥问题的:

管程是如何解决线程间的同步问题的:

在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列,如下图,条件变量A和条件变量B分别都有自己的等待队列。

通用的线程生命周期:初始状态、可运行状态、运行状态、休眠状态和终止状态。

Java中线程共有六种状态:

度量性能的核心指标:

创建多少线程合适,要看多线程具体的应用场景。

例如,有三个方法A、B、C,他们的调用关系是A->B->C(A调用B,B调用C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。

局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了,局部变量应该和方法同生共死。此时你应该会想到调用栈的栈帧,调用栈的栈帧就是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。事实上,的确是这样的,局部变量就是放到了调用栈里。于是调用栈的结构就变成了下图这样。

那调用栈和线程之间是什么关系呢?

答案是:每个线程都有自己独立的调用栈。

因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。再次重申一遍:没有共享,就没有伤害。

方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个响当当的名字叫做线程封闭,比较官方的解释是:仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会有并发问题,性能杠杠的。

采用线程封闭技术的案例非常多,例如从数据库连接池里获取的连接Connection

将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。

对于这些不会发生变化的共享变量,建议你用final关键字来修饰。

识别共享变量间的约束条件非常重要。因为这些约束条件,决定了并发访问策略。

共享变量之间的约束条件,反映在代码里,基本上都会有if语句,所以,一定要特别注意竞态条件。

并发访问策略方案:

如何解决这三个问题呢?Java中提供了Java内存模型,以应对可见性和有序性问题;提供了互斥锁,以应对原子性问题。

互斥锁是解决并发问题的核心工具,但它也可能会带来死锁问题。

管程,是Java并发编程技术的基础,是解决并发问题的万能钥匙。并发编程里两大核心问题——互斥和同步,都是可以由管程来解决的。

THE END
1.2023年全球数据泄露事件TOP100美国消费贷款公司TMX Finance披露了近三个月前首次发现的客户数据严重泄露事件。该公司向受影响的客户提供为期12个月的免费信用监控和Experian的身份保护,但敦促他们定期检查账户对账单,以发现任何异常活动。 现代汽车遭受数据泄露,影响了法国和意大利的客户 现代汽车披露了一起数据泄露事件,影响了预订试驾的意大利和法国车https://www.51cto.com/article/778354.html
2.区块链技术目前七块连接的技术正确理什么阶比特币的技术是基于密码学签名技术的, 你的账户安全由你的私钥保护, 如果不添加别的手段,比如在别人那里备份, 或者在别的地方备份, 你丢失了密钥, 账户里的 钱是没有人能给你找回来的。 想想人们丢银行卡, 忘记密码的频率, 这个问题有多大, 不用我说大家也懂 https://blog.csdn.net/heqinghua217/article/details/79026483
3.保险万能账户追加安全吗?1、有保底利率,提供收益:万能账户本身是一个生钱账户,主要是主险年金保险,或者两全保险、增额寿险的钱放入进去进行增值,下有保底利率,不会存在亏损,因此保险万能账户追加安全; 2、受到法律保护:保底利率写进合同条款中,是规则白纸黑字写入合同,比如说万能账户保底利率在1.75%—2%之间,受到法律保护,可以给消费者的安https://m.csai.cn/v/73287.html
4.分红协议范文6篇(全文)27、下列关于万能保险的万能账户的说法中,错误的是( )。 A、万能账户可以是单独账户 B、万能账户不可以是公司普通账户的一部分 C、保险公司应当为万能保险设立万能账户 D、万能账户能够提供资产价值、账户价值和结算利息等信息 28、( )的人员不属于保险公司、保险代理机构不得发放执业证书的类型。 A、未持有资格证https://www.99xueshu.com/a/ugatuxvxxoh7.html
5.保险公司的万能账户里存钱安全吗?如果倒闭了存的钱还能取出来吗..关于您提到的保险公司万能账户存钱的安全性问题,一般来说,保险公司万能账户是由保险公司管理和维护的,https://www.66law.cn/question/45274192.aspx
6.金融街:金融街控股股份有限公司2024年面向专业投资者公开发行(2)发行人在债券存续期内,出现违反上述第(1)条约定的交叉保护承 诺情形的,发行人将及时采取措施以在半年内恢复承诺相关要求。 (3)当发行人触发交叉保护情形时,发行人将在?2?个交易日内告知受托管 理人并履行信息披露义务。 (4)发行人违反交叉保护条款且未在上述第(2)条约定期限https://stock.stockstar.com/notice/SN2024042400034493.shtml
7.每日热点0901"万能"的叶黄素补充剂真的是智商税? 运动突发不适体力不支还是猝死前兆? 血钙不低就不用补钙?这12条关于骨质疏松的谣言,你需要知道! 9月1日起定向使用,北京医保个人账户资金怎么用? 从“治已病”到“治未病”,做健康生活的守护者 上海养老服务综合体办到百姓家门口 https://www.sccdc.cn/Article/View?id=20473
8.人寿鑫帐户(万能帐户)可以用作存款帐户存款吗?人寿鑫帐户,也称为万能帐户,是一种结合了保险和投资功能的保险产品。通常情况下,人寿鑫帐户并不适合用作存款帐户存款。虽然人寿鑫帐户可以提供一定程度的灵活性和投资回报,但其主要目的是为保险需要而设计的,而非作为纯粹的存款工具。人寿鑫帐户通常会将被投保人的保险费用分配给两部分:一部分用于支付保险费用和其他相https://www.xyz.cn/toptag/renshouxinzhanghu-751748.html