Java 8 Concurrency — спокойный анализ Synchronized (Часть 1)

Java задняя часть JVM

1. Java-блокировки

1.1 Семантика блокировки памяти

  • Блокировки допускают взаимоисключающее выполнение критических секций, а также позволяют потоку, снявшему блокировку, отправить сообщение потоку той же блокировки.
  • Снятие блокировки должно следовать принципу «Происходит до» (锁规则:解锁必然发生在随后的加锁之前)
  • Конкретная производительность блокировки в JavaSynchronizedиLock

1.2 Снятие блокировки

После того, как поток A снимает блокировку, он сбрасывает общую операцию изменения в основную память.

1.3 Приобретение замков

Когда поток B получает блокировку, JMM делает недействительной локальную память потока, и код критической секции, защищенный монитором, должен прочитать общую переменную из основной памяти.

1.4 Снятие и получение замков

  • Получение блокировки имеет ту же семантику памяти, что и чтение volatile, читатели могут обратиться к автору.Concurrency Fan @Java Memory Model & Volatile One Text Pass (версия 1.7)
  • Поток A снимает блокировку, по сути, поток A сообщает следующему потоку, который получает блокировку, что он изменил общую переменную.
  • Поток B получает блокировку, по сути, поток B получает сообщение о том, что поток A говорит ему изменить общую переменную (перед снятием блокировки)
  • Поток A снимает блокировку, а затем за блокировку конкурирует поток B. Суть в том, что поток A отправляет сообщение потоку B через основную память, чтобы сообщить ему, что он изменил общую переменную.

2. Обзор синхронизированного

  • Механизм синхронизации:Synchronized — это реализация механизма синхронизации Java, а именно механизма мьютекса, который, как известно, является взаимоисключающим.
  • Мьютекс:Это означает, что блокировка каждого объекта может быть назначена только одному потоку за раз и может быть занята только одним потоком за раз.
  • эффект:Синхронизированный используется для обеспечения того, чтобы только один поток мог одновременно войти в критическую секцию, обеспечивая при этом видимость, атомарность и порядок общих переменных.
  • использовать:Когда поток пытается получить доступ к методу кода синхронизации (блоку), он должен сначала получить блокировку, блокировка должна быть снята при выходе или выдаче исключения.

3. Использование синхронизированного

3.1 Три метода применения Synchronized

Пополнить:Преимущество использования синхронизированных блоков кода заключается в том, что другие потоки могут по-прежнему получать доступ к синхронизированным блокам кода, которые не синхронизированы (это)

3.2 Синхронизированные правила использования

/**
  * 先定义一个测试模板类
  *     这里补充一个知识点:Thread.sleep(long)不会释放锁
  *     读者可参见笔者的`并发番@Thread一文通`
  */ 
