Basic introduction to

In the program can achieve the lock function through synchronized, for it can be called the built-in lock, is directly provided by the Java language level for us to use, can implicitly obtain the lock in the program. But the way it is used is solidified and can only be obtained before release. In addition, in the process of use, when a thread acquires the lock of a resource, other threads must wait to acquire the lock resource. Synchronized does not provide interruption or timeout acquisition operations.

To solve these problems, display locks were introduced. Three common methods are provided in display locks: Lock(), unLock(), and try lock().

The standard usage of lock

The lock (); / / lock lock. // business logic try{i++; }finally{// unLock lock.unlock (); }Copy the code

Do not write the process of acquiring the lock in a try, because if an exception occurs while acquiring the lock, the exception is thrown at the same time as the lock is released.

A finally block releases the lock to ensure that the lock is eventually released after it is acquired

When to use Synchronized or Lock

If you don’t need to consider these features of trying to acquire a lock or breaking a lock during lock use. Use synchronized. Synchronized has many optimizations in the JDK, such as lock optimizations and upgrades.

Synchronized also uses less memory to display locks. Why? Synchronized is a content at the language level, while lock is an interface. The operation can only be performed after obtaining its object instance when using lock, especially in the case of many locks, if there is no special requirement, it is recommended to use synchronized.

ReentrantLock

Standard mode of use

According to the source code, Lock itself is an interface, so for its implementation classes, the most common is ReentrantLock.

So how should ReentarntLock be used? It’s actually quite simple, just follow the specification:

Public class LockTest extends Thread{private static int count = 100000; private static int value = 0; private static Lock lock = new ReentrantLock(); @Override public void run() { for (int i = 0; i < count; i++) { lock.lock(); try { System.out.println(Thread.currentThread().getName()+" : "+value); value++; }finally { lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { LockTest l1 = new LockTest(); LockTest l2 = new LockTest(); l1.start(); l2.start(); TimeUnit.SECONDS.sleep(5); System.out.println(value); }}Copy the code

Note that unlocking should be done in finally, because an exception may occur during the execution of the business logic and the lock may not be released.

Synchronized is used either on methods or blocks of statements. When an exception occurs, representing a break from the executing block of code, the lock is released. The display lock itself is an instance of an object. If the lock is locked and no release operation is performed, the lock will remain in place.

reentrant

ReentrantLock, commonly referred to as a ReentrantLock, is a recursive, non-blocking synchronization mechanism. It can be equivalent to the use of synchronized, but ReentrantLock provides a more powerful and flexible locking mechanism than synchronized, reducing the probability of deadlocks occurring.

Simply put, the same thread can apply for the right to use the lock more than once it has acquired it. The synchronized keyword implicitly supports re-entry, such as a synchronized modified recursive method in which the thread of execution acquires the lock multiple times after it has been acquired. ReentrantLock When the lock() method is called, threads that have acquired the lock can call the lock() method again to acquire the lock without blocking.

Its internal implementation process is as follows:

  1. Each lock is associated with a thread holder and a counter. When the counter is 0, it means that the lock is not held by any thread, so the thread may obtain the lock and call the corresponding method.
  2. When a thread successfully requests the lock, the JVM records the thread holding the lock and sets the counter to 1, at which point other threads must wait to acquire the lock.
  3. When the thread that holds the lock requests the lock again, the lock is acquired again, and the counter is incremented.
  4. When the thread holding the lock exits the synchronized code block, the counter is decremented, and when the counter is zero, the lock is released

Synchronized reentrant

public class SynDemo { public static synchronized void lock1(){ System.out.println("lock1"); lock2(); } public static synchronized void lock2(){ System.out.println("lock2"); } public static void main(String[] args) { new Thread(){ @Override public void run() { lock1(); } }.start(); }}Copy the code

The execution result

lock1
lock2
Copy the code

As can be seen from the results, when the same thread calls multiple synchronization methods, when it successfully obtains the lock for the first time, and then uses other synchronization methods, it can still continue to call down, without blocking. Reentrant lock is implemented.

Already reentrant

public class ReentrantTest { private static Lock lock = new ReentrantLock(); private static int count = 0; public static int getCount() { return count; } public void test1(){ lock.lock(); try { count++; test2(); }finally { lock.unlock(); } } public void test2(){ lock.lock(); try { count++; }finally { lock.unlock(); } } static class MyThread implements Runnable{ private ReentrantTest reentrantTest; public MyThread(ReentrantTest reentrantTest) { this.reentrantTest = reentrantTest; } @Override public void run() { for (int i = 0; i < 10000; i++) { reentrantTest.test1(); } } } public static void main(String[] args) throws InterruptedException { ReentrantTest reentrantTest = new ReentrantTest(); new Thread(new MyThread(reentrantTest)).start(); TimeUnit.SECONDS.sleep(2); System.out.println(count); }}Copy the code

A run shows that there is no blocking despite multiple locks, which means it is also reentrant supported.

Fair and unfair locks

The principle of

In multithreading concurrency, when there are multiple threads to acquire the same lock at the same time, if the lock is obtained first according to the longest waiting time, it represents a fair lock. On the contrary, if it is random, the CPU time slice polling to which thread, which thread will acquire the lock, it represents an unfair lock.

So fair lock or unfair lock which performance is better? The answer is that unfair locking performs well because it makes full use of the CPU and reduces the time it takes for a context switch to wake up a thread.

Fair lock

Not fair lock

Code implementation

In both ReentarntLock and Synchroized, the default is non-fair lock, and ReentrantLock can be turned on using a fair lock with an argument.

1) ReentrantLock Fair lock

Public class FairLockTest {private static Lock Lock = new ReentrantLock(true); public static void test(){ for (int i = 0; i < 2; i++) { lock.lock(); Try {system.out.println (thread.currentThread ().getName()+" lock "); TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }} public static void main(String[] args) {new Thread(" Thread "){@override public void run() {test(); } }.start(); New Thread(" Thread B"){@override public void run() {test(); } }.start(); }}Copy the code

According to the results, it can be seen that the process of acquiring locks is carried out in accordance with the fair strategy.

2) ReentrantLock Unfair lock

You only need to pass no arguments when instantiating a ReentrantLock to be an unfair lock. According to the execution result, it can be seen that the lock is obtained according to an unfair policy.

ReentrantLock versus synchronized

Similarities:

Lock synchronization is done in a blocking manner, that is, if one thread acquires an object lock and executes a block of synchronized code, all other threads will block until the thread that acquires the lock releases the lock.

Difference:

  1. Synchronized is a Java language keyword that is mutually exclusive at the native syntax level and requires JVM implementation, while ReentrantLock is an API level mutually exclusive provided after JDK1.5. You need lock() and unlock() methods in conjunction with a try/finally block to do this.
  2. Synchronized is convenient and simple to use, and the compiler guarantees lock release and lock release. ReentrantLock requires manual declaration to lock and release. To avoid deadlock caused by manual lock release, it is best to declare lock release in finally.
  3. ReentrantLock has better lock granularity and flexibility than synchronized.

ReentrantReadWriteLock

ReentrantLock or synchronized can be called exclusive lock or exclusive lock, which can be understood as pessimistic lock. These locks allow only one thread to access at the same time. But for Internet projects, the vast majority of scenarios are more reading than writing, by a ratio of about 10:1. In the database scenario, read/write separation occurs for read/write processing.

In a scenario where there are too many reads and too few writes, you can also use ReentrantReadWriteLock to lock read and write data for processing service code. It implements the ReadWriteLock interface and maintains a pair of locks, namely read locks and write locks.

Read/write lock feature

Read operations are not mutually exclusive, write operations are mutually exclusive, and read and write operations are mutually exclusive

Fairness: Supports both fairness and unfairness

Reentrancy: Lock reentrancy is supported

Lock degradation: A write lock can be degraded to a read lock in the order of obtaining a write lock and obtaining a read lock in the write lock. Read locks cannot be upgraded to write locks.

Implementation principle of read/write lock

ReentrantReadWriteLock implements the ReadWriteLock interface, which maintains a pair of related locks, one for read and one for write operations.

ReadWriteLock defines two methods. ReadLock () returns the lock used for read operations, and writeLock() returns the lock used for write operations. ReentrantReadWriteLock is defined as follows:

Its internal writeLock() is used to acquire the writeLock, and readLock() is used to read the lock

Read-write lock demo

Read/write locks are mutually exclusive, mutually exclusive, and mutually exclusive. Here is an example to illustrate the effect:

public class ReentrantReadWriteLockDemo { private static int count = 0; private static class WriteDemo implements Runnable{ ReentrantReadWriteLock lock ; public WriteDemo(ReentrantReadWriteLock lock) { this.lock = lock; } @Override public void run() { for (int i = 0; i < 5; i++) { try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.writeLock().lock(); count++; System.out.println(" write lock: "+count); lock.writeLock().unlock(); } } } private static class ReadDemo implements Runnable{ ReentrantReadWriteLock lock ; public ReadDemo(ReentrantReadWriteLock lock) { this.lock = lock; } @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.readLock().lock(); count++; System.out.println(" read lock: "+count); lock.readLock().unlock(); } } public static void main(String[] args) { ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); WriteDemo writeDemo = new WriteDemo(lock); ReadDemo readDemo = new ReadDemo(lock); For (int I = 0; int I = 0; int I = 0; i < 3; i++) { new Thread(writeDemo).start(); } for (int i = 0; i < 3; i++) { new Thread(readDemo).start(); }}}Copy the code

