Home >Java >javaTutorial >Shared implementation of objects in Java concurrent programming

Shared implementation of objects in Java concurrent programming

WBOY
WBOYforward
2023-04-23 17:25:071621browse

1. Visibility

Generally, we cannot guarantee that the thread performing the read operation can see the values ​​written by other threads, because each thread has its own caching mechanism. To ensure visibility of memory write operations between multiple threads, a synchronization mechanism must be used.

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

The above code seems to output 42, but in fact it may not be terminated at all, because the reading thread can never see the value of ready; it is very likely to output 0, because the reading thread sees the writing of ready value, but do not see the value written to number afterwards. This phenomenon is called "reordering". In the absence of synchronization, the compiler, processor, runtime, etc. may make some unexpected adjustments to the execution order of operations.

So, whenever data is shared between multiple threads, correct synchronization should be used.

1.1 Invalid data

Unless synchronization is used, it is very likely to obtain the invalid value of the variable. The invalid values ​​may not appear at the same time, and a thread may obtain the latest value of one variable and the invalid value of another variable. Invalid data may also lead to some confusing failures, such as unexpected exceptions, corrupted data structures, inaccurate calculations, infinite loops, etc.

1.2 Non-atomic 64-bit operations

For non-volatile long and double variables, the JVM allows a 64-bit read or write operation to be decomposed into two 32-bit operations. Therefore, it is very likely that the upper 32 bits of the latest value and the lower 32 bits of the invalid value will be read, resulting in a random value being read. Unless they are declared with the keyword volatile or protected with a lock.

1.3 Locking and Visibility

When a thread executes a synchronized code block protected by a lock, it can see the results of all previous operations of other threads in the same synchronized code block. Without synchronization, the above guarantees cannot be achieved. The meaning of locking is not limited to mutual exclusion behavior, but also includes visibility. To ensure that all threads see the latest value of a shared variable, all threads performing read or write operations must be synchronized on the same lock.

1.4 Volatile variables

When a variable is declared as a volatile type, neither the compiler nor the runtime will reorder the operations on the variable together with other memory operations. Volatile variables are not cached in registers or other places invisible to the processor, so reading a volatile variable always returns the most recently written value. The locking mechanism can ensure both visibility and atomicity, while volatile variables can only ensure visibility.

Volatile variables should be used if and only if all of the following conditions are met:

  • The writing operation to the variable does not depend on the current value of the variable, or can ensure Only use a single thread to update the value of a variable.

  • This variable will not be included in the invariance condition along with other state variables.

  • No need to lock when accessing variables.

2. Release and leakage

Publishing an object means that the object can be used in code outside the current scope. Methods of publishing objects include: references to non-private variables, references returned by method calls, publishing inner class objects that imply references to external classes, etc. When an object is released that should not be released, it is called a leak.

public class ThisEscape {
   private int status;
   public ThisEscape(EventSource source) {
      source.registerListener(new EventListener() {
         public void onEvent(Event e) {
            doSomething(e);
         }
      });
      status = 1;
   }

   void doSomething(Event e) {
      status = e.getStatus();
   }

   interface EventSource {
      void registerListener(EventListener e);
   }

   interface EventListener {
      void onEvent(Event e);
   }

   interface Event {
      int getStatus();
   }
}

Because the instance of the inner class contains an implicit reference to the outer class instance, when ThisEscape releases the EventListener, it also implicitly releases the ThisEscape instance itself. But at this time, the variable status has not been initialized, causing this reference to be leaked in the constructor. You can use a private constructor and a public factory method to avoid incorrect construction processes:

public class SafeListener {
    private int status;
    private final EventListener listener;
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
        status = 1;
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
        status = e.getStatus();
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
        int getStatus();
    }
}

3. Thread closure

One way to avoid using synchronization is not to share. If data is accessed only within a single thread, no synchronization is required, which is called thread closure. Thread containment is a programming consideration and must be implemented in the program. Java also provides some mechanisms to help maintain thread closure, such as local variables and ThreadLocal.

3.1 Ad-hoc thread closure

