An article to understand the familiar and unfamiliar thread locks in Java

1. Data synchronization

1. Atomic class

For the atomicity of multi-threaded data, in addition to locking,  Java a series of atomic classes (such as etc.) are provided  AtomicInteger to ensure the atomicity of the data. The core obtains  Unsafe the instance through reflection to realize  CAS the operation, ensuring the atomicity of the data.

Taking  AtomicInteger a class as an example, its common methods and their descriptions are listed in the following table.

method effect
set() Sets a value for an object.
get() Get the object value.
incrementAndGet() Returns the value of the object incremented by 1, similarly there is getAndIncrement().
decrementAndGet() Returns the value of the object after decrementing by 1, similarly there is getAndDecrement().

Also as  AtomicInteger an example, the sample code is as follows:

 
 

java

copy code

public void AtomicDemo() { AtomicInteger atomicInteger = new AtomicInteger(1); // 修改值 atomicInteger.set(2); System.out.println("get: " + atomicInteger.get()); // 等价自增 atomicInteger.incrementAndGet(); System.out.println("incrementAndGet: " + atomicInteger); // 等价自减 atomicInteger.decrementAndGet(); System.out.println("decrementAndGet: " + atomicInteger); }

(1)AtomicReference

In addition to the built-in  AtomicInteger equivalent atomic class,  AtomicReference atomic operations can be provided for generic classes.

AtomicReference The declared object operation  AtomicInteger is similar to the atomic class that has been encapsulated by other systems. You can set the initialization value through keywords when defining  new , or  set() set the value through subsequent methods, and its operations are all thread-safe.

 
 

java

copy code

public void referenceDemo() { AtomicReference<Integer> atomicReference = new AtomicReference<>(0); atomicReference.updateAndGet(it -> it + 1); System.out.println("Get: " + atomicReference.get()); AtomicReference<User> userAtomic = new AtomicReference<>(new User("Alex")); userAtomic.getAndUpdate(it -> new User("Beth")); System.out.println("Get: " + userAtomic.get()); } static class User { private String name; public User(String name) { this.name = name; } }

2. Volatile

Before understanding  volatile keywords, we need to have a preliminary understanding of  JVM the working mechanism of memory.

(1) Memory mechanism

The memory in  JVM the medium can be roughly divided into  主内存 and  本地内存. By default, all variable declarations are stored in the medium  主内存 . When a thread needs to modify the value of a variable, it needs to  主内存 be read from it to the thread in the form of a copy  本地内存 . After the change is completed, the new value Rewrite it back  主内存. However, there is a time lag in the process of reading and writing back. If the data is not written back in time  主内存, the value obtained by other processes will still be the value before the change, which will cause data inconsistency.

(2) Volatile role

In order to solve the impact caused by the read and write time difference mentioned above,  keywords Java are provided in  volatile , that is,  volatile the declared variables are visible to any thread in real time, and at the same time, it can avoid the reordering of program instructions.

Note that it can only guarantee the variable  可见性, that is, the main memory data will be updated immediately when the variable changes, but if  volatile the variable is operated concurrently, the atomicity problem will still occur. At the same time, it should be noted that  volatile it can only modify  类变量 and  , and it  is illegal 实例变量 for  方法参数 and and  so on.常量

(3) Example demonstration

The following is an example to demonstrate  volatile the effect. In the example, two read and write threads are created, one is used to update data, and the other is used to monitor the  num change of variables and print out.

The complete test code is as follows, which are executed  num without  volatile modification and with  volatile modification.

 
 

java

copy code

public class VolatileExample { private volatile static int num = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { int local = num; while (local < 3) { // 监控 num 值变化,记录最新值 if (num != local) { System.out.println("receive change: " + num); local = num; } } }, "Reader").start(); new Thread(() -> { int local = num; while (local < 3) { // 修改 num 值 System.out.println("change to: " + ++local); num = local; try { // 休眠使 Reader 获取变化 sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Updater").start(); // 等待线程结束 TimeUnit.SECONDS.sleep(10); } }

volatile In the case of unused, when  the  Updater updated  num value is not written back to the main memory immediately,  Reader the data that cannot be read is still the variable before the change, so the  num != local condition is always  judged false, and the last printed information  update is therefore missing.

 
 

txt

copy code

// 未使用 volatile change to: 1 receive change: 1 change to: 2 change to: 3

In the case of use  , because  the multi-thread visibility of the variable volatile is realized  ,  the thread can also monitor   the actual change status of the variable.numReadernum

 
 

txt

copy code

// 使用 volatile change to: 1 receive change: 1 change to: 2 receive change: 2 change to: 3 receive change: 3

(4) Singleton mode

volatile It is usually used for status monitoring records under multi-threading, and it can also be used with locks to implement singleton mode.

In the following example code   , a simple singleton mode is realized by collaborating  volatile with and  . In the case of multi-threading,  the visibility of the instance   is guaranteed  , and at the same time  , the object is not repeatedly initialized by using the guarantee.synchronizedvolatileinstancesynchronized

Mainly note that the instance object needs to be judged twice in the synchronization block here  synchronized , assuming that if  线程A the lock is successfully acquired and instantiated under concurrent conditions, other threads are in the spin lock state during this process, when  线程A the object instantiation is completed and After  volatile the data is written back to the main memory through the keyword and the lock is released, the previously spinning thread will acquire the lock and execute the synchronization block content. If no secondary judgment is made, it will cause repeated instantiation, and because the instance declaration is judged  volatile accordingly The result will be  false returning the completed instance's object for exiting the synchronized block.

 
 

java

copy code

public class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { // 实例为空则获取锁 if (instance == null) { synchronized (Singleton.class) { // synchronized 防止多线程同时初始化实例 if (instance == null) instance = new Singleton(); } } return instance; } }

3. Command rearrangement

In the previous point, it was mentioned that  Volatile instruction rearrangement can be prohibited. First, let me introduce what instruction rearrangement is.

When running  Java a file, when there is no strong dependency between the lines of code, its execution order is different according to the order of writing, as shown in the example, the execution  int b = 20 may be  int a = 10 before the actual compilation, because the two are not strongly related, whichever comes first None of the definitions affect the correct execution of the program. But  int sum = a + b it must  be run after a and  b is defined, because  sum the value of is strongly dependent on  a and  b , this process is instruction rearrangement.

image.png

Variables modified by  Volatile keywords need to be read before writing, and must be written back into the main memory immediately after the writing is completed. The order of reading and writing is definite and cannot be changed, so it is impossible to implement instruction repetition. Row.

4. ThreadLocal

ThreadLocal As the name suggests, it is a separate storage space for each thread, which is usually used to store certain state values ​​in the thread. The variables between different threads  ThreadLocal exist independently and cannot communicate with each other, and the child threads will inherit  ThreadLocal the variables of the parent thread.

Note that  ThreadLocal after the use is completed, it must be  remove() recycled through the method, because  ThreadLocal the variable will be recycled as the cycle of the thread is terminated, but when it comes to the thread pool, each thread will not necessarily be destroyed after completing the task and returning to the thread pool. Entering the idle state means that the creation  ThreadLocal will always exist and cannot be garbage collected, resulting in memory leaks, so it is  recommended to  use it together and  release it  in  the setting ThreadLocal operation   .try catchfinallyremove()

method effect
get() Create a storage object for the current thread.
set() Modifies the value of the current thread's local object.
remove() Releases the current thread's local objects.
(1) Example introduction

In the following example, we create an  ThreadLocal<Integer> object that can store  Integer values ​​of a type that, when passed in different threads,  get() will initialize a separate copy for each thread.  Then we create two sub-threads, and get and set values ​​through threadLocal.get() the AND  method in each sub-thread  .threadLocal.set()

Running the example, you can see that each thread has its own independent value and will not interfere with each other. The two sub-threads output their own hash values.

 
 

java

copy code

public class ThreadLocalExample { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 创建两个子线程 Thread thread1 = new Thread(new MyRunnable()); Thread thread2 = new Thread(new MyRunnable()); // 启动子线程 thread1.start(); thread2.start(); try { // 等待子线程执行完毕 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } static class MyRunnable implements Runnable { @Override public void run() { // 在子线程中获取值 System.out.println("Thread value: " + threadLocal.get()); // 在子线程中设置新值 threadLocal.set(Thread.currentThread().hashCode()); // 再次获取值 System.out.println("Thread value after set: " + threadLocal.get()); } } }

(2) Application scenarios

When non-thread-safe operations are involved in concurrent operations, it is usually used to  ThreadLocal avoid concurrent operations on non-thread-safe objects.

If  Java the time formatting  SimpleDateFormat object is not thread-safe, an exception will be thrown when multiple threads operate concurrently. In this case, you can  ThreadLocal define a separate  SimpleDateFormat object for each thread.

In the following example, a thread pool with a capacity of simulating concurrency is defined  5 , and the implementation time is formatted in each thread  SimpleDateFormat . If  for a common  SimpleDateFormat object is defined in the previous step of the loop and called in each child thread, an exception will be thrown .

 
 

java

copy code

public class SafeDateFormatTest { private static final ThreadLocal<DateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> { return new SimpleDateFormat("yyyy-MM-dd"); }); public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); List<Future<Date>> results = new ArrayList<Future<Date>>(); // perform 10 date conversions for (int i = 0; i < 10; i++) { results.add(threadPool.submit(() -> { Date date; try { date = dateFormatThreadLocal.get().parse("2023-01-01"); } catch (ParseException e) { throw new RuntimeException(e); } return date; })); } threadPool.shutdown(); // look at the results for (Future<Date> result : results) { System.out.println(result.get()); } } }

Two, the introduction of the lock

1. Basic categories

Java There are many classifications of thread locks if subdivided. Here we only briefly introduce some commonly encountered classifications. Note that the following classifications are not mutually exclusive.

  • optimistic lock

    Optimistic locking, as the name implies, is to lock in a more relaxed manner. It is believed that other threads do not change the value but only read when holding the lock. Therefore, multiple objects are allowed to hold locks at the same time, and the peer version number mechanism is controlled to achieve consistency. .

  • pessimistic lock

    Pessimistic locking is just the opposite of optimistic locking. Any process of a task may change variables while holding a lock, so other processes cannot acquire the same lock instance during the lock holding period.

  • spin lock

    A spin lock is when the lock instance is acquired by another process and the current process repeatedly tries to acquire the lock (blocking), then such behavior is called a spin lock.

  • fair lock

    A fair lock means that when the lock is occupied by a process, other processes will enter the queue and wait if they need to acquire the lock. A first-come, first-served allocation mechanism is adopted. If the lock is  new ReentrantLock(true) created, it is a fair lock.

  • unfair lock

    Unfair lock means that after the lock is held by the process, other processes will randomly allocate it if they need to acquire the lock. This mechanism may lead to thread starvation, that is, the process that requested the first has been unable to acquire the lock. Common  synchronized and  ReentrantLock all Unfair lock.

  • reentrant lock

    A reentrant lock means that a lock instance can be acquired repeatedly by the same process. For example, a  ReentrantLock typical reentrant lock usually needs to be used with a counter. When the counter returns to zero, the lock is released.

  • non-reentrant lock

    A non-reentrant lock means that the lock instance cannot be acquired repeatedly by the same process. It synchronized is a reentrant lock itself, but it can achieve a non-reentrant effect with a counter.

2. The meaning of the lock

In a single thread, we clearly know which thread is manipulating variables, but obviously all this is not so clear in multithreading, because there is an obvious problem, that is, variable conflicts.

count To give a simple example, there are two threads in the program, each of which needs to perform  self-increment and self-decrement on the variable  1000 . Theoretically, the result value after the program is executed  count should be  0 . The sample code is as follows:

 
 

java

copy code

private int count = 0; public void demo() throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { count += 1; } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { count -= 1; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); // 输出结果未知 System.out.println(count); }

However, when the above program is executed, it is found that the results obtained for each output are different, and  0 the results are different each time. This is actually because  Java the corresponding steps of addition, subtraction, multiplication, and division in the underlying compilation are more than one step.

For example, the most commonly used self-increment  i = i + 1 corresponds to three steps in the underlying compilation implementation:  读取 ->  增加 ->  写入 , and any problem in any of these three steps will cause the final result to be abnormal. Going back to the above code, when  线程 1 the auto-increment operation is performed, it may actually only be done  读取 增加 . Before the writing is completed,  线程 2 the variable instance is obtained to complete the self-decrement  写入 , which leads to the auto-increment failure  -1 .

3. Hierarchical lock

In  JDK1.8 ,  the lock JVM will be upgraded according to the usage  synchronized , and it can generally follow the following path: 偏向锁 ->  轻量级锁 ->  重量级锁. Locks can only be upgraded and cannot be downgraded, so once they are upgraded to heavyweight locks, they can only be scheduled by the operating system.

The most important thing in the lock upgrade process is the object header  MarkWord , which contains  four parts Thread ID: ,  AgeBiasedTag . Among them, Biased there are  1bit size and Tag yes  2bit , and the lock upgrade is carried out according to   the three variable values ​​of Thread IdBiased, and  .Tag

  • Bias lock

    In the case where only one thread uses the lock, biased locks can ensure higher efficiency.

    When the first thread accesses the synchronization block for the first time, it will first check   whether  Mark Word the flag in  the object header is  , so as to judge whether the object lock is in the lock-free state or the biased lock state (anonymous biased lock).  It is also the default state of the lock. Once the thread acquires the lock, it will write   its own thread to it   . Before other threads acquire the lock, the lock is in a biased lock state.Tag0101IDMarkWord

  • lightweight lock

    When the next thread participates in the biased lock competition, it will first judge   whether  MarkWord the thread saved in  the  lock is equal to this thread. If not, the biased lock will be revoked immediately and upgraded to a lightweight lock.IDID

    Each thread participating in the competition will generate one in its own thread stack  LockRecord(LR), and then each thread  will  set the lock object header as a pointer to itself  by (CAS) spinning (that is, constantly trying to acquire the lock)   , which Successful thread setting means which thread acquires the lock.MarkWordLR

    When the lock is in the state of a lightweight lock, it can no longer be judged by a simple comparison  Tag value. Every time the lock is acquired, it needs to be spin. Of course, spin is also for scenarios where there is no lock competition. For example, after one thread finishes running, another thread acquires the lock. However, if the spin fails for a certain number of times, the lock will expand into a heavyweight lock.

  • heavyweight lock

    Heavyweight locks are our  synchronized intuitive understanding. In this case, the thread will hang, enter the operating system kernel state, wait for the scheduling of the operating system, and then map back to the user state. System calls are expensive, hence the name heavyweight locks.

    If the shared variables of the system are highly competitive, locks will quickly expand to heavyweight locks, and these optimizations will exist in name only. If the concurrency is very serious, you can  -XX:-UseBiasedLocking disable the biased lock through parameters. In theory, there will be some performance improvement, but it is not sure in practice.

3. Commonly used locks

In multi-threading, it is often necessary to lock shared variables or operations, so as to ensure that only one thread can operate on variables at the same time, and realize the atomicity of variables. The following is the lock object commonly used in the introduction  Java .

1. synchronized

synchronized It is one of the most basic lock objects. By adding  an synchronized implicitly lockable  this keyword before the method, other processes cannot obtain it when they want to access the variable. Only the lock that is automatically released after the execution of the synchronization block code ends. Other processes can Acquired again, so as to achieve the purpose of atomicity.

We can get the desired result by modifying the sample code mentioned earlier.

 
 

java

copy code

private int count = 0; public void demo() throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized(this) { // 加锁 synCount += 1; } } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized(this) { // 加锁 synCount -= 1; } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); }

2. ReentrantLock

 A more lightweight locking operation Java is provided in  ReentrantLock

We know that a lock can only be acquired by one thread at the same time, so when a lock is held by other threads, the user  synchronized will always try to acquire the lock until the lock object is acquired. Once the logic is abnormal, it is easy to have an infinite loop. In  ReentrantLock addition to the basic locking,  tryLock() a method is provided to set the timeout period of the lock acquisition. If the lock is not acquired after the expiration, the block will be blocked and the remaining code will be executed.

(1)lock()

The lock operation can be realized by  lock() passing it. The same thread object can only hold one lock. Note that  lock() if the lock operation fails, it will always try to acquire the lock, causing the program to be blocked and unable to execute subsequent codes.

When using it, it is recommended to put the code block  try catch in and release the lock in  finally it  unlock() to prevent the program abnormality from causing the lock to not be released normally and causing deadlock problems.

 
 

java

copy code

public void demo1() throws Exception { Lock lock = new ReentrantLock(); Thread thread1 = new Thread(() -> { // 加锁 lock.lock(); try { sleep(10 * 1000); } finally { // 一定要释放锁 lock.unlock(); } }); Thread thread2 = new Thread(() -> { // 锁被线程 1 持有无法获取 lock.lock(); System.out.println("get lock"); }); thread1.start(); Thread.sleep(500); thread2.start(); thread1.join(); thread2.join(); }

(2)tryLock()

tryLock() The function is  lock() similar to , but it can set the lock timeout period. If the lock is not acquired within the specified time, it will continue to return  false and continue to execute the remaining code.

For example, in the following example,  thread2 after trying to lock for two seconds, if it fails to lock successfully, it will return  false and continue to execute the subsequent code. If it is replaced with  lock() a method, it will enter a deadlock cycle, resulting in a waste of program resources.

 
 

java

copy code

public void demo2() throws Exception { Lock lock = new ReentrantLock(); Thread thread1 = new Thread(() -> { // 加锁, 但不释放锁 lock.lock(); }); Thread thread2 = new Thread(() -> { try { // 尝试加锁,两秒后未取锁继续执行后续内容 boolean isLock = lock.tryLock(2, TimeUnit.SECONDS); System.out.println("carry on..."); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); Thread.sleep(500); thread2.start(); thread1.join(); thread2.join(); }

3. ReadWriteLock

Although the above two types of locks can achieve atomicity, a one-size-fits-all lock will obviously have a slight discount in efficiency. Therefore,  Java a series of read-write locks are provided in ,  ReadWriteLock which is one of them.

In the business where viewing needs are greater than modification, for example, for functions such as forums, multiple users are allowed to view at the same time but prohibit multiple users from modifying at the same time. The mutual exclusion logic is stripped through the read-write lock to achieve finer-grained concurrency control and ensure thread safety. achieve good performance in the case.

(1) Features

ReadWriteLock Divided into  读锁 and  写锁 , its characteristics are as follows:

  • 读锁 They are not mutually exclusive, 读锁 and can be held by multiple threads at the same time.
  • 写锁 They are also mutually exclusive, and at most one thread can hold them at the same time  写锁 .
  • 读锁 and  写锁 are mutually exclusive, that is,  读锁 and  写锁 cannot be held at the same time, and only one of the two is in the holding state.
(2) Initialization

ReadWriteLock The initialization method is as follows:

 
 

java

copy code

public void init() { // 声明一个读写锁 ReadWriteLock rwLock = new ReentrantReadWriteLock(); // 读锁,允许多线程持有 Lock rLock = rwLock.readLock(); // 写锁,仅允许单线程持有 Lock wLock = rwLock.writeLock(); }

(3) Synchronization example

After understanding the basic concept of read-write lock, we can understand its function more deeply through simple examples.

读线程 For the convenience of follow-up tests, an AND  is defined here  写线程 , and the time-consuming simulation program is simulated through sleep operation.

 
 

java

copy code

class ReadThread extends Thread { private String name; public ReadThread(String name){ this.name = name; } @Override public void run() { rLock.lock(); try { System.out.println(name + " do read."); // Simulate program timing TimeUnit.SECONDS.sleep(3); System.out.println(name + " sleep over."); } catch (InterruptedException e) { e.printStackTrace(); } finally { rLock.unlock(); } } } class WriteThread extends Thread { private String name; public WriteThread(String name){ this.name = name; } @Override public void run() { wLock.lock(); try { System.out.println(name + " do write."); // Simulate program timing TimeUnit.SECONDS.sleep(3); System.out.println(name + " sleep over."); } catch (InterruptedException e) { e.printStackTrace(); } finally { wLock.unlock(); } } }

 Based on the read and write threads declared above, three examples of 双读锁 , ,  一读一写 and ,  respectively, are provided below  .双写锁

The specific test code is as follows:

 
 

java

copy code

/** * 双读锁示例,不互斥 */ public void demo1() throws InterruptedException { Thread reader1 = new ReadThread("Reader-1"); Thread reader2 = new ReadThread("Reader-2"); // Start thread reader1.start(); TimeUnit.MILLISECONDS.sleep(200); reader2.start(); // waiting for finish. reader1.join(); reader2.join(); } /** * 一读一写,互斥 */ public void demo2() throws InterruptedException { Thread reader1 = new ReadThread("Reader-1"); Thread writer1 = new WriteThread("Writer-1"); // Start thread reader1.start(); TimeUnit.MILLISECONDS.sleep(200); writer1.start(); // waiting for finish. reader1.join(); writer1.join(); } /** * 双写锁,互斥 */ public void demo3() throws InterruptedException { Thread writer1 = new WriteThread("Writer-1"); Thread writer2 = new WriteThread("Writer-2"); // Start thread writer1.start(); TimeUnit.MILLISECONDS.sleep(200); writer2.start(); // waiting for finish. writer1.join(); writer2.join(); }

Running the above program, you can see that  demo1 the read operation of the two threads in the read lock is basically triggered at the same time, while the  demo2 write thread cannot acquire the lock while the read thread is running, and the write thread is triggered only after the read thread exits and releases the lock. In the same  demo3 way, the second writing thread is triggered after the writing thread releases the lock.

According to the results, the previous conclusions are also verified. Read locks are allowed to coexist, but read locks and write locks are mutually exclusive, and write locks are also mutually exclusive before.

 
 

txt

copy code

// demo1 Reader-1 do read. Reader-2 do read. Reader-1 sleep over. Reader-2 sleep over. // demo2 Reader-1 do read. Reader-1 sleep over. Writer-1 do write. Writer-1 sleep over. // demo3 Writer-1 do write. Writer-1 sleep over. Writer-2 do write. Writer-2 sleep over.

4. StampedLock

StampedLock It is also divided into read-write lock, but it is a kind of optimistic lock, and different operations are distinguished by providing a version number for the lock.

(1) Pessimistic lock

StampedLockreadLock() The and  in   writeLock() are pessimistic locks, the specific effect is  ReentrantReadWriteLock similar to and, and will not be introduced in detail here. For specific methods, refer to the table below.

method effect
readLock() Get a read lock and return the version number for releasing or upgrading the lock.
isReadLocked() Determine whether there is currently a read lock.
getReadLockCount() Get the number of currently active read locks.
unlockRead() Release the corresponding read lock according to the version number passed in.
writeLock() Get a write lock and return the version number for releasing or upgrading the lock.
isWriteLocked() Determine whether there is currently a write lock.
unlockWrite() Release the corresponding write lock according to the version number passed in.
(2) Optimistic lock

A version number can be returned by  tryOptimisticRead() passing. Note that there is no lock-free state at this time. The  validate(stamp) method can be used to verify whether there is a write happening at this time, that is, whether there is a write lock in the holding state. Different processing logic can be designed according to the result.

Commonly used interface methods and their corresponding description information refer to the table below.

method effect
tryOptimisticRead() Optimistic read, returns a version number.
validate() Determine whether the write lock has been acquired after the acquisition of the corresponding version.
tryConvertToReadLock() Attempts to upgrade the current lock to a read lock, returning 0 to indicate failure.
tryConvertToWriteLock() Attempts to upgrade the current lock to a write lock, returning 0 to indicate failure.
unlock() Release all locks (read locks and write locks) corresponding to the version number.

The following is an example of upgrading from an optimistic lock to a pessimistic lock, and   the specific content is omitted here WriteThread for the operation of acquiring a write lock  .writeLock

 
 

java

copy code

public class RwTest { private static long stamp = 1L; private static final StampedLock stampedLock = new StampedLock(); @Test public void optimisticDemo() throws InterruptedException { // Start write thread Thread write = new WriteThread(); write.setName("Writer-1"); write.start(); TimeUnit.MILLISECONDS.sleep(100); // Optimistic read stamp = stampedLock.tryOptimisticRead(); try { // 验证是否发生写操作 if (stampedLock.validate(stamp)) { // 若是从乐观读升级为悲观读 stamp = stampedLock.tryConvertToReadLock(stamp); if (stamp != 0L) { // convert success System.out.println("Is read lock: " + stampedLock.isReadLocked()); System.out.println("Convert read success, do read."); } } } catch (Exception e) { e.printStackTrace(); } finally { stampedLock.unlock(stamp); } } }

5. Distributed lock

In actual production and development, in order to achieve high availability, we deploy services on different nodes through the cluster. At this time, a single user operation may be executed repeatedly by multiple nodes in the cluster. For this situation, the above two types of locks are obviously not suitable, because both are for single-node services, so distributed locks are needed at this time.

In simpler terms, the entire cluster environment can be regarded as a main thread, and each node is a thread. Distributed locks are used to control the atomicity of operations between different nodes.

1. RedissonLock

RedissonLock It is an implementation-based  Redis distributed lock. The following describes its use in detail.

(1) Redis connection

Because it is based on  Redis the implementation, the service connection needs to be created in advance.

 
 

java

copy code

public void connect() { Config config = new Config(); config.useSingleServer() // Redis 服务地址 .setAddress("redis://127.0.0.1:6379") // 设置密码 .setPassword("123456") // 设置存储数据库 .setDatabase(2); // 创建连接 redissonClient = Redisson.create(config); }

(2) Basic use

RedissonLock The usage method is  ReentrantLock basically the same as the actual one, the difference is that the former will store the lock object in  Redis the memory, so I won’t give specific examples here, but just introduce its common methods.

It should be noted that if the expiration time is not set when locking, it will be the default  30s .

 
 

java

copy code

public void infoDemo() throws InterruptedException { RLock lock = redissonClient.getLock("test"); // 加锁 lock.lock(); // 加锁,并指定超时时间 lock.lock(6000, TimeUnit.MILLISECONDS); // 异步加锁 RFuture<Void> future = lock.lockAsync(); // 异步加锁, 指定锁超时时长 future = lock.lockAsync(6000, TimeUnit.MILLISECONDS); // tryLock() 可以设置等待时间 boolean isL1 = lock.tryLock(); System.out.println("tryLock: " + isL1); // 尝试获取锁 waitTime: 等待时间; leaseTime: 锁过期时间 // 在 5 秒内未取到锁返回 false, 取到锁则设置锁过期为 6 秒 boolean isL2 = lock.tryLock(5 * 1000, 6 * 1000, TimeUnit.MILLISECONDS); System.out.println("tryLock: " + isL2); // 解锁 lock.unlock(); // 是否加锁 System.out.println("is locked: " + lock.isLocked()); // 加锁次数 System.out.println("lock count: " + lock.getHoldCount()); }

(3) Watchdog mechanism

RedissonLock The default lock expiration time is  30s , if the task takes longer than  30s the lock will be released in advance, but if the expiration time is manually set, sometimes it is impossible to accurately estimate the task time consumption, and there may be a waste of resources caused by holding the lock for too long Condition. In response to this situation,  RedissonLock a watchdog mechanism is introduced  watchDog . The watchdog program will hold the lock at fixed intervals. If the lock is still held, it will automatically renew the lock time, so as to realize the dynamic lock time.

The default watchdog time is  , that is  , the status of the current lock will be read  30severy other time  . If the lock is still held, the lock time will be automatically extended  . Of course, you can also specify the watchdog program time when creating a connection   . If it is set,   it will check the status of the lock   every other time  . If the lock is not released, it will be extended .10s30ssetLockWatchdogTimeout()60s60/3=20s60s

Let's look at a specific example.  线程 1 The expiration time is not specified when the lock is created, so the default expiration time is  30s , but because the watchdog mechanism will  10s read the lock status every other time, it  sleep() takes time to simulate the task  60s , so  30s when the lock The expiration time will automatically renew again  30s, so  线程 2 the lock will not be acquired. The lock can only be acquired after the lock release watchdog mechanism expires after the sleep is over  线程 2 .

 
 

java

copy code

public void watchDogDemo() throws InterruptedException { RLock lock = redissonClient.getLock("test"); Thread thread1 = new Thread(() -> { // 不指定时间,默认 30s 过期 lock.lock(); try { // 模拟任务耗时,看门狗会自动给续期 Thread.sleep(60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 任务结束释放锁 lock.unlock(); } }).start(); // 间隔启动第二个线程 Thread.sleep(500); Thread thread2 = new Thread(() -> { // 无法拿到锁 lock.lock(); try { System.out.println("get lock"); } finally { // 任务结束释放锁 lock.unlock(); } }).start(); }

6. Thread tool

1. CountDownLatch

CountDownLatch It can be understood as a counter, and we can control the flow of threads by cleverly applying it.

(1) Initialization

The counter  await() method will judge whether the current counter value is  0 , otherwise it will enter the blocking state.

 
 

java

copy code

public void demo() { // 初始化值为 1 的计数器 CountDownLatch latch = new CountDownLatch(1); // 计数器减 1 latch.countDown(); // 获取计数器大小 latch.getCount() try { // 等待计数器归零, 进入阻塞 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); }

(2) Concurrent Simulation

The task simulation of concurrent events can be quickly realized through CountDownLatch. The specific examples are as follows:

 
 

java

copy code

public void concurrentDemo() throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { int finalI = i; new Thread(() -> { try { // 阻塞,等待计数器归 0 模拟并发 latch.await(); // 需进行的并发操作 System.out.println(finalI); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 计数器递减 latch.countDown(); TimeUnit.SECONDS.sleep(1); } }

2. CyclicBarrier

CyclicBarrier It is also translated as a fence, that is, batch thread control can be realized, and it is triggered when a group of threads reach the critical state.

(1) Initialization

The effect of thread blocking is achieved through  await() the method. When the specified number of threads enter the blocking state,  CyclicBarrier the event defined in will be triggered. Note that when the event throws an exception, an exception will be  await() thrown here  BrokenBarrierException .

 
 

java

copy code

public void demo1() { // 当指定数量的线程到达 await 状态则触发事件 CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> { // 定义事件 System.out.println("\nAll threads have reached the barrier!\n"); }); for (int i = 0; i <= 3; i++) { new Thread(() -> { String threadName = Thread.currentThread().getName(); System.out.println(threadName + " has reached the barrier."); try { // 进入阻塞,等待其它线程到达 await 状态 cyclicBarrier.await(); } catch (Exception e) { // 若 CyclicBarrier 事件抛出异常则会抛到此处 e.printStackTrace(); } System.out.println(threadName + " has continued executing."); }, "Thread-" + i).start(); } }

(2) Synchronization example

After understanding the basic usage, let's look at a more practical case.

One hundred thousand random numbers are generated through  Random the library simulation and the purpose of summing is achieved. In order to achieve more efficient purposes, the array set is split into  3 sub-arrays and summed separately, and finally  3 the task results are added together. After the calculation of each subtask is completed,  await() it enters the block and waits for other subtasks to complete the calculation. When  3 all the subtasks are calculated and enters the block,  CyclicBarrier the final accumulation is triggered, and the results of the three tasks are added to get the final result.

 
 

java

copy code

public void demo2() throws InterruptedException { List<Integer> list = new ArrayList<>(); for (int i = 1; i < 100000; i++) { list.add(new Random().nextInt(100)); } // 将数组拆分为 3 个子任务 List<List<Integer>> subLists = new ArrayList<>(); subLists.add(list.subList(0, 30000)); subLists.add(list.subList(30000, 70000)); subLists.add(list.subList(70000, 99999)); // 设置栅栏触发事件 List<Integer> result = new ArrayList<>(); CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> { AtomicInteger total = new AtomicInteger(0); result.forEach(it -> { total.getAndUpdate(i -> i + it); }); System.out.println("\nAll task finish, result is : " + total); }); // 提交计算子任务 for (int i = 0; i < 3; i++) { int batch = i; new Thread(() -> { AtomicInteger sum = new AtomicInteger(0); subLists.get(batch).forEach(sum::getAndAdd); result.add(sum.get()); try { String threadName = Thread.currentThread().getName(); System.out.println(threadName + " calculate finish, wait other task done."); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } }, "Thread-" + i).start(); } TimeUnit.SECONDS.sleep(5); }

3. Semaphore

Semaphore It can achieve the effect of limiting the number of thread access, which can be roughly understood as a blocked thread queue. Only a specified number of threads can access resources at the same time, and it will be blocked after reaching the threshold.

Semaphore The effect is similar to that of ordinary thread locks, which are used to limit concurrent access, but the difference is that they  Semaphore are allowed to be held by multiple threads at the same time.

(1) Initialization

Semaphore It also provides  access to resources acquire() in  tryAcquire() two ways.

 
 

java

copy code

public void demo1() throws InterruptedException { // 初始化并指定并发大小 Semaphore semaphore = new Semaphore(3); // 请求访问,并发数满则阻塞 semaphore.acquire(); // 请求访问,失败不会阻塞 boolean b1 = semaphore.tryAcquire(); boolean b2 = semaphore.tryAcquire(3, TimeUnit.SECONDS); try { System.out.println(UUID.randomUUID()); } finally { // 释放资源 semaphore.release(); } }

(2) Synchronization example

IO We know that blocking is common, so in business scenarios such as file downloads, multi-threading must be used to achieve multi-task downloads. However, if the number of concurrency is not reasonably limited, system resources will be heavily occupied IO This situation can be achieved by creating a dedicated thread pool resource of a specified size, but often there are other services in a system that need to use threads. It is obviously inappropriate and resource-consuming to create a separate thread pool for each module

Semaphore It solves this problem perfectly, through which the number of concurrent threads of the same resource can be limited. Let's look at an example below to let us understand better:

 
 

java

copy code

/** * 初始化大小为 3,即只能同时 3 个任务并发 */ private static final Semaphore semaphore = new Semaphore(3); public void demo2() throws InterruptedException { for (int i = 0; i < 5; i++) { new MyThread().start(); } // 等待主线程中子任务结束 TimeUnit.SECONDS.sleep(25); } static class MyThread extends Thread { @Override public void run() { try { // 达到指定数量访问将阻塞 semaphore.acquire(); try { System.out.println(UUID.randomUUID()); // 模拟耗时 TimeUnit.SECONDS.sleep(3); } finally { // 一定要释放! semaphore.release(); } } catch (InterruptedException e) { e.printStackTrace(); } } }

Seven, thread deadlock

1. Introduction to Deadlock

Thread deadlock, as the name implies, means that two threads wait for each other to release the lock object, causing both threads to enter an infinite loop of waiting. In multi-threaded tasks, if the acquisition and release of locks are not properly designed, deadlock problems are very likely to occur. .

To give a simple example, user-1 in  user-2 order to ensure the consistency of the data, that is, if one party deducts the money but the other party does not receive it, in order to ensure the consistency of the data, it is necessary to lock the two objects at the same time during the transfer operation, that is, after  user-1 locking Lock  user-2 it, and then perform specific transfer business operations.

After converting the above text logic into the corresponding code, it is as follows:

 
 

java

copy code

public void transfer(User fromUser, User toUser, double amount) { synchronized(toUser) { synchronized(fromUser) { // 扣除来源用户余额 fromUser.debit(amount); // 增加目标用户余额 toUser.credit(amount) } } }

In the above example, when there are  two threads of transfer user-1 to  user-2 account and  user-2 transfer to  user-1 account at the same time, due to the opposite order of action, the first one of the two threads in the above code is  synchronized successfully locked,  user-1 and  user-2 both will enter the locked state at this time . When it comes to the second  synchronized time, the lock held by thread one  user-2 is waiting for the lock released by thread two  user-1 , and the lock held  user-1 by thread two is waiting for the lock released by thread one  user-2 . Both are waiting for the other party to unlock, resulting in a deadlock state.

2. Solutions

There are two common ways to avoid the deadlock problem. The first is to determine the execution order of locks, and the second is to wait for  ReentrantLock non  tryLock() -blocking locking.

The second method is relatively simple, and the deadlock problem can be avoided by implementing a timeout exit through  tryLock() the method. Of course, the disadvantage is that one of the tasks will fail to execute.

The following focuses on the analysis of the implementation ideas of the first method.

The core of method 1 is to determine the locking sequence of the process, and to ensure the consistency of the locking sequence for the same group of operations. As in the previous  example of user-1-  user-2 to-  user-2 2  user-1 transfer, because it is guaranteed that no matter how it is triggered, it is guaranteed to  user-1 lock the pair first and then  user-2 lock the pair, or the order is reversed, but the key point is to ensure that the application uses this order every time it is called.

Slightly modify the above example, by calculating the hash value of the object for comparison to ensure the consistency of the locking sequence, and by introducing a third locking object to prevent the two objects from being consistent, the corresponding code example is as follows  hashCode() :

 
 

java

copy code

public final Object obj = new Object(); public void transfer(User fromUser, User toUser, double amount) { int fromId = fromUser.hashCode(); int toId = toUser.hashCode(); if(fromId < toId) { synchronized(toUser) { synchronized(fromUser) { fromUser.debit(amount); toUser.credit(amount) } } } else if (fromId > toId) { synchronized(fromUser) { synchronized(toUser) { fromUser.debit(amount); toUser.credit(amount) } } } else { synchronized(obj) { synchronized(fromUser) { synchronized(toUser) { fromUser.debit(amount); toUser.credit(amount) } } } } }

 

Guess you like

Origin blog.csdn.net/BASK2312/article/details/132318905