第一章:并发编程中的三个问题
1.可见性
目标
学习什么是可见性问题
可见性概念
可见性(Visibility):是指一个线程对共享变量进行修改,另一个要立即得到修改后的最新值。
可见性演示
案例演示:一个线程根据boolean类型的标记flag,while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。
/*
目标:演示可见性问题
1.创建一个共享变量
2.创建一条线程不断读取共享变量
3.创建一条线程修改共享变量
*/
public class TestVisibility {
// 1.创建一个共享变量
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
// 2.创建一条线程不断读取共享变量
new Thread(() -> {
while(flag){
}
}).start();
Thread.sleep(2000);
// 3.创建一条线程修改共享变量
new Thread(() -> {
flag = false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
结论:并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
2.原子性
目标
学习什么是原子性问题
原子性概念
原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰,要么都不执行
原子性演示
案例演示:5个线程各自执行1000次i++
public class Test02Atomicity {
// 1. 定义一个共享变量number
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
// 2. 对number进行1000次的++操作
// 3. 运行5个线程来进行
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
number++;
}
});
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + number);
}
}
由于number++是由多条语句组成
其中,对于number++而言(number为静态变量),实际会产生如下的JVM字节码指令:
由此可见number++是由多条语句组成,以上多条指令再一个线程的情况下是不会出现问题的,但是再多线程的情况下执行时,比如出现一个线程在执行13:iadd时,另一个线程又执行了9:getstatic。会导致两次number++,实际上只+1。
小结
并发编程时会出现原子性问题,一个线程在访问的过程中,另一个线程也对变量进行访问,最后写回的时候,以最后执行的线程的结果返回。
3.有序性
有序性概念
有序性(Ordering):是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就算我们编写代码时的顺序。
第二章:Java内存模型(JMM)
在介绍Java内存模型之前,先看计算机内存模型。
计算机结构简介
冯诺依曼,提出计算机由五大组成部分。
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多的等待时间。内存的读写速度成为了计算机运行的瓶颈。于是有了CPU和主内存之间增加缓存的设计。
Java内存模型
1.Java内存模型的概念
Java Memory Model(Java内存模型 / JMM),千万不要和Java内存结构混淆。
Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
工作内存
每一个线程都有自己的工作内存,工作内存只能存储该线程对共享变量的副本。线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
2.Java内存模型的作用
Java内存模型是一套多线程读写共享数据时,对共享数据的可见性、有序性和原子性的规则和保障
主要有:synchronized、volatile
JMM内存模型与CPU硬件内存架构的关系:
3.主内存和工作内存的交互
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子性的、不可再分的。
补充:
4.线程间通信
线程间通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
线程之间的通信有哪些? 主要有三种:等待 - 通知、共享内存、管道流
等待 - 通知:
待补充
第三章:synchronized保证三大特性
synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized (锁对象) {
// 受保护资源;
}
synchronized与原子性
public class Test02Atomicity {
// 1. 定义一个共享变量number
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2. 对number进行1000次的++操作
// 3. 运行5个线程来进行
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
synchronized (obj){
number++;
}
}
});
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + number);
}
}
synchronized保证原子性的原理
保证同一时间只有一个线程操作代码块中的内容。synchronized保证原子性的原理就是synchronized保证只有一个线程拿到锁,能够进入同步代码块。
synchronized与可见性
public class Test01Visibility {
// 1.创建一个共享变量
private static boolean flag = true;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2. 创建一个线程不断读取共享变量
new Thread(() -> {
while (flag){
synchronized (obj){
}
}
}).start();
Thread.sleep(2000);
// 3. 创建一条线程修改共享变量
new Thread(() -> {
while (flag){
flag = false;
System.out.println("线程修改了变量值为false");
}
}).start();
}
}
synchronized保证可见性的原理,执行synchronized时,对应lock原子操作会刷新工作内存中共享变量的值。
补充:System.out.println();中也包含synchronized关键字
synchronized与有序性
为什么要重排序
为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
as-if-serial语义
as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确。
synchronized保证有序性的原理
synchronized后,虽然依然会发生重排序,但是我们的同步代码块可以保证只有一个线程执行同步代码块中的代码来保证有序性。
第四章:synchronized的特性
可重入特性
什么是可重入
一个线程可以多次执行synchronized,重复获取同一把锁
class MyThread extends Thread {
@Override
public void run(){
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块1");
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块2");
}
}
}
}
可重入原理
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他线程是不可以的。
可重入的好处
1.可以避免死锁
2.可以更好的封装代码
小结
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁了,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
不可中断特性
什么是不可中断
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
synchronized是不可中断的
Lock既有可中断,又可不可中断。
synchronized:
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 1.定义一个Runnable
Runnable run = () -> {
// 2.在Runnable定义同步代码块
synchronized (obj){
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 3.先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
// 4.后开启一个线程来执行同步代码块(阻塞状态)
Thread t2 = new Thread(run);
t2.start();
// 5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState()); // 处于Blocked状态阻塞
}
Lock的不可中断:
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
}
// 演示Lock不可中断
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try{
lock.lock();
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
System.out.println(t1.getState());
System.out.println(t2.getState()); // 使用Lock方法不可中断
}
Lock的可中断:
// 演示Lock可中断
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try{
b = lock.tryLock(3, TimeUnit.SECONDS);
if (b){
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定时间没有得到锁做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b){
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
// System.out.println("停止t2线程前");
// t2.interrupt();
// System.out.println("停止t2线程后");
//
// Thread.sleep(1000);
// System.out.println(t1.getState());
// System.out.println(t2.getState());
}
第五章 synchronized的原理
javap反汇编
简单写一个 synchronized的代码,如下:
public class Demo01 {
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj){
System.out.println("1");
}
}
public synchronized void test(){
System.out.println("a");
}
}
要看 synchronized的原理,但是由于 synchronized 是关键字,看不到源码,所以对文件进行反汇编
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field obj:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String 1
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 7: 0
line 8: 6
line 9: 14
line 10: 24
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String a
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
monitorenter
首先看一下JVM规范中对于monitorenter的描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
1. monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2. 若线程已拥有monitor的所有权,允许它冲入monitor,则进入monitor的进入数加1
3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,知道monitor的进入数变为0,才能重新尝试获取monitor的所有权
monitorenter小结:
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建,是JVM的线程执行的这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待。
monitorexit
首先我们来看一下JVM规范中对于monitorexit的描述:
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
monitorexit释放锁。
monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。
面试题synchronized出现异常会释放锁吗?
会释放锁。
同步方法
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。
面试题: synchronized与Lock的区别
- synchronized是关键字,而Lock是一个接口。
- synchronized会自动释放锁,而Lock必须手动释放锁。unlock()
- synchronized是不可中断的,Lock可以中断也可以不中断。
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能。trylock()
- synchronized能锁住方法和代码块,而Lock只能锁住代码块。
- Lock可以使用读锁提高多线程读效率。
- synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。(构造时创建是否是公平锁)
深入JVM源码
monitor监视器锁
详细见有道云笔记
monitor是重量级锁
可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。
用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:
第六章:JDK6 synchronized优化
CAS
CAS概述和作用
CAS(Compare And Swap,比较 相同 在交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。
CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。
CAS和volatile实现无锁并发
public class Demo01 {
// 1. 定义一个共享变量number
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
// 2. 对number进行1000次的++操作
// 3. 运行5个线程来进行
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.incrementAndGet(); // 保证变量赋值的原子性
}
});
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + atomicInteger.get());
}
}
CAS 原理
通过刚才AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。
Unsafe 类介绍
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使 用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对 象不能直接调用,只能通过反射获得。
Unsafe实现CAS
乐观锁和悲观锁
悲观锁从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁, 这样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock 也是一种悲观锁。性能较差!
乐观锁从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。
CAS这种机制我们也可以将其称之为乐观锁。综合性能好!
CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以 实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。
1.因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
2.但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
小结
CAS的作用? Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理 器保证。
CAS的原理?CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果内存地址V和旧的预期值 A相等就修改内存地址值为B 。详情请见AtomicInteger的实现代码。
synchronized锁升级过程
高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性 自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为 了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
无锁–》偏向锁–》轻量级锁–》重量级锁
Java对象的布局
术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
对象头
当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是存在锁对象的对象头中的。
HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp 。
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};
从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
// 省略其他代码
};
在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata
_mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息
_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、 _compressed_klass 表示压缩类指针。
对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指 针,及对象指向它的类元数据的指针。
Mark World
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类 型是 markOop 。源码位于 markOop.hpp 中。
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:
klass pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的 实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对 象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内 存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
1、 每个Class的属性指针(即静态变量)
2、 每个对象的属性指针(即对象变量)
3、 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对 象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
对象头 = Mark Word + 类型指针(未开启指针压缩的情况下):
在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;
实例数据
就是类中定义的成员变量。
对齐填充
对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的 自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的 整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充 来补全。
可通过导入依赖查看Java对象布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
小结
java对象由3部分组成,对象头,实例数据,对齐数据
对象头分为两部分:Mark World + Klass pointer
偏向锁
什么是偏向锁
偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多 线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID即可。
不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的 CAS原子操作的性能消耗,不然就得不偿失了。
偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
1、 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
2、 同时使用 CAS 操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何 同步操作,偏向锁的效率高。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
偏向锁的撤销
1、偏向锁的撤销动作必须等待全局安全点
2、暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
3、撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用
- XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争 状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。
偏向锁好处
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多 数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。
在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。但在应用程序启动几秒钟之后才 激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常 情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。
小结
偏向锁的原理:
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 “01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每 次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
偏向锁的好处:
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
轻量级锁
什么是轻量级锁
轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要 替代重量级锁。
轻量级锁原理
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下: 获取锁
1)线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的Mark Word复制到该锁记录中,官方称之为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)
2)线程自旋,自旋成功则获得锁,进入步骤3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)
3)锁的持有线程执行同步代码,执行完CAS替换Mark Word成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)
4)CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程
轻量级锁的释放
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
1、取出在获取轻量级锁保存在Displaced Mark Word中的数据。
2、用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
3、如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。
对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如 果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁 比重量级锁更慢。
轻量级锁的好处
在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
小结
轻量级锁的原理是什么? 将对象的Mark Word复制到栈帧中的Lock Recod中。Mark Word更新为指向Lock Record的指针。
轻量级锁好处是什么? 在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
自旋锁
synchronized (Demo01.class) {
...
System.out.println("aaa");
}
前面我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从 用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很 短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个 或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行 时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自 旋) , 这项技术就是所谓的自旋锁。
自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在 JDK 6中 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本 身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等 待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而 不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果 自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改。
适应性自旋锁
在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上 的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持 有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持 续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取 这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控 信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享 数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们 是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确 定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有 许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的 想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上 都没有同步。
public class Demo01 {
public static void main(String[] args) {
contactString("aa", "bb", "cc");
}
public static String contactString(String s1, String s2, String s3) {
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}
StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的 动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃 逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除 掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
对象逃逸
对象逃逸有两种:
方法逃逸(对象逃出当前方法):
当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中。
线程逃逸(对象逃出当前线程):
这个对象甚至可能被其他线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作 用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线 程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对 象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操 作也会导致不必要的性能损耗。
public class Demo01 {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append("aa");
}
System.out.println(sb.toString());
}
}
小结
什么是锁粗化?JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放 到这串操作的外面,这样只需要加一次锁即可。
平时写代码对Synchronized的优化
减少Synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。
synchronized (Demo01.class) {
System.out.println("aaa");
}
降低 synchronized锁的粒度
讲一个锁拆分成多个锁提高并发度。
Hashtable hs = new Hashtable();
hs.put("aa", "bb");
hs.put("xx", "yy");
读写分离
读取时不加锁,写入和删除时加锁
ConcurrentHashMap,CopyOnWriteArrayList 和 ConyOnWriteSet
Volitile关键字的工作原理(含与Synchronized的对比)
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。