The JUC Series of Java Interview: Java Lock Encyclopedia

Fair lock and unfair lock of Java lock

concept

Fair lock

It means that multiple threads acquire locks in the order in which they apply for locks, which is similar to queuing to buy food, first come first, first come first serve, it is fair, that is, queue

Unfair lock

Refers to the order in which multiple threads acquire locks, not in the order of applying locks. It is possible that the applied thread will obtain the lock first than the first applied thread. In a high concurrency environment, priority may be reversed or hungry threads may be caused ( That is, a thread has not been able to get the lock)

How to create

The creation of ReentrantLock in the concurrent package can specify the boolean type of the destructor to obtain a fair lock or an unfair lock. The default is an unfair lock

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

The difference between the two

Fair lock : It is very fair. In a concurrent environment, each thread will first check the waiting queue maintained by this lock when acquiring a lock. If it is empty or the current thread is the first in the waiting queue, the lock will be occupied. No The person will be added to the waiting queue, and the rules of installing FIFO will be taken from the queue later.

Unfair locks: Unfair locks are more rude, and they try to hold the lock directly. If the attempt fails, a method similar to fair locks is used.

Digression

Java ReenttrantLock specifies whether the lock is fair through the constructor. The default is an unfair lock, because the advantage of an unfair lock is that the throughput is larger than that of a fair lock.对于synchronized而言,也是一种非公平锁

Why Synchronized cannot prohibit instruction rearrangement, but it can guarantee order

Preface

First of all, we have to analyze this question. This simple question actually contains a lot of information. To answer this question well, the interviewer must at least know the concept:

  • Java memory model
  • Concurrent programming order problem
  • Order rearrangement
  • synchronized锁
  • Reentrant lock
  • Exclusive lock
  • as-if-serial semantics
  • Single thread & multi thread

Standard Answer

In order to further enhance all aspects of computer capabilities, many optimizations have been made at the hardware level, such as processor optimization and instruction rearrangement, but the introduction of these technologies will cause orderliness problems.

First explain what is the order problem, and also know what causes the order problem

We also know that the best way to solve the order problem is to prohibit processor optimization and instruction rearrangement, just like the use of memory barriers in volatile.

Show that you know what instruction rearrangement is and how it is implemented

However, although many hardware will do some rearrangement for optimization, in Java, no matter how the ordering is, it cannot affect the execution result of a single-threaded program. This is the as-if-serial semantics. The prerequisite for all hardware optimizations is to comply with the as-if-serial semantics.

The as-if-serial semantics protects single-threaded programs. Compilers that comply with as-if-serial semantics, runtime and processors jointly create an illusion for programmers who write single-threaded programs: single-threaded programs are programmatically executed. To be executed sequentially. The as-if-serial semantics frees single-threaded programmers from worrying about reordering disturbing them, or worrying about memory visibility issues.

Focus! Explain what is the semantics of as-if-serial, because this is the first key word of this question, the answer is half right

Let's talk about synchronized. It is a lock provided by Java. You can lock objects in Java through it, and it is an exclusive and reentrant lock.

Therefore, when a thread executes a piece of code modified by synchronized, it will lock it first, and then unlock it after execution. After the lock is locked and before it is unlocked, other threads cannot obtain the lock again, and only this locking thread can repeatedly obtain the lock.

Introduce the principle of synchronized, this is the second key point of this question, you can basically get full marks here.

Synchronization ensures that the code modified by synchronized is executed in a single thread at the same time by means of exclusive lock. So, this satisfies a key premise of as-if-serial semantics, that is, single-threaded . Because of the as-if-serial semantic guarantee, the orderliness of single-threaded naturally exists.

Spin lock of Java lock

Spinlock: Spinlock means that the thread trying to acquire the lock will not block immediately, but will try to acquire the lock in a circular manner. The advantage of this is to reduce the consumption of thread context switching, but the disadvantage is that the loop consumes CPU

In the original comparison and exchange mentioned, the bottom layer uses spin. Spin is multiple attempts and multiple accesses. The state that will not block is spin.

image-20200315154143781

Pros and cons

Advantages: loop comparison acquisition until success, there is no blocking similar to wait

Disadvantages: When more and more threads are spinning continuously, CPU resources will be consumed continuously due to the execution of the while loop

Handwritten spin lock

The spin lock is completed through the CAS operation. The A thread first calls the myLock method to hold the lock for 5 seconds. Then B comes in and finds that there is currently a thread holding the lock, which is not null, so it can only wait by spinning until A releases the lock. Then grabbed

/**
 * 手写一个自旋锁
 *
 * 循环比较获取直到成功为止,没有类似于wait的阻塞
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到

 */
public class SpinLockDemo {
    
    

    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() {
    
    
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {
    
    

        }
    }

