Some questions about the use of locks in Java

1. Get started

First, let's look at a long, easy-to-dissuade example to see what problems you can spot and where you can optimize.

If you have no patience, you can skip it. The basic logic of the code implementation is: uniformly send log data to the remote server.

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;


public class TaskHelper {

    private static SendDataTask sendDataTask;
    private static BlockingQueue<String> dataQueue = new LinkedBlockingQueue<>(100000);

    public static void main(String[] args) {
        sendDataTask = new SendDataTask();
        Thread thread = new Thread(sendDataTask, "SendDataTask");
        thread.setDaemon(true);
        thread.start();
    }

    public void submit(String data) {
        if (StringUtils.isNotBlank(data)) {
            dataQueue.add(data);
            sendDataTask.signalQueue();
        }
    }

    private static class SendDataTask implements Runnable {

        private ReentrantLock lock = new ReentrantLock();
        private Condition notEmpty = lock.newCondition();

        private int checkPeriod = 10 * 1000;
        private volatile boolean stop = false;

        @Override
        public void run() {
            while (!stop) {
                if (isEmpty()) {
                    awaitQueue();
                }
                try {
                    Thread.sleep(checkPeriod);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (stop) {
                    break;
                }
                check();
            }
        }

        public void check() {
            boolean timedOut = false;
            List<String> dataList = new ArrayList<>();
            for (; ; ) {
                if (timedOut) {
                    return;
                }
                try {
                    String data = dataQueue.poll(100, TimeUnit.MILLISECONDS);
                    if (StringUtils.isBlank(data)) {
                        timedOut = true;
                        if (dataList.size() > 0) {
                            sendData(dataList);
                        }
                    } else {
                        dataList.add(data);
                        if (dataList.size() > 100) {
                            sendData(dataList);
                        }
                    }
                } catch (InterruptedException ignore) {
                }
            }
        }

        public void stop() {
            this.stop = true;
            //执行关闭逻辑
        }

        private boolean isEmpty() {
            return dataQueue.size() == 0;
        }

        private void awaitQueue() {
            boolean flag = lock.tryLock();
            if (flag) {
                try {
                    notEmpty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }

        public void signalQueue() {
            boolean flag = false;
            try {
                flag = lock.tryLock(100, TimeUnit.MILLISECONDS);
                if (flag)
                    notEmpty.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (flag)
                    lock.unlock();
            }
        }

        public void sendData(List<String> data) {
            System.out.println("数据处理业务逻辑");
        }
    }

}

Just started, I also thought why not create ThreadPoolExecutor with Executors to handle?

I think it's probably:

  1. Data cannot be lost. If the concurrency is high, it is difficult to handle the rejection logic of the thread pool.
  2. When the program is closed, it is necessary to save the unsent data to avoid losing data
  3. A retry mechanism is required to handle network exceptions

I omitted the logic of the retry part. If you are interested, you can learn about the following library:

<dependency>
    <groupId>com.github.lowzj</groupId>
    <artifactId>java-retrying</artifactId>
    <version>1.2</version>
</dependency>

But is it really necessary to use LinkedBlockingQueue, is it necessary to use Lock?

This problem, we will solve it later, let's look at the relevant knowledge first.

2. synchronized与Lock

The main difference between synchronized and Lock is:

  1. The synchronized lock is unfair, whether the Lock lock is fair can be set
  2. The synchronized lock is uninterruptible. Whether the Lock lock responds to interruption can be set
  3. Synchronized does not acquire lock timeout mechanism, Lock lock can set lock timeout time

In fact, JDK optimizes synchronized by biased lock (CAS thread ID), lightweight lock (spin), etc. In fact, the performance is not worse than Lock, you can see synchronized later.

Personally, unless you need to set fair locks, need lock response interruptions, need to acquire lock timeouts, need read-write separation locks, etc., try to use synchronized at other times.

Because of performance issues, it's not worth considering using Lock most of the time, unless you really have extreme performance requirements and have done a lot of comparative testing.

Because the way Lock is used is relatively complicated, more tests should be considered, otherwise, the problem caused by a bug is definitely more harmful than the possible performance degradation.

3. Lock and Condition

Synchronized implements the waiting notification mechanism through Object's wait, notify, notifyAll, and Lock requires the cooperation of Condition.

See a simple example:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockCondition {

    private final Lock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final Task[] tasks = new Task[100];
    private int addIndex;
    private int getIndex;
    private int count;

    public void addTask(Task task) throws InterruptedException {
        lock.lock();
        try {
            // 注意使用的是while,不是if
            while (count == tasks.length) {//如果队列满了
                notFull.await();//notFull条件不满足了,需要等待
            }
            tasks[addIndex++] = task;
            if (addIndex == tasks.length){
                addIndex = 0;
            }
            count++;
            notEmpty.signal();//添加数据之后,非空条件满足了,就可以唤醒等在这个条件上的线程了
        } finally {
            lock.unlock();
        }
    }

    public Task getTask() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {//如果队列为空
                notEmpty.await();//队列为空,非空条件不满足了,需要等待
            }
            Task task = tasks[getIndex++];
            if (getIndex == tasks.length) {
                getIndex = 0;
            }
            count--;
            notFull.signal();//数据被取出之后,notFull条件满足了,需要唤醒等待notFull条件的线程
            return task;
        } finally {
            lock.unlock();
        }
    }

