同期の深い理解(1)-同期の最初の理解

一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して8日目です。クリックしてイベントの詳細をご覧ください

同期

Synchronizedは、Javaのキーワードです。つまり、synchronizedキーワードは、メソッドとコードブロックを同期することにより、メソッドとコードブロックのオブジェクトをロックします。同時に、このオブジェクトの複数のスレッドは、このオブジェクトのロックを保持する単一のスレッドによってのみ呼び出しおよび実行できます。この記事では、ロックの理由と同期の使用法、および使用法の違いを紹介します。

なぜそれをロックするのですか?

簡単に言うと、ロックとは、複数のスレッドでの同時実行の競合を回避することです。ロックは、スレッド(プロセス)がロックを操作できるようにするデータ保護メカニズムです。ファイルがロックされると、他のスレッド(プロセス)はロックの性質に基づきます。 。(読み取り/書き込みロック、非ブロックのブロック)。以下に、コードの問題によるロックがない場合に存在する問題について説明します。クラスSyncClassにはクラス変数elem=0があり、メソッドprintElem(int count)およびaddElem(int count)は、それぞれelemcount時間を出力または実行するために使用されます。

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();
}
复制代码

2つのスレッドが開始され、それぞれ10回の印刷と1回の加算操作が実行されます。結果は次のとおりです。printThreadスレッドによって印刷された要素が、2回目の印刷中に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
复制代码

同期の役割

同期の次の3つの機能:

  • 原子性:トランザクションの1つ以上の操作が実行されると、それらはすべて成功または失敗します。同期されたクラスまたはオブジェクトに対するすべての操作は、操作を実行する前に取得する必要があるため、アトミックです。
  • 可见性:在多线程环境下,该资源的状态、值信息等对其他线程都是可见的。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

下面我们看下一个未加锁的对象与加锁后其标记位的状态(使用org.openjdk.jol:jol-core):

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上持有轻量级锁,同时也表明synchronized关键值所加的锁为轻量级锁。

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

変更された静的メソッドを実行した結果は次のとおりです。同期された静的メソッドを変更すると、インスタンスオブジェクトsyncClassのロックフラグは001(何も意味しない)であり、クラスオブジェクトは010(つまりクラスオブジェクトには、ヘビーウェイトロックが追加されています(ロック拡張の問題を含み、興味のある学生はそれを自分で理解できます)。

image.png

したがって、同期された変更されたインスタンスメソッドと変更された静的メソッドの違いは次のとおりです。

  1. Synchronizedは、非静的メソッドを変更します。これは、メソッドを呼び出すオブジェクトを実際にロックします。これは、一般に「オブジェクトロック」と呼ばれます。
  2. Synchronizedは静的メソッドを変更します。静的メソッドは、一般に「クラスロック」と呼ばれるこのクラスのオブジェクトを実際にロックします。

エピローグ

この記事では、同期の使用法と、インスタンスメソッドとクラスメソッドの違いを簡単に紹介します。ロックの同期実装の原則については、次の記事で説明します。

おすすめ

転載: juejin.im/post/7084629438301732877