logo头像
ICQL

jc_并发关键字

volatile

1)作用

如果一个字段被声明为 volatile
1)可见性:java内存模型会确保一个线程修改 这个变量的值 会对其他线程立即可见
2)有序性:禁止指令重排序
3)原子性:jmm内存模型规定,变量的load和store操作本身就是原子的

注意:

(1)基本类型:指的是值,修改值以后其他线程会立即可见
(2)引用类型:指的是地址,修改地址后其他线程会立即可见,修改这个变量的字段不会对其他线程立即可见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//验证volatile引用类型
//打印jit编译的汇编代码
//vm参数,-Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Application.main
public class TestObject {
public int test;
}
public class Application {
private static volatile TestObject testObject = new TestObject();
public static void main(String[] args) throws Exception {
//testObject = null;//第1次运行
//testObject.test = 10;//第2次运行
}
}

//根据汇编结果可以看到:
//第1次运行,关键汇编代码,有lock xxx代码,说明修改变量的地址 volatile 有效
//0x0000000003832d8c: lock add dword ptr [rsp],0h ;*putstatic testObject
// ; - work.icql.jvm.Application::main@1 (line 10)
//
//第2次运行,关键汇编代码,没有lock xxx代码,说明修改变量的字段,该 volatile 变量 无效

2)实现原理

(1)jvm层面-jmm内存模型语义

jmm规定的volatile重排序

具体的,jvm用 内存屏障 来实现jmm内存模型规定的上述volatile语义(可见性、有序性)

内存屏障类型:
(1)LoadLoad屏障:在执行Load2前必须执行完Load1
(2)LoadStore屏障:在执行Store前必须执行完Load
(3)StoreStore屏障:在执行Store2前必须执行完Store1
(4)StoreLoad屏障:在执行Load前必须执行完Store

理论上,对于volatile变量,jvm插入的内存屏障顺序如下:

volatile读操作
LoadLoadBarrier,保证 volatile读 不能与 普通读/volatile读 重排序
LoadStoreBarrier,保证 volatile读 不能与 普通写/volatile写 重排序

StoreStoreBarrier,保证 普通读或写/volatile写 不能与 volatile写 重排序
volatile写操作
StoreLoadBarrier,保证 volatile写 不能与 (普通读或写)/volatile读或写 重排序

上面有几点要注意的是:
store前必须要load,这是jmm规定的
所以StoreStoreBarrier和StoreLoadBarrier会额外的保证了相关load操作
StoreLoadBarrier中多余的保证了 volatile写 不能与 普通读或写 重排序
但是不影响正确性,所以没关系

实际上,jvm插入的内存屏障和cpu有关
(1)X86架构处理器,只会对 写-读 做重排序,不会对 读-读、读-写 和 写-写 这3种操作做重排序
所以,jvm对X86处理器,只会插入 StoreLoad屏障
(2)单核处理器没有必要插入内存屏障,针对单核处理器,jvm没有使用内存屏障

//例如orderAccess_linux_x86.inline.hpp,图片来源于https://zhuanlan.zhihu.com/p/133851347
orderAccess_linux_x86.inline.hpp

(2)硬件层面

X86架构处理器使用lock前缀的cpu指令实现内存屏障(volatile)

上图的源码中有体现,1)作用 中验证volatile引用类型也有体现

具体查看 jc_原子操作篇

3)volatile缓存行填充

伪共享问题:

缓存系统中是以缓存行(cache line)为单位存储的
一般的cpu缓存行为64kb
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行
就会无意中影响彼此的性能

解决办法:

使用一些无意义的变量填充整个对象
使 有意义数据+无意义数据 的内存占用大于64b
即保证一个缓存行中只能有有意义数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//jdk7,Disruptor框架中的应用
//使用继承避免jdk7优化掉无用的字段
//
//左边7个字节+有效值value+右边7个字节
//这样可以使得不管如何,一个缓存行中都只有 有效值value
//没有其他有意义的数据

