【并发编程JUC】 synchronized 简谈

1. 线程状态转换

java并发编程状态基础,和锁没什么关系,但是很重要,就放这里吧。
在这里插入图片描述

2. synchronized 关键字

2.1 通过 ReentrantLock 查看锁的代码层原理

因为synchronized底层是由c/c++来实现的,不容易查看,所以先用 reentrantLock来看看锁住一个对象的时候究竟是怎样实现线程互斥的。

        
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 业务代码
Thread.sleep(1000);
lock.unlock();

lock()方法是进行加锁操作,我们去看lock()的源码可以看到以下代码(以公平锁为例)。

// 第一步
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {    	//============ 判断当前锁的状态
        if (!hasQueuedPredecessors() &&      //============ 判断是否需要排队
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  1. 加锁成功时,需要判断当前锁的状态 c == 0, 并且不需要排队!hasQueuedPredecessors(),就会使用 CAS改变 c值, 然后返回 true,则acquire()函数正常返回。
  2. 加锁失败(比如锁被其他线程持有), 此时 c!=0, 会先判断是否是当前线程持有锁current == getExclusiveOwnerThread(), 如果不是直接返回 false,返回false之后,acquire()会在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))调用LockSupport.park(this);方法挂起线程。

所以总结锁的本质:

  1. 加锁成功 ---- lock()正常返回
  2. 加锁失败 ---- lock()阻塞
  3. 加锁的本质就是改变一个变量的值

2.2 synchronized锁原理

2.2.1 问题引起

reentrantLock可知,锁的本质就是改变一个变量的值,对于synchronized而言,这个变量就是存储在对象头信息中。

在工程中引起openjdk打印出对象的头信息。

引入下面依赖:

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.8</version>
</dependency>

以下面对象为例子:

public class Tiger {
    boolean name;
}

打印对象头:

Tiger tiger = new Tiger();
System.out.println(ClassLayout.parseInstance(tiger).toPrintable());

对象头信息如下:

在这里插入图片描述

可以看到一个new一个对象时,在堆先会放12个字节的对象头信息(object header),然后才是对象的成员变量所占字节数,最后是java内存对齐相关的东西,一个java对象所在的字节大小必须是8的倍数。

2.2.2 对象头

java的对象头(object header)分为两个部分,前8个字节表示标记字段Mark Word,后面4个字节表示类型指针Klass Point。其中Klass Point是对象指向它所属类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,锁标记就存在Mark Word中,下面看一下Mark Word每个bit在不同状态下的含义。

在这里插入图片描述

  1. 一个对象不做任何处理,就是上图的无锁可偏向状态

  2. 一个对象如果计算了hashcode之后就会进入到无锁不可偏向状态,因为前面那些位需要存储hashcode无法再存储需要偏向的线程 ID. 注意 lombok.Data注解重写了hashcode,hashcode将不会存在对象头中,因此计算了hashcode之后还是可偏向状态。

    • 没有@Data注解
    @NoArgsConstructor
    //@Data
    public class Tiger {
        boolean name;
        char ch;
        int age;
    }
    
    
    Tiger tiger = new Tiger();
    System.out.println(ClassLayout.parseInstance(tiger).toPrintable());
    
    System.out.println("Hashcode: " + Integer.toHexString(tiger.hashCode()));
    System.out.println(ClassLayout.parseInstance(tiger).toPrintable());
    
    synchronized (tiger){
       System.out.println(Thread.currentThread().getName());
       System.out.println(ClassLayout.parseInstance(tiger).toPrintable());
    }
    

运行结果:
在这里插入图片描述

计算了hashcode之后变成不可偏向状态,加锁时直接变成轻量锁。

  • @Data注解

代码相同,就是加上@Data注解,可以看到hashcode并没有存在对象头中,对象锁也是可偏向的,因此加锁时是偏向锁。

在这里插入图片描述

  1. 锁的膨胀过程

在这里插入图片描述

2.2.3 jvm 参数

jvm打印所有的 jvm 参数:-XX:+PrintFlagsInitial

jvm偏向锁延迟时间:-XX:BiasedLockingStartupDelay=0, 默认是4000ms

jvm 轻量锁默认自旋次数 -XX:PreInflateSpin=10. – 自旋10次。轻量锁自旋到指定次数如果还拿不到锁就会膨胀为重量锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值