synchroniezd 关键字的使用方法和底层原理

一、对synchronized的理解

synchronized,是java的关键字,使用它可以来修饰对象实例、代码块、方法和类,使其保证再任意时刻只能被一个线程访问。

二、使用方法

synchronized的最主要的三种使用方法
(1) 修饰同步代码块: :synchronized(this)或者synchronized(类实例对象),相当于给当前对象实例加锁
(2) 修饰普通(实例)方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
(3) 修饰静态方法: 作用于类所有对象实例,相当于给类加锁 ,进入代码块前要获得当前类对象的锁

修饰同步代码块:
public class SyncObjectTest implements Runnable{
  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    if (threadName.startsWith("A")) {
      syncObbject();
    }
  }

  /**
   * 修饰同步代码块
   */
  private  void syncObbject() {
    System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObbject" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
    synchronized (this) {
      try {
        System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObbject Start" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        Thread.sleep(1000);
        System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObbject End" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args) {
    SyncObjectTest syncObject = new SyncObjectTest();
    Thread A_Thread1 = new Thread(syncObject, "A_Thread1");
    Thread A_Thread2 = new Thread(syncObject, "A_Thread2");
    A_Thread1.start();
    A_Thread2.start();
  }
}

执行结果:
在这里插入图片描述
分析:线程A_Thread1 和A_Thread2 初始化的是同一个实例对象
(1)可以看到,A_Thread1先进入到syncObbject方法中,并开始执行同步代码块里的代码
(2)B_Thread2也进入到syncObbject中,但是没有执行同步代码块,进入阻塞状态,等待线程A_Thread1 执行完毕才去执行的同步代码块。
结论:synchronized 修饰同步代码块时,是对象锁

修饰实例方法:
public class SyncObjectTest implements Runnable{
  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    if (threadName.startsWith("A")) {
      //syncObbject();
    } else if(threadName.startsWith("B")) {
      syncObbjectMehthod();
    }
  }


  /**
   * 修饰实例方法
   */
  private synchronized void syncObbjectMehthod() {
    System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObjectMethod" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
    try {
      System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObjectMethod Start" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
      Thread.sleep(1000);
      System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncObjectMethod End" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
    }catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    SyncObjectTest syncObject = new SyncObjectTest();
    Thread A_Thread1 = new Thread(syncObject, "A_Thread1");
    Thread A_Thread2 = new Thread(syncObject, "A_Thread2");
    Thread B_Thread1 = new Thread(syncObject, "B_Thread1");
    Thread B_Thread2 = new Thread(syncObject, "B_Thread2");
//    A_Thread1.start();
//    A_Thread2.start();
    B_Thread1.start();
    B_Thread2.start();
  }
}

执行结果:
在这里插入图片描述
分析:
(1)线程B_Thread1 先进入到同步实例方法中,并开始执行方法中的代码
(2)线程B_Thread2 一开始进入阻塞状态,等待线程B_Thread1执行完毕后释放锁,才开始执行方法中的代码
结论:synchronized 修饰实例方法时,是对象锁

当线程去同时去执行同步代码块和实例方法时:

  public static void main(String[] args) {
    SyncObjectTest syncObject = new SyncObjectTest();
    Thread A_Thread1 = new Thread(syncObject, "A_Thread1");
    Thread A_Thread2 = new Thread(syncObject, "A_Thread2");
    Thread B_Thread1 = new Thread(syncObject, "B_Thread1");
    Thread B_Thread2 = new Thread(syncObject, "B_Thread2");
    A_Thread1.start();
    A_Thread2.start();
    B_Thread1.start();
    B_Thread2.start();
  }

执行结果:
在这里插入图片描述
分析:
(1)线程A_Thread2 和A_Thread1 先进入到方法中
(2)线程B_Thread1 先获得对象锁,然后等待B_Thread1执行完成释放锁后,A_Thread1 获得对象锁执行同步代码块中的代码,此时A_Thread2进入进入阻塞状态
(3)线程A_Thread2等待A_Thread1 执行完毕后,执行同步代码块中的的代码
(4)A_Thread2执行完毕释放锁后,线程B_Thread2获得到锁,执行同步实例方法中的代码
结论:当synchronized修饰同步代码块和实例方法时,是获得的同一种锁,是互斥锁。

修饰静态方法:

