JAVA并发编程入门篇,思考同步锁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
public class ThreadSafty {

private static int count = 0;

public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0 ; i < 1000; i++) {
new Thread(()->{
ThreadSafty.incr();
},"threadSafty" + i).start();
}
TimeUnit.SECONDS.sleep(3);
System.out.println("运行结果是:" + count);
}

}

变量count的运行结果始终是小于等于1000的随机数,因为线程的可见性和原子性。

一、多线程访问的数据安全性

如何保证线程并行运行的数据安全性问题,这里首先能够想到的是加锁吧。关系型数据库中有乐观锁、悲观锁,那么什么是锁呢?它是处理并发的一种手段,实现互斥的特性。

在Java语言中实现锁的关键字是Synchronized

二、Synchronized的基本应用

2.1 Synchronized的三种加锁方式

  • 静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
1
synchronized  static void method(){}
  • 修饰代码块:指定加锁对象,进入同步代码前要获得指定对象的锁
1
2
3
void method(){
synchronized (SynchronizedDemo.class){}
}
  • 修改实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    1
    2
    3
    4
    5
    6
    7
    8
    Object lock = new Object();
    //只针对于当前对象实例有效.
    public SynchronizedDemo(Object lock){
    this.lock = lock;
    }
    void method(){
    synchronized(lock){}
    }

2.2 Synchronized锁是如何存储数据的呢?

以对象在jvm内存中是如何存储作为切入点,去看看对象里面有什么特性能够实现锁的

2.2.1 对象在Heap内存中的布局

在Hotspot虚拟机中,对象在堆内存中的布局,可以分为三个部分:

  • 对象头:包括对象标记、类元信息
  • 实例数据
  • 对齐填充

Hotspot 采用instanceOopDescarrayOopDesc 来描述对象头,arrayOopDesc 对象用来描述数组类型的。
instanceOopDesc的定义在Hotspot源码中的instanceOop.hpp文件中,另外,arrayOopDesc的定义对应arrayOop.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
};

#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

看源码instanceOopDesc继承自oopDesc,oopDesc定义在oop.hpp文件中:

1
2
3
4
5
6
7
8
9
10
11
12
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:记录对象和锁有关的信息,属于markOop类型,_metadata:记录类元信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }

public:
// Constants
enum {
age_bits = 4,//分代年龄
lock_bits = 2,//锁标识
biased_lock_bits = 1,//是否为偏向锁
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,//对象的hashCode
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2//偏向锁的时间戳
};
......

markOopDesc记录了对象和锁有关的信息,也就是我们常说的Mark Word,当某个对象加上Synchronized关键字时,那么和锁有关的一系列操作都与它有关。
32位系统Mark Word的长度是32bit64位系统则是64bit

Mark Word里面的数据会随着锁的标志位的变化而变化的。

2.2.2 Java中打印对象的布局

pom依赖

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1
System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
1
2
3
4
5
6
7
8
com.sy.sa.thread.SynchronizedDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 31 00 00 00 (00110001 00000000 00000000 00000000) (49)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

大端存储和小端存储

1
2
0     4        (object header)                           31 00 00 00 (00110001 00000000 00000000 00000000) (49)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
1
2
16进制: 0x 00 00 00 00 00 00 00 01
(64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0

0 01 (无锁状态)

  • 通过最后三位来看锁的状态和标记。
1
2
3
4
5
6
7
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
0 4 (object header) a8 f7 76 02 (10101000 11110111 01110110 00000010) (41351080)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

000表示为轻量级锁

2.2.3 为什么什么对象都能实现锁?

Java 中的每个对象都派生自 Object 类,而每个Java ObjectJVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。

线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码:

1
2
3
4
5
ObjectMonitor* monitor() const {
assert(has_monitor(), "check");
// Use xor instead of &~ to provide one extra tag-bit check.
return (ObjectMonitor*) (value() ^ monitor_value);
}

多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系。

2.3 Synchronized的锁升级

锁的状态有:无锁、偏向锁、轻量级锁、重量级锁。 锁的状态根据竞争激烈程度从低到高不断升级。

2.3.1 偏向锁

1
2
3
4
5
存储(以32位为例):线程ID(23bit)
Epoch(2bit)
age(4bit)
是否偏向锁(1bit)
锁标志位(2bit)

当一个线程加入了Synchronized同步锁之后,会在对象头(Object Header)存储线程ID,后续这个线程进入或者退出这个同步代码块的代码时,不需要再次加入和释放锁,而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果线程ID相等,就表示偏向锁偏向于当前线程,就不需要再重新获得锁了。

1
2
3
4
5
6
7
8
9
10
11
com.sy.sa.thread.ClassLayoutDemo object internals:
OFFSET SIZE  TYPE DESCRIPTION                VALUE
  0   4    (object header)              05 e8 45 03
(00000101 11101000 01000101 00000011) (54913029)
  4   4    (object header)              00 00 00 00
(00000000 00000000 00000000 00000000) (0)
  8   4    (object header)              05 c1 00 f8
(00000101 11000001 00000000 11111000) (-134168315)
  12   4    (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2.3.2 轻量级锁

1
2
存储(以32位为例):指向栈中锁记录的指针(30bit)
锁标志位(2bit)

如果偏向锁关闭或者当前偏向锁指向其它的线程,那么这个时候有线程去抢占锁,那么将升级为轻量级锁。

轻量级锁在加锁的过程中使用了自旋锁,JDK1.6之后使用了自适应的自旋锁。

2.3.3 重量级锁

1
2
存储(以32位为例):指向互斥量(重量级锁)的指针(30bit)
锁标志位(2bit)

当轻量级锁膨胀为重量级锁后,线程只能被挂起阻塞等待被唤醒了。先来看一个重量级锁的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HeavyweightLock {

public static void main(String[] args) {
HeavyweightLock heavyweightLock = new HeavyweightLock();
Thread t1 = new Thread(()->{
synchronized (heavyweightLock) {
System.out.println("tl lock");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
}
},"heavyheightLock");
t1.start();
synchronized (heavyweightLock) {
System.out.println("main lock");
System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
}
}

}

运行后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com.sy.sa.thread.HeavyweightLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

tl lock
com.sy.sa.thread.HeavyweightLock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

每一个Java对象都会与一个监视器monitor关联,可以把它理解成一把锁,当一个线程要执行用Synchronized修改的代码块或者对象时,该线程最先获取到的是Synchronized修饰对象的monitor
重量级加锁的基本流程:

monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。

2.3.4 锁升级总结

  • 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。
  • 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一把锁的情况。
  • 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。