public class SynchronizedDemo {
    public static synchronized void staticMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod");
    }
    public static void staticMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2");
        synchronized (SynchronizedDemo.class){
            System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void synMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod");
    }
    public synchronized void synMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2");
    }
    public void method(){
        System.out.println(Thread.currentThread().getName() + "访问了普通方法method");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问普通方法method");
    }
    private Object lock = new Object();
    public void chunkMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod3(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法");
        //同步代码块
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void stringMethod(String lock){
        synchronized (lock){
            while (true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.2.1 Общий метод и вызов синхронного метода не связаны друг с другом

Когда поток входит в метод синхронизации, другие потоки могут получить доступ к другим методам без синхронизации.
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用普通方法
        synDemo.method();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步方法
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-1访问了同步方法synMethod
Thread-0访问了普通方法method
Thread-0结束访问普通方法method
Thread-1结束访问同步方法synMethod
//分析:通过结果可知,普通方法和同步方法是非阻塞执行的

3.2.2 Все синхронизированные методы доступны только одному потоку

Когда поток выполняет синхронизированный метод, другие потоки не могут получить доступ ни к одному синхронизированному методу.
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了同步方法synMethod
Thread-0结束访问同步方法synMethod
Thread-0访问了同步方法synMethod2
Thread-0结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod2
Thread-1结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod
Thread-1结束访问同步方法synMethod
//分析:通过结果可知,任务的执行是阻塞的,显然Thread-1必须等待Thread-0执行完毕之后才能继续执行

3.2.3 Синхронизированный код одного и того же блокировки можно получить только одним потоком одновременно

Когда блок кода синхронизации является одной и той же блокировкой, к этому методу могут получить доступ все потоки, но к одному и тому же блоку кода синхронизации блокировки может получить доступ только один поток.
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.synMethod2();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock  
...停顿等待...
Thread-1在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
//分析可知:
//1.对比18行和19行可知,即使普通方法有同步代码块,但方法的访问是非阻塞的,任何线程都可以自由进入
//2.对比20行、22行以及25行和27行可知,对于同一个锁的同步代码块的访问一定是阻塞的


3.2.4 Порядок выполнения нескольких кодов синхронизации, которые обращаются к одной и той же блокировке в одно и то же время между потоками, не является фиксированным.

  • Порядок выполнения нескольких кодов синхронизации, которые получают доступ к тому же блокировку одновременно между потоками, не фиксированы, даже если используется тот же блокировку объектов, который сильно отличается от метода синхронизации
  • ? ? Читатели могут сначала подумать, почему возникает такая проблема? ?
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod2();
        synDemo.chunkMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod2方法
Thread-0在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1在chunkMethod方法中获取了lock

//分析可知:
//现象:对比20行、22行和24行、25行可知,虽然是同一个lock对象,但其不同代码块的访问是非阻塞的
//原因:根源在于锁的释放和重新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又重新获取到锁优先执行了
//注意:但有一点是必须的,对于同一个锁的同步代码块的访问一定是阻塞的
//补充:同步方法之所有会被全部阻塞,是因为synDemo对象一直被线程在内部把持住就没释放过,论把持住的重要性!

3.2.5 Неблокирующий доступ между разными замками

  • Поскольку объекты блокировки трех методов использования различны, они не будут иметь никакого влияния друг на друга.
  • Но есть два исключения:
    • 1. Когда объект класса, используемый в блоке кода синхронизации, согласуется с объектом класса, он принадлежит к одной и той же блокировке в соответствии с приведенным выше3.2.3в общем
    • 2. Когда блок кода синхронизации использует это, то есть он принадлежит к той же блокировке, что и метод синхронизации с использованием блокировки, в соответствии с вышеизложенным3.2.2и3.2.3в общем
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
    Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
    Thread thread3 = new Thread(() -> staticMethod());
    Thread thread4 = new Thread(() -> staticMethod2());
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}
---------------------
//输出:
Thread-1访问了chunkMethod3方法
Thread-1在chunkMethod3方法中获取了this
Thread-2访问了静态同步方法staticMethod
Thread-0访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock
Thread-3访问了静态同步方法staticMethod2
...停顿等待...
Thread-2结束访问静态同步方法staticMethod
Thread-3在staticMethod2方法中获取了SynchronizedDemo.class
//分析可知:
//现象:对比16行、18行和24行、25行可知,虽然是同一个lock对象,但其不同代码块的访问是非阻塞的
//原因:根源在于锁的释放和重新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又重新获取到锁优先执行了

3.3 Повторный вход в Synchronized

  • Повторная блокировка:Когда поток снова запрашивает свой собственный критический ресурс, удерживающий блокировку объекта, это блокировка с повторным входом, и запрос будет выполнен успешно.
  • выполнить:После того, как поток получает блокировку объекта, ему разрешается снова запрашивать блокировку объекта.Для каждого повторного входа количество записей монитора +1
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了同步方法synMethod
Thread-0结束访问同步方法synMethod
Thread-0访问了同步方法synMethod2
Thread-0结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod2
Thread-1结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod
Thread-1结束访问同步方法synMethod
//分析:对比16行和18行可知,在代码块中继续调用了当前实例对象的另外一个同步方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现

3.4 Синхронизированная и строковая блокировка

  • Скрытая опасность:Из-за функции кэширования пула констант String в JVM, поэтомуТот же литерал — тот же замок! ! !
  • Уведомление:Настоятельно не рекомендуется использовать String в качестве объекта блокировки, вместо этого следует использовать другие некэшированные объекты.
  • намекать:Если у вас есть вопросы о тексте, пожалуйста, просмотрите основы строки, вы не объясняете здесь.
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
    Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循环...
//分析:输出结果永远都是Thread-0的死循环,也就是说另一个线程,即Thread-1线程根本不会运行
//原因:同步块中的锁是同一个字面量

3.5 Синхронизированные и неизменяемые блокировки

  • Скрытая опасность:При использовании неизменяемых объектов класса (конечный класс) в качестве объектных блокировок использование синхронизированного также будет иметь проблемы с параллелизмом.
  • причина:Из-за непеременной функции, когда блокировка все еще рассчитывается как блокировка, все еще выполняется операция вычисления, будет сгенерирован новый объект блокировки.
  • Уведомление:Серьезно не рекомендуется использовать конечный класс в качестве объекта блокировки и при этом выполнять над ним операции вычисления.
  • Пополнить:Хотя String также является окончательным классом, причиной этого является буквальный постоянный пул
public class SynchronizedDemo {
    static Integer i = 0;   //Integer是final Class
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0;j<10000;j++){
                    synchronized (i){
                        i++;
                    }
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}
---------------------
//输出:
14134
//分析:跟预想中的20000不一致,当使用Integer作为对象锁时但还有计算操作就会出现并发问题

С помощью декомпиляции мы обнаружили, что выполнение операции i++ эквивалентно выполнению i = Integer.valueOf(i.intValue()+1)

Глядя на реализацию метода valueOf Integer, можно увидеть, что каждый раз создается новый объект Integer, а блокировка изменилась! ! !

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);  //每次都new一个新的锁有木有!!!
}

3.6 Синхронизация и взаимоблокировка

  • Мертвый замок:При необходимости между потокамиВ ожидании замков друг друга, образуется тупик, в результате чего возникает бесконечный цикл
  • Уведомление: Дедлоки строго запрещены в коде! ! !
public static void main(String[] args) {
    Object lock = new Object();
    Object lock2 = new Object();
    Thread thread1 = new Thread(() -> {
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "获取到lock锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        synchronized (lock2){
            System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + "获取到lock锁");
            }
        }
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-1获取到lock2锁
Thread-0获取到lock锁
.....
//分析:线程0获得lock锁,线程1获得lock2锁,但之后由于两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",无法往下执行,从而形成死循环,即死锁,之后一直在做无用的死循环,严重浪费系统资源

мы используемjstackГлядя на работу каждого потока этой задачи, вы можете обнаружить, что оба потока заблокированы BLOCKED

Очевидно, мы обнаружили, что Java-level=deadlock, то есть тупик, два потока ждут блокировок друг друга.


Синхронизированный паспорт (версия 1.8) Зависит от Хуан Чжипэн Кира создавать, использовать Creative Commons Attribution - Международное лицензионное соглашение NonCommercial 4.0Лицензия.