深入理解synchronized(一)——初识synchronized

486 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

synchronized

synchronized是java中的一个关键字,简单来说,synchronized关键字以同步方法和同步代码块的方式,为方法和代码块上的对象加锁。使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行,本文将介绍为什么要加锁以及synchronized的用法以及使用上的区别。

为什么要加锁?

简单的说,加锁是为了避免多线程下并发冲突,锁是一种数据保护机制,可允许某一个线程(进程)进行操作锁,当文件锁上时,其他线程(进程)根据锁的性质(读写锁,阻塞非阻塞)。下面我们通过代码问题说明不加锁时存在的问题。 类SyncClass具有一个类变量elem=0,方法printElem(int count)与addElem(int count)方法使得分别用来对elem打印或执行加1 count次。

public static void testNoLock(int count) {
    Thread printThread = new Thread(new Runnable() {
        @Override
        public void run() {
            printElem(count);
        }
    });
    Thread addThread = new Thread(new Runnable() {
        @Override
        public void run() {
            addElemCount(count);
        }
    });
    printThread.start();
    addThread.start();
}

启动了两个线程分别执行打印与加1操作10次,结果如下,可以看到printThread线程打印的elem在第二次打印时已经被addThread线程所修改,如果要保证printThread打印时的elem都是相等不变的,我们需要加锁使得printThread打印elem时,资源elem不被其他线程所获取。

elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10

synchronized的作用

synchronized的下面三个作用:

  • 原子性:一个事务的一个或多个操作在执行时,要么全部执行成功、要么执行失败。 被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得
  • 可见性:在多线程环境下,该资源的状态、值信息等对其他线程都是可见的。synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。
  • 有序性:程序执行的顺序按代码先后执行。synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。 另外synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

synchronized的用法

synchronized可以修饰静态方法、成员函数,同时还可以直接定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是。其用法有以下三种:

  1. 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。例如synchronized void method(int count).
  2. 修饰静态方法:因为静态方法属于类成员,因此synchronized是给当前类加锁,会作用于类的所有对象实例,进入同步代码块要获得当前class的锁。例如static synchronized void method(int count).
  3. 修饰代码块:可以指定加锁的对象,对给定对象或类加锁synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

下面我们通过printElem(int count)与addElem(int count)方法通过synchronized使用使得在线程情况下保证printElem(int count)打印的不受addElem(int count)所影响。

修饰实例方式

如下使用synchronize修饰printElem(int count)与addElem(int count)后,首先我们创建一个对象实例,然后开启两个线程进行调用:

/**
 * 使用synchronized修饰实例方法addElem与printElem
 */
public class SyncClass {

    public static int elem = 0;

    public static void main(String[] args) {
        SyncClass syncClass = new SyncClass();
        Thread printThread = new Thread(new Runnable() {
            @Override
            public void run() {
                syncClass.printElem(10);
            }
        });
        Thread addThread = new Thread(new Runnable() {
            @Override
            public void run() {
                syncClass.addElemCount(10);
            }
        });
        printThread.start();
        addThread.start();
    }

    // 对elem加1 count 次 饰静态方法
    public synchronized void addElemCount(int count) {
        for (int i = 0; i < count; i++) {
            elem++;
            System.out.println("elem 加1");
        }
    }

    // 打印elem 10次。 synchronized修饰实例方法
    public synchronized void printElem(int count) {
        for (int i = 0; i < count; i++) {
            System.out.println("elem is " + elem);
        }
    }
}

执行结果如下,由于synchronized对printElem与addElem修饰后,在执行printElem时,会对syncClass的实例对象加锁,当addElem执行时会去获取syncClass的实例对象信息(该对象的状态信息对所有线程是可见的),发现该对象已被加锁,此时会等待对象锁释放,因此addElem会在printElem执行完成后再执行。

elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1

修饰静态方法

如下代码,通过synchroized修饰静态方法,其执行结果与上面修饰实例方法结果一致。

/**
 * synchronized修饰静态方法addElem与printElem
 */
// 对elem加1 count 次 饰静态方法
    public synchronized static void addElem(int count) {
        for (int i = 0; i < count; i++) {
            elem++;
            System.out.println("elem 加1");
        }
    }

    // 打印elem 10次。 synchronized修饰实例方法
    public synchronized static void printElem(int count) {
        for (int i = 0; i < count; i++) {
            System.out.println("elem is " + elem);
        }
    }

修饰代码块

如下synchronized也可以修饰代码块通过指定加锁对象,达到与修饰实例方法或静态方法的类似的效果。

public static void addElem(int count) {
    synchronized (SyncStatic.class){
        for (int i = 0; i < count; i++) {
            elem++;
            System.out.println("elem 加1");
        }
    }

}

synchronized修饰静态方法与修饰实例方法的区别

上面说到synchronized修饰静态方法是对当前类加锁,而修饰实例方式是对类的实例对象加锁,许多同学不是很理解这点,下面我们通过实例来说明二者的区别。首先我们需要了解下对象锁信息是是如何存储的。

对象头信息

对象头信息里保存了对象的加锁信息, java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、gc标记状态。对象头由Mark word和class pointer两部分组成,如果是数组,还包括数组长度.

image.png 而Mark Word中包含标记位lock与biased_lock,其表示含义如下:

9e0662a9093f1da134201e7203f2150f33d8bf29.png

下面我们看看一个未加锁的对象与加锁后其标记位的状态(引入jar包org.openjdk.jol:jol-core,使用ClassLayout打印对象头信息):

SyncClass syncClass = new SyncClass();
// 打印未加锁的对象头信息
System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
synchronized (syncClass){
    // 打印加锁后的对象头信息
    System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
}

输出结果如下,可对照上图可知,未加锁时biased_lock与lock为001表示无锁状态,而使用synchronized给对象加锁后变为000状态,说明对象syncClass上持有轻量级锁。

image.png

synchronized修饰静态方法VS修饰实例方法

我们实现一个类SyncClass提供以下两个方法testMethod1与testMethod2,分别为synchronized修饰实例方法与修饰静态方法,为了观察SyncClass实例对象与SyncClass类对象的状态,我们在这两个方法中死循环等待。

/**
 * 使用synchronized修饰实例方法与静态方法
 */
public class SyncClass {

    public static int elem = 0;

    public static void main(String[] args) {
        SyncClass syncClass = new SyncClass();
        // 启动testMethod1或testMethod2
        Thread thread= new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                syncClass.testMethod1();
            }
        });
        thread.start();
        System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
        System.out.println(ClassLayout.parseInstance(SyncClass.class).toPrintable());
    }

    //  synchronized修饰实例方法
    public synchronized void testMethod1() throws InterruptedException {
        while (true) {
            Thread.sleep(1000);
        }
    }

    // synchronized修饰静态方法
    public static synchronized void testMethod2() throws InterruptedException {
        while (true) {
            Thread.sleep(1000);
        }
    }
}

执行修饰实例方法的结果如下,可以看到synchronized修饰实例方法时,实例对象syncClass的锁标记位为000,实例对象上被加上了轻量级锁。而类对象上无锁。

image.png

执行修饰静态方法的结果如下,可以看到synchronized修饰静态方法时,实例对象syncClass的锁标记位为001,表示无所,而类对象为010,表示类对象被加上了重量级锁(涉及到锁膨胀的问题,有兴趣的同学可自行了解下)。

image.png

故synchronized修饰实例方法与修饰静态方法区别如下:

  1. synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
  2. synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

结语

本文简单介绍了synchronized的用法,以及作用在实例方法与类方法上的区别,至于synchronized实现锁的原理,我们下篇文章再见。