    /**
     * 解锁
     */
    public void myUnLock() {
    
    

        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {
    
    

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {
    
    

            // 开始占有锁
            spinLockDemo.myLock();


            try {
    
    
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程,先执行
        try {
    
    
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {
    
    

            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();

    }
}

The final output

t1	 come in 
.....五秒后.....
t1	 invoked myUnlock()
t2	 come in 
t2	 invoked myUnlock()

The first output is t1 come in

Then 1 second later, the t2 thread starts and finds that the lock is occupied by t1. All the compareAndSet methods are continuously executed to compare until t1 releases the lock, that is, 5 seconds later, t2 successfully acquires the lock and then releases it.

Reentrant locks and recursive locks ReentrantLock

concept

Reentrant locks are recursive locks

It means that after the outer function of the same thread acquires the lock, the inner recursive function can still acquire the code of the lock. When the same thread acquires the lock in the outer method, it will automatically acquire the lock when it enters the inner method.

In other words:线程可以进入任何一个它已经拥有的锁所同步的代码块

ReentrantLock / Synchronized is a typical reentrant lock

Code

Re-entrant lock is to add a lock to a method1 method, method 2 is also locked, then they have the same lock

public synchronized void method1() {
    
    
	method2();
}

public synchronized void method2() {
    
    

}

In other words, we only need to enter method1, then it can also directly enter method2 method, because they have the same lock.

effect

The biggest role of reentrant locks is to avoid deadlocks

Reentrant lock verification

Proof Synchronized

/**
 * 可重入锁(也叫递归锁)
 * 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
 *
 * 也就是说:`线程可以进入任何一个它已经拥有的锁所同步的代码块`

 */

/**
 * 资源类
 */
class Phone {
    
    

    /**
     * 发送短信
     * @throws Exception
     */
    public synchronized void sendSMS() throws Exception{
    
    
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");

        // 在同步方法中,调用另外一个同步方法
        sendEmail();
    }

    /**
     * 发邮件
     * @throws Exception
     */
    public synchronized void sendEmail() throws Exception{
    
    
        System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
    }
}
public class ReenterLockDemo {
    
    


    public static void main(String[] args) {
    
    
        Phone phone = new Phone();

        // 两个线程操作资源列
        new Thread(() -> {
    
    
            try {
    
    
                phone.sendSMS();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
    
    
            try {
    
    
                phone.sendSMS();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

Here, we have written a resource class phone, which has two synchronization methods with synchronized, namely sendSMS and sendEmail. In the sendSMS method, we call sendEmail. Finally, two threads were opened for testing in the main thread at the same time, and the final result was:

t1	 invoked sendSMS()
t1	 invoked sendEmail()
t2	 invoked sendSMS()
t2	 invoked sendEmail()

This means that when the t1 thread enters sendSMS, it has a lock, and the t2 thread cannot enter at the same time. Until the t1 thread holds the lock and executes the sendEmail method, the lock is released, so that t2 can enter

t1	 invoked sendSMS()      t1线程在外层方法获取锁的时候
t1	 invoked sendEmail()    t1在进入内层方法会自动获取锁

t2	 invoked sendSMS()      t2线程在外层方法获取锁的时候
t2	 invoked sendEmail()    t2在进入内层方法会自动获取锁

Proof of ReentrantLock

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

/**
 * 资源类
 */
class Phone implements Runnable{
    
    

    Lock lock = new ReentrantLock();

    /**
     * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
     */
    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void setLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
    
    
            lock.unlock();
        }
    }

    @Override
    public void run() {
    
    
        getLock();
    }
}
public class ReenterLockDemo {
    
    


    public static void main(String[] args) {
    
    
        Phone phone = new Phone();

        /**
         * 因为Phone实现了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

Now we use ReentrantLock to verify. First, the resource class implements the Runnable interface, rewrites the Run method, and calls the get method inside. When the get method enters, the lock is added.

    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
        }
    }

Then in the method, call another setLock method with lock

    public void setLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
    
    
            lock.unlock();
        }
    }

In the final output, we can find that the result is the same as the synchronized method. After the outer method acquires the lock, the thread can directly enter the inner layer.

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock

What happens when we add two locks to the getLock method? (Ali interview)

    public void getLock() {
    
    
        lock.lock();
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
            lock.unlock();
        }
    }

The final result is the same, because no matter how many locks are in it, the others are the same lock, which means that they can be opened with the same key.

What happens when we add two locks to the getLock method, but only unlock one lock?

public void getLock() {
    lock.lock();
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "\t get Lock");
        setLock();
    } finally {
        lock.unlock();
        lock.unlock();
    }
}

got the answer

t3	 get Lock
t3	 set Lock

That is to say, the program is directly stuck and the thread cannot come out, which means that we apply for a few locks, and finally need to release a few locks.

What happens when we only add one lock, but use two locks to unlock it?

    public void getLock() {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
    
    
            lock.unlock();
            lock.unlock();
        }
    }

At this time, the running program will report an error directly

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)

Guess you like

Origin blog.csdn.net/weixin_43314519/article/details/110195932