    private static class Task{

    }
}

Although this example is very rough, it basically contains the elements of using Lock and Condition:

  1. Condition is obtained through Lock
  2. await in a while loop, not if
  3. Both await and signal must first acquire the lock and then execute
  4. Release the lock in finally: lock.unlock()
  5. Execute await when the condition is not met
  6. Execute signal after the condition is met

To better understand how Lock is used, the relevant implementation classes of JDK's BlockingQueue are definitely the best tutorial examples. Read the source code of these classes, such as LinkedBlockingQueue.

4. LinkedBlockingQueue main method

add element method illustrate
add Add an element to the queue, throw an exception if the queue is full
put Add elements to the queue, blocking until the addition is successful
offer Add elements to the queue, return false if the addition is successful, you can set an add timeout
get element method illustrate
remove Gets and removes the element at the head of the queue, throws an exception if the queue is empty
take Add elements from the queue and wait until the acquisition is successful
poll Add to get elements from the queue, without returning null, you can set a get timeout
peek Get element from queue add, no return null
drainTo Add to get elements from the queue, you can get more than one at a time, you can set the maximum number of acquisitions

Most of the other blocking queue methods in the JDK have similar logic, which is basically divided into:

  1. whether to throw an exception (add remove)
  2. block (put take)
  3. Timeout or return directly (offer poll)

5. About synchronized

5.1 Example description

public class Synchronize {

    private Object lock = new Object();

    public static synchronized void doSomethingA(){

    }

    public static void doSomethingB(){
        synchronized (Synchronize.class){

        }
    }

    public synchronized void doSomethingC(){

    }

    public void doSomethingD(){
        synchronized (this){

        }
    }

