Jingdong Interviewer: Tell me about the difference between synchronized and ReentrantLock!

Preface

Earlier we introduced a lot of content about multi-threading. In multi-threading, there is a very important subject that we need to overcome, that is, thread safety. Thread safety issues refer to data pollution or other unexpected program running results caused by simultaneous operations between threads in multiple threads.

Thread safe

1) Non-thread-safe cases

For example, if A and B transfer money to C at the same time, suppose that C's original balance is 100 yuan, and A transfers 100 yuan to C, and it is in the process of transferring. At this time, B also transfers 100 yuan to C. At this time, A transfers to C successfully. , The balance becomes 200 yuan, but B inquires that C’s balance is 100 yuan in advance, and it is also 200 yuan after the transfer is successful. When both A and B have completed the transfer to C, the balance is still 200 yuan instead of the expected 300 yuan, which is a typical thread safety problem.

2) Non-thread-safe code example

It doesn't matter if you don't understand the above content, let's look at the specific code that is not thread-safe:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -› addNumber());
        Thread thread2 = new Thread(() -› addNumber());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

The execution results of the above program are as follows:

number:12085

The result of each execution may vary slightly, but it is almost never equal to the (correct) cumulative sum of 20000.

3) Thread-safe solution

The thread-safe solution has the following dimensions:

  • Data is not shared, single thread is visible, such as ThreadLocal is single thread visible;
  • Use thread-safe classes, such as StringBuffer and safety classes under JUC (java.util.concurrent) (will be specifically introduced in the following article);
  • Use synchronization codes or locks.

Thread synchronization and lock

1)synchronized

① Introduction to synchronized

Synchronized is a synchronization mechanism provided by Java. When a thread is operating a synchronized code block (synchronized modified code), other threads can only block and wait for the original thread to execute before executing.

② synchronized use

synchronized can modify the code block or method, the sample code is as follows:

// 修饰代码块
synchronized (this) {
    // do something
}
// 修饰方法
synchronized void method() {
    // do something
}

Use synchronized to complete the non-thread-safe code at the beginning of this article.

Method 1: Use synchronized to decorate the code block, the code is as follows:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        Thread sThread2 = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

The execution results of the above program are as follows:

number:20000

Method 2: Use synchronized modification method, the code is as follows:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› addNumber());
        Thread sThread2 = new Thread(() -› addNumber());
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public synchronized static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

The execution results of the above program are as follows:

number:20000

③ Synchronized realization principle

The essence of synchronized is to achieve thread safety by entering and exiting the Monitor object. Take the following code as an example:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("Java");
        }
    }
}

When we use javap to compile, the generated bytecode is as follows:

Compiled from "SynchronizedTest.java"
public class com.interview.other.SynchronizedTest {
  public com.interview.other.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."‹init›":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/interview/other/SynchronizedTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Java
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

It can be seen that the JVM (Java Virtual Machine) uses the monitorenter and monitorexit instructions to achieve synchronization. The monitorenter instruction is equivalent to locking, and the monitorexit is equivalent to releasing the lock. The monitorenter and monitorexit are implemented based on Monitor.

2)ReentrantLock

① Introduction to ReentrantLock

ReentrantLock (reentry lock) is a lock implementation provided by Java 5, and its function is basically the same as synchronized. The reentry lock acquires the lock by calling the lock() method, and releases the lock by calling unlock().

② Use of ReentrantLock

The basic use of ReentrantLock, the code is as follows:

Lock lock = new ReentrantLock();
lock.lock();    // 加锁
// 业务代码...
lock.unlock();    // 解锁

Use ReentrantLock to improve the non-thread-safe code at the beginning of this article, please refer to the following code:

public class LockTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // ReentrantLock 使用
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Try to acquire the lock

ReentrantLock can try to access the lock without blocking, using the tryLock() method, which is specifically used as follows:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        Thread.sleep(2 * 1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock());
        Thread.sleep(2 * 1000);
        System.out.println(reentrantLock.tryLock());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

The execution result of the above code is as follows:

false
true

Try to acquire the lock for a period of time

