JVM系列(六)Java内存模型和线程安全

多任务处理在现代计算机操作系统中几乎已是一项必备的功能。因为计算机的的运算能力太强大了,就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的浪费,而让计算机同时处理几项任务则是最容易想到,也是被证明是非常有效的“压榨”手段。由此我们引出了多线程。多线程的实现方式主要有三种:使用内核线程实现,使用用户线程实现和使用用户线程和轻量级进程混合使用。

在JDK1.2之前,Java使用的是号称“绿色线程”的用户线程实现。而在JDK1.2之后,Java线程模型被替换成为基于操作系统原生线程模型来实现。当然不同的操作系统支持的线程模型不同,在windows与Linux中使用的是一对一线程模型实现的,即一条java线程就映射到一条轻量级进程中,因为window和Linux系统提供的线程模型是一对一的。

内核进程与轻量级进程和Java进程的关系图

内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。而轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,轻量级进程与Java进程之间的关系以为1:1(Linux和Windows)。

Java内存与线程模型

从上图中可以看出:

当我们从主内存复制到线程工作内存时,必须有两个动作:第一,由主内存执行读(read)的操作;第二,由线程工作内存执行加载(load)操作。当我们把现成工作内存的数据复制到主内存时,也会有两个操作:第一,由线程工作内存执行存储(store)操作;第二,由主内存执行写(write)操作。

当虚拟机遇到使用变量(use)的命令时,工作内存会把使用的变量传递给线程执行引擎(线程)。当虚拟机遇到给变量赋值(assign)命令时,线程工作内存会接受从线程执行引擎传递过来给工作内存赋值的变量。

这里有三点需要注意

  • 每一个操作都是原子的,即执行期间不会被中断。

  • –每一个线程有一个工作内存和主存独立。对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。

  • 如果需要在其他线程中立即可见,需要使用 volatile 关键字

线程安全

当多个线程访问一个对象是,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者是在调用方法进行任何其他的协调操作,调用这个对象的行文都可以获得正确的结果,那这个对象就是线程安全的。

实现线程安全有三种方式:

  • 互斥同步:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。互斥是实现同步的一种手段(临界区,互斥量,信号量)。在Java中最基本的互斥同步手段就是synchronized关键字。
  • 非堵塞同步:如果没有线程竞争共享数据,那么操作就成功。如果有线程竞争,产生了冲突,那么就采取补偿措施(一般是cas操作)
  • 无同步方案:如果一个方法或者是代码不涉及共享数据,天生就是线程安全的,那么就不需要同步操作。譬如:可重入代码,线程本地存储。

CAS操作

CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。

E==V?V=N:;return V;

锁优化

为了在线程之间更高效地共享数据,解决竞争问题,从而提升效率,在JDK1.6加入了各种锁优化技术。

自旋锁与自适应自旋锁

如果有两个线程同时并行执行,可以让后面那个线程“稍微等一下”,但不放弃CPU的执行,为了让线程“等一下”可以让线程执行一个忙循环(自旋)。JDK1.6中-XX:+UseSpinning开启,JDK1.7中,去掉此参数,改为内置实现。

自适应自旋锁就是根据对象获取cpu执行的可能性来对对象加锁。随着程序运行和性能监控的不断完善,虚拟机对锁的预测越准,虚拟机就会变得越来越聪明。

消除锁

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < CIRCLE; i++) {
craeteStringBuffer(“JVM”, “Diagnosis”);
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println(“craeteStringBuffer: “ + bufferCost + “ ms”);
}

public static String craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}

每个StringBuffer.append()方法中都有一个同步块,锁就是这个sb对象。虚拟机观察变量sb,发现sb不会被全局访问到,不会有线程安全。所以会在编译之后,这段代码会忽略所有的同步操作而直接执行。

锁粗化

像上面的代码有两个连续的append方法,如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,那么虚拟机将会把加锁范围扩展(粗化)到整个操作序列的外部。