Lock down

Read-write locks support lock degradation, but do not support lock escalation. A write lock can be downgraded to a read lock, but a read lock cannot be upgraded to a write lock. What does that mean? A thread that has acquired a write lock can acquire a read lock of the same lock again because support for ReentrantReadWriteLock refers to a lock that internally maintains two locks. A thread that has acquired a read lock cannot acquire a write lock of the same lock again.

1) The write lock degrades the read lock

public class LockDegradeDemo1 { private static class Demo{ ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); Public void fun1(){// get the writeLock lock.writelock ().lock(); System.out.println("fun1"); fun2(); lock.writeLock().unlock(); } public void fun2(){// get readLock lock.readLock().lock(); System.out.println("fun2"); lock.readLock().unlock(); } } public static void main(String[] args) { new Demo().fun1(); }}Copy the code

According to the execution result, when a thread acquires the write lock, it can continue to acquire the read lock of the same lock.

2) Read lock Upgrade write lock

public class LockDegradeDemo2 { private static class Demo{ ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); Public void fun1(){// get the writeLock lock.writelock ().lock(); System.out.println("fun1"); //fun2(); lock.writeLock().unlock(); } public void fun2(){// get readLock lock.readLock().lock(); System.out.println("fun2"); fun1(); lock.readLock().unlock(); } } public static void main(String[] args) { new Demo().fun2(); }}Copy the code

The value can be obtained based on the execution result. When a thread acquires a read lock, it cannot continue to acquire a write lock.

Performance Optimization Demo

In the case of more reads and less writes, read and write locks can optimize the performance of original synchronized for program execution.

public class Sku { private String name; private double totalMoney; Private int storeNumber; Public Sku(String name, double totalMoney, int storeNumber) {this.name = name; this.totalMoney = totalMoney; this.storeNumber = storeNumber; } public double getTotalMoney() { return totalMoney; } public int getStoreNumber() { return storeNumber; } public void changeNumber(int sellNumber){ this.totalMoney += sellNumber*25; this.storeNumber -= sellNumber; }}Copy the code
Public interface SkuService {// Get the information about the product Sku getSkuInfo(); Void setNum(int number); }Copy the code

Run as synchronized

public class SkuServiceImplSync implements SkuService{ private Sku sku; public SkuServiceImplSync(Sku sku) { this.sku = sku; } @Override public synchronized Sku getSkuInfo() { try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return this.sku; } @Override public synchronized void setNum(int number) { try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } sku.changeNumber(number); }}Copy the code
Public class SkuExec {static final int readWriteRatio = 10; Static final int minthreadCount = 3; Private static class implements Runnable{private SkuService SkuService; public ReadThread(SkuService skuService) { this.skuService = skuService; } @Override public void run() { long start = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { skuService.getSkuInfo(); } system.out.println (thread.currentThread ().getName()+" (system.currentTimemillis ()-start)+"ms"); Private static class WriteThread implements Runnable{private SkuService SkuService; public WriteThread(SkuService skuService) { this.skuService = skuService; } @Override public void run() { long start = System.currentTimeMillis(); Random r = new Random(); for(int i=0; i<10; I++){skuService. SetNum (r.nextint (10)); } system.out.println (thread.currentThread ().getName() +" "+(system.currentTimemillis ()-start)+"ms---------"); }} public static void main(String[] args) throws InterruptedException {Sku Sku = new Sku("computer", 1000010000); SkuService skuService = new SkuServiceImplSync(sku); for(int i = 0; i<minthreadCount; i++){ Thread setT = new Thread(new WriteThread(skuService)); for(int j=0; j<readWriteRatio; j++) { Thread getT = new Thread(new ReadThread(skuService)); getT.start(); } TimeUnit.MILLISECONDS.sleep(100); setT.start(); }}}Copy the code

The execution result

Thread-0 writing commodity data: 58ms--------- thread-11 writing commodity data: 58ms--------- thread-22 writing commodity data: 58ms--------- thread-4 reading commodity data: 2577ms thread-5 reading commodity data: 3029ms thread-6 reading commodity data: 3040ms thread-9 reading commodity data: 4016ms thread-29 reading commodity data: 4980ms Thread-15 Reading commodity data: 8971ms Thread-13 reading commodity data: 9742ms Thread-18 reading commodity data: 10257ms Thread-19 reading commodity data: 10417ms Thread-25 Reading commodity data: 10805ms Thread-26 reading commodity data: 11250ms Thread-27 reading commodity data: 11645ms Thread-31 reading commodity data: 12137ms Thread-3 Reading commodity data: 13257ms Thread-7 reading commodity data: 13714ms Thread-10 reading commodity data: 13911ms Thread-32 reading commodity data: 13730ms Thread-28 Reading commodity data: 14101ms Thread-23 Reading commodity data: 14409ms Thread-1 reading commodity data: 14808ms Thread-20 reading commodity data: 14986ms Thread-17 Reading commodity data: 15150ms Thread-14 Reading commodity data: 15691ms Thread-16 reading commodity data: 16312ms Thread-21 reading commodity data: 16494ms thread-24 reading commodity data: 16514ms thread-30 reading commodity data: 16637ms thread-8 reading commodity data: 16867ms thread-2 reading commodity data: 16982ms thread-12 reading commodity data takes 16986msCopy the code

The command is executed in ReentrantReadWriteLock format

public class SkuServiceImplReen implements SkuService{ private Sku sku; public SkuServiceImplReen(Sku sku) { this.sku = sku; } private ReadWriteLock lock = new ReentrantReadWriteLock(); private Lock readLock = lock.readLock(); private Lock writeLock = lock.writeLock(); @Override public Sku getSkuInfo() { readLock.lock(); try { TimeUnit.MILLISECONDS.sleep(5); return this.sku; } catch (InterruptedException e) { e.printStackTrace(); return null; } finally { readLock.unlock(); } } @Override public void setNum(int number) { writeLock.lock(); try { TimeUnit.MILLISECONDS.sleep(5); sku.changeNumber(number); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); }}}Copy the code

Modifying the startup class

SkuService skuService = new SkuServiceImplReen(sku);

The execution result

Thread-0 time spent writing commodity data: 66ms--------- thread-11 time spent writing commodity data: 68ms--------- thread-22 time spent writing commodity data: 62ms--------- thread-2 time spent reading commodity data: 765ms thread-8 reading commodity data: 764ms thread-10 reading commodity data: 764ms thread-5 reading commodity data: 765ms thread-1 reading commodity data: 765ms thread-4 reading commodity data: 765ms thread-7 reading commodity data: 764ms thread-6 reading commodity data: 764ms thread-3 reading commodity data: 765ms thread-9 reading commodity data: 770ms thread-15 reading commodity data: 760ms Thread-17 Reading commodity data: 760ms Thread-12 reading commodity data: 760ms Thread-13 reading commodity data: 760ms Thread-14 reading commodity data: 760ms Thread-18 reading commodity data: 759ms Thread-16 Reading commodity data: 760ms Thread-20 Reading commodity data: 765ms Thread-19 reading commodity data: 765ms Thread-21 reading commodity data: 765ms Thread-31 reading commodity data: Thread-28 reading commodity data: 705ms thread-25 reading commodity data: 705ms thread-30 reading commodity data: 704ms thread-29 reading commodity data: 705ms thread-27 reading commodity data: Thread-26 reading commodity data: 705ms thread-23 reading commodity data: 705ms thread-24 reading commodity data: 705ms thread-32 reading commodity data: 704msCopy the code

According to the final results, the performance improvement is very large.

StamptedLock

The stamptedLock class is an update to the ReentrantReadWriteLock read-write lock. It provides an optimistic read mode and an API that optimizes access to read and write locks. StamptedLock is an update to the ReentrantReadWriteLock read-write lock. This allows for more fine-grained concurrency control.

ReentrantReadWriteLock has a problem

Writer thread hunger is also an issue when using read/write locks. This is mainly because read and write locks are mutually exclusive. For example, when thread A holds the read lock and reads data, thread B has to queue up the data to obtain the write lock modification. Thread C reads the data, so thread C can acquire the read lock, and thread B has to wait for thread C to release the read lock. Since the read operation is much larger than the write operation in this scenario, there may be many threads to read data and obtain the read lock, so the thread B that wants to obtain the write lock has to wait for a long time, which eventually leads to hunger.

Writer thread hunger can be partially addressed by fair locking, but it comes at the expense of system throughput.

StampedLock characteristics

1) The method of obtaining the lock will return a ticket (stamp). When the value is 0, it means that the lock failed to be obtained, while other values represent success.

2) Lock release method, all need to transfer the ticket returned when acquiring the lock, so as to control is the same lock.

3) StampedLock is not reentrant. If a thread already holds a write lock, acquiring the write lock will cause a deadlock.

4) StampedLock provides three modes to control read and write operations: write lock, pessimistic read lock, optimistic read lock

Write lock: Use a lock similar to ReentrantReadWriteLock, which is an exclusive lock. After one thread acquires this lock, other requesting threads block and wait. A write lock can be obtained only when there is no thread holding the write lock or pessimistic read lock. A ticket will be returned after the write lock is successfully obtained. When the write lock is released, the ticket obtained during the lock acquisition needs to be passed.

Pessimistic read lock: A shared lock similar to ReentrantReadWriteLock that can be held by multiple threads at the same time. When no thread acquires a write lock, multiple threads can acquire a pessimistic read lock at the same time. When a pessimistic read lock is acquired, a ticket is returned and the thread blocks to acquire the write lock. When the lock is released, the ticket acquired when the lock was acquired needs to be passed.

Optimistic read lock: This lock is a new addition to StampedLock. Think of it as a weaker version of a pessimistic lock. When no thread holds a write lock, an optimistic read lock is acquired and a ticket is returned. Notably, it assumes that no data will be modified after an optimistic read lock is acquired, and that the optimistic read lock will not block the write operation. So how does it keep the data consistent? Optimistic read lock will copy the required data when it obtains the ticket. When it actually reads the data, it will call the API in StampedLock to verify whether the ticket is valid. If a thread acquires a write lock and modifies the data between the time the ticket is acquired and the time the data is consumed, the ticket is invalidated. If true is returned when verifying the validity of the ticket, the ticket is still valid and the data has not been modified, the original data will be read directly. When flase is returned, it means that the ticket is invalid and the data has been modified, and the latest data will be copied again for use. Optimistic read locks use very short read-only code, which can reduce lock contention between threads, thereby improving system throughput. But for the read lock to obtain data results must be verified.

5) Read locks and write locks can be converted to each other in StampedLock. In ReentrantReadWriteLock, a write lock can be degraded to a read lock, but a read lock cannot be upgraded to a write lock.

Example:

Class Point {private double x, y; Private final StampedLock sl = new StampedLock(); Void move(double deltaX, double deltaY) {double stamp = sl.writelock (); try { x += deltaX; y += deltaY; } finally {// release write lock sl.unlockwrite (stamp); }} double distanceFromOrigin() {long stamp = sl.tryoptimisticread (); // Get an optimistic read lock double currentX = x, currentY = y; // Read two fields into the local local variable if (! Sl.validate (stamp)) {// Check whether other write locks occur after the optimistic read lock is issued. stamp = sl.readLock(); // If so, we get a read-pessimistic lock again. Try {currentX = x; // Read two fields into the local variable currentY = y; } finally {sl.unlockRead(stamp);} finally {sl.unlockread (stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } void moveIfAtOrigin(double newX, double newY) {long stamp = sl.readLock(); Long ws = sl.tryconvertToWritelock (stamp); If (ws! = 0L) { stamp = ws; // If the ticket is replaced successfully x = newX; // State change y = newY; // State change break; } else {// if not successful sl.unlockread (stamp); // stamp = sl.writelock (); }}} finally {// release the read lock or write lock sl.unlock(stamp); }}}Copy the code