多线程在概念上类似抢占式多任务处理,线程的合理使用能够提升程序的处理能力,但是使用的同时也带来了弊端,对于共享变量访问就会产生安全性的问题。下面来看一个多线程访问共享变量的例子:
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的三种加锁方式
- 静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
synchronized static void method(){}
- 修饰代码块:指定加锁对象,进入同步代码前要获得指定对象的锁
void method(){
synchronized (SynchronizedDemo.class){}
}
- 修改实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
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 采用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);
}
};
#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
看源码instanceOopDesc继承自oopDesc,oopDesc定义在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:记录对象和锁有关的信息,属于markOop类型,_metadata:记录类元信息
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的长度是32bit,64位系统则是64bit。
Mark Word里面的数据会随着锁的标志位的变化而变化的。
2.2.2 Java中打印对象的布局
pom依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
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
大端存储和小端存储
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)
16进制: 0x 00 00 00 00 00 00 00 01
(64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0
0 01 (无锁状态)
- 通过最后三位来看锁的状态和标记。
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 Object 在 JVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。
线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码:
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 偏向锁
存储(以32位为例):线程ID(23bit)
Epoch(2bit)
age(4bit)
是否偏向锁(1bit)
锁标志位(2bit)
当一个线程加入了Synchronized同步锁之后,会在对象头(Object Header)存储线程ID,后续这个线程进入或者退出这个同步代码块的代码时,不需要再次加入和释放锁,而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果线程ID相等,就表示偏向锁偏向于当前线程,就不需要再重新获得锁了。
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 轻量级锁
存储(以32位为例):指向栈中锁记录的指针(30bit)
锁标志位(2bit)
如果偏向锁关闭或者当前偏向锁指向其它的线程,那么这个时候有线程去抢占锁,那么将升级为轻量级锁。
轻量级锁在加锁的过程中使用了自旋锁,JDK1.6之后使用了自适应的自旋锁。
2.3.3 重量级锁
存储(以32位为例):指向互斥量(重量级锁)的指针(30bit)
锁标志位(2bit)
当轻量级锁膨胀为重量级锁后,线程只能被挂起阻塞等待被唤醒了。先来看一个重量级锁的代码:
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());
}
}
}
运行后的结果:
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,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一把锁的情况。
- 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。
来源:oschina
链接:https://my.oschina.net/u/145473/blog/4288370