聊聊jvm之并发

衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS与QPS类似)是最重要的指标之一。

1、硬件效率与缓存一致性

绝大多数任务不可能只靠处理器计算,处理器至少需要与内存交互,如读取运算数据、存储运算结果,这些I/O操作很难消除,由于计算机的存储设备与处理器的运算速度存在几个数量级的差距,故计算机系统加了一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲。基于高速缓存的存储交互解决了处理器与内存的速度矛盾,同时引入了一个新的问题:缓存一致性(Cache Coherence),每个处理器都有自己的缓冲,同时又共享同一主内存,当多个处理器运算任务涉及到同一块内存区域,可能导致数据不一致情况。此时需要各个处理器访问缓存时遵循一些协议,这类协议有:MSI、MESI、MOSI、Synapse、Firefly、Dragon Protocol。除增加高速缓存外,处理器可能会对输入的代码进行乱序执行优化,在计算之后将乱序执行结果重组,保证与顺序执行结果一致。

2、Java内存模型(Java Memory Modal,JMM)

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。JMM规定了所有变量都存储在主内存中,每条线程有自己的工作内存,工作内存保存了主内存变量的副本拷贝,变量的读取复制操作必须在工作内存中进行,线程间变量的传递需要通过主内存来完成。

变量的拷贝和同步,JMM定义了8种操作来完成:

lock(锁定):作用于主内存变量,标识为一条线程独占

unlock(解锁):作用于主内存变量,释放锁定状态

read(读取):作用于主内存变量,将变量的值从主内存传输到工作内存,以便随后load动作作用

load(载入):作用于工作内存,将read操作的变量放入工作内存副本当中

use(使用):作用于工作内存,将变量的值传递给执行引擎,虚拟机遇到使用变量的值时都会有这个操作

assign(赋值):作用于工作内存,把从执行引擎接收到的值赋给工作内存变量

store(存储):作用于工作内存,将工作中内存变量传递给主内存,以便write操作

write(写入):作用于主内存,把store操作从工作内存的变量值放入主内存变量中

由此可见,read\load成对出现、store/write成对出现,JMM还规定了执行上述操作需要满足的规则:

①、不允许read和load,store和write单独出现

②、不允许线程丢弃最近的assign操作,规定变量改变之后必须同步到主内存

③、不允许线程无缘由地从工作线程同步到主内存(需要执行assign之后)

④、use、store之前必须要执行assig、load,新变量只能在主内存中产生

⑤、一个变量同一时刻只能由一个线程进行lock,多次lock需要对应此时unlock,变量才会被解锁

⑥、对一个变量执行了lock操作,会清空工作内存此变量的值。在执行引擎使用前需要重新load或assign

⑦、执行unlock之前,必须将此变量同步会主内存中

3、volatile变量特殊规则:

①保存此变量对所有线程可见性。但在并发下一样不安全。volatile变量在各个线程的工作内存中存在不一致性问题,但当执行引擎每次使用,都会进行刷新,也就可认为不存在一致性问题。由于java的运算并非原子操作,volatile变量在并发情况下并不是安全的(A线程使用前已经刷新,此时B线程将新值赋给工作内存,此时A计算的数据就不是安全的)。

②禁止指令重排序,首先指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应的电路单元处理,但并不是指令任意排序,CPU需要正确处理指令依赖情况以保障程序得到正确的执行结果。禁止指令重排序通过设置内存屏障来实现,指令重排序不能把后面的指令重排序到内存屏障之前的位置。

volatile变量的读性能与普通变量几乎没什么差别,但写操作会慢一些,需要在本地代码插入许多内存屏障指令来保证处理器不发生乱序执行。即便如此,大多数volatile的总开销要比锁低。

4、先行发生原则(happens-before),判断数据是否存在竞争、线程是否安全的主要依据。如果说A操作先行发生于B操作,那么在B操作发生之前,A操作的影响能被B操作观察到,“影响”包括修改内存中共享变量的值、发送了消息、调用了方法等。

①、程序次序规则,一个线程内按程序代码顺序执行,前面的代码先于后面的代码。

②、管程锁定规则,一个unlock操作先于后面的lock操作

③、volatile规则,volatile前面的写操作先于后面的读操作

④、线程启动规则,start()方法先于此线程的每一个动作

⑤、线程终止规则,线程中所有操作都先于对此线程的终止检测,Thread.join(),Thread.isAlive()检测线程是否终止

⑥、线程中断规则,对线程interrup()方法调用先行被中断线程检测到中断事件的发生,通过Thread.interrupted检测是否有中断

⑦、对象终结规则,一个对象的初始化完成先行于finalize()方法

⑧、传递性,A先行于B,B先行于C,则A先行于C。

5、线程

线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。

实现进程的方式:内核线程实现、用户线程实现和用户线程+轻量级进程混合实现。

①、内核线程实现,这种线程由内核来完成线程切换,这种线程由叫轻量级进程,每个轻量级进程都由一个内核线程支持,比例1:1。每个轻量级进程都可作为一个独立的调度单元,即使阻塞了,也不会影响进程继续工作。基于内核实现,故线程的创建、析构、同步,都需要进行系统调用,需要在用户态和内核态之间频繁转换。

