并发学习目录

java并发学习总结

线程问题的由来(我的理解)

        一个java程序要运行,可能需要os和java线程交互(也就是数据互通),而操作系统有不同,从而java为了统一就有了JMM模型去控制线程通信,JMM就像是一个设计图,规定了轮廓,为了是轮廓实现,制定了规则hanpens-before,有了标准就可以去具体叫人搭建了,如synchronizedVolatile等,还要知道阻塞队列使用分析synchronized 与 ReentrantLock 对比架构基础远程通信协议http

影响服务器吞吐量的因素

硬件:cpu、磁盘、内存、网络

软件:线程数、jvm内存分配、网络通信、磁盘IO

java线程安全的本质

      线程执行结果具有正确性

线程三大特性(参考这篇

原子性

原子性就指是把一个操作或者多个操作视为一个整体,在执行的过程不能被中断的特性叫原子性。

原因:我们执行一个操作时,可能会把操作拆分成多个指令交给CPU执行。IO、内存、CPU缓存他们的操作速度有着巨大的差距,操作系统就有了进程和时间片的概念

在了解java内存模型的时候我们先了解8个原子操作的概念(参考https://blog.csdn.net/qq_33808244/article/details/100836342):
1.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。从主内存读取数据。
2.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。将读取的数据放入工作内存中。
3.use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。获取工作内存中的数据然后执行引擎处理数据
4.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。工作内存接收执行引擎处理的结果赋值给工作内存中的变量
5.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。把工作内存改变的值传回给主内存。
6.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。将工作内存传输的值写入主内存。
7.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。store的时候进行lock。
8.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。write之后解锁。
然后加上这8个操作之后的图如下(以flag为例)
在这里插入图片描述
工作内存从主内存read到flag的值,然后通过load将flag存入工作内存之中,use操作之后执行引擎获取到flag值,进行操作之后,flag值变为true,然后通过assign操作将flag的值更新到工作内存之中,工作内存通过store存储flag变量,最后主内存将flag变量write,然后线程获取到的就是flag的最新值。

由此推出可以解决方式:CAS

有序性

编译器优化,处理器(cpu)的优化可能会带来的重排序问题(下面解释来自

举个例子,CPU-0将要执行两条指令,分别是:

  1. STORE x
  2. LOAD y

当CPU-0执行指令1的时候,发现这个变量x的当前状态为Shared,这意味着其它CPU也持有了x,因此根据缓存一致性协议,CPU-0在修改x之前必须通知其它CPU,直到收到来自其它CPU的ack才会执行真正的修改x。但是,事情没有这么简单。现代CPU缓存通常都有一个Store Buffer,其存在的目的是,先将要Store的变量记下来,注意此时并不真的执行Store操作,然后待时机合适的时候再执行实际的Store。有了这个Store Buffer,CPU-0在向其它CPU发出disable消息之后并不是干等着,而是转而执行指令2(由于指令1和指令2在CPU-0看来并不存在数据依赖)。这样做效率是有了,但是也带来了问题。虽然我们在写程序的时候,是先STORE x再执行LOAD y,但是实际上CPU却是先LOAD y再STORE x,这个便是CPU乱序执行(reorder)的一种情况!

当你的程序要求指令1、2有逻辑上的先后顺序时,CPU这样的优化就是有问题的。但是,CPU并不知道指令之间蕴含着什么样的逻辑顺序,在你告诉它之前,它只是假设指令之间都没有逻辑关联,并且尽最大的努力优化执行

解决有序性办法:设置内存屏障

可见性

我们在操作CPU缓存过程中,由于多个CPU缓存之间独立不可见的特性,导致共享变量的操作结果无法预期

解决:为解决了缓存不可见问题硬件程序就制定了一套保证缓存之间可见的协议MESI(Modified Exclusive Share Invalid),MESI协议通过标识缓存数据的状态,来决定CPU何时把缓存的数据写入到内存,何时从缓存读取数据,何时从内存读取数据。

MESI是一个不完美的解决方案,为了弥补MESI造成的性能问题,所以就有了本地缓存(store bufferes),让其能异步mesi。CPU0 只需要在写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 invalidate 消息,然后继续去处理其 他指令。 当收到其他所有 CPU 发送了 invalidate acknowledge 消息 时,再将 store bufferes 中的数据数据存储至 cache line 中。最后再从缓存行同步到主内存。

但它又引入了其他的问题,因为把数据同步到内存中的过程是异步的,数据保存到内存中的时间点没办法保证;这个过程中当前CPU可以从store bufferes里面读取到最新的数据,但是其他缓存就有可能读取到的是之前的无效数据。

exeToCPU0和exeToCPU1分别在两个独立的CPU上执行。 假如 CPU0 的缓存行中缓存了 isFinish 这个共享变量,并 且状态为(E)、而 Value 可能是(S)状态。 那么这个时候,CPU0 在执行的时候,会先把 value=10 的 指令写入到storebuffer中。并且通知给其他缓存了该value 变量的 CPU。在等待其他 CPU 通知结果的时候,CPU0 会 继续执行 isFinish=true 这个指令。 而因为当前 CPU0 缓存了 isFinish 并且是 Exclusive 状态,所 以可以直接修改 isFinish=true。这个时候 CPU1 发起 read 操作去读取 isFinish 的值可能为 true,但是 value 的值不等 于 10。 这种情况我们可以认为是 CPU 的乱序执行,也可以认为是 一种重排序,而这种重排序会带来可见性的问题

如:加入X86 的 memory barrier 指令包括 lfence(读屏障) sfence(写 屏障) mfence(全屏障)。

Store Memory Barrier(写屏障) 告诉处理器在写屏障之前 的所有已经存储在存储缓存(store bufferes)中的数据同步 到主内存,简单来说就是使得写屏障之前的指令的结果对 屏障之后的读或者写是可见的

Load Memory Barrier(读屏障) 处理器在读屏障之后的读 操作,都在读屏障之后执行。配合写屏障,使得写屏障之前 的内存更新对于读屏障之后的读操作是可见的

Full Memory Barrier(全屏障) 确保屏障前的内存读写操作 的结果提交到内存之后,再执行屏障后的读写操作 有了内存屏障以后,对于上面这个例子,我们可以这么来 改,从而避免出现可见性问题

JMM 中内存屏障 

所以在硬件上提供了处理方法---设置内存屏障。但是内存屏障的指令并非每个操作系统都是统一的,不同的操作系统的指令和效果可能各不相同,而且不同的缓存方式也有可能不尽相同,所以为了让写程序的人只需要关心程序上的代码而不需要关心这么底层的逻辑,我们的编程语言都会有一套统一管理这套内存可见性的规则,而JAVA用来管理这套内存可见性规则的模型就叫JMM

JMM 属于语言级别的抽象内存模型,可以简单理解为对硬 件模型的抽象,它定义了共享内存中多线程程序读写操作 的行为规范:在虚拟机中把共享变量存储到内存以及从内 存中取出共享变量的底层实现细节。通过这些规则(happen-before 规则 , as-if-serial,volatile、synchronized、final...)来规范对内存的读写操作从而保证指令的正 确性,它解决了 CPU 多级缓存、处理器优化、指令重排序 导致的内存访问问题,保证了并发场景下的可见性。

需要注意的是,JMM 并没有限制执行引擎使用处理器的寄 存器或者高速缓存来提升指令执行速度,也没有限制编译 器对指令进行重排序,也就是说在 JMM 中,也会存在缓存 一致性问题和指令重排序问题。只是 JMM 把底层的问题抽 象到 JVM 层面,再基于 CPU 层面提供的内存屏障指令, 以及限制编译器的重排序来解决并发问题

                                                                    Java内存模型与硬件内存架构的关系图

 

                                                                                 Java线程和硬件处理机图

线程是什么

java对象的发布与逃逸(参考这篇

发布  发布的意思是使一个对象能够被当前范围之外的代码所使用

逃逸   一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见,可能会读到未初始化的值

对象如何安全发布

  1. 在静态初始化函数中初始化一个对象引用
  2. 将对象的引用保存到volatile类型域或者AtomicReference
  3. 将对象的引用保存到某个正确构造对象的final类型域中,初始化安全
  4. 将对象的引用保存到一个由锁保护的域中,读写上锁

1,2,4点就是单例模式的实现,而final是通过不变来保证对象的发布安全

java中的并发应用

1. 可见性有序性原子性  synchronized

2.可见性有序性 Volatile final关键字

3.原子性  atomic

稍微了解JUC包

附加知识

MESI(参考https://www.cnblogs.com/z00377750/p/9180644.html)

CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):

M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。

当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。

同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

I: 无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。

java对象结构(参考这篇

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)

  1. markword
    第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
  2. klass
    对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.,有几种指针:klass是普通指针,_compressed_klass是压缩类指针
  3. 数组长度(只有数组对象有)
    如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

)、实例数据(Instance Data)对齐填充(Padding)(HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。)

 实例说明(参考https://www.bbsmax.com/A/kvJ3k69nJg/

class Model
{
    public static int a = 1;
    public int b;
 
    public Model(int b) {
        this.b = b;
    }
}
 
public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

 

注意下面三个说明有利于理解instanceKlass和instanceOopDesc

  1. 方法区:类信息、类变量(静态变量和常量)、方法
  2. 堆:对象、成员变量
  3. 栈:局部变量

执行new的时候,JVM 做了什么工作。首先,如果这个类没有被加载过,JVM就会进行类的加载,并在JVM内部创建一个instanceKlass对象表示这个类的运行时元数据(相当于Java层的Class对象)。初始化对象的时候(执行invokespecial A::),JVM就会创建一个instanceOopDesc对象表示这个对象的实例,然后进行Mark Word的填充,将元数据指针指向Klass对象,并填充实例变量。

也就是说Klass代表的类的元数据是不会直接暴露给对象进行访问的,而是通过Klass的java.lang.class对象来进行间接的访问。这样做可以规范外部对Klass访问的行为,防止对元数据造成破坏

换句话说(看了https://www.it610.com/article/1279161492024868864.htm):Java虚拟机通过栈帧的对象引用找到Java堆中的instanceOopDesc,这样就可以访问Java对象的实例信息,当需要访问具体的类型等信息是,可以通过instanceOopDesc中的元数据指针来找到方法区中对应的instanceKlass。

instanceKlass:继承自klass的instanceKlass,包含了实例对象所属类型(即类信息)的元数据,让我们知道是实例是哪个类型的

instanceOopDesc(java对象实例):有实例数据(成员变量)

成员变量重排序顺序

  1. double (8字节) 和 long (8字节)
  2. int (4字节) 和 float (4字节)
  3. short (2字节) 和 char (2字节):char在java中是2个字节。java采用unicode,2个字节(16位)来表示一个字符。
  4. boolean (1字节) 和 byte (1字节)
  5. reference引用 (4/8 字节)
  6. <子类字段重复上述顺序>

32位虚拟机在不同状态下markword结构如下图所示:

这里写图片描述

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

lock 

 

状态标志位存储内容
未锁定01对象哈希码、对象分代年龄
轻量级锁定00指向锁记录的指针
膨胀(重量级锁定)10执行重量级锁定的指针
GC标记11空(不需要记录信息)
可偏向01偏向线程ID、偏向时间戳、对象分代年龄

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向管程Monitor的指针。

伪共享(参考http://ifeve.com/falsesharing/

cache line,cpu的缓存系统中是以缓存行(cache line)为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节,cache line是cache和memory之间数据传输的最小单元

Write-through(直写模式)在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单;缺点是因为数据修改需要同时写入存储,数据写入速度较慢。

Write-back(回写模式)在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储;缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

原因:

jdk1.8用@sun.misc.Contended解决

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值