Когда поток небезопасен? Как добиться потокобезопасности? Как расширить потокобезопасный класс?

Java задняя часть переводчик Безопасность

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

Когда возникает небезопасность потока?

  • Операции не атомарны. Несколько потоков выполняют определенный фрагмент кода.Если на результат этого кода влияет время выполнения между различными потоками, и возникает непредвиденный результат, то есть возникает состояние гонки, возникает небезопасность потока;

    Общие сценарии:

    1. count++. Который сам состоит из трех операций: чтение, изменение, запись, многопоточность, из-за разного времени выполнения потока это может привести к тому, что количество выполнения будет только два потока плюс 1, а исходная цель действительно хочет, чтобы каждое выполнение все плюс 1 ;
    2. синглтон. Несколько потоков могут выполняться одновременноinstance == nullустанавливается, а затем создаются два новых объекта, и первоначальная цель состоит в том, чтобы надеяться, что всегда будет только один объект;
    public MyObj getInstance(){
       if (instance == null){
           instance = new MyObj();
       }
       return instance
    }
    

    Решение: когда текущий поток работает с этим кодом, другие потоки не могут работать с ним.

    Общие сценарии:

    1. Одно состояние использует некоторые классы атомарных переменных в пакете java.util.concurrent.atomic Обратите внимание, что при наличии нескольких состояний каждая операция является атомарной и не является атомарной при использовании в комбинации;
    2. замок. Например, используйте synchronized, чтобы окружить соответствующий блок кода, чтобы обеспечить взаимное исключение между несколькими потоками.Обратите внимание, что его следует включать только в блок кода, который должен обрабатываться как можно более атомарно;

    Повторный вход синхронизированного

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

    class Paxi {
       public synchronized  void sayHello(){
           System.out.println("hello");
       }
    }
    
    class  MyClass extends Paxi{
       public synchronized void  dosomething(){
           System.out.println("do thing ..");
           super.sayHello();
           System.out.println("over");
       }
    }
    

    Его вывод

    do thing ..
    hello
    over
    
  • Модификации не видны. Поток чтения не может воспринимать значение, записанное другими потоками.

    Общие сценарии:

    1. Изменение порядка. При отсутствии синхронизации компилятор, процессор и среда выполнения могут корректировать порядок выполнения операций, то есть порядок записанного кода отличается от реального порядка выполнения, что приводит к считыванию недопустимого значения.
    2. Чтение переменных типа long, double и т.д. JVM позволяет разбить 64-битную операцию на две 32-битные операции, при этом при чтении и записи в разных потоках может быть прочитана неправильная комбинация старших и младших битов.

    Общие сценарии:

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

    Примечание: Volatile не гарантирует атомарность операций, таких какcount++Операция также является рискованной, она гарантирует только возврат последнего значения при чтении. Преимущество его использования заключается в том, что доступ к переменной Volatile не выполняет операцию блокировки и, следовательно, не блокирует поток.

Как добиться потокобезопасности без синхронизации?

  1. Тема закрыта. Чтобы получить доступ к данным только в одном потоке, методы закрытия потока включают следующее:
    • Специальные темы закрыты. То есть вы можете написать свою собственную программу для достижения этого, например, гарантировать, что программа выполняет изменчивые операции только в одном потоке.读取-修改-写入
    • Стек закрыт. Все операции отражаются в стеке потока выполнения, например локальная переменная в методе
    • Класс ThreadLocal. Внутренне поддерживает независимую копию каждого потока и переменной
  2. Поделиться только для чтения. то есть с использованием неизменяемых объектов.
    • Используйте final, чтобы изменить поле, чтобы «значение» поля было неизменным.

      Обратите внимание, что если final изменяет ссылку на объект, такую ​​как set, значение, содержащееся в нем, само по себе является изменяемым.

    • Создайте неизменяемый класс для хранения нескольких изменяемых данных.

      class OneValue{
         //创建不可变对象,创建之后无法修改,事实上这里也没有提供修改的方法
          private final BigInteger  last;
          private final BigInteger[] lastfactor;
          public OneValue(BigInteger  i,BigInteger[] lastfactor){
             this.last=i;
             this.lastfactor=Arrays.copy(lastfactor,lastfactor.length);
          }
         public BigInteger[] getF(BigInteger  i){
              if(last==null || !last.equals(i)){
                  return null;
              }else{
                  return Arrays.copy(lastfactor,lastfactor.length)
              }
         }
      }
      class MyService {
         //volatile使得cache一经更改,就能被所有线程感知到
         private volatile OneValue cache=new OneValue(null,null); 
         public void handle(BigInteger i){
             BigInteger[] lastfactor=cache.getF(i);
             if(lastfactor==null){
                lastfactor=factor(i);
                //每次都封装最新的值
                cache=new OneValue(i,lastfactor)
             }
             nextHandle(lastfactor)
         }
      }
      

