synchronized
概念
- 意思为同步,同步的等意思;主要用于处理多线程并发。
- synchronized的底层实现虚拟机里没有规范要求,由具体的JVM实现(如hotspot)。
- 保证原子性,保证可见性,保证有序性。
- 可重入属性。
- 修饰的方法/代码块中执行如果出现异常(不try catch的话),那么会默认释放锁。
- 最好不要锁基本数据类型的包装类和String,详情见:
- 使用对象的锁时,new出来的对象尽量加上final,不然后面可能把引用指向其他new的对象,这时候就会出问题。
final Object o = new Object();
o = new Object(); //不要这样做,所以用加上final避免这种操作。
用法
锁对象
public class Test {
private int num = 10;
private Object o = new Object();
public void test1(){
//正确说法:其他线程如果要执行{}中的代码,必须拿到对象o的锁
synchronized (o){
num--;
}
}
public void test2(){
//正确说法:其他线程如果要执行{}中的代码,必须拿到本对象的锁
synchronized (this){
num--;
}
}
}
锁方法
public class Test {
private int num = 10;
//等同上面的例子对执行代码synchronized(this)
public synchronized void test(){
num--;
}
}
锁class
public class Test {
private static int num = 10;
public static void test1(){
synchronized(Test.class){
num--;
}
}
public void test2(){
synchronized(Test.class){
num--;
}
}
}
锁静态方法
public class Test {
private static int num = 10;
//等同上面的例子对执行代码synchronized(Test.class)
public synchronized static void test(){
num--;
}
}
静态与非静态的区别
对于非静态synchronized(this)的方法,由于其使用需要对象new出来,所以锁只针对于这个对象而已,假如再new出来一个,那么他们的锁机制是互相不干扰的。静态方法/非静态方法中使用synchronized(xxx.class)则对所有对象起作用的。
/**
* 本例子演示在非静态方法锁class。
* 可以看到,虽然我们new出了两个Test,但由于synchronized(Test.class),所以锁还是起作用的,就是锁class对所有对象都起作用。
* 假如把synchronized(Test.class)换成synchronized(this),那么由于锁的是对象,所以锁只能各自的对象起作用。
*
* 注意的是,这跟test2Method方法加不加static,num加不加static是没关系的,
* 只是如果test2Method方法加了static,那么不能synchronized(this),因为static方法属于类方法,非static方法属于实例方法,没有this概念。
*/
public class Test {
private static int num = 5;
public static void test2Method(){
synchronized(Test.class){ //假如把Test.class换成this会咋样?
while (num != 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println(Thread.currentThread().getName() + ":" + num);
}
num = 5;
}
}
public static void main(String[] args) {
new Thread(() -> {
Test test1 = new Test();
test1.test2Method();
},"thread1").start();
new Thread(() -> {
Test test1 = new Test();
test1.test2Method();
},"thread2").start();
}
/*输出结果:
thread1:4
thread1:3
thread1:2
thread1:1
thread1:0
thread2:4
thread2:3
thread2:2
thread2:1
thread2:0
*/
/*synchronized(Test.class)换成synchronized(this)的输出结果:
thread1:3
thread2:3
thread2:2
thread1:2
thread2:1
thread1:0
thread1:4
thread1:3
thread1:2
thread1:1
thread1:0
*/
}
可重入属性
synchronized可重入属性,也叫可重入锁,即同一线程下的synchronized可重复可递归调用。
public class Test {
synchronized void m1(){
System.out.println("调用m1");
m2(); //如果不支持可重入属性,那么这里就会造成死锁(因为m1已经获取本对象的锁了,再执行m2由于锁还在m1那里)
}
synchronized void m2(){
System.out.println("调用m2");
}
public static void main(String[] args) {
new Test().m1();
}
/* 输出结果:
调用m1
调用m2
*/
}
public class Test {
synchronized void m1(){
System.out.println("调用m1");
}
class Child extends Test{
@Override
synchronized void m1() {
//如果不支持可重入,那么也会死锁
//补充,调用父类的方法时,锁的也是自己this
super.m1();
}
}
public static void main(String[] args) {
new Test().new Child().m1();
}
/* 输出结果:
调用m1
*/
}
锁细化,锁粗化,锁消除
//锁细化
public class Test {
int count = 0;
//不应该加在方法上
synchronized void m1() throws InterruptedException {
Thread.sleep(1000); //模拟业务
count++;
Thread.sleep(1000);//模拟业务
}
//应该使用加在代码上
void m2() throws InterruptedException {
Thread.sleep(1000); //模拟业务
synchronized (this){
count++;
}
Thread.sleep(1000);//模拟业务
}
//这种优化就叫做锁的细化
}
//很多子操作需要加锁,那么直接把锁的范围扩大,这叫做锁粗化。
锁粗化
锁消除
synchronized与ReentrantLock的区别
synchronized系统自动加锁自动解锁,ReentrantLock需要手动加锁手动解锁。
底层上synchronized是四种锁升级的实现,ReentrantLock是CAS。
具体见:
https://zhuanlan.zhihu.com/p/126085068
对象的内存布局
用jol(java object layout)工具打印new Object()
import org.openjdk.jol.info.ClassLayout;
public class Test {
/*<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>*/
public static void main(String[] args) {
//加锁前
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//加锁后
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
/* 打印结果:
加锁前:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁后:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 28 f2 d9 02 (00101000 11110010 11011001 00000010) (47837736)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/
}
}
图解
- markword(8字节) + class point(4字节) 合为对象头(12字节)。
- 锁信息记录在markword。
- 对象属于哪个类,由class point指向那个类。
- instance data 成员变量的内存占用(如一个类里面有个int属性,那么这里就是4个字节,如果是new Object,那占用就是0)。
- padding,当上面的几个加起来不能被8整除时,需要此字段来对齐。之所以要对齐,这种方式cpu读起来更快。
- 一个new Object()的内存占用就是markword(8字节) + class point(4字节)+ instance data(0字节)+ padding (4字节,其他加起来只有12,得补4才能被8整除) = 16字节 ,这跟jol打印的结果一致。
扩展
-XX后面的一些指令为我们在使用java命令的默认带的一些参数。
里面的参数的作用可以百度。
主要有个-XX:+UseCompressedClasssPoointers,这个是开启压缩指针的功能,jdk 64位的,class point应该为8个字节,开启此参数,就是压缩成4字节。
-XX:+UseCompressedOops,假如有个对象里面的成员变量String指针指向一个String对象,这个指针也是应该为8,开启此参数,就是压缩成4字节。
底层实现
在JDK早期synchronized是重量级的,通过OS/内核申请实现。
后来有锁升级机制,执行时间短/竞争不激烈(线程少),为自旋锁,如果
后面执行时间长/竞争激烈(线程多,消耗CPU),那么就从自旋锁(处于用户态)升级为重量级锁(处于内核态),其中的升级条件在jdk1.6后由jvm自适应自旋;之前是有线程自旋达到10次或超过CPU核数的一半时,会升级,也可以通过-XX:PreBlockSpin设置。
由上面的对象的布局的例子可以看出,加锁后对象头发生了变化,表明锁的信息记录在markword中。
实现过程:
1.代码层面:关键字synchronized
2.字节码class层面:monitorenter monitorexit
3.锁升级
4.lock comxchg
锁升级
- 升级过程:new(无锁) -> 偏向锁(只有单个线程进行synchronized时) -> 自旋锁(轻量级锁) -> 重量级锁(操作系统申请)
- 在不同的阶段,markword的8个字节的结构如图:
- 分代年龄,对象被gc回收一次,年龄会+1,当到达一定次数后,会从年轻代到老年代。
- hashCode只能调用了对应方法后才会存在
- 锁降级在某些特定情况下会发生,在gc时(vmthread),但是在gc时降级也没意义了。