1-synchronized八锁
锁对象:理论上可以是任意的唯一对象
synchronized 是可重入、不公平的重量级锁
原则上:
锁对象建议使用共享资源
在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源
在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类
8锁案例说明:
1-标准访问有ab两个线程,请问先打印邮件还是短信
2-sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
3-添加一个普通的hello方法,请问先打印邮件还是hello
4-有两部手机,请问先打印邮件还是短信
5-有两个静态同步方法,有1部手机,请问先打印邮件还是短信
6-有两个静态同步方法,有2部手机,请问先打印邮件还是短信
7-有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
8-有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
注意:代码根据实际情况改动测试
public class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----sendEmail");
}
public synchronized void sendSMS() {
System.out.println("-----sendSMS");
}
public void hello() {
System.out.println("-------hello");
}
}
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
// phone.hello();
// phone2.sendSMS();
}, "b").start();
}
}
结论:
1-2两种情况:
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
3-4
加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁了,情况立刻变化。
5-6都换成静态同步方法后,情况又变化
三种synchronized锁的内容有一些差别:
对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
对于同步方法块,锁的是synchronized括号内的对象
7-8
***当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
***所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
***所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
***具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
***但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
2-锁原理
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁
- Mark Word 结构:最后两位是锁标志位。
什么任何一个对象都可以成为一个锁?
Java Object类是所有类的父类,也就是说Java的所有类都继承了Object,子类可以使用Object的所有方法。ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp。追溯底层可以发现每个对象天生都带着一个对象监视器
2.1-对象在堆内存中布局
在 64 位系统中, Mark Word 占了 8 个字节,类型指(kclass pointers)针占了 8 个字节(没有开启压缩的情况下,开启压缩4个字节,jvm默认开始压缩),一共是 16 个字节。
对象头包含两部分:对象标记Mark Word和类元信息(又叫类型指针klcass pointers)。
默认存储对象的HashCode、分代年龄和锁标志位等信息。
这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
GC年龄采用4位bit存储,最大为15,
例如MaxTenuringThreshold参数默认值就是15
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据:存放类的属性(Field)数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
2.2-对象在JVM的大小和分布
<!-- 定位:分析对象在JVM的大小和分布 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
关闭指针压缩
-XX:-UseCompressedClassPointers
自定义类的大小:
3-锁升级
synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化;
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁// 随着竞争的增加,只能锁升级,不能降级
3.1-偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:
当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低
当一个对象已经计算过 hashCode,就再也无法进入偏向状态了
添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
3.2-轻量级锁
一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见);
轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化。
3.3-重量级锁
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。