«Улучшайте способности, зарплату можно увеличить» — Java Concurrency Synchronized

Java

Добро пожаловать в публичный аккаунт【Технический блог Ccww], впервые была запущена оригинальная техническая статья

Прошлые статьи:

Введение в синхронизацию

Безопасность потоков имеет решающее значение в параллельном программировании, и основными причинами проблем с безопасностью потоков являются:

  • Критический ресурс, есть общие данные
  • Несколько потоков работают вместе для обмена данными

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

То есть, когда ключевое слово synchronized используется для выполнения кода, оно проверяет, доступна ли блокировка, затем получает блокировку, выполняет код и, наконец, освобождает блокировку. Есть три способа использования синхронизированного:

  • Синхронизированный метод: синхронизировать текущий объект экземпляра и получить блокировку текущего экземпляра перед вводом кода синхронизации.
  • Синхронизированный статический метод: синхронизировать объект класса текущего класса и получить блокировку объекта текущего класса перед вводом кода синхронизации.
  • Блок синхронизированного кода: объект в синхронизированных скобках, блокировка данного объекта и получение блокировки данного объекта перед входом в базу синхронизированного кода.

Синхронный метод

Первый взгляд на синхронизированное ключевое слово не используется, как показано ниже:

public class ThreadNoSynchronizedTest {

    public void method1(){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method1");
    }

    public void method2() {		
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ThreadNoSynchronizedTest  tnst= new ThreadNoSynchronizedTest();

        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run() {
                tnst.method1();
            }
        });

        Thread t2 = new Thread(new Runnable() {			
            @Override
            public void run() {
                tnst.method2();
            }
        });
        t1.start();
        t2.start();
    }
}

В приведенном выше коде метод1 задерживает метод2 более чем на 2 с, поэтому в случае, когда потоки выполнения t1 и t2, результат выполнения:

method2
method1

Когда метод1 и метод2 используют ключевое слово synchronized, код выглядит следующим образом:

public synchronized void method1(){
	try {
		Thread.sleep(2000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	System.out.println("method1");
}
	
public synchronized void method2() {		
	System.out.println("method2");
}

На этом этапе, поскольку метод 1 занимает блокировку, метод 2 должен дождаться завершения выполнения метода 1, прежде чем выполняться.Результат выполнения:

method1
method2

Следовательно, синхронизированная блокировка является текущим объектом. Синхронизированный метод текущего объекта может выполнять только один из них одновременно. Другой синхронизированный метод необходимо приостановить и ожидать, но это не влияет на выполнение не- синхронный метод. Следующий синхронизированный метод эквивалентен синхронизированному блоку (содержит весь метод synchronized(this)).

public synchronized void method1(){
		
}

public  void method2() {		
	synchronized(this){	
	}
}

Синхронизированный статический метод

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

public class TreadSynchronizedTest {

    public static synchronized void method1(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println("method1");
    }

    public static void method2() {		
        synchronized(TreadTest.class){
            System.out.println("method2");
        }
    }

    public static void main(String[] args) {		
        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run() {
                TreadSynchronizedTest.method1();
            }
        });

        Thread t2 = new Thread(new Runnable() {			
            @Override
            public void run() {
                TreadSynchronizedTest.method2();
            }
        });
        t1.start();
        t2.start();
    }

}

Так как класс используется в качестве блокировки, существует конкуренция между методом 1 и методом 2. Synchronized(ThreadTest.class) в методе 2 эквивалентно добавлению synchronized непосредственно перед void в объявлении метода 2. Результат выполнения приведенного выше кода по-прежнему является результатом вывода метода1 сначала:

method1
method2

Блок синхронизированного кода

Синхронизированные блоки кода используются в блоках кода, которые имеют дело с критическими ресурсами.Коды, которым не требуется доступ к критическим ресурсам, не должны конкурировать за ресурсы, что снижает конкуренцию между ресурсами и повышает производительность кода. Пример кода выглядит следующим образом:

public class TreadSynchronizedTest {

    private Object obj = new Object();

    public void method1(){
        System.out.println("method1 start");
    	synchronized(obj){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("method1 end");
         }
    }

    public void method2() {
        System.out.println("method2 start");


        // 延时10ms,让method1线获取到锁obj
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        synchronized(obj){
            System.out.println("method2 end");
        }
    }

    public static void main(String[] args) {
        TreadSynchronizedTest tst = new TreadSynchronizedTest();
        Thread t1 = new Thread(new Runnable() {			
            @Override
            public void run() {
                tst.method1();
            }
        });

    	Thread t2 = new Thread(new Runnable() {			
            @Override
            public void run() {
                tst.method2();
              }
    	});
        t1.start();
        t2.start();
    }
}

Результат выполнения следующий:

method1 start
method2 start
method1 end
method2 end

