Thinking logic of computer programs (66) and thinking 66

Source: Internet
Author: User

Thinking logic of computer programs (66) and thinking 66

We mentioned two problems of multi-thread shared memory in the previous section. One is the race condition and the other is the memory visibility. we mentioned that one solution to these two problems is to use the synchronized keyword, this topic describes this keyword.

Usage

Synchronized can be used to modify the instance method, static method, and code block of the class. Let's take a look at it.

Instance method

In the previous section, we introduced an example of counting. When multiple threads execute counter ++ concurrently, unexpected results may occur because the statement is not an atomic operation, this problem can be solved using synchronized.

Let's look at the Code:

public class Counter {    private int count;    public synchronized void incr(){        count ++;    }        public synchronized int getCount() {        return count;    }}

Counter is a simple Counter class. Both the incr method and the getCount method are modified with synchronized. After synchronized is added, the code in the method becomes an atomic operation. When multiple threads concurrently update the same Counter object, no problem occurs. Let's look at the code used:

public class CounterThread extends Thread {    Counter counter;    public CounterThread(Counter counter) {        this.counter = counter;    }    @Override    public void run() {        try {            Thread.sleep((int) (Math.random() * 10));        } catch (InterruptedException e) {        }        counter.incr();    }    public static void main(String[] args) throws InterruptedException {        int num = 100;        Counter counter = new Counter();        Thread[] threads = new Thread[num];        for (int i = 0; i < num; i++) {            threads[i] = new CounterThread(counter);            threads[i].start();        }        for (int i = 0; i < num; i++) {            threads[i].join();        }        System.out.println(counter.getCount());    }}

Similar to the previous section, we have created 100 threads and passed the same counter object. Each thread is mainly used to call the Counter incr method, the main thread waits for the subthread to end and outputs the counter value. This time, no matter how many times it runs, the result is 100 correct.

Here, what exactly does synchronized do? It seems that synchronized allows only one thread to execute instance methods at the same time, but this understanding is inaccurate. Multiple Threads can execute the same synchronized instance method at the same time, as long as they access different objects, such:

Counter counter1 = new Counter();Counter counter2 = new Counter();Thread t1 = new CounterThread(counter1);Thread t2 = new CounterThread(counter2);t1.start();t2.start();

Here, t1 and t2 threads can execute the Counter incr method at the same time, because they access different Counter objects, one being counter1 and the other being counter2.

Therefore, the synchronized instance method actually protects the method calls of the same object and ensures that only one thread can be executed at the same time. Specifically, the synchronized instance method protects the current instance object, that is, this. this object has a lock and a waiting queue. The lock can only be held by one thread, other threads trying to obtain the same lock need to wait. The process of executing the synchronized instance method is roughly as follows:

The actual execution process of synchronized is much more complex than that, and the Java virtual machine uses a variety of optimization methods to improve performance, but we can understand it in this simple way.

When the current thread cannot obtain the lock, it will join the waiting queue and wait, and the thread status will change to BLOCKED.

We can emphasize that synchronized protects objects rather than code. As long as you access the synchronized Method of the same object, even different code will be accessed in a synchronous order, for example, for the two instance methods getCount and incr in Counter, for the same Counter object, one thread executes getCount and the other executes incr. They cannot be executed at the same time and will be executed in the synchronized synchronous sequence.

In addition, it should be noted that the synchronized method cannot prevent non-synchronized methods from being executed at the same time. For example, if you add a non-synchronized Method to the Counter class:

public void decr(){    count --;}

This method can be executed at the same time as the synchronized incr method, which usually produces unexpected results. Therefore, when you protect variables, you need to add synchronized to all methods that access the variable.

Static Method

Synchronized can also be used for static methods, such:

public class StaticCounter {    private static int count = 0;    public static synchronized void incr() {        count++;    }    public static synchronized int getCount() {        return count;    }}

As we mentioned above, synchronized protects objects. For instance methods, this is the current instance object. For static methods, which object is protected? It is a class Object. Here it is StaticCounter. class. In fact, each object has a lock and a waiting queue, and the class object is no exception.

The synchronized static method and the synchronized instance method protect different objects. Different threads can execute the synchronized static method at the same time, and the other executes the synchronized instance method.

Code block

In addition to the modifier, synchronized can also be used to wrap code blocks. For example, for the previous Counter class, the equivalent code can be:

public class Counter {    private int count;    public void incr(){        synchronized(this){            count ++;            }    }        public int getCount() {        synchronized(this){            return count;        }    }}

Synchronized brackets contain protected objects. For instance methods, this, {} contains the code for Synchronous execution.

For the previous StaticCounter class, the equivalent code is:

public class StaticCounter {    private static int count = 0;    public static void incr() {        synchronized(StaticCounter.class){            count++;            }        }    public static int getCount() {        synchronized(StaticCounter.class){            return count;            }    }}

