[Idea of Java Programming] Understanding synchronized

Usage and Rationale

Synchronized can be used to decorate instance methods, static methods and code blocks of classes

instance method

When introducing the basics of concurrency, part of it was about race conditions, when multiple threads access and operate on the same object, and because the statement is not an atomic operation, you get incorrect results. This place can be handled with synchronized

public class Counter {

    private int count;

    public synchronized void incr() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

}

Counter is a simple counter, incr method and getCount method are decorated with synchronized.
After adding synchronized, the code in the method becomes an atomic operation, and there is no problem when multiple threads update the same Counter object concurrently.

public class CounterThread extends Thread {

    Counter counter;

    public CounterThread(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            counter.incr();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        Thread[] threads = new Thread[num];
        Counter counter = new Counter();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new CounterThread(counter);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println(counter.getCount());
    }

}

This is the modified code. Multiple threads are counted, and the result is expected.

It seems that synchronized allows only one thread to execute the instance method at the same time. In fact, multiple threads can execute the same synchronized instance method at the same time, as long as the objects they access are different.

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

The two threads t1 and t2 here can execute the incr method of Counter at the same time, because they access different Counter objects.
Therefore, synchronized instance methods protect method calls of the same object , ensuring that only one thread executes at the same time. The object has a lock and a wait queue, the lock can only be held by one thread, and other threads trying to acquire the same lock need to wait.

Synchronized protects objects rather than code. As long as the synchronized methods of the same object are accessed, even different codes will be accessed in synchronization 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, and they cannot be executed at the same time.

static method

For static methods, synchronized protects the class object.

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

code block

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

The object in the synchronized brackets is the protected object, which can be any object. Any object has a lock and a waiting queue, that is, any object can be used as a lock object.

Further understanding of synchronized

reentrancy

For the same execution thread, it can be called directly after it acquires the synchronized lock and then calls other code that needs the same lock.

Reentrancy is achieved by recording the lock holding thread and the number of holdings: when calling the code protected by synchronized, check whether the object is locked, if so, check whether it is locked by the current thread, and if so, increase The number of holdings; if it is not locked by the current thread, it will join the waiting queue. When the lock is released, the number of holdings is reduced, and the entire lock is released when the number becomes 0.

memory visibility

In the basics of concurrency, the problem of memory visibility is mentioned. Multiple threads can share access and operation of the same variable, but the modification of a shared variable by one thread may not be seen by another thread immediately, or even forever. can not see.

Synchronized ensures memory visibility, all writes are written back to memory when the lock is released, and the latest data is read from memory after the lock is acquired.

deadlock

Use synchronized or other locks, pay attention to deadlocks.
The so-called deadlock is: there are two threads a and b, a holds lock A and is waiting for lock B, and b holds lock B and is waiting for lock A. a and b enter the state of waiting for each other

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) {
                        e.printStackTrace();
                    }
                    System.out.println("A sleep over");
                    synchronized (lockB) {
                        System.out.println("A 持有了B锁");
                    }
                }
            }
        };
        aThread.start();
    }
    private static void startThreadB() {
        Thread bThread = new Thread(){
            @Override
            public void run() {
                synchronized (lockB) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("B sleep over");
                    synchronized (lockA) {
                        System.out.println("B 持有了A锁");
                    }
                }
            }
        };
        bThread.start();
    }
    public static void main(String[] args) {
        startThreadA();
        startThreadB();
    }
}

After running, aThread and bThread are caught in waiting for each other.
How to solve?
1. You should avoid holding one lock while applying for another lock. If you do need multiple locks, all code should apply for the locks in the same order.
2. Use Display Lock

Sync container

There are methods in Collections that return thread-safe synchronized containers

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 to achieve safety.

The thread safety here refers to container objects, which means that when multiple threads access the same container object concurrently, no additional synchronization operations are required.

With synchronized, all method calls become atomic operations. Is it absolutely safe? No, at least the following situations need to be considered

Compound operation

First understand what is a composite operation

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;
        }
        return map.put(key, value);
    }   
}

EnhancedMap is a decorative class that converts Map objects into synchronous container objects and adds a putIfAbsent method, which adds value only when there is no corresponding key in the original Map.
Every method of map is safe, but is this resurrection method putIfAbsent safe? the answer is negative. This is a composite operation of checking and then updating. In the case of multi-threading, multiple threads may have completed the check step and found that there is no corresponding key in the Map, and then call put, which destroys the security. .

Pseudo synchronization

Is it safe to add synchronized to the above putIfAbsent method? The answer is not necessarily

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

Writing like the above, it still cannot be synchronized, because the synchronized object is wrong. putIfAbsent uses the EnhancedMap object synchronously, while put uses the map object. In this way, if one thread calls put and one calls putIfAbsent, it is still not safe.

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

iterate

For synchronized container objects, while a single operation is safe, iteration is not.

private static void startModifyThread(List<String> list) {
    Thread modifyThread = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                list.add(i + "");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    modifyThread.start();
}
private static void startIteratorThread(List<String> list) {
    Thread iteratorThread = new Thread(){
        @Override
        public void run() {
            while(true) {
                for(String str: list) {
                    System.out.println(str);
                }
            }
        }
    };
    iteratorThread.start();
}

public static void main(String[] args) {
    List<String> list = Collections.synchronizedList(new ArrayList<String>());
    startModifyThread(list);
    startIteratorThread(list);
}

Structural changes are not allowed when using the enhanced for loop iteration. Synchronizing containers does not solve this problem. If you want to do this, you need to lock the entire container object when traversing

Thread iteratorThread = new Thread(){
    @Override
    public void run() {
        while(true) {
            synchronized(list) {
                for(String str: list) {
                    System.out.println(str);
                }
            }           
        }
    }
};

concurrent container

The performance of synchronous containers is relatively low, so you can use special concurrent container classes
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324770134&siteId=291194637
Recommended