②、用户线程实现,广义上不属于内核线程,就是用户线程,轻量级进程也属于用户线程。狭义上,完全建立在用户空间的线程才是用户线程,系统内核不感知线程实现。线程的建立、同步、销毁和调度完全在用户态中完成。因而这类线程快速且低消耗,可支持更大规模的线程数量,部分高性能数据库中多线程即用户线程实现,进程与用户线程比例为1:N。但因操作系统只把资源分配到进程,而线程的创建、切换、调度都需要考虑,却类似阻塞、多处理器中线程映射到其他处理器等问题几乎无法处理。

③、用户线程+轻量级进程,用户进程任然建立在用户空间,操作系统提供的轻量级进程作为用户线程和内核线程沟通的桥梁,大大降低了进程被完全阻塞的风险。用户线程与轻量级进程比例为N:M。

Sun JDK中,Windows版本与Linux版本都是使用1:1线程模型实现。

6、线程调度指系统为线程分配处理器使用权的过程,主要有协同式线程调度和抢占式线程调度。

①、协同式线程调度,线程的执行时间由线程本身调度,工作执行完之后,主动通知系统切换到另一个线程上。导致线程执行时间不可控,一直不通知系统切换,程序就一直阻塞。

②、抢占式线程调度,每个线程由系统分配执行时间,线程切换不由程序本身决定(Thread.yield()只能让出cpu时间,无法获取执行时间),java线程就使用抢占式调度,同时可设置优先级(10个级别)建议系统给某些线程多分配一点执行时间。

7、线程状态:

①、新建(New):创建后尚未启动。

②、运行(Runable):包括操作系统线程状态中的Runing和Ready,即可能正在运行,也可能正在等待分配CPU时间。

③、无限等待(Waiting):这种状态的线程不会被分配CPU时间,需要等待其他线程显式地唤醒(Object.wait() Thread.join() LockSupport.park())。

④、限期等待(Time Waiting):这种状态线程也不会被分配CPU时间,但不需要等待其他线程显式地唤醒,一定时间会由系统自动唤醒(Thread.sleep() Object.wait(time) Thread.join(time) LockSupport.parkNanos() LockSupport.parkUnit())。

⑤、阻塞(Blocked):阻塞与等待的区别在于,阻塞在等待获取一个排它锁,而等待状态则等待一段时间或者唤醒动作发生。

⑥、结束(Terminated):已终止线程状态,线程已经执行结束。

8、线程安全:当多个线程访问一个对象时,不用考虑线程在运行时的调度和交替执行,也不需要进行额外的同步,或其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。java语言中的线程安全:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

①、不可变:final关键字,只要一个不可变对象被构建出来,就不会改变。(this引用逃逸情况例外,如在构造方法还未初始化内部静态属性,就已经被其他线程使用,有可能造成NPE)

②、绝对线程安全:如Vector,所有的方法都添加了synchronized,但是不经调控,有可能抛出错误

③、相对线程安全:如Vector、HashTable等

④、线程兼容:需要使用同步手段实现线程安全

⑤、线程对立:无论是否采取同步措施,都无法在多线程环境中并发使用,如Thread类的suspend()和resume()

9、锁优化:

①、自旋锁:挂起线程和恢复线程都需要转入内核态完成,这些操作为并发带来了很大的压力。很大共享数据的锁定状态只会持续很短一段时间,让请求锁的线程暂不放弃CPU时间,而进行一个忙循环,等待锁释放。

②、锁消除:指虚拟机即时编辑器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。

③、锁粗化:在存在锁竞争是一般推荐将同步代码块限制尽量小,为了线程尽可能拿到锁。但当一系列操作都对同一个对象反复加锁和解锁,则导致不必要的开销,存在这种情况一般会将范围扩展到整个操作序列,实现一次加解锁。

④、轻量级锁:HotSpot虚拟机的对象头分两部分信息,其中一部分存储对象自身的运行时数据(另一部分时类元数据),如哈希码、GC分代年龄等,称为Mark Word,是实现轻量级锁的关键。轻量级锁的依据是对于绝大部分锁,在整个同步周期内都不存在竞争。轻量级锁的上锁过程:如果对象没有被锁定,虚拟机在栈帧上建立一个所记录(Lock Record),存储当前对象的Mark Word拷贝,然后虚拟机尝试CAS操作将对象Mark Word更新为指向Lock Record的指针。如果操作失败,先检查是否已经在当前栈帧,如果在可直接进入同步代码块,否则已经被其他线程抢占。如果有两条以上的线程竞争同一个锁,轻量级锁就不再有效,要膨胀为重量级锁,等待锁的线程会进入阻塞状态。

⑤、偏向锁:当锁对象第一次被线程获取,即设置为偏向模式(01),同时使用CAS操作把获取这个锁的线程ID记录在Mark Word中,操作成功,后续每次进入同步块时,都不必进行同步操作。如果另一个线程尝试获取锁,偏向模式结束,恢复到未锁定或轻量级锁。

你可能感兴趣的:(聊聊jvm之并发)