Как создавать потокобезопасные классы?

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

    Это принцип, используемый Collections.synchronizedList в java. Часть кода

      public static <T> List<T> synchronizedList(List<T> list) {
               return (list instanceof RandomAccess ?
                       new SynchronizedRandomAccessList<>(list) :
                       new SynchronizedList<>(list));
           }
    

    Реализация SynchronizedList, обратите внимание, что используемый здесь мьютекс является встроенной блокировкой.

           static class SynchronizedList<E>
               extends SynchronizedCollection<E>
               implements List<E> {
               private static final long serialVersionUID = -7754090372962971524L;
       
               final List<E> list;
              public E get(int index) {
                   synchronized (mutex) {return list.get(index);}
               }
               public E set(int index, E element) {
                   synchronized (mutex) {return list.set(index, element);}
               }
               public void add(int index, E element) {
                   synchronized (mutex) {list.add(index, element);}
               }
               public E remove(int index) {
                   synchronized (mutex) {return list.remove(index);}
               }
           }
    

    Реализация мьютекса

    static class SynchronizedCollection<E> implements Collection<E>, >Serializable {
        private static final long serialVersionUID = 3053995032091335093L;
        final Collection<E> c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize
        SynchronizedCollection(Collection<E> c) {
            if (c==null)
            throw new NullPointerException();
            this.c = c;
            mutex = this; // mutex实际上就是对象本身
            }
    

    что такое режим монитора

    Режим мониторинга Java инкапсулирует все изменяемые состояния объекта и защищает его собственной встроенной блокировкой объекта, которая является своего рода закрытием экземпляра. Например, HashTable — это используемый режим монитора. Его операция get заключается в использовании синхронизированной встроенной блокировки для обеспечения безопасности потоков.

    public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }
    
    • Встроенный замок
      Каждый объект имеет встроенные блокировки. Встроенный замок также известен как замок монитора. Или его можно назвать просто монитором
      Когда поток выполняет синхронизированный метод объекта, он автоматически получает встроенную блокировку объекта и автоматически освобождает встроенную блокировку, когда метод возвращается.Даже если во время выполнения возникает исключение, оно будет автоматически снято.
      Следующие два способа написания эквивалентны:

      synchronized void myMethdo(){
          //do something
      }
      void myMethdo(){ 
          synchronized(this){
          //do somthding
          } 
          
      }
      

      официальная документация

    • частный замок

      public class PrivateLock{
          private Object mylock = new Object(); //私有锁
          void myMethod(){
              synchronized(mylock){
                  //do something
              }
          }
      }
      

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

    • блокировка класса
      Модифицированный на статическом методе, все объекты класса имеют общую блокировку.

  2. Делегируйте потокобезопасность потокобезопасным классам

Если отдельные компоненты в классе потокобезопасны, должен ли класс иметь дело с потокобезопасностью?

С учетом доступности.

  1. Существует только один компонент, и он потокобезопасен.

    public class DVT{
        private final ConcurrentMap<String,Point> locations;
        private final Map<String,Point> unmodifiableMap;
            
        public DVT(Map<String,Point> points){
            locations=new ConcurrentHashMap<String,Point>(points);
            unmodifiableMap=Collections.unmodifiableMap(locations);
            }
            
        public Map<String,Point> getLocations(){
            return unmodifiableMap;
            }
            
        public Point getLocation(String id){
            return locations.get(id);
            }
            
        public void setLocation(String id,int x,int y){
            if(locations.replace(id,new Point(x,y))==null){
                throw new IllegalArgumentException("invalid "+id);
                }
            }
            
        }
        
        public class Point{
            public final int x,y;
            public Point(int x,int y){
                this.x=x;
                this.y=y;
            }
        }
    

    Анализ безопасности потоков

    • Сам класс Point не может быть изменен, поэтому он является потокобезопасным, и метод Point, возвращаемый DVT, также является потокобезопасным.
    • Объект, возвращаемый методом getLocations DVT, не поддается изменению и является потокобезопасным.
    • Фактическая операция setLocation - это ConcurrentHashMap, которая также является потокобезопасной.

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

  2. Безопасность потока делегирована нескольким переменным состояния
    До тех пор, пока множество переменных состояния независимы, комбинированный класс не увеличивает инвариантность на несколько переменных состояний между ними.依赖的增加则无法保证线程安全

    public class NumberRange{
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);
        
        public void setLower(int i){
        //先检查后执行,存在隐患
        if (i>upper.get(i)){
            throw new IllegalArgumentException('can not ..');
            }
            lower.set(i);
                
            }
                
        public void setUpper(int i){
        //先检查后执行,存在隐患
            if(i<lower.get(i)){
            throw new IllegalArgumentException('can not ..');
            }
            upper.set(i);
            }
                
        }
    

    SetLower и Setupper - это первая проверка, выполняемая после «операции, но недостаточно для обеспечения механизма блокировки атомной операции. Предполагая, что оригинальный диапазон (0,10), потоковых вызовов SELLOWER (5), установленная настройка (4) неправильное время выполнения может привести к результатам (5,4)

Как расширить существующий потокобезопасный класс?

Предполагается, что необходимо расширить функцию «добавить, если нет».

  1. Измените исходный код напрямую. Но обычно нет возможности изменить исходный код
  2. наследовать. Наследуйте исходный код и добавляйте новые функции. Однако стратегия синхронизации хранится в двух файлах.Если базовая стратегия синхронизации изменится, могут возникнуть проблемы.
  3. комбинация. Поместите класс во вспомогательный класс, передайте код действия вспомогательного класса. Например, расширение Collections.synchronizedList. В этот период вам нужно обратить внимание на механизм блокировки.
        public class ListHelper<E>{
            public List<E> list=Collections.synchronizedList(new ArrayList<E>());
            ...
            public synchronized boolean putIfAbsent(E x){
                boolean absent = !list.contains(x);
                if(absent){
                   list.add(x);
                }
                return absent;
            }
        }
    
    Здесь putIfAbsent не обеспечивает потокобезопасности, потому что встроенная блокировка списка не ListHelper, то есть остальные методы putIfAbsent относительно списка не являются атомарными. Collections.synchronizedList заблокирован в самом списке, правильный способ
    public  boolean putIfAbsent(E x){
        synchronized(list){
            boolean absent = !list.contains(x);
            if(absent){
                list.add(x);
            }
            return absent;
        }
    }
    

    Кроме того, независимо от того, является ли управляемый класс потокобезопасным или нет, к классу можно равномерно добавить дополнительный уровень блокировки. Справочник по реализации Метод Collections.synchronizedList