Java 基础 —— synchronized 关键字详解

一、synchronized 三大特性

在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而 synchronized 关键字则是用来保证线程同步的。

synchronized 关键字可以保证并发编程的三大特性:原子性、可见性、有序性。而 volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的 synchronized。

(1)原子性:一个或多个操作要么全部执行成功,要么全部执行失败。synchronized 关键字可以保证只有一个线程拿到锁,访问共享资源
(2)可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。可见性是通过Java内存模型中的 “对一个变量 unlock 操作之前,必须要同步到主内存中如果对一个变量进行 lock 操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中 load 操作或 assign 操作初始化变量值” 来保证的;
(3)有序性:程序的执行顺序会按照代码的先后顺序执行。有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。

二、synchronized 原理

1、Java 对象头

synchronized 实际是作用在对象上的,那锁的实现肯定也与对象在内存中的存储有关系。
在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
在这里插入图片描述

(1)实例数据:存放类的属性数据信息,包括父类的属性信息;
(2)对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
(3)对象头:Java 对象头一般占有 2 个机器码(在 32 位虚拟机中,1 个机器码等于 4 字节,也就是 32bit,在 64 位虚拟机中,1 个机器码是 8 个字节,也就是 64bit),但是如果对象是数组类型,则需要 3个 机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Synchronized 用的锁就是存在 Java 对象头里的,那么什么是 Java 对象头呢?Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下:
在这里插入图片描述
Mark Word 用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
在这里插入图片描述

2、对象头中 Mark Word 与线程中 Lock Record

在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是 01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的 Mark Word 的拷贝,官方把这个拷贝称为 Displaced Mark Word。

Lock Record 是线程私有的数据结构,每一个线程都有一个可用 Lock Record 列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的 MarkWord 中的 Lock Word 指向Lock Record的起始地址),同时 Lock Record 中有一个 Owner 字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:
在这里插入图片描述

3、监视器(Monitor)

任何一个对象都有一个 Monitor 与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。Synchronized 在 JVM 里的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

(1)MonitorEnter 指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁,过程如下:

1)如果 monitor 的进入数为0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者;
2)如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1;
如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权;

(2)MonitorExit 指令:插入在方法结束处和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit;

执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。

所有的Java对象是天生的 Monitor,每一个 Java 对象都有成为Monitor的潜质,因为在 Java 的设计中 ,每一个 Java 对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者 Monitor 锁。

Monitor 对象存在于每个 Java 对象的对象头 Mark Word 中(存储的指针的指向), Synchronized 的语义底层是通过一个 monitor 的对象来完成,也是为什么 Java 中任意对象可以作为锁的原因。同时 wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。

三、synchronized 的使用

synchronized 关键字可以实现什么类型的锁?

(1)悲观锁:synchronized 关键字实现的是悲观锁,每次访问共享资源时都会上锁。
(2)非公平锁:synchronized 关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
(3)可重入锁:synchronized 关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
(4)独占锁或者排他锁:synchronized 关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

synchronized 的锁粒度

synchronized 的锁可作用于 Java 方法,或者是一个代码块。无论何种用法,所起到的作用仅限于类锁/对象锁。 Java 的 synchronized 锁的是对象,也只锁对象。
synchronized从锁的是谁的维度一共有两种情况:

(1)锁住对象实例:对象锁是基于对堆内存内对象的头部加锁信息,每一个对象锁都是不同的锁。特别的,synchronized(this) 是对 this 所对应的对象加锁。

1)synchronized 修饰一个使用了 static 关键字修饰的方法;
2)当 synchronized 修饰一个 xxx.class 的代码块;

(2)锁住类:类锁是基于对类对应的 java.lang.Class 加锁信息,锁的是该类的所有实例对象,该类的所有对象都使用同一把类锁

1)synchronized 修饰 this 的代码块;
2)synchronized 修饰一个对象 xxx 的代码块;
3)synchronized 修饰一个不使用 static 关键字修饰的方法

synchronized 的用法有四种:

(1)修饰一个代码块: 一个线程正在访问一个对象中的 synchronized(this 或 其他对象) 同步代码块时,其他试图访问该对象(要是同一个对象)的线程将被阻塞。
(2)修饰一个普通方法: 在方法的前面加 synchronized,public synchronized void method(),此方法等于修饰整个方法代码块。
(3)修饰一个静态的方法:public synchronized static void method(),静态方法是属于类的,同样的,synchronized 修饰的静态方法锁定的是这个类的所有对象。
(4)修饰一个类:synchronized(ClassName.class),synchronized 作用于一个类时,是给这个类加锁,该类的所有对象都将加同一把锁。

1、修饰非静态的代码块

public void run(){
    
    

	synchronized(获取所的地方){
    
    
		被锁住的代码块
	}
}

这个获取锁的地方是什么呢?我们可以这么理解:

前面的 synchronized 原理中讲到,每个 Java 对象的对象头中都存放关于锁的信息,每一个 Java 对象都可以成为一把锁。synchronized() 的参数可以是某个 Java 对象也可以是某个类 xxx.class:如果是 Java 对象,那么这个 Java 对象成为一把锁,如果是类 xxx.class,则这个类的所有对象都是锁而且是同一把锁。这把锁就把 synchronized 修饰的代码块给锁住,获取锁的地方和被锁住的代码块是不必需要有任何关联的。当线程要执行这段代码块时就必须获得锁才可以,未获得锁的线程则被阻塞。