    public void doSomethingE(){
        synchronized (lock){
            
        }
    }
}

If it is a static method , the lock object is the class of the class, that is, the following two methods of locking are equivalent:

public static synchronized void doSomethingA(){

}

public static void doSomethingB(){
    synchronized (Synchronize.class){

    }
}

If it is an instance method , the object is the instance itself, that is, the following two methods of locking are basically equivalent:

public synchronized void doSomethingC(){

}

public void doSomethingD(){
    synchronized (this){

    }
}

Static methods and instance methods use different locks, so they do not affect each other.

If some methods need to reduce the granularity of the lock, a new object can be used as the lock object to reduce lock contention.

5.2 Lock Upgrade

The reason why there is a lock escalation process is because the JDK optimizes the synchronized keyword.

No lock -> Bias lock -> Lightweight lock -> Heavyweight lock

Each Java object has an object header, taking 32 bits as an example.

When unlocked:

Object's HashCode Generation age Is it biased to lock lock flag
25th place 4th First place 2nd place (01)

Bias lock:

thread ID Epoch (bias lock time) Generation age Is it biased to lock lock flag
23rd place 2nd place 4th First place 2nd place (01)

Because locks not only do not have multi-thread competition, but are always acquired multiple times by the same thread, biased locks avoid synchronization by CAS operations on thread IDs.

You can set the bias lock on and off through XX:-UseBiasedLocking=false, and you can set whether the bias lock is delayed to open through the XX:BiasedLockingStartupDelay=0 parameter, and 0 means that the delay is turned off.

Lightweight lock:

Lightweight locks are considered because, most of the time, a thread holds the lock for a very short time, and the context switching overhead is avoided by a short spin when the lock is not acquired.

The logic of the spin is probably through the while(true) empty wait, which will consume the CPU.

To give an example in life, it is like boiling water. When you first waited and found that the water was not boiling, you went straight back. Later, when you knew that the water was about to boil, when you went to check again, you decided to wait a few seconds to avoid When I go back, the water may boil before I sit down.

You can use the -XX:+UseSpinning parameter to set whether to enable the lock spin, and use the -XX:PreBlockSpin parameter to set the number of spins

Heavyweight Lock:

The heavyweight lock is the initial implementation of synchronize. The monitor lock monitor inside the object depends on the Mutex Lock implementation of the underlying operating system.

6. About Java Object Header

I won't go into details here, you can use the jol of open jdk to view:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.14</version>
</dependency>
import org.junit.Test;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;

public class JOLTest {

    private static class User{
        private Integer id;
        private String name;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public synchronized void doSomething(){
            System.out.println(ClassLayout.parseInstance(this).toPrintable());
            System.out.println("#################");
        }
    }

    @Test
    public void test(){
        User user = new User();
        user.setId(1);
        user.setName("tim");
        //查看对象内部信息
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
        System.out.println("-------------");
        //查看对象外部信息
        System.out.println(GraphLayout.parseInstance(user).toPrintable());
        System.out.println("-------------");
        //查看对象占用空间总大小
        System.out.println(GraphLayout.parseInstance(user).totalSize());
        System.out.println("-------------");
        user.doSomething();
    }
}

You can use the -XX:+UseCompressedOops parameter to compare the difference between whether the pointer is compressed or not.

7. Back to the beginning

Going back to our original program, what's the problem?

In fact, there are two most important questions:

  1. The LinkedBlockingQueue queue has implemented lock-related logic, and we don't need to add a lock when we use it ourselves
  2. Hold the lock to sleep, Thread.sleep()

In fact, JDK has provided us with enough tools, many times we do not need to write too much relatively low-level code.

Below, a simplified example is provided for reference only:

import java.util.LinkedList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;


public class MyTaskHelper {

    private static final BlockingQueue<String> DATA_QUEUE = new LinkedBlockingQueue<>(1000);

    private static volatile boolean stop = false;

    private static Thread thread;

    static {
        start();
    }

    public static void addTask(String task) throws InterruptedException {
        DATA_QUEUE.put(task);
    }

    public static synchronized void start(){
        if(stop){
            if(thread == null || !thread.isAlive()) {
                thread = new Thread(new SendDataTask(), "MyTaskHelper");
                thread.setDaemon(true);
                thread.start();
            }
            stop = false;
        }
    }

    public static void stop(){
        stop = true;
        System.out.println("处理未发送数据逻辑");
    }

    private static class SendDataTask implements Runnable {

        @Override
        public void run() {
            LinkedList<String> datas = new LinkedList<>();
            while (!stop){
                datas.clear();
                DATA_QUEUE.drainTo(datas,100);
                System.out.println("发送日志数据逻辑 datas");
            }
        }
    }
}

8. References

OpenJDK Object Header

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324105590&siteId=291194637