synchronized详解

synchronized介绍

由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可 。

synchronized 方法synchronized 代码块

同步方法 : public synchronized void method(int args) {} 

synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 , 方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行。

同步块 : synchronized (Obj ) { }

Obj 称之为 同步监视器

  • Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class

存在的问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起 ;
  • 在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题 ;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置 , 引起性能问题 ;
  • 若将一个大的方法申明为synchronized 将会影响效率

锁的本质

如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。
synchronized详解

  • 锁是一个“对象”,作用如下?
  1. 这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
  2. 如果这个对象被某个线程占用,记录这个线程的thread ID。
  3. 这个对象维护一个thread id list,记录其他所有阻塞的、等待获取拿这个锁的线程。在当前线程释放锁之后从这个thread id list里面取一个线程唤醒。
  • 锁如何实现?

在对象头里,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异。

对象锁和类锁

  • 对象锁和类锁锁的不是同一个锁
  • synchronized修饰在非静态方法上和synchronized(this){} 同步代码块效果是一样的
  • synchronized修饰在静态方法上和 synchronized (SyncTest1.class) {} 同步代码块效果是一样的
  • synchronized修饰在非静态方法表示锁的是当前对象,修饰静态方法表示锁的是类对象(一个类在jvm中只有一个class对象)
public class TestSynchronized {

    // 对象锁   ===   synchronized(this){}
    public synchronized void test1() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    // 类锁   ===    synchronized(TestSynchronized.class){}
    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

运行结果:
test2 : 4
test1 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test1 : 1
test2 : 1
test1 : 0
test2 : 0

Process finished with exit code 0

  • synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁

  • 非静态方法:给对象加锁(可以理解为给这个对象的内存上锁,注意 只是这块内存,其他同类对象都会有各自的内存锁),这时候在其他一个以上线程中执行该对象的这个同步方法(注意:是该对象)就会产生互斥。

  • 静态方法: 相当于在类上加锁(*.class 位于代码区,静态方法位于静态区域,这个类产生的对象公用这个静态方法,所以这块内存,N个对象来竞争), 这时候,只要是这个类产生的对象,在调用这个静态方法时都会产生互斥。

锁升级

锁的状态总共有四种,级别由低到高依次为:无锁偏向锁轻量级锁重量级锁。在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),目的是为了提高获得锁和释放锁的效率。

Synchronized实现原理:
已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。

  • 对象头主要是由**MarkWord(对象标记)Klass Point(类元信息)**组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)
  • 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
  • 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的
    synchronized详解Synchronized锁对象是存在锁对象的对象头的MarkWord中,MarkWord在64位的虚拟机中的结构:
    synchronized详解

实现原理:
线程的生命周期存在5个状态,startrunningwaitingblockingdead
synchronized详解
对于一个synchronized修饰的方法(代码块)来说:

  • 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态;
  • 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取;
  • 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner区;
  • 如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1;

Synchronized修饰的代码块/方法如何获取monitor对象?

1.Synchronized修饰代码块:
Synchronized代码块同步在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。

同步代码块如下:

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

通过javap对class字节码文件反编译可以得到反编译后的代码:

public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=3, locals=3, args_size=1
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter  //注意此处,进入同步方法
     4: aload_0
     5: dup
     6: getfield      #2             // Field i:I
     9: iconst_1
    10: iadd
    11: putfield      #2            // Field i:I
    14: aload_1
    15: monitorexit   //注意此处,退出同步方法
    16: goto          24
    19: astore_2
    20: aload_1
    21: monitorexit //注意此处,退出同步方法
    22: aload_2
    23: athrow
    24: return
  Exception table

可以看出同步方法块在进入代码块时插入了monitorentry语句,在退出代码块时插入了monitorexit语句,为了保证不论是正常执行完毕(第15行)还是异常跳出代码块(第21行)都能执行monitorexit语句,因此会出现两句monitorexit语句。

2.Synchronized修饰方法:

Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。
同步方法代码如下:

public class SyncMethod {
   public int i;
   public synchronized void syncTask(){
           i++;
   }
}
public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}

可以看出方法开始和结束的地方都没有出现monitorentry和monitorexit指令,但是出现的ACC_SYNCHRONIZED标志位。

锁升级转换:
synchronized详解
synchronized详解

上一篇:synchronized 中的 4 个优化,你知道几个?


下一篇:java保留字