一个问题引发的思考
public class App {
public static int count=0;
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main( String[] args ) {
for(int i=0;i<1000;i++){
new Thread(()->App.incr()).start();
}
System.out.println("运行结果:"+count);
}
}
运行代码结果:
运行多次你会发现他的结果是一个小于等于1000的数。这是为什么呢?
在说这个问题之前,我们先了解一下并发编程的三大特性
多线程的三大特性
- 原子性:即一个操作或多个操作,要么全部执行且执行的过程不会被打断,要么全部不执行
- 可见性:多个线程访问同一个变量时,一个线程修改了变量的值,其他线程能够立即看的到修改的值
- 有序性:即程序执行的顺序按照代码的先后顺序执行。JVM在真正执行代码的时候并不会按照代码的顺序执行,因为这里可能会出现指令重排序。指令重排序,是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。
很明显,上面那个问题导致的原因就是原子性和可见性。
上述代码中,count++看似是原子操作,其实里面涉及到:
- 获取 i 的值
- 自增
- 再赋值给 i
这三步操作。我么可以通过查看字节码文件去验证一下
javap -v App.class
通过javap -v App.class命令可以看到count++的字节码
12: getstatic #5 // 获取count静态域,并将其压入栈顶
15: iconst_1 // int型常量值1入栈
16: iadd // 栈顶两int型数值相加
17: putstatic #5 // 给count赋值
很明显。count++无法保证原子性,那多线程是怎么去解决的?那就是锁
锁(synchronized)
今天我们先讲讲synchronized
synchronized的使用
1.修饰方法
synchronized void demo(){
}
synchronized static void demo2(){
}
2.修饰代码块
void demo3(){
synchronized (this){
}
}
Object obj=new Object();
void demo4(){
synchronized (obj){
}
}
synchronized锁的范围
1、实例锁
实例锁的范围是当前对象实例里,demo方法和demo1方法都是一样
synchronized void demo(){
}
void demo(){
synchronized (this){
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo=new SynchronizedDemo(object);
//锁的互斥性。t1执行则t2等待
new Thread(()->{
synchronizedDemo.demo();
},"t1").start();
new Thread(()->{
synchronizedDemo.demo();
},"t2").start();
}
2、类锁
类锁修饰静态方法或者类对象,demo4方法和demo5方法都是一样
synchronized static void demo04(){
}
void demo05(){
synchronized (SynchronizedDemo.class){
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2=new SynchronizedDemo();
//锁的互斥性。
new Thread(()->{
synchronizedDemo.demo5();
},"t1").start();
new Thread(()->{
synchronizedDemo2.demo5();
},"t2").start();
}
3、代码块
Object lock=new Object();
//只针对于当前对象实例有效.
public SynchronizedDemo(Object lock){
this.lock=lock;
}
void demo(){
synchronized(lock){
}
}
public static void main(String[] args) {
Object object=new Object();
Object object1=new Object();
SynchronizedDemo synchronizedDemo=new SynchronizedDemo(object);
SynchronizedDemo synchronizedDemo2=new SynchronizedDemo(object);
//锁的互斥性。
new Thread(()->{
synchronizedDemo.demo();
},"t1").start();
new Thread(()->{
synchronizedDemo2.demo();
},"t2").start();
}
锁的存储
锁的存储跟对象头有关系,我们先来看一下对象在heap的布局,以及对象头(mark word)的布局:
我们再来仔细分析一下锁在对象头里面是如何存储的:
markOop.hpp
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// 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)
//
这个是在32系统下所在对象头的存储内容
为了更形象的看到锁对象的布局,我们采用OpenJDK提供的方法去打印类的布局
首先,导入包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
其次,在代码中打印
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
synchronized (classLayoutDemo){
System.out.println("locking");
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
运行结果:
我们看最后三位01001 0 00,对应的是没有偏量级锁,是轻量级锁
锁的升级
synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。
当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
偏向锁的目的在于消除数据在无竞争状态下尽量减少不必要的同步原语,进一步提高程序的运行性能
获得偏向锁的流程图:
轻量级锁
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
获得轻量级锁的流程图:
重量级锁
升级为重量级锁,锁标志的状态只会变为“10”,此时mark word中存储的是指向重量级所得指针,此时等待锁的线程都会进入阻塞状态。注:其他几种锁是没有阻塞状态的。
重量锁实例:
public static void main(String[] args) {
LockDemo lockDemo=new LockDemo();
Thread t1=new Thread(()->{
synchronized (lockDemo){
System.out.println("t1 抢占到锁");
System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
}
});
t1.start();
synchronized (lockDemo){
System.out.println("Main 抢占锁");
System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
}
}
synchronized 原理
每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解为一把锁,当一个线程想要执行一段被synchronized修饰的方法或者代码块时,该线程就得先获得synchronized修饰的对象对应的monitor
monitorenter表示去获得一个监视器
monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器
其流程图如下:
我们通过一段代码来演示一下:
public class ClassLayoutDemo {
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
synchronized (classLayoutDemo){
System.out.println("locking");
}
}
}
然后使用javap -v ClassLayoutDemo .class查看编译之后的具体信息:
{
public org.example.ClassLayoutDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/ClassLayoutDemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class org/example/ClassLayoutDemo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String locking
17: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 12: 0
line 13: 8
line 14: 12
line 15: 20
line 17: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 classLayoutDemo Lorg/example/ClassLayoutDemo;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 25
locals = [ class "[Ljava/lang/String;", class org/example/ClassLayoutDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "ClassLayoutDemo.java"
可以看到在同步块的入口和出口分别有 monitorenter,monitorexit 指令。