tryLock() has an extension method tryLock(long timeout, TimeUnit unit) to try to acquire the lock for a period of time. The specific implementation code is as follows:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        System.out.println(LocalDateTime.now());
        Thread.sleep(2 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
        System.out.println(LocalDateTime.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

The execution result of the above code is as follows:

2019-07-05 19:53:51
true
2019-07-05 19:53:53

It can be seen that the lock is directly acquired by thread two after sleeping for 2 seconds, so the timeout parameter in the tryLock(long timeout, TimeUnit unit) method refers to the maximum waiting time for acquiring the lock.

③ ReentrantLock precautions

When using ReentrantLock, you must remember to release the lock, otherwise the lock will be permanently occupied.

Related interview questions

1. What are the commonly used methods of ReentrantLock?

Answer: The common methods of ReentrantLock are as follows:

  • lock(): used to obtain the lock
  • unlock(): used to release the lock
  • tryLock(): try to acquire the lock
  • getHoldCount(): Query the number of times the current thread executes the lock() method
  • getQueueLength(): Returns the number of threads that are queuing to acquire this lock
  • isFair(): Whether the lock is a fair lock

2. What are the advantages of ReentrantLock?

Answer: ReentrantLock has the feature of acquiring the lock in a non-blocking way, using the tryLock() method. ReentrantLock can interrupt the acquired lock. After acquiring the lock using the lockInterruptibly() method, if the thread is interrupted, an exception will be thrown and the currently acquired lock will be released. ReentrantLock can acquire the lock within the specified time range, using the tryLock(long timeout, TimeUnit unit) method.

3. How does ReentrantLock create a fair lock?

Answer: new ReentrantLock() creates an unfair lock by default. If you want to create a fair lock, you can use new ReentrantLock(true).

4. What is the difference between fair lock and unfair lock?

Answer: Fair lock refers to the order in which threads acquire locks in accordance with the lock order, while non-fair lock refers to the lock grab mechanism. The thread that lock() first does not necessarily acquire the lock first.

5. What is the difference between lock() and lockInterruptibly() in ReentrantLock?

Answer: The difference between lock() and lockInterruptibly() is that if the thread is interrupted while acquiring the thread, lock() will ignore the exception and continue to wait for the acquiring thread, while lockInterruptibly() will throw an InterruptedException. Problem analysis: Execute the following code, and use lock() and lockInterruptibly() in the thread to view the running results. The code is as follows:

 Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            interruptLock.lock();
            //interruptLock.lockInterruptibly();  // java.lang.InterruptedException
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

If you execute the following code, you will find that the program will not report an error when using lock(), and exit directly after the operation is completed; while using lockInterruptibly() will throw an exception java.lang.InterruptedException, which means: if the thread is interrupted on the way to get the thread , Lock() will ignore the exception and continue to wait to acquire the thread, while lockInterruptibly() will throw an InterruptedException.

6. What is the difference between synchronized and ReentrantLock?

Answer: Both synchronized and ReentrantLock guarantee thread safety, and their differences are as follows:

  • ReentrantLock is more flexible to use, but there must be a cooperative action to release the lock;
  • ReentrantLock must manually acquire and release the lock, while synchronized does not need to manually release and open the lock;
  • ReentrantLock only applies to code block locks, while synchronized can be used to modify methods, code blocks, etc.;
  • The performance of ReentrantLock is slightly higher than that of synchronized.

7. ReentrantLock's tryLock(3, TimeUnit.SECONDS) means waiting for 3 seconds before acquiring the lock. Is this statement correct? why?

Answer: No, tryLock(3, TimeUnit.SECONDS) means that the maximum waiting time for acquiring the lock is 3 seconds, during which time it will always try to acquire instead of waiting 3 seconds before acquiring the lock.

8. How does synchronized realize lock upgrade?

Answer: There is a threadid field in the object header of the lock object. The threadid is empty when it is accessed for the first time. The JVM (Java Virtual Machine) allows it to hold a biased lock and sets the threadid to its thread id. At this time, it will first judge whether the threadid is consistent, especially the thread ID. If it is consistent, it can be used directly. If it is inconsistent, the upgrade bias lock is a lightweight lock. The lock is acquired through a certain number of spin cycles without blocking. After a certain number of executions, the lock is acquired. Will be upgraded to a heavyweight lock, enter the block, the whole process is the process of lock upgrade.

to sum up

This article introduces two ways of thread synchronization, synchronized and ReentrantLock. ReentrantLock is more flexible and efficient. However, ReentrantLock can only modify the code block. Using ReentrantLock requires the developer to manually release the lock. If you forget to release the lock, the lock will always be Occupied. Synchronized uses a wider range of scenarios, which can modify common methods, static methods and code blocks, etc. The editor also summarizes a multi-threaded mind map here, in order to facilitate the friends to better organize the technical points, and share it with everyone!

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-0WEYlMrX-1614240957624)(https://ask8088-private-1251520898.cn-south.myqcloud.com/developer -images/article/7948575/n2ecyj3vob.png?q-sign-algorithm=sha1&q-ak=AKID2uZ1FGBdx1pNgjE3KK4YliPpzyjLZvug&q-sign-time=1614240359;1614247559&q-key-time=1614240359-q-key-time=1614240359-q-keylist =&q-signature=d02e79d208db8b3706d7c9fd331fd49efb9bdaf9)]

Guess you like

Origin blog.csdn.net/QLCZ0809/article/details/114089183