В приведенном выше коде выполняется метод method2, сначала печатается start метода 2, а затем выполняется блок синхронизации.Поскольку obj получен методом 1 в это время, метод 2 может быть выполнен только после завершения выполнения метода 1. Следовательно, Сначала печатается конец метода1, а затем конец метода2.

Синхронный принцип

Synchronized — это вид блокировки, реализованный JVM, в котором получение и снятие блокировки являются инструкциями monitorenter и monitorexit соответственно.

Добавляется сегмент кода с ключевым словом synchronized, и сгенерированный файл байт-кода будет иметь еще две инструкции monitorenter и monitorexit, а также будет иметь еще один бит флага ACC_SYNCHRONIZED,

Когда метод вызывается, вызывающая инструкция проверяет, установлен ли флаг доступа ACC_SYNCHRONIZED к методу.Если он установлен, поток выполнения сначала получит монитор, а затем выполнит тело метода после успешного получения и отпустит монитор после выполнения метода.

Во время выполнения метода ни один другой поток не может снова получить тот же объект монитора. На самом деле разницы по сути нет, но синхронизация метода реализована неявным образом, без необходимости байткода.

После Java 1.6 синхронизация делится на предвзятые блокировки, облегченные блокировки и тяжеловесные блокировки в реализации, предвзятые блокировки включены по умолчанию в java1.6, а облегченные блокировки будут расширены до тяжеловесных блокировок, данные о блокировке хранятся в заголовке объекта .

  • Предвзятая блокировка: используется, когда только один поток обращается к синхронизированному блоку, и блокировка получена через операцию CAS.
  • Облегченные блокировки: при наличии нескольких потоков, которые быстро обращаются и синхронизируются, смещенные блокировки будут обновлены до облегченных замков. Когда потоку не удается получить облегченную блокировку, это означает, что существует конкуренция, и облегченная блокировка расширится до тяжелой блокировки. войдет непосредственно в состояние блокировки.
  • Тяжеловесная блокировка: если получение блокировки не удается, поток напрямую блокируется, поэтому будет происходить переключение контекста потока, а производительность будет наихудшей.

Оптимизация блокировки - адаптивное прядение

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

Самый простой способ решить эту проблему — указать количество вращений, например, дать ему прокрутиться 10 раз, и войти в состояние блокировки, если блокировка не была получена. Но JDK использует более разумный адаптивный способ вращения, проще говоря, если поток завершится успешно, количество вращений в следующий раз будет больше, а если вращение не удастся, количество вращений будет уменьшено.

Оптимизация блокировки — огрубление блокировки

Следует хорошо понимать концепцию укрупнения блокировки, которая заключается в объединении операций блокировки и разблокировки, которые соединяются несколько раз, в одну, и расширении нескольких последовательных блокировок в блокировку с большей областью действия. Например:

public class StringBufferTest {
     StringBuffer stringBuffer = new StringBuffer();
     public void append(){
         stringBuffer.append("a");
         stringBuffer.append("b");
         stringBuffer.append("c");
     }
 }

Здесь каждый раз, когда вызывается метод stringBuffer.append, его необходимо блокировать и разблокировать.Если виртуальная машина обнаружит серию операций блокировки и разблокировки одного и того же объекта, она будет объединена в более широкий диапазон операций блокировки и разблокировки. Операция, то есть блокировка, выполняется при первом методе добавления и разблокируется после последнего метода добавления.

Оптимизация блокировки — устранение блокировки

Ликвидация блокировки - удаление ненужных блокирующих операций. Согласно технологии Escape Code, если это оценивает, что в кусочке кода данные на куче не сберегаются от текущего потока, то этот код можно считать потоком, а замки не нужны. Посмотрите на следующую программу:

public class SynchronizedTest02 {

     public static void main(String[] args) {
         SynchronizedTest02 test02 = new SynchronizedTest02();        
         for (int i = 0; i < 10000; i++) {
             i++;
         }
         long start = System.currentTimeMillis();
         for (int i = 0; i < 100000000; i++) {
             test02.append("abc", "def");
         }
         System.out.println("Time=" + (System.currentTimeMillis() - start));
     }

     public void append(String str1, String str2) {
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
     }
}

Хотя StringBuffer Append является синхронным методом, StringBuffer в этой программе принадлежит локальной переменной и не выходит из метода, поэтому этот процесс является потокобезопасным и может быть устранен.

Синхронизированные недостатки

Sychronized заставит ресурс, который не получил блокировку, перейти в состояние Block, а затем перейти в состояние Running после конкуренции за ресурс.Этот процесс включает в себя переключение между режимом пользователя операционной системы и режимом ядра, а стоимость относительно высокий.

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

Вы в порядке, офицеры? Если вам это нравится, проведите пальцем, чтобы нажать 💗, нажмите, чтобы подписаться! ! Спасибо за Вашу поддержку!

Добро пожаловать в публичный аккаунт【Технический блог Ccww], впервые была запущена оригинальная техническая статья