在 Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同步资源的时候直接先尝试加锁。
加锁可以使一段代码(类实例,方法,代码块)在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是程序的执行性能,所以为了在一定程度上减少获得锁和释放锁带来的性能消耗,在 jdk6 之后便引入了“偏向锁”和“轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。
注意:锁可以升级但不能降级。
/**
* 模拟测试synchronized
*/
public class SynchronizeTest {
private Integer sync = 1;
public static void main(String[] args) {
SynchronizeTest test = new SynchronizeTest();
synchronized (test.sync){
System.out.println(Thread.currentThread().getId() + ": 进入 synchronized ");
}
}
}
可以通过Idea或者命令行javap -verbose SynchronizeTest.class,看到编译后的字节码
编译结果:
0 new #4
3 dup
4 invokespecial #5 >
7 astore_1
8 aload_1
9 getfield #3
12 dup
13 astore_2
14 monitorenter ==>对象监视器进入
15 getstatic #6
18 new #7
21 dup
22 invokespecial #8 >
25 invokestatic #9
28 invokevirtual #10
31 invokevirtual #11
34 ldc #12 <: 进入 synchronized >
36 invokevirtual #13
39 invokevirtual #14
42 invokevirtual #15
45 aload_2
46 monitorexit ==>对象监视器离开
47 goto 55 (+8)
50 astore_3
51 aload_2
52 monitorexit ==>对象监视器离开(防止出现异常,死锁)
53 aload_3
54 athrow
55 return
在使用 synchronized 来同步代码块的时候,经编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。当执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。而 synchronized 用的锁是存放在 Java对象头 中的。
由上面JVM指令可知,刚开始synchronized的锁是JVM 级别的。只有当锁升级到重量级锁才会到CPU的mutex级。
Java 对象头是Java对象的一部分(Java对象包括:对象头,实例数据,对齐补充,数组还有数组长度)
我们以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象监视器。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
关于自旋,简言之就是让线程喝杯咖啡小憩一下,用代码解释就是:
1 2 3 |
do { // do something } while (自旋的规则,或者说自旋的次数) |
引入自旋这一规则的原因其实也很简单,因为阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。并且在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,这部分操作的开销其实是得不偿失的。
所以,在物理机器有多个处理器的情况下,当两个或以上的线程同时并行执行时,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋。如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。