public class SyncObjectTest implements Runnable{
  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    if (threadName.startsWith("A")) {
      syncObbject();
    } else if(threadName.startsWith("B")) {
      syncObbjectMehthod();
    } else if (threadName.startsWith("C")) {
      syncStaticMethod();
    }
  }
  /**
   * 修饰静态方法
   */
  private synchronized  static void syncStaticMethod() {
    System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncStaticMethod" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
    try {
      System.out.println("ThreadName:" + Thread.currentThread().getName() + "_syncStaticMethod Start" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
      Thread.sleep(1000);
      System.out.println("ThreadName:" + Thread.currentThread().getName() + "_ssyncStaticMethod End" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
    }catch (Exception e) {
      e.printStackTrace();
    }

  }

  public static void main(String[] args) {
    Thread C_Thread1 = new Thread(new SyncObjectTest(), "C_Thread1");
    Thread C_Thread2 = new Thread(new SyncObjectTest(), "C_Thread2");
    C_Thread1.start();
    C_Thread2.start();
  }
}

执行结果:
在这里插入图片描述
分析:线程C_Thread1 和C_Thread2 初始化的是同一个类的不同实例对象
(1)线程C_Thread1先获得锁,C_Thread2等待C_Thread1执行完毕后才执行的静态方法中的代码。
结论:当synchronized修饰静态方法时,是类锁

注意:
(2)类锁其实是一把特殊的对象锁。由于同一个类只有一把对象锁,所以当同一个类的不同实例对象使用类锁时,也是同步的,是互斥的。
(2)当同一个实例对象使用对象锁和类锁时,没有互斥性(可以用上面的例子试一下)

用synchronized实现的线程安全的单例模式:
package com.tuniu.htl.ctrip.controller;

public class SingletonTest {

  private static volatile  SingletonTest singletonTest;

  private static SingletonTest getSingletonTest() {
    //先判断对象是否已经实例过,没有实例化过才进入加锁代码
    if (singletonTest == null) {
      synchronized (SingletonTest.class) {
        if (singletonTest == null) {
          singletonTest = new SingletonTest();
        }
      }
    }
    return singletonTest;
  }
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance =memory;     //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

memory =allocate();    //1:分配对象的内存空间 
instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);  //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

创建一个对象的过程:
https://www.cnblogs.com/happystudyhuan/p/10705221.html

三、synchronized的底层原理

3.1 synchronized 底层实现

Java对象再内存中的布局:

  • 对象头
  • 实例数据
  • 对齐填充

一般而言,synchronized 使用的锁对象是存储在Java对象头中的,其主要结构是Mark Word和Class Metadata Address
在这里插入图片描述

jdk1.6之后的Mark Word:

在这里插入图片描述

监视器锁:monitor

每个Java对象天生就自带了一把锁moniror,叫做内部锁或者监视器锁。
monitor 是用java虚拟机中用c++实现的ObjectMonitor()
在这里插入图片描述

monitor锁的竞争、获取和释放:

在这里插入图片描述

3.2下面根据对象锁和类锁分别看一下底层实现:
3.2.1 synchronized 修饰同步代码块的情况
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
在这里插入图片描述
从上面可以看出:
(1)synchronized的对象锁其实是一种重量级锁。
(2)synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
(3)当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

3.2.2 synchronized 修饰方法的情况
public class SynchronizedDemo2 {
    public synchronized void syncTask() {
        System.out.println("Hello Angin");
    }
}

在这里插入图片描述
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

四、jdk1.6之后对synchronized的底层优化
先说一下1.6之前synchronized的缺点:

(1)早期版本,synchronized属于重量级锁,Monitor依赖于底层的Mutex Lock实现,效率低下。
(2)线程之间的状态切换需用从用户态变成核心态,开销较大,时间成本较高。

1.6之后的优化,减少了对重量级锁的使用:

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
分别讲一下这几种锁:
(1)自旋锁
许多情况下,共享数据的锁定状态尺寸时间很多,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。我们为了让另外一个线程不挂起或者进入阻塞态,我们只需要让线程执行一个忙循环(自旋),不放弃cpu的执行时间。这项技术就叫做自旋。
缺点:如果一个锁被其他线程占用的时间很长,会带来很多性能上的花销
(2)自适应自旋锁,jdk1.6之后引入
① 自旋的次数不在固定
②有前一次在同一个锁上的自旋时间和状态决定,如果在上一次获得同一锁对象时自旋等待的时间较短,并且持有锁的线程正在运行当中,我们就认为该锁自旋时获取到锁的可能性很大,就会增加自旋次数或者自旋等待时间
(3) 锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
(4) 锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。所以。要加大加锁的范围。

synchronized锁的四种状态:

无锁、偏向锁、轻量级锁、重量级锁。他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁膨胀(升级)的方向:
无锁 -》偏向锁-》轻量级锁-》重量级锁
(1)偏向锁
在大多数情况下,锁不存在多线程竞争,总是由同一个线程获得。
核心思想:
如果一个线程获取到锁,那么锁进入偏向模式,其实Mark Word的结构也进入到偏向锁结构。当线程再次请求获得该锁时,
无需进行任何同步操作,即获得锁的过程只需要检查Mark Word的标记为偏向锁,并且当前线程id等于Mark Word的ThreadId,这样省去了大量申请锁的时间。

(2)轻量级锁

①倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。偏向锁运行在一个线程进入同步代码块的情形,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
②轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。

适应的场景:线程交替执行同步块

轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!

偏向锁、轻量级锁、重量级锁的汇总
在这里插入图片描述

五、锁的内存语义

当线程释放锁时,java内存模型会把该线程对应的的本地内存的共享变量刷新到主内存中去。
当线程获取锁时,java内存模型会把该线程的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中获取共享变量
图解:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值