public void demoMethod(){
synchronized(lock){
//do sth.
}
//做其他不需要的同步的工作,但能很快执行完毕
synchronized(lock){
//do sth.
}
}
//锁粗化后
public void demoMethod(){
//整合成一次锁请求
synchronized(lock){
//do sth.
//做其他不需要的同步的工作,但能很快执行完毕
}
}

Mark Word

在HotSpot虚拟机的对象头中分为两个部分,第一部分是用于存储对象自身的运行时数据,譬如:哈希码,GC分代年龄,在32位和64位的操作系统中分别为32bit和64bit,官方称之为“Mark Word”。另一部分用户存储指向方法去对象类型的指针。

在32位的HotSpot虚拟机中的Mark Word长度为32bit,其中25位存放对象哈希码,4bit用户存储分代年龄,2bit用于存储锁标志位,1bit固定为0,其他状态(轻量级锁,重量级锁,GC标记,可偏向)等信息可以看下图

轻量级锁

在Java中使用操作系统互斥量来实现的传统锁为“重量锁”,轻量锁是在没有多线程的竞争下减少传统重量锁使用操作系统互斥量产生的性能消耗。但是轻量级锁并不是来取缔重量级锁的。

上锁过程:

1,当代码进入到同步快的时候,如果同步对象没有被锁定(锁标志位位 “01”),虚拟机会在当前栈帧创建一个名为锁记录(Lock Record)的空间,用于存放当前锁对象的Mark Word的拷贝。

2,虚拟机使用cas操作尝试将对象的Mark word指向为Lock Recoed的指针。

3,如果指向成功,将对象Mark Word的锁标志位修改为“00”。此时这个线程就拥有了该对象的锁,该对象处于轻量级锁状态。

4,如果指向失败,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧,如果指向当前栈帧,那就可以直接进入同步代码块执行。否则说明该对象有其他线程抢占了。同时说明有多个线程争用同一个锁,那么轻量级锁将会膨胀为重量级锁,锁标志位为“10”。Mark Word存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

解锁过程

1,锁对象的Mark word如果指向线程的锁记录,那就用cas操作把当前对象的Mark word与Mark word指向的锁记录替换回来。

2,如果替换成功,说明解锁成果,整个同步完成。否则,说明有其他线程尝试过获取该锁,那就要在释放锁定同时,唤醒被挂起的线程。

轻量级锁能提升程序同步性能的依据是: 对于绝大部分的锁,在整个同步周期内都是不存在竞争的;如果没有竞争,轻量级锁使用CAS 操作避免了使用互斥量的开销;但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS 操作,因此在有竞争的case下, 轻量级锁会比传统的重量级锁更慢;

偏向锁

偏向锁的目的:消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能;

如果说轻量级锁是在无竞争的情况使用CAS 操作去消除同步使用的互斥量:那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS 操作都不做了;

偏向锁的偏: 它的意思是这个锁会偏向于 第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步;

偏向锁的原理:若当前虚拟机启用了偏向锁,那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01, 即偏向模式;同时使用CAS 操作把获取到这个锁的线程的ID 记录在对象的 Mark Word之中,如果 CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作;

当有另一个线程去尝试获取这个锁时,偏向模式就结束了:根据锁对象目前是否处于被锁定的状态, 撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行;

偏向锁可以提高带有同步但无竞争的程序性能;如果程序中大多数的锁总是被多个不同的线程访问:那偏向模式是多余的;

偏向锁 轻量级锁状态转换以及对象Mark Word的关系

总结:

当进入同步代码块时,锁对象首先会尝试偏向锁,如果失败则尝试轻量级锁。如果轻量级锁失败,则进入自旋锁。再失败,就尝试重量级锁,使用操作系统的互斥量在操作系统层挂起。

坚持原创技术分享,您的支持将鼓励我继续创作!
  • 本文作者: XiuYu.Ge
  • 本文链接: 331.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!