(1)获取锁的地方为 this:对象锁

 class syncTest implements Runnable {
    
    static int i = 0;   //共享资源@Override
     public void run() {
    
    
         //其他操作.......
         synchronized (this){
    
       //this 表示当前对象实例
             for (int j = 0; j < 10000; j++) {
    
    
                 i++;
             }
         }}

this 所代表的意思是该代码块所在类的对象实例。若通过类 syncTest 创建不同对象,则这些对象锁拥有的锁都是各个不同对象,也就是各自不同的锁

(2)获取锁的地方为对象实例:对象锁

public class test{
    
    

    public static void main(String[] args) throws InterruptedException {
    
    

        Object lock = new Object();		// 对象锁
        
        Thread A = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (lock) {
    
    
                    System.out.println("A 1");
                    try {
    
    
                        lock.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println("A 2");
                    System.out.println("A 3");
                }
            }
        });
        Thread B = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (lock) {
    
    
                    System.out.println("B 1");
                    System.out.println("B 2");
                    lock.notify();
                    System.out.println("B 3");
                }
            }
        });

        A.start();
        B.start();
    }
}

所有线程用的锁都是同一个锁 lock,因此这种方式是多个线程共享了同一把对象锁。

(3)获取锁的地方为 xxx.class:共享类锁

 class syncTest implements Runnable {
    
    static int i = 0;   //共享资源@Override
     public void run() {
    
    
         //其他操作.......
         synchronized (syncTest.class){
    
       // 使用syncTest.class,表示class类锁
             for (int j = 0; j < 10000; j++) {
    
    
                 i++;
             }
         }}

xxx.class 所代表的意思是某个类,若通过类 syncTest 创建不同对象,则这些对象锁拥有的锁都是类锁,也就是都拥有同样的锁

试看以下代码:

public class AccountingSync implements Runnable{
    
    
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    @Override
    public void run() {
    
    
    	synchronized(this){
    
    
        for(int j=0;j<10000;j++){
    
    
            i++;
        }
       }
    }
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AccountingSync instance1 = new AccountingSync();
        AccountingSync instance2 = new AccountingSync();
        Thread t1=new Thread(instance1);
        Thread t2=new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t1 线程执行结束再去执行
        t2.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t2 线程执行结束再去执行
        System.out.println(i);    // 必须等 t1 和 t2 线程执行完再执行该行代码,不然输出错误。
    }
}

输出结果是 11006,而非 20000。

示例中虽然使用 synchronized 关键字修饰了,但是 synchronized() 的参数是 this,因此两次 new AccountingSync() 操作建立的是两个不同的对象,也就是说存在两个不同的对象锁,线程 t1 和 t2 使用的是不同的对象锁,所以不能保证线程安全。

正确示例1:

public class AccountingSync implements Runnable{
    
    
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    @Override
    public void run() {
    
    
    	synchronized(this){
    
    
        for(int j=0;j<10000;j++){
    
    
            i++;
        }
       }
    }
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AccountingSync instance1 = new AccountingSync();
        // AccountingSync instance2 = new AccountingSync();
        Thread t1=new Thread(instance1);
        Thread t2=new Thread(instance1);
        t1.start();
        t2.start();
        t1.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t1 线程执行结束再去执行
        t2.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t2 线程执行结束再去执行
        System.out.println(i);    // 必须等 t1 和 t2 线程执行完再执行该行代码,不然输出错误。
    }
}

上述两个线程的 target 是同一个对象,所以持有同一把锁,所以能够实现线程安全。

正确示例2:

public class AccountingSync implements Runnable{
    
    
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    @Override
    public void run() {
    
    
    	synchronized(AccountingSync.class){
    
    
        for(int j=0;j<10000;j++){
    
    
            i++;
        }
       }
    }
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AccountingSync instance1 = new AccountingSync();
        AccountingSync instance2 = new AccountingSync();
        Thread t1=new Thread(instance1);
        Thread t2=new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t1 线程执行结束再去执行
        t2.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t2 线程执行结束再去执行
        System.out.println(i);    // 必须等 t1 和 t2 线程执行完再执行该行代码,不然输出错误。
    }
}

示例中 synchronized() 的参数是 xxx.class,锁就是 xxx.class 类的所有实例对象,因此两次 new AccountingSync() 操作建立的两个不同的对象共享同一把类锁,线程 t1 和 t2 使用的是同一把类锁,所以能保证线程安全。

3、修饰普通方法:对象锁

public synchronized void staticA(){
    
    

}

这种方式相当于

public void run(){
    
    

	synchronized(this){
    
    
		...
	}
}

3、修饰静态方法:类锁

public static synchronized void staticA(){
    
    

}

当 synchronized 作用于静态方法,锁就是当前的 class 类的所有实例对象,所有线程都共用同一把类锁

示例:

 class syncTest implements Runnable {
    
    private static int i = 0;   //共享资源private static synchronized void add() {
    
    
         i++;
     }@Override
     public void run() {
    
    
         for (int j = 0; j < 10000; j++) {
    
    
             add();
         }
     }public static void main(String[] args) throws Exception {
    
    
      ​ ​
         Thread t1 = new Thread(new syncTest());
         Thread t2 = new Thread(new syncTest());​
         t1.start();
         t2.start();
         t1.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t1 线程执行结束再去执行
        t2.join();    // 阻塞主线程,让主线程(main函数)必须等待至 t2 线程执行结束再去执行
         System.out.println(i);    // 必须等 t1 和 t2 线程执行完再执行该行代码,不然输出错误。
     }
 }

虽然 t1 和 t2 使用不同的对象作为锁,但是这些对象锁都是同一把类锁,所以能够实现线程安全。

四、synchronized 的优化

Guess you like

Origin blog.csdn.net/IT__learning/article/details/121115655