synchronized
2018-10-26 本文已影响0人
嗷大喵儿
1.解决的问题
解决多线程数据共享及同步
2.使用方式
2.1修饰实例方法
作用于当前实例,进入同步代码需要获取当前实例的锁
synchronized void addA(){
a+=1;
}
等价于
void addA() {
synchronized (this) {
a += 1;
}
}
2.2 修饰静态方法
作用于当前类,进入同步代码需要获取当前类的锁
static synchronized void addA(){
a+=1;
}
等价于
void addA() {
synchronized (当前类.class) {
a += 1;
}
}
2.3 修饰代码块
作用于锁对象,进入同步代码需要获取当前锁对象的锁
void addA(){
synchronized (object){
a+=1;
}
}
3.验证
3.1 修饰实例方法/代码块 this
public class AccountingStaticSync implements Runnable {
static int a;
private String threadName;
synchronized void addA() {
a += 1;
}
/**
* synchronized 修饰实例方法等同于 synchronized (this)
*/
// void addA(){
// synchronized (this) {
// a += 1;
// }
// }
public void run() {
for (int index = 0; index < 100000; index++) {
addA();
Thread.yield();
}
}
public AccountingStaticSync(String threadName) {
this.threadName = threadName;
}
public static void main(String[] args) throws Exception {
int threadCount = 15;
List<Thread> threads = new LinkedList<Thread>();
for (int index = 0; index < threadCount; index++) {
threads.add(new Thread(new AccountingStaticSync(String.valueOf(index))));
}
for (int index = 0; index < threadCount; index++) {
threads.get(index).start();
}
for (int index = 0; index < threadCount; index++) {
threads.get(index).join();
}
System.out.println("a=" + a);
}
}
执行结果.png
当多个线程以该方式修改共享数据时,导致数据不一致的发生
3.2 修饰静态方法/共享对象
public class AccountingStaticSync implements Runnable {
static AccountingStaticSync sync = new AccountingStaticSync("sync");
static int a;
private String threadName;
static synchronized void addA() {
a += 1;
}
/**
* synchronized 修饰静态方法等同于 synchronized (this.class) 或 synchronized (sync object)
*/
// void addA(){
// synchronized (AccountingStaticSync.class) {
// a += 1;
// }
// }
//
// void addA(){
// synchronized (sync) {
// a += 1;
// }
// }
public void run() {
for (int index = 0; index < 100000; index++) {
addA();
Thread.yield();
}
}
public AccountingStaticSync(String threadName) {
this.threadName = threadName;
}
public static void main(String[] args) throws Exception {
int threadCount = 15;
List<Thread> threads = new LinkedList<Thread>();
for (int index = 0; index < threadCount; index++) {
threads.add(new Thread(new AccountingStaticSync(String.valueOf(index))));
}
for (int index = 0; index < threadCount; index++) {
threads.get(index).start();
}
for (int index = 0; index < threadCount; index++) {
threads.get(index).join();
}
System.out.println("a=" + a);
}
}
执行结果.png
修饰静态方法/this.class/sync object 均是阻塞在同一个对象上,因此可以安全地执行
4.底层原理
4.1 同步对象
当synchronized修饰实例方法/对象时,是基于对象头以及monitorenter 和 monitorexit 指令来实现同步的
4.1.1 对象头
对象实例.png实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可
4.1.2MarkWord
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例 |
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
锁状态 | 25bit | 4bit | 1bit是否偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象hashcode | 对象分代年龄 | 0 | 01 |
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,
除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
Mark Word.png
4.1.3 重量锁---synchronized
重量锁中的指针指向一个 monitor 对象,该 monitor伴随锁对象生成和销毁
monitor 的数据结构如下
_ownerObjectMonitor() {
_header = NULL;
_count = 0; //记录锁个数,重入时,_count + 1, 释放时 _count - 1
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //记录持有锁的线程,因此是可重入的
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
- _owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合
- 当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
- 无论执行代码的过程中成功或异常,均会释放锁
4.2同步方法
静态同步方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的
- 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中
- JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法
- 当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法
- 最后再方法完成(无论是正常完成还是非正常完成)时释放monitor
- 无论执行代码的过程中成功或异常,均会释放锁