This is the 22nd day of my participation in the Gwen Challenge

In my last article, I summarized how threads are created and some of their properties, as well as discussing thread sharing and the atomicity and memory visibility issues that come with it. Here’s how to use the synchronized keyword to solve those two problems.


1. The usage and basic principles of synchronized

Synchronized can modify instance methods, static methods, and code blocks.

Since counter++ is not an atomic operation, the output in multithreading is often not what we expected. Now let’s see how to solve this problem in three ways.

(1) Modify instance methods

public class Counter { private int counter = 0; public synchronized void incr() { counter++; } public synchronized int getCounter() { return counter; }}Copy the code

The Counter class is a simple Counter class with two methods, one that increments the count and the other that returns the count, and both of which are synchronized, so that the code inside the method is atomic and there is no problem when multiple threads update the same Counter object.

public class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i = 0; i < 1000; i++) { counter.incr(); } } public static void main(String[] args) throws InterruptedException { int num = 1000; 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.getCounter()); }}Copy the code

No matter how many times I run it, I get 1000 by 1000.

So what exactly does synchronized do here? On the surface, this means that only one thread can execute the instance method at a time, but there is one condition: the same object. Yes, synchronized allows threads to execute sequentially if multiple threads access an instance method of the same object, whereas synchronized allows multiple threads to access the same synchronized method at the same time if they access different objects.

Such as

Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new CounterThread(c1);
Thread t2 = new CounterThread(c2);
t1.start();
t2.start();
Copy the code

In this case, both threads T1 and T2 can execute Counter incr at the same time, because they are accessing different Counter objects.

Conversely, if you access synchronized methods of the same object, then even different synchronized methods need to wait. For example, getCounter and incr in the Counter class, for the same Counter object, one thread executes getCounter and the other thread executes incr. Although they are different methods, they still cannot be executed at the same time and will be executed sequentially by synchronized synchronization.

So synchronized actually protects method calls on the same object, ensuring that only one thread executes at the same time. More specifically, synchronized protects the current instance object, namely this, which has a lock and a wait queue. The lock can only be owned by one thread, and other threads must wait to acquire the same lock. The general process for executing an instance method of synchronized modification is as follows:

1. Try to acquire the lock, if so, proceed to the next step, otherwise join the wait queue, block and wait to wake up, and the thread state becomes BLOCKED. 2. Execute the code inside the instance method. 3. Release the lock. If there are waiting threads in the waiting queue, the system wakes up one of them.

The actual execution of synchronized is much more complicated than that, but it can be understood in this simple way.

It should also be noted that synchronized methods do not prevent non-synchronized methods from being executed simultaneously. Adding a non-synchronized method to a Counter class, for example, can be executed together with incr methods, which can often have unexpected results. For a variable, it is common to add synchronized to all methods that access the variable.

(2) Modify static methods

public class StaticCounter { private static int counter = 0; public static synchronized void incr() { counter++; } public static synchronized int getCounter() { return counter; }}Copy the code

Synchronized protects the current instance object this. Synchronized protects the current instance object this. Synchronized protects the current instance object this. Is a class object. In the example above, StaticCounter. Class, each object has a lock and a wait queue, and class objects are no exception.

Because synchronized static and synchronized instance methods protect different objects, two different threads can execute one synchronized static method and the other synchronized instance method.

(3) Modify the code block

public class Counter { private int counter = 0; public void incr() { synchronized(this) { counter++; } } public int getCounter() { synchronized(this) { return counter; }}}Copy the code

Synchronized is the protected object in the parenthesis. For instance methods, this. For the StaticCounter class above, the equivalent code is as follows

public class StaticCounter { private static int counter = 0; public static void incr() { synchronized(StaticCounter .class) { counter++; } } public static int getCounter() { synchronized(StaticCounter .class) { return counter; }}}Copy the code

Synchronized objects can be any object, which has a lock and a wait queue, or any object can be a lock object.

For example, the equivalent code for Counter could be as follows

public class Counter { private int counter = 0; private Object lock = new Object(); public void incr() { synchronized(lock) { counter++; } } public int getCounter() { synchronized(lock) { return counter; }}}Copy the code

Learn more about synchronized

After introducing the basic usage and principle of synchronized, it is further introduced from the following three aspects

  • reentrancy
  • Memory visibility
  • A deadlock

(1) Reentrancy Reentrancy means that if a thread has acquired a lock, it can call other code that requires the same lock directly. For example, within a synchronized instance method, you can call other synchronized instance methods directly.

Reentrant is achieved by recording the thread and number of locks held. When the code of synchronized protection is called, check whether the object is locked; if so, check whether it is held by the current thread; if so, increase the number of held; if not, the thread joins the waiting queue; when the lock is released, reduce the number of held; when the number of held becomes 0, the whole lock is released.

(2) Memory visibility

Synchronized guarantees memory visibility in addition to atomicity. When the lock is released, all writes are written to memory, and when the lock is acquired, the latest data is read from memory.

But synchronized is a bit expensive to use just for memory visibility, and we can use the volatile keyword to modify variables, such as the memory visibility problem in the previous article. The code can be written as follows to solve the memory visibility problem.

public class VisibilityDemo {
    private static volatile boolean shutdown = false;
    static class HelloThread extends Thread {
        @Override
        public void run() {
            while(!shutdown) {
                System.out.println("1");
            }
            System.out.println("exit hello");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new HelloThread().start();
        Thread.sleep(1000);
        shutdown = true;
        System.out.println("exit main");
    }
}
Copy the code

You can see that the shutdown variable is decorated with volatile. With volatile, Java inserts special instructions to ensure that the variable is read to the latest value in memory, not the cached value.

(3) The use of synchronized or other locks may lead to deadlock. For example, there are two threads, A and B. Thread A holds lock A and waits for lock B, and thread B holds lock B and waits for lock A, so that a and B wait for each other and never execute.

public class DeadLockDemo { private static Object lock1 = new Object(); private static Object lock2 = new Object(); private static void startThreadA() { Thread thread1 = new Thread() { @Override public void run() { synchronized (lock1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { } } } }; thread1.start(); } private static void startThreadB() { Thread thread2 = new Thread() { @Override public void run() { synchronized (lock2) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { } } } }; thread2.start(); } public static void main(String[] args) { startThreadA(); startThreadB(); }}Copy the code

You should avoid holding one lock while applying for another, and if you do need multiple locks, all code should apply for the locks in the same order. For the example above, you can agree to apply for lock1 first and lock2 later.


The articles

Concurrency basics basic concepts of threads

Concurrency basics: Collaboration between threads