并发与并行的区别:
单核并发,从微观上来讲是串行的,宏观上来讲是并行的,多核并行,不管从微观还是宏观上来讲都是并行的。
与计算机的发展有关,在早期的计算机中都是单核CPU的,为了充分的提高计算机的性能,使得宏观上具有多个进程运行的效果,其实微观上是多个指令快速切换到效果
计算机的发展是由单核到多核的过程,目的都是为了最大化的利用CPU资源
并行是从微观是多个指令一起运行,宏观上是多个物理内核同时执行。
JMM内存模型:共享内存模型,每个线程都有自己的一份本地内存,用来存储自己操作的变量,多个线程之间的通信是通过共享主存中的变量数据来实现的,每个线程操作完自己的变量后,自己的本地内存保留一份,拷贝一份到主存中是其他线程能够操作该变量
对于一个全局变量他是共享的(针对全局,肯定是共享的),首先它是保存在主内存中,其他线程从主存中读取这个全局变量值,经历的步骤:read(读到现成的工作内存,未赋值)----load(完成赋值)-----use(将赋值好的变量值赋值到cpu寄存器中)
分时操作系统 会基于优先级抢占,while(true)优先级相对来说很高,看起来这个操作一直在占用cpu时间片,如果cpu是有限的,那么优先级低的线程抢占不到时间片导致饥饿,出现死锁。
Thread.yield-----让出cpu时间片,有个上下文切换的过程,这个过程中本地缓存变量失效,下一次读会从主存中加载数据。
回写主存:store-----write 当线程A中的while(true)循环结束的时,才能读取到线程B回写到主存中的变量值。
线程有自己独立的内存空间,创建一个线程会给他分配内存空间,Java如果创建过的线程会导致资源耗尽,包括内存资源。
操作系统类似于Redis也会有内存淘汰策略,while(true)中一直在用的本地内存中的变量不会立即刷主存,但是过程中如果shortWait(100000);一段时间,本地内存变量缓存会被清除,下次操作最新的变量。
线程本地内存中存储的变量缓存淘汰之后,可见性就能够保证
- 保存上下文和加载上下文:一个线程A在执行操作的过程中,中途让出时间片,这个时候进行一次保上下文的操作。另一个线程B抢到时间片来执行自己的操作,当线程B执行完之后,线程要继续执行之前的操作,这个时候会进行一次加载上下文的操作。
- 先线程上下文切换时间一般是5-10ms跟硬件架构有关。因此高并发情况下避免开很多的线程,是为了避免频繁的上下文切换,因此会考虑使用消息中间件。
volatile
Volatile底层用了lock前缀指令,而lock前缀指令会让缓存失效,其他线程如果需要的话会立即从主存中读取修改过的变量,以保证可见性。因为lock指令具有内存屏障的效果,所以说volatile是通过内存屏障实现可见性?的。汇编lock指令 volatile可以保证有序性(禁止指令重排序)和可见性,有序行性的目的是为了保证可见性。
lock前缀指令实现内存屏障的效果干的两件事:
- 用了Lock指令保证一个线程对本地内存中的变量的修改结果立即刷回主存,同时让你在其他处理器的缓存副本直接失效,这样其他线程如果需要的话会立即从主存中读取修改过的变量,中间过程是非常快的以此来保证可见性。
- 让缓存失效,让缓存失效就会立即重新去加载最新的值,保证了最新更新的值的可见性。
lock前缀指令不是内存屏障的指令,但是有内存屏障的效果,保证立即刷回主存
Lock前缀指令会等待之前所有的指令完成,并且所有的缓冲写操作写回内存,之后才开始执行。
线程的本地缓存和主存是一个逻辑空间但是他们最终会映射到硬件,JMM尽可能保证映射到硬件的高级缓存。
- Synchronize底层也会去调storefence,一调Storefence就实现了内存屏障的效果
- LockSuport.unpark也是内存屏障
- Thread.sleep(1);这个也可以保证可见性实现内存屏障。
- Thread.yeild通过上下文切换实现可见性,内部没有内存屏障
- Integer-----Integer内部有大量使用到了final关键字,线程对final修饰的变量也要保证可见性,常量,各个线程都要读到。
Java中线程底层所有的方法都是调用park、unpark机制实现等待唤醒机制。wait notify也是
总结:Java中可见性如何保证?只有两种
- jvm层面storeLoad内存屏障 x86系统架构中 lock前缀指令代替了mfence
- 上下文切换 Thread.yeild