class LhsPadding{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding{
protected volatile long value;
}
class RhsPadding extends Value{
protected long p9, p10, p11, p12, p13, p14, p15;
}
1
2
3
4
5
6
7
//jdk8
//可以直接使用注解@sun.misc.Contended,使各个变量在Cache line中分隔开
//同时jvm需要添加参数-XX:-RestrictContended来关闭对此注解的限制
@Contended
public final static class FillLong {
public volatile long value = 0L;
}


synchronized

1)作用

synchronized用来实现同步,java中的每一个对象都可以作为锁

2种使用形式:

1)同步方法
(1)对于普通同步方法,锁是当前实例对象
(2)对于静态同步方法,锁是当前类的Class对象
2)同步代码块
(2)锁是synchonized括号里配置的对象

2)实现原理

(1)jvm层面

(1)对于同步方法,jvm采用ACC_SYNCHRONIZED标记符来实现同步
(2)对于同步代码块,jvm采用monitorenter、monitorexit两个字节码指令来实现同步

上述两者都是通过 Monitor(管程或监视器) 实现的,每个对象都有自己的Monitor
同步方法是隐式的,当调用同步方法时,会先检查是否有ACC_SYNCHRONIZED,如果有则需要先获得监视器锁,方法执行完后要释放监视器锁
同步代码块则显示的直接使用的两个字节码指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public static void main(String[] args) throws Exception {
synchronized (""){
System.out.println("wowo");
}
}

public synchronized void test(){
System.out.println("wowo");
}

public synchronized static void test2(){
System.out.println("wowo");
}

//编译后,javap -v XXX.class查看字节码,查看关键部分如下,可以得知上述结论
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String wowo
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 10: 0
line 11: 5
line 12: 13
line 13: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
Exceptions:
throws java.lang.Exception

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 #4 // String wowo
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lwork/icql/jvm/Application;

public static synchronized void test2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String wowo
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 20: 0
line 21: 8
(1.1)对象头markword

对象结构详情请查看 jvm_内存管理篇-对象内存布局(64位虚拟机)

64位jvm中java对象不同状态下的markword
markword

lock + biased_lock 共同确定了上述5种不同的状态,状态不同,整个markword标识的含义不同

1)无锁状态:
lock:锁标记,2位,值为01
biased_lock:偏向锁标记,1位,值为0,代表非偏向锁状态
age:垃圾回收java对象年龄,4位,垃圾回收时对象每次在survivor区复制一次,年龄增加1次,具体查看 jvm_垃圾回收篇
identity_hashcode:对象hashCode,31位,方法System.identityHashCode()计算得到的,延迟计算,计算后写入到这部分。其他状态下markword没有空间存储此值,hashcode移动到每个对象的监视器Monitor中
unused:未使用到的

2)偏向锁状态:
biased_lock:偏向锁标记,1位,值为1,代表偏向锁状态
thread:持有偏向锁的线程ID,54位
epoch:用于批量重偏向/撤销,2位,较为复杂

3)轻量级锁状态:
ptr_to_lock_record:指向栈中锁记录的指针,62位

4)重量级锁状态:
ptr_to_heavyweight_monitor:指向对象监视器Monitor的指针,62位

部分内容参考 https://blog.csdn.net/SCDN_CP/article/details/86491792

(1.2)锁的升级过程

jdk1.6之后为了减少使用同步锁的性能消耗,优化了synchonized,使用synchonized时锁会有一个升级的过程

无锁状态(匿名偏向状态) -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态

内容来自这位大佬 https://github.com/farmerjohngit/myblog ,防止丢失,内容已缓存
概述
偏向锁
轻量级锁
重量级锁

是非公平锁

当一个线程想获取锁时,
先试图插队,
如果占用锁的线程释放了锁,
下一个线程还没来得及拿锁,
那么当前线程就可以直接获得锁;
如果锁正在被其它线程占用,
则排队,
排队的时候就不能再试图获得锁了,
只能等到前面所有线程都执行完才能获得锁

(2)硬件层面

1)更改markword值是使用的cas操作(Atomic::cmpxchg)
2)重量级锁使用的是POSIX中mutex锁
https://github.com/farmerjohngit/myblog/issues/7



final

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
(防止对象引用逃逸)
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

微信打赏

赞赏是不耍流氓的鼓励