The synchronized synchronization object can be any object. Any object has a lock and a waiting queue, or any object can be used as a lock object. For example, the equivalent code of Counter can also be:

public class Counter {    private int count;    private Object lock = new Object();        public void incr(){        synchronized(lock){            count ++;            }    }        public int getCount() {        synchronized(lock){            return count;        }    }} 

Understanding synchronized

This article introduces the basic usage and principles of synchronized. We will further understand synchronized from the following perspectives:

  • Reentrant
  • Memory visibility
  • Deadlock

Reentrant

Synchronized has an important feature that can be reentrant. That is to say, for the same execution thread, when it acquires the lock and calls other code that requires the same lock, it can be called directly. For example, in a synchronized instance method, other synchronized instance methods can be called directly. Reentrant is a natural attribute and should be easy to understand. It is emphasized that not all locks are reentrant (as described in the following sections ).

Reentrant is implemented by record lock holding threads and holding quantity. When calling code protected by synchronized, check whether the object has been locked. If yes, check whether the thread is locked. If yes, increase the hold quantity. If not locked by the current thread, add the lock to the waiting queue. When the lock is released, reduce the hold quantity, the entire lock is released only when the number changes to 0.

Memory visibility

For complex operations, synchronized can implement atomic operations to avoid competing conditions. But does synchronized need to be added to the obvious atomic operation methods? For example, for the following switch class Switcher, it only has a boolean variable on and the corresponding setter/getter method:

public class Switcher {    private boolean on;    public boolean isOn() {        return on;    }    public void setOn(boolean on) {        this.on = on;    }}

Is there a problem when multiple threads access the same Switcher object at the same time? There is no race condition problem, but as mentioned in the previous section, there is a memory visibility problem, and synchronized can solve this problem.

In addition to atomic operations, synchronized also plays an important role in ensuring memory visibility. When the lock is released, all writes will be written back to the memory. After obtaining the lock, read the latest data from the memory.

However, to ensure memory visibility, synchronzied has a high cost. A more lightweight method is to add a modifier volatile to the variable, as shown below:

public class Switcher {    private volatile boolean on;    public boolean isOn() {        return on;    }    public void setOn(boolean on) {        this.on = on;    }}

After volatile is added, Java inserts special commands when operating the corresponding variables to ensure that the latest memory value is read and written, rather than the cached value.

Deadlock

When synchronized or other locks are used, pay attention to deadlocks. deadlock is similar to this phenomenon. For example, there are two threads a and B, and a holds the lock A, waiting for the lock B, B holds the lock B and waits for the lock A, a, and B to wait for each other. At last, no one can execute the lock. The sample code is as follows:

public class DeadLockDemo {    private static Object lockA = new Object();    private static Object lockB = new Object();    private static void startThreadA() {        Thread aThread = new Thread() {            @Override            public void run() {                synchronized (lockA) {                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                    }                    synchronized (lockB) {                    }                }            }        };        aThread.start();    }    private static void startThreadB() {        Thread bThread = new Thread() {            @Override            public void run() {                synchronized (lockB) {                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                    }                    synchronized (lockA) {                    }                }            }        };        bThread.start();    }    public static void main(String[] args) {        startThreadA();        startThreadB();    }}

After running, the aThread and bThread are waiting for each other. How can this problem be solved? First, try to avoid applying for another lock while holding one lock. If multiple locks are required, all codes should apply for the lock in the same order. For example, for the above example, we can agree to apply for lockA first and then apply for lockB.

However, such conventions may be difficult to implement in complex project code. Another method is to use the explicit Lock interface Lock described in the subsequent sections. It supports the methods of trying to obtain a Lock (tryLock) and obtaining a Lock with a time limit, using these methods, you can release the lock that you already hold when you cannot obtain the lock, and then try to get the lock again or simply give up to avoid deadlocks.

What if a deadlock still occurs? Java does not take the initiative to handle it. However, with some tools, we can find deadlocks in the running process. For example, the jstack command in Java reports the deadlocks found. For the above program, on my computer, jstack has the following report:

Synchronization container and precautions

Synchronous container

We have introduced Collection methods in section 54, which can return thread-safe synchronous containers, such:

public static <T> Collection<T> synchronizedCollection(Collection<T> c)public static <T> List<T> synchronizedList(List<T> list)public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

They add synchronized to all container methods for security, such as SynchronizedCollection. Some of the Code is as follows:

static class SynchronizedCollection<E> implements Collection<E> {    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;    }    public int size() {        synchronized (mutex) {return c.size();}    }    public boolean add(E e) {        synchronized (mutex) {return c.add(e);}    }    public boolean remove(Object o) {        synchronized (mutex) {return c.remove(o);}    }    //....}

Here, thread security targets container objects. It means that when multiple threads concurrently access the same container object, no additional synchronization operations are required and no error results are returned.