Ad-hoc thread closure means that the responsibility for maintaining thread closure is entirely borne by the program implementation. Using volatile variables is a way to achieve Ad-hoc thread closure. As long as it is guaranteed that only a single thread performs write operations on shared volatile variables, then it is safe to perform "read-modify-write" operations on these variables. , the visibility of volatile variables ensures that other threads can see the latest value.

Ad-hoc thread closure is very fragile, so use it as little as possible in your program. Where possible, use other thread containment techniques such as stack containment and ThreadLocal.

3.2 Stack closure

In stack closure, objects can only be accessed through local variables. They are located on the execution thread's stack and cannot be accessed by other threads. Even though these objects are not thread-safe, they are still thread-safe. However, it's worth noting that only the person writing the code knows which objects are stack-enclosed. Without clear instructions, subsequent maintainers can easily leak these objects by mistake.

3.3 ThreadLocal类

使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                try {
                    return DriverManager.getConnection(DB_URL);
                } catch (SQLException e) {
                    throw new RuntimeException("Unable to acquire Connection, e");
                }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

从概念上看,你可以将ThreadLocal8742468051c85b06f0a0af9e3e506b5c视为包含了Mapdd13f7ef6939263b34f16ddd764e4ff9对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。

4. 不变性

如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能改变

  • 对象的所有域都是final类型

  • 对象是正确创建的,在对象创建期间,this引用没有泄露

public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。

4.1 final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

4.2 使用volatile类型来发布不可变对象

因式分解Sevlet将执行两个原子操作:

  • 更新缓存

  • 通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果

每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:

public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i, BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

5 安全发布

5.1 不正确的发布

像这样将对象引用保存到公有域中就是不安全的:

public Holder holder;
public void initialize(){
    holder = new Holder(42);
}

由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }
}

上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。

5.2 不可变对象与初始化安全性

即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

5.3 安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:

  • 在静态初始化函数里初始化一个对象引用。

  • 将对象的引用保存到volatile类型的域或者AtomicReference对象中。

  • 将对象的引用保存到某个正确构造对象的final类型域中。

  • 将对象的引用保存到一个由锁保护的域中。

线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。

  • 通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。

  • By placing an object into a BlockingQueue or ConcurrentLinkedQueue, you can safely publish the object to any thread that accesses the object from these queues.

5.4 De facto immutable objects

If an object is technically mutable, but its state will not change after it is published, then this object Called de facto immutable objects. A de facto immutable object that is safely published can be safely used by any thread without additional synchronization. For example, maintain a Map object that stores the latest login time of each user:

public Map7c36a245a97922f78d3224cbec5818e1 lastLogin =
Collections.synchronizedMap(new HashMapIf the value of the Date object will not change after being put into the Map, then the synchronization mechanism in synchronizedMap is enough to allow the Date value to be published safely, and no additional synchronization is required when accessing these Date values.

5.5 Mutable objects

For mutable objects, synchronization is not only needed when publishing the object, but also needs to be used every time the object is accessed to ensure the visibility of subsequent modification operations. The publishing requirements of an object depend on its mutability:

  • Immutable objects can be published by any mechanism.

  • The fact is that immutable objects must be published in a safe way.

  • Mutable objects must be released in a safe manner and must be thread-safe or protected by a lock.

5.6 Safe shared objects

When using and sharing objects in concurrent programs, you can use some practical strategies, including:

  • Thread closure. A thread-enclosed object can only be owned by one thread, the object is enclosed in that thread, and can only be modified by this thread.

  • Read-only sharing. Without additional synchronization, a shared read-only object can be accessed concurrently by multiple threads, but no thread can modify it. Shared read-only objects include immutable objects and de facto immutable objects.

  • Thread-safe sharing. A thread-safe object is synchronized internally so that multiple threads can access it through the object's public interface without further synchronization.

  • Protected object. Protected objects can only be accessed by holding a specific lock. Protected objects include objects that are encapsulated in other thread-safe objects, as well as objects that are released and protected by a specific lock.

The above is the detailed content of Shared implementation of objects in Java concurrent programming. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete