Многопоточность Java — синхронизированный и переменный параллельный доступ

Java

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

Проблема непоточной безопасности существует в "переменных экземпляра".Если это приватная переменная внутри метода, то проблемы "непоточной безопасности" не возникает.

1 Synchronized

1.1 синхронизированные методы

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

public class ThreadTest {
    public static void main(String[] args) {
        Add add = new Add();
        Add add1 = new Add();
        ThreadAA threadAA = new ThreadAA(add);
        threadAA.start();
        ThreadBB threadBB = new ThreadBB(add1);
        threadBB.start();
    }
}

class ThreadAA extends Thread{
    private Add a;
    public ThreadAA(Add add){
        this.a = add;
    }
    @Override
    public void run(){
        a.add("a");
    }
}

class ThreadBB extends Thread{
    private Add b;
    public ThreadBB(Add add){
        this.b = add;
    }
    @Override
    public void run(){
        b.add("b");
    }
}

class Add{
    private int num = 0;
    //同步方法
    synchronized public void add(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("add a end");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("add b end");
            }
            System.out.println(username + " name " + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

распечатать результат

add a end
add b end
b name 200
a name 100

Из результатов видно, что порядок печати не синхронный, а чередующийся, это связано с тем, чтоБлокировки, полученные с помощью ключевого слова synchronized, являются блокировками всех объектов.. Таким образом, в приведенном выше примере поток сначала выполняет метод с ключевым словом synchronized, и этот поток удерживает блокировку объекта, которому принадлежит метод, затем другие потоки могут только ждать в состоянии, при условии, что несколько потоков обращаются к одному и тому же объекту. .

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

//将上面的ThreadTest类中的main方法进行修改
public class ThreadTest {
    public static void main(String[] args) {
        Add add = new Add();
//        Add add1 = new Add();
        ThreadAA threadAA = new ThreadAA(add);
        threadAA.start();
        ThreadBB threadBB = new ThreadBB(add);
        threadBB.start();
    }
}

результат операции

add a end
a name 100
add b end
b name 200

На этот раз, чтобы увидеть больше результатов операции - это порядок печати.

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

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

public class ThreadFunction {
    public static void main(String[] args) {
        ObjFunction objFunction = new ObjFunction();
        FunA funA = new FunA(objFunction);
        funA.setName("a");
        funA.start();
        FunB funB = new FunB(objFunction);
        funB.setName("b");
        funB.start();

    }
}

class FunB extends Thread{
    private ObjFunction objFunction;
    public FunB(ObjFunction objFunction){
        this.objFunction = objFunction;
    }
    @Override
    public void run(){
        objFunction.objMethod();
    }
}

class FunA extends Thread{
    private ObjFunction objFunction;
    public FunA(ObjFunction objFunction){
        this.objFunction = objFunction;
    }
    @Override
    public void run(){
        objFunction.objMethod();
    }
}

class ObjFunction{
    public void objMethod(){
        try{
            System.out.println(Thread.currentThread().getName() + " start");
            synchronized (this) {
                System.out.println("start time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end time = "+ System.currentTimeMillis());
            }
            System.out.println(Thread.currentThread().getName() + " end");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

результат операции

a start
b start
start time = 1559033466082
end time = 1559033468083
a end
start time = 1559033468083
end time = 1559033470084
b end

Как можно видеть,Код вне блока синхронного кода выполняется асинхронно, а код внутри блока синхронного кода выполняется синхронно. И объект блокировки синхронизированного (этого) также является текущим объектом.

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

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

Ключевые слова Synchronized также могут быть применены к статическому методу Static, который представляет собой класс Class, соответствующий текущему *.java файлу в качестве объекта блокировки.

Объект блокировки, удерживаемый методом статической синхронизации = synchronized(class)

public class ThreadTest {
    public static void main(String[] args) {
        ThreadAA threadAA = new ThreadAA();
        threadAA.start();
        ThreadBB threadBB = new ThreadBB();
        threadBB.start();
    }
}

class ThreadAA extends Thread{
    @Override
    public void run(){
        Add.add("a");
    }
}

class ThreadBB extends Thread{
    @Override
    public void run(){
        Add.add("b");
    }
}

class Add{
    private static int num = 0;
    //同步方法
    synchronized static public void add(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("add a end");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("add b end");
            }
            System.out.println(username + " name " + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

результат операции

add a end
a name 100
add b end
b name 200

1.4 синхронизированные классы

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

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

1.5 синхронизированный повторный вход в блокировку

Он имеет синхронизированную функцию блокировки REENTRANT, I.E. Синхронизирован в использовании, когда заблокируйте ниток, чтобы придать объект, запрос блокировки объектов еще раз, когда этот объект можно снова заблокировать. То есть, когда внутренний синхронный метод / код кода для вызова метода настоящего синхронизованного / блока, вы всегда можете получить.

public class ThreadAgain {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Service().service1();
            }
        }).start();
    }
}

class Service{
    synchronized public void service1(){
        System.out.println("service1");
        service2();
    }

    synchronized private void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized private void service3() {
        System.out.println("service3");
    }

}

результат операции

service1
service2
service3

2 volatile

Основная цель ключевого слова volatile — сделать переменную видимой для нескольких потоков.

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

Сначала посмотрите код

public class MyVolatile {
    public static void main(String[] args) {
        try {
            RunThread runThread = new RunThread();
            runThread.start();
            Thread.sleep(2000);
            runThread.setRun(false);
            System.out.println("为runThread复制false");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

class RunThread extends Thread{
    private boolean isRun = true;

    public boolean isRun() {
        return isRun;
    }

    public void setRun(boolean run) {
        isRun = run;
    }

    @Override
    public void run(){
        System.out.println("进入了run方法");
        while (isRun == true){
        }
        System.out.println("退出run方法,线程停止");
    }
}

Как видно из консоли, поток не заканчивается. Эта проблема вызвана тем, что значение в частном стеке и значение в рабочем стеке не синхронизированы.Чтобы решить эту проблему, вы можете использовать ключевое слово volatile.

Измените код в классе RunThread.

class RunThread extends Thread{
   volatile private boolean isRun = true;

    public boolean isRun() {
        return isRun;
    }

    public void setRun(boolean run) {
        isRun = run;
    }

    @Override
    public void run(){
        System.out.println("进入了run方法");
        while (isRun == true){
        }
        System.out.println("退出run方法,线程停止");
    }
}

Запустите его снова, и поток завершится нормально.

Хотя ключевое слово volatile может сделать переменные экземпляра видимыми между несколькими потоками, volatile имеет фатальный недостаток, заключающийся в том, что оно не поддерживает атомарность.

Убедитесь, что volatile не поддерживает атомарность

public class IsAtomic {
    public static void main(String[] args) {
        MyAtomicRun[] myAtomicRuns = new MyAtomicRun[100];
        for (int i = 0;i<100;i++){
            myAtomicRuns[i] = new MyAtomicRun();
        }
        for (int i = 0;i<100;i++){
            myAtomicRuns[i].start();
        }
    }
}

class MyAtomicRun extends Thread{
    volatile public static int count;
    private static void count(){
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println("count: " + count);
    }
    @Override
    public void run(){
        count();
    }
}

распечатать

//篇幅较长,没有全部粘贴
count: 5000
count: 4900
count: 4800
count: 4700
count: 4600
count: 4500
count: 4400
count: 4400

Судя по результатам вывода, в нашем идеальном состоянии он не выводит 10000.

внести улучшения в код

class MyAtomicRun extends Thread{
    volatile public static int count;
    //需要使用同步静态方法,这样是以class为锁,才能达到同步效果
    synchronized private static void count(){
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println("count: " + count);
    }
    @Override
    public void run(){
        count();
    }
}

распечатать

count: 9300
count: 9400
count: 9500
count: 9600
count: 9700
count: 9800
count: 9900
count: 10000

На этот раз вывод является правильным результатом.

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

2.1 Сравнение энергозависимой и синхронизированной

  1. volatile — это облегченная реализация синхронизации потоков, поэтому производительность определенно выше, чем у synchronized, и volatile может изменять только переменные, тогда как synchronized может изменять методы и блоки кода.
  2. Volatile не будет блокироваться при доступе к нескольким потокам, тогда как синхронизированный блокируется.
  3. Volatile может гарантировать видимость данных, но не может гарантировать атомарность, в то время как синхронизированный может косвенно гарантировать и атомарность, и видимость, поскольку синхронизированный синхронизирует данные в частной и общедоступной памяти.
  4. Volatile решает проблему видимости между переменными между несколькими потоками; Synchronized решает вопрос синхронизации ресурсов доступа между несколькими потоками.

2.2 Работа с переменными в памяти

Такая операция, как операция ++ над переменной, измененной приведенным выше ключевым словом volatile, на самом деле не является атомарной операцией, то есть она не является потокобезопасной.

этапы работы i++:

  1. Получить значение i из памяти
  2. вычислить я
  3. напиши я на память

Если другая нить также модифицирует значение i во время расчета второго этапа, то в настоящее время появится грязные данные.

img

  • фазы чтения и загрузки: копирование переменных из основной памяти в рабочую память текущего потока;
  • использовать и назначать фазы: выполнять код и изменять значения общих переменных;
  • Этапы сохранения и записи: обновите значения переменных, соответствующие основной памяти, с данными рабочей памяти.

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

Для переменных, измененных ключевым словом volatile, JVM гарантирует только актуальность значения, загруженного из основной памяти в рабочую память.