对于程序员而言,不管是在平常的工作中还是面试中,都会经常用到或者被问到synchronized。在多线程并发编程中,synchronized早已是元老级的角色了,很多人都称其为重量级锁,但是随着Java SE 1.6对其进行各种优化之后,便显得不再是那么的重了。
也正是因为多线程并发的出现,便产生了线程安全这样的问题,对于线程安全的主要原因如下:
而对于解决这样的一个问题的办法是:同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作
此时便产生了互斥锁,互斥锁的特性如下:
对于Java而言,synchronized关键字满足了以上的要求。
首先我们要知道synchronized锁的不是代码,锁的是对象。
根据获取的锁的分类:获取对象锁和获取类锁:
1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号()的实例对象
2.同步非静态方法(synchronized method),锁是当前对象是实例对象
1.同步代码块(synchronized(类.class)),锁是小括号()中的类对象(Class对象)
2.同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)
对象锁和类锁的总结:
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁,那么锁存在哪里呢?我们先来看一段代码:
public class SyncBlockTest {
public void syncsTask() {
synchronized (this) {
System.out.println("Hello");
}
}
public synchronized void syncTask() {
System.out.println("Hello Baby");
}
}
在使用javac工具把上面代码变异成class,然后使用javap工具查看编译好的class文件,如下:
public com.interview.javabasic.thread.SyncBlockTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 8: 0
public void syncsTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Hello
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 10: 0
line 11: 4
line 12: 12
line 13: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/interview/javabasic/thread/SyncBlockTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void syncTask();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String Hello Baby
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
}
同步代码块:从上面的字节码中可以看出,同步语句块的实现是使用monitorenter和monitorexit指令的,monitorenter指向同步代码块的开始位置,它首先去获取PrintStream这个类,然后传入“Hello”这个参数,然后再调用PrintStream中的println()方法去打印,monitorexit指明同步代码块的结束位置,当执行到monitorenter时,当前线程将试图获取对象锁所对应的monitor的持有权。
同步方法:从上面代码的syncTask()方法字节码中看,这里面并没monitorenter和monitorexit,且字节码较短,其实这里方法的同步是隐式的,是无需通过字节码指令控制,在上面可以看到一个“ACC_SYNCHRONIZED”这样的一个访问标志,用来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查ACC_SYNCHRONIZED是否被设置,如果被设置,当前线程将会持有monitor,然后再执行方法,最后不管方法是否正常完成都会释放monitor。
Java对象头和monitor是实现synchronized的基础,下面将会说说关于Java对象头和monitor。
Java对象头:
hotspot虚拟机中,对象在内存的布局分布分为3个部分:对象头,实例数据,和对齐填充。
对象头的结构如下:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64 bit | Mark Word | 默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息 |
32/64 bit | Class Metadata | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类型的数据 |
32/64 bit | Array length | 数组的长度(如果当前的对象是数组 ) |
mark word 被设计为非固定的数据结构,以便在极小的空间内存储更多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的Hotspot虚拟机中,如果对象处于未被锁定的情况下。那么Mark Word 的32bit空间中有25bit用于存储对象的哈希码、4bit用于存储对象的分代年龄、2bi用于t存储锁的标记位、1bit固定为0,而在其他的状态下(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储结构如下:
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希吗、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
monitor:
每个Java对象天生就自带了一把看不见的锁,它可以视为是一种同步工具或者是一种同步机制,monitor还是线程私有的数据结构,每一个线程都有一个可用monitor 列表,同时还有一个全局的可用列表,如上面所说每一个被锁住的对象都会持有一个monitor。monitor结构如下:
Java6以后,对锁进行了大量的优化,例如:AdaptiveSpinning(自适应自旋)、Lock Eliminate(锁消除)、Lock Coarsening(锁粗化)、Lightweight Locking(轻量级锁)、Biased Locking(偏向锁)等等
自旋锁
定义:所谓的自旋锁就是让没有获取到锁的线程继续等待一会儿,但不放弃CPU的执行时间,这是的等一会和不放弃CPU的时间即是自旋锁。
自适应自旋锁
锁消除
锁消除是对锁更彻底的优化,JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
例如Java中的StringBuffer 它是线程安全的,由于其append()方法只会在内部使用的,也就不可能被其他线程引用,因此对于这个变量sb而言,便不属于共享资源了,JVM会自动消除内部的锁。
锁粗化
原则上我们都知道,在加同步锁的时候,尽可能将同步块的作用范围先知道尽量小的范围,及只在共享数据的实际工作范围中进行同步,这样是为了需要同步的数量尽可能的变小,在存在锁同步竞争中,使得等待锁的时间减小。
上述情况,大部分时候是正确的,但是如果存在一系列频繁的操作,对同一个对象反复的加锁、解锁, 甚至加锁时是在循环体中操作的,这样即使没有线程竞争,频繁的进行互斥锁操作,也是导致不必要的性能开销。
对于解决这样的问题,我们只有尽可能的扩大加锁的范围,例如下面的循环100次append,JVM会自己检测到这样的一个问题,就会将加锁的次数减至一次。
public static String copyString(String target){
int i = 0;
StringBuffer sb = new StringBuffer();
while (i<100){
sb.append(target);
}
return sb.toString();
}
synchronized锁有四种状态分别为:无锁、偏向锁、轻量级锁、重量级锁
锁的膨胀方向:无锁——>偏向锁——>轻量级锁——>重量级锁
作用:减少同一线程获取锁的代价
引入偏向锁是因为大多数情况下,锁并不存在多线程竞争,总是由同一线程多次获得
以下内容引自:【死磕Java并发】—–深入分析synchronized的实现原理
获取锁
释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
获取锁
释放锁
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
版权声明:尊重博主原创文章,转载请注明出处:https://blog.csdn.net/zfy163520