Synchronized

互斥锁的本质是-->共享资源

锁的使用

可以修饰在方法层面和代码块层面

class Test{
    synchronized void demo(){
        //临界区
    }
    //修饰代码块
    Object obj = new Object();
    void demo(){
        synchronized(obj){
            //临界区
        }
    }
    
}

锁的存储(对象头)

// 32 bits:
// --------
//       hash:25 ------------>| age:4  biased_lock:1 lock:2 (normal object)
//       JavaThread*:23 epoch:2 age:4  biased_lock:1 lock:2 (biased object)
//       size:32 ------------------------------------------>| (CMS free block)
//       PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1  age:4  biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1  age:4  biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)

锁的作用范围

- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

打印类的布局

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

96位(正常128) -->压缩以后的

查看对象头(0.9版本)

public class ClassLayoutDemo {
    public static void main(String[] args) {
        final ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
        System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
    }
}

com.example.threaddemo.base.print.ClassLayoutDemo 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)                           ba c3 00 f8 (10111010 11000011 00000000 11111000) (-134167622)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • OFFSET:偏移地址,单位字节;

  • SIZE:占用的内存大小,单位为字节;

  • TYPE DESCRIPTION: 类型描述,其中object header为对象头;

    1. object header:对象头;

    2. loss due to the next object alignment:由于对象对齐而导致的丢失(有4Byte是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0Byte)。

  • VALUE : 对应内存中当前存储的值;

  • Instance size:实例字节数值大小(此处一个空的java对象(不包含任意字段属性)实例,其实例大小为16Byte)。

大端存储和小端存储

“字节序”是个什么鬼?

public class ByteOrderTest {
    public static void main(String[] args) {
        ByteOrder byteOrder = ByteOrder.nativeOrder();
        System.out.println(byteOrder);
    }
}

// LITTLE_ENDIAN
// java 是 小端存储。低位字节在前,高位字节在后。
// 内存地址,高位 -> 低位
 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)

16进制: 0x 00 00 00 00 00 00 00 01
(64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 001 (无锁状态)
通过最后三位来看锁的状态和标记。

通过打印加锁类来查看对象头

public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    synchronized (synchronizedDemo){
        System.out.println("locking");
        System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
    }
}

输出结果(轻量级锁)最后三位为000表示轻量级锁

com.example.demo.SynchronizedDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           e8 f1 5d 03 (11101[000] 11110001 01011101 00000011) (56488424)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           54 c3 00 20 (01010100 11000011 00000000 00100000) (536920916)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

字节码指令

- javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令
- 同一时刻只能有一个线程获取到由synchronized所保护对象的监视器

原理

jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁

前提须知:

  • 对象头
  - 对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
  - java对象头是实现synchronized的锁对象基础,使用的锁对象存储在java对象头中。
  - 是轻量级锁和偏向锁的关键
  • Mark Word(标记字)
 - Mark word 用于存储对象自身的运行数据。
 - 哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
 - Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)

锁的升级

锁的级别从低到高逐步升级, 无锁(001)->偏向锁(101)->轻量级锁(00)->重量级锁(10)

偏向锁

在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设计了偏向锁。偏向锁,顾名思义,就是偏向于某个线程。

当一个线程访问了加了同步锁的代码块,会在对象头中存储当前线程的ID,后续这个线程进入和退出加了同步锁的代码块,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再进行尝试获得锁了,引入偏向锁是为了再无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。)

轻量级锁

如果偏向锁被关闭或者当前偏向锁已经被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。

重量级锁

  • 多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标被释放的时候,唤醒这些线程;
  • java线程的阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock();
  • 升级为重量锁时,所标志的状态变成“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程会进入阻塞状态

重量锁的例子

public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    Thread thread = new Thread(()->{
        synchronized(synchronizedDemo){
            System.out.println("thread locking");
        }
    });
    thread.start();
    synchronized (synchronizedDemo){
        System.out.println("locking");
        System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
    }
}

每一个java对象都会与一个监视器monitor关联,我们可以把它理解为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程要先得到synchronized修饰的对象对应的monitor。

monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他的被阻塞的线程可以尝试去获得这个监视器。

monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响锁的性能

任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变成BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。
为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:
   1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
   2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,
     把该线程当做自己的熟人。如下图第二种情形。
   3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。
   4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会通过一个对象内部的监视器(monitor)来登记和管理排队的线程,
     其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。如下图第四种情形。

img

总结

  • 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块是,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。

  • 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的LockRecord,该字段存储锁对象原本的标记字段,他针对的是多个线程在不同时间段内申请同一把锁的情况。

  • 重量级锁会阻塞和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。

monitor

  我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象在hotspot虚拟机中,
通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多

扩展:

自旋锁(CAS)

  自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。
  虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,
如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起

wait和notify

  调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,
并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

题外话:

wait和notify为什么需要在synchronized里面?

  wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
  对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

调用synchronized和wait是否进入相同队列?

synchronized实际上有两个队列WaitSet和entryList
1.如果有多个线程进入同步代码块时候,首先进入entryList
2.若有一个线程获取到monitor锁之后,就赋值给当前线程,计数器+1
3.如果线程调用了wait方法之后,会释放锁,当前线程置为null,计数器-1,同时进入WaitSet等待被唤醒,调用notify或者notifyAll之后,又会进入entryList队列当中竞争锁。
4.如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
posted @ 2019-10-29 21:48  snail灬  阅读(262)  评论(0编辑  收藏  举报