With synchronized added, all method calls become atomic operations. Is it absolutely safe when the client is called? No, at least pay attention to the following situations:

  • Compound operations, such as checking before updating
  • Pseudo Synchronization
  • Iteration

Compound operation

Let's first look at the compound operation. Let's look at the code section:

Public class EnhancedMap <K, V> {Map <K, V> map; public EnhancedMap (Map <K, V> map) {this. map = Collections. synchronizedMap (map);} public V putIfAbsent (K key, V value) {V old = map. get (key); if (old! = Null) {return old;} map. put (key, value); return null;} public void put (K key, V value) {map. put (key, value );}//... other code}

EnhancedMap is a decoration class that accepts a Map object and calls synchronizedMap to convert it to synchronize the map of the container object. A putIfAbsent method is added only when no corresponding key exists in the original Map.

Every method of map is safe, but is this composite method putIfAbsent safe? Obviously, it is no. This is a composite operation that checks and then updates. In the case of multithreading, multiple threads may have completed the check, all of them find that there is no corresponding key in the Map, and then they will call put, which destroys the expected semantics of the putIfAbsent method.

Pseudo Synchronization

Can synchronized be added to this method for security? As follows:

public synchronized V putIfAbsent(K key, V value){    V old = map.get(key);    if(old!=null){        return old;    }    map.put(key, value);    return null;}

The answer is no! Why? The error object has been synchronized. PutIfAbsent uses the EnhancedMap object synchronously, while other methods (such as the put Method in the Code) use the map object returned by Collections. synchronizedMap. The two are different objects. To solve this problem, all methods must use the same lock. You can use the object lock of EnhancedMap or map. If the EnhancedMap object is used as the lock, synchronized must be added to all methods in EnhancedMap. Use map as the lock. The putIfAbsent method can be changed:

public V putIfAbsent(K key, V value){    synchronized(map){        V old = map.get(key);         if(old!=null){             return old;         }         map.put(key, value);         return null;        }}

Iteration

For synchronous container objects, although a single operation is secure, iteration is not. Let's take a look at an example to create a synchronous List object, one thread modifies the List, and the other traverses to see what will happen. The code is:

private static void startModifyThread(final List<String> list) {    Thread modifyThread = new Thread(new Runnable() {        @Override        public void run() {            for (int i = 0; i < 100; i++) {                list.add("item " + i);                try {                    Thread.sleep((int) (Math.random() * 10));                } catch (InterruptedException e) {                }            }        }    });    modifyThread.start();}private static void startIteratorThread(final List<String> list) {    Thread iteratorThread = new Thread(new Runnable() {        @Override        public void run() {            while (true) {                for (String str : list) {                }            }        }    });    iteratorThread.start();}public static void main(String[] args) {    final List<String> list = Collections            .synchronizedList(new ArrayList<String>());    startIteratorThread(list);    startModifyThread(list);}

Run the program and the program throws a concurrent modification exception:

Exception in thread "Thread-0" java.util.ConcurrentModificationException    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)    at java.util.ArrayList$Itr.next(ArrayList.java:831)

We have introduced this exception before. If a structural change occurs in the container while traversing, this exception will be thrown, and the synchronization container does not solve this problem. If you want to avoid this exception, you need to lock the entire container object during traversal. For example, the above Code, startIteratorThread can be changed:

private static void startIteratorThread(final List<String> list) {    Thread iteratorThread = new Thread(new Runnable() {        @Override        public void run() {            while (true) {                synchronized(list){                    for (String str : list) {                    }                    }            }        }    });    iteratorThread.start();}

Concurrent container

In addition to the above considerations, the performance of the synchronization container is also relatively low, when the concurrency traffic is relatively large, the performance is very poor. Fortunately, there are many container classes specifically designed for concurrency in Java, such:

  • CopyOnWriteArrayList
  • ConcurrentHashMap
  • Concurrent1_queue
  • ConcurrentSkipListSet

These container classes are thread-safe, but they do not use synchronized, have no iteration problems, directly support some composite operations, and have much higher performance. What problems can they solve? How to use it? What is the implementation principle? We will leave it for subsequent chapters.

Summary
This section describes in detail the usage and implementation principles of synchronized. To further understand synchronized, it introduces reentrant, memory visibility, deadlocks, and so on. Finally, this section describes the synchronization containers and their precautions, such as composite operations, pseudo synchronization, iteration exceptions, and concurrent containers.

In addition to competing access to the same resource, multiple threads often need to collaborate with each other. How can they work together? The following section describes the basic mechanism of collaboration: wait/notify.

(As in other sections, all the code in this section is in the https://github.com/swiftma/program-logic)

----------------

For more information, see the latest article. Please pay attention to the Public Account "lauma says programming" (scan the QR code below), from entry to advanced, ma and you explore the essence of Java programming and computer technology. Retain All copyrights with original intent.

 

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.