High concurrency Phaser, ReadWriteLock, StampedLock (2)

Phaser

PhaserIt is a synchronization tool class introduced by JDK7, which is suitable for the processing of some tasks that need to be staged. Its functions  are somewhat similar to CyclicBarrier and CountDownLatch, functionally similar to CountDownLatch and CyclicBarrier, but the supported scenarios are more flexible, similar to a multi-stage fence, and more powerful. Let's compare the functions of these three :

Synchronizer effect
CountDownLatch Countdown counter, set the counter value initially, threads can wait on the counter, when the counter value returns to 0, all waiting threads continue to execute
CyclicBarrier Loop fence, initially set the number of participating threads, when the thread reaches the fence, it will wait for other threads to arrive, when the total number of reaching the fence meets the specified number, all waiting threads will continue to execute
Phaser Multi-stage fence, you can set the number of participating threads at the beginning, and you can also register/logout participants midway. When the number of participants arriving meets the number set by the fence, the stage will be upgraded (advance)

scenes to be used

Compared with the previous CyclicBarrier and CountDownLatch, this is a little difficult to understand. Here is a scene: Marriage

A wedding is bound to be divided into many stages, such as the arrival of the guests, the wedding ceremony, the bride and groom worshiping heaven and earth, entering the bridal chamber, having a banquet, and the departure of the guests, etc. If different people are regarded as different threads, then different threads need The stages are different. For example, the bride and groom may have to go through the entire process, while the guests may only have a few steps.

Code example:

Person

  static class Person {
        String name;

        public Person(String name) {
            this.name = name;
        }

        public void arrive() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 到达现场!\n", name);
        }

        public void eat() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 吃完!\n", name);
        }

        public void leave() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 离开!\n", name);
        }

    }
}

 MarriagePhaser

    static class MarriagePhaser extends Phaser {
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {

            switch (phase) {
                case 0:
                    System.out.println("所有人到齐了!");
                    return false;
                case 1:
                    System.out.println("所有人吃完了!");
                    return false;
                case 2:
                    System.out.println("所有人离开了!");
                    System.out.println("婚礼结束!");
                    return true;
                default:
                    return true;
            }
        }
    }

Test Phaser

public class TestPhaser {
    static Random r = new Random();
    static MarriagePhaser phaser = new MarriagePhaser();

    static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        phaser.bulkRegister(5);

        for(int i=0; i<5; i++) {
            final int nameIndex = i;
            new Thread(()->{

                Person p = new Person("person " + nameIndex);
                p.arrive();
                phaser.arriveAndAwaitAdvance();

                p.eat();
                phaser.arriveAndAwaitAdvance();

                p.leave();
                phaser.arriveAndAwaitAdvance();
            }).start();
        }

    }

print result

person 0 到达现场!
person 2 到达现场!
person 4 到达现场!
person 1 到达现场!
person 3 到达现场!
所有人到齐了!
person 2 吃完!
person 0 吃完!
person 4 吃完!
person 3 吃完!
person 1 吃完!
所有人吃完了!
person 3 离开!
person 1 离开!
person 0 离开!
person 4 离开!
person 2 离开!
所有人离开了!
婚礼结束!

Phaser common methods

Phaser() //默认的构造方法,初始化注册的线程数量为0
Phaser(int parties)//一个指定线程数量的构造方法

In addition, Phaser also supports the construction method of Tiering type with parent-child relationship, mainly to reduce the multiplexing of Phaser in the form of groups to reduce competition and improve throughput when the number of registrants is large. This form is generally uncommon, so it is no longer here Mentioned, those who are interested can refer to the official website documentation.

Several other common methods:

register()//Add a new registrant
bulkRegister(int parties)//Add a specified number of multiple registrants
arrive()// Arrive at the fence point and execute directly without waiting for other threads
arriveAndAwaitAdvance()//Arrive the fence point , must wait for all other registrants to arrive at
arriveAndDeregister()//arrive at the fence point, and cancel yourself without waiting for other registrants to arrive at
onAdvance(int phase, int registeredParties)//After multiple threads reach the registration point, this method will be called.

  • arriveAndAwaitAdvance()  The current thread finishes executing the current stage, waiting for other threads to complete the current stage. If the current thread is the last one not reached in this stage, this method directly returns the serial number of the next stage (the serial number of the stage starts from 0), and this method of other threads also returns the serial number of the next stage.
  • arriveAndDeregister()  This method immediately returns the serial number of the next stage, and the number of other threads that need to wait is reduced by one, and the current thread is removed from the members that need to wait later. If the Phaser is a child Phaser of another Phaser (hierarchical Phaser will be discussed later), and this operation causes the number of members of the current Phaser to be 0, then this operation will also remove the current Phaser from its parent Phaser.
  • The arrive() method returns the serial number of the next stage directly without any waiting.
  • awaitAdvance(int phase)  This method waits for a certain phase to complete. Returns immediately if the current phase is not equal to the specified phase or if this Phaser has been terminated. The stage number is generally returned by the arrive() method or the arriveAndDeregister() method. Return the sequence number of the next phase, or return the value specified by the parameter (if the parameter is negative), or directly return the sequence number of the current phase (if the current Phaser has been terminated).
  • awaitAdvanceInterruptibly(int phase) has the same  effect as awaitAdvance(int phase) , the only difference is that if the thread is interrupted while the method is waiting, the method will throw InterruptedException .
  • awaitAdvanceInterruptibly(int phase, long timeout, TimeUnit unit) has the same  effect as awaitAdvanceInterruptibly(int phase) , the difference is that TimeoutException will be thrown if it times out .
  • bulkRegister(int parties)  registers multiple parties. If the current phaser has been terminated, this method has no effect and returns a negative number. If the onAdvance method is executing when this method is called , this method waits for it to complete. If the Phaser has a parent Phaser, the specified party number is greater than 0, and the previous party number of the Phaser is 0, then the Phaser will be registered in its parent Phaser.
  • forceTermination()  forces the Phaser to enter the terminated state. The number of parties already registered will not be affected. If the Phaser has sub-Phasers, all of its sub-Phasers enter the terminated state. If the Phaser is already terminated, this method call has no effect.

ReadWriteLock

According to the translation, read-write locks, as the name suggests, are read-locked when reading and write-locked when writing. This is a clever solution to a performance problem of synchronized: mutual exclusion between reading and reading.

ReadWriteLock is also an interface, the prototype is as follows:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

This interface has only two methods, read lock and write lock. That is to say, when we write a file, we can separate reading and writing into two locks and assign them to threads, so that reading and reading do not affect each other, reading and writing are mutually exclusive, and writing and writing are mutually exclusive . Improve the efficiency of reading and writing files. This interface also has an implementation class ReentrantReadWriteLock, which we will learn below.

Let's take a look at the effect achieved by using synchronized when multiple threads read files at the same time. The code is as follows:

public class ReadAndWriteLock {

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        for(int i=0; i<5; i++){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在进行读操作……");
        }
        System.out.println(thread.getName() + ":读操作完毕!");
        long end = System.currentTimeMillis();
        System.out.println("用时:"+(end-start)+"ms");
    }

    public static void main(String[] args) {
        final ReadAndWriteLock lock = new ReadAndWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}

The test results are as follows:

Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:读操作完毕!
用时:112ms
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:读操作完毕!
用时:107ms

We can see that even when reading a file, after adding the synchronized keyword, reading and reading are mutually exclusive, that is to say, we must wait for Thread-0 to finish reading before it is Thread-0's turn. 1 thread reads, but cannot read the file at the same time. In this case, when a large number of threads need to read the file at the same time, the efficiency of the read-write lock is obviously higher than the implementation of the synchronized keyword. Let's test it out, the code is as follows:

public class ReadAndWriteLock {
	ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	public void get(Thread thread) {
		lock.readLock().lock();
		try{
			System.out.println("start time:"+System.currentTimeMillis());
			for(int i=0; i<5; i++){
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(thread.getName() + ":正在进行读操作……");
			}
			System.out.println(thread.getName() + ":读操作完毕!");
			System.out.println("end time:"+System.currentTimeMillis());
		}finally{
			lock.readLock().unlock();
		}
	}
	
	public static void main(String[] args) {
		final ReadAndWriteLock lock = new ReadAndWriteLock();
		new Thread(new Runnable() {
			@Override
			public void run() {
				lock.get(Thread.currentThread());
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				lock.get(Thread.currentThread());
			}
		}).start();
	}
}

It should be noted that if a thread has already occupied the read lock, if other threads want to apply for the write lock at this time, the thread applying for the write lock will wait for the release of the read lock. If a thread has already occupied the write lock, if other threads apply for a write lock or a read lock at this time, the applied thread will wait for the release of the write lock. Read locks and write locks are mutually exclusive.

Next, let's verify the mutual exclusion relationship of the read-write lock. The code is as follows:

public class ReadAndWriteLock {
   ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	public static void main(String[] args) {
		final ReadAndWriteLock lock = new ReadAndWriteLock();
    // 建N个线程,同时读
		ExecutorService service = Executors.newCachedThreadPool();
		service.execute(new Runnable() {
			@Override
			public void run() {
				lock.readFile(Thread.currentThread());
			}
		});
		// 建N个线程,同时写
		ExecutorService service1 = Executors.newCachedThreadPool();
		service1.execute(new Runnable() {
			@Override
			public void run() {
				lock.writeFile(Thread.currentThread());
			}
		});
	}
	// 读操作
	public void readFile(Thread thread){
		lock.readLock().lock();
		boolean readLock = lock.isWriteLocked();
		if(!readLock){
			System.out.println("当前为读锁!");
		}
		try{
			for(int i=0; i<5; i++){
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(thread.getName() + ":正在进行读操作……");
			}
			System.out.println(thread.getName() + ":读操作完毕!");
		}finally{
         System.out.println("释放读锁!");
			lock.readLock().unlock();
		}
	}
	// 写操作
	public void writeFile(Thread thread){
		lock.writeLock().lock();
		boolean writeLock = lock.isWriteLocked();
		if(writeLock){
			System.out.println("当前为写锁!");
		}
		try{
			for(int i=0; i<5; i++){
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(thread.getName() + ":正在进行写操作……");
			}
			System.out.println(thread.getName() + ":写操作完毕!");
		}finally{
         System.out.println("释放写锁!");
			lock.writeLock().unlock();
		}
	}
}

The test results are as follows:

// 读锁和读锁测试结果:
当前为读锁!
当前为读锁!
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:读操作完毕!
pool-2-thread-1:读操作完毕!
释放读锁!
释放读锁!
// 测试结果不互斥
 
// 读锁和写锁,测试结果如下:
当前为读锁!
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:读操作完毕!
释放读锁!
当前为写锁!
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:写操作完毕!
释放写锁!
// 测试结果互斥
 
// 写锁和写锁,测试结果如下:
当前为写锁!
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:写操作完毕!
释放写锁!
当前为写锁!
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:写操作完毕!
释放写锁!
// 测试结果互斥

Summary of ReadWriteLock

Reading efficiency can be improved using ReadWriteLock:

  • ReadWriteLockOnly one thread is allowed to write;
  • ReadWriteLockAllow multiple threads to read concurrently when not writing;
  • ReadWriteLockIt is suitable for scenarios with more reads and fewer writes.

StampedLock

The above ReadWriteLockcan solve the problem that multiple threads can read at the same time, but only one thread can write.

If we analyze it deeply ReadWriteLock, we will find that it has a potential problem: if a thread is reading, the writing thread needs to wait for the reading thread to release the lock before acquiring the writing lock, that is, writing is not allowed during the reading process, which is a pessimistic reading Lock.

To further improve the efficiency of concurrent execution, Java 8 introduces a new read-write lock: StampedLock.

StampedLockCompared with and ReadWriteLock, the improvement is that it is also allowed to write after acquiring the write lock during the read process! In this way, the data we read may be inconsistent, so a little extra code is needed to determine whether there is writing during the reading process. This kind of read lock is an optimistic lock.

Optimistic locking means optimistically estimating that there is a high probability that there will be no writes during the reading process, so it is called optimistic locking. Conversely, pessimistic locking means that writing is rejected during the reading process, that is, writing must wait. Obviously, the concurrency efficiency of optimistic locking is higher, but once there is a small probability of writing and the read data is inconsistent, it needs to be detected and read again.

Let's look at an example:

public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

Compared with ReadWriteLock, the lock for writing is exactly the same, the difference is for reading. Note that first we tryOptimisticRead()acquire an optimistic read lock and return the version number. Then read. After the reading is completed, we validate()verify the version number. If there is no writing during the reading process, the version number remains unchanged and the verification is successful. We can continue with subsequent operations with confidence. If there is a write during the read, the version number will change and validation will fail. On failure, we read again by acquiring a pessimistic read lock. Since the probability of writing is not high, the program can obtain data through optimistic read locks in most cases, and in rare cases use pessimistic read locks to obtain data.

It can be seen that StampedLocksubdividing read locks into optimistic reads and pessimistic reads can further improve concurrency efficiency. But it also comes at a price:

First, the code is more complex

The second StampedLockis a non-reentrant lock, and the same lock cannot be acquired repeatedly in one thread.

StampedLockIt also provides a more complex function of upgrading pessimistic read locks to write locks, which is mainly used in if-then-update scenarios: that is, read first, if the read data meets the conditions, return, if the read data does not meet the conditions , and try writing again.

StampedLockSummary

StampedLockOptimistic read locks are provided, which can be replaced ReadWriteLockto further improve concurrency performance;

StampedLockIt is a non-reentrant lock.

 Learn more JAVA knowledge and skills, pay attention to and private message bloggers (learning) to learn for free JAVA courseware,
source code, installation package, and the latest interview materials of big factories, etc.

Guess you like

Origin blog.csdn.net/m0_67788957/article/details/123781715