1 为什么用synchronized
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以访问特定的代码,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
2 怎么用synchronized
1 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
3 synchronized底层实现
底层利用对象头的Mark Word(标记字符)标记锁的状态,实现1.6以后synchronized锁的由无锁–偏向锁–轻量级锁–重量级锁状态的升级;利用monitor(监视器)来获取锁,实现同步。
对象在内存中的布局
我们都知道,Java对象存储在堆(Heap)内存。括起来分为对象头、对象体(实例数据)和对齐字节((对象填充)。如下图所示:
Java对象头
对象头由两部分组成,一部分用于存储自身的运行时数据,称为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。
对象头 = Mark Word + 类型指针
1.对象头中的Mark Word(标记字)用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,在Java对象头中有2bit用来标识对象存储锁的,比如jdk1.6以后synhronized通过改变锁状态实现对锁的升级和优化。
2.Klass Word是一个指向方法区中Class信息的指针,意味该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分
对象体(实例数据)
实例数据部分是对象真正存储的有效信息, 也是在程序代码中所定义的各种类型的字段内容
对齐字节(对象填充)
在64位JVM内存管理系统中要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
在IDE的项目中引入openJdk的jar包可以打印出java对象头详细信息
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-cli</artifactId>
<version>0.9</version>
</dependency>
测试的代码实例
public class Layout {
static Student stu = new Student();
public static void main(String[] args) {
System.out.println("start");
//打印对象头信息
System.out.println(ClassLayout.parseInstance(stu).toPrintable());
synchronized (stu) {
System.out.println("lock ing");
}
System.out.println("end");
}
}
public class Student {
Integer age; //年龄
Integer clazz; //班级
}
运行打印的结果
monitor
每个对象都有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态。当java代码里用synchronized关键字修饰时,通过javap反编译可查看含有monitorenter和monitorexit等指令,案例如下:
public class DiCeng {
public static void main(String[] args)
{
Object obj=new Object();
synchronized (obj) //把obj对象锁住
{
System.out.println("hello sun");
}
}
}
先将Java程序编译:javac -encoding UTF-8 DiCeng.java ;
然后进行反编译:javap -c DiCeng ;显示如下:
当一个线程进进入用synchronized关键字修饰的方法或代码块的时候,要想访问受保护的数据(即需要获取对象的Monitor)时,先通过monitorenter命令进行加锁,持有该对象的Monitor,当退出同步方法或同步代码块时,执行monitorexit命令解锁释放Monitor。此时其他的线程可以恢复访问。