007. Use of J.U.C locks

Keywords: Programming Redis Database jvm less

1. Lock API

1. Locks package class hierarchy

2. Lock interface

Method Signature describe
void lock(); Acquire locks (endless)
boolean tryLock(); Acquire locks (just try it)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; Acquire locks (out of date)
void lockInterruptibly() throws InterruptedException; Acquire locks (at the mercy of others)
void unlock(); Release lock
Condition newCondition();
  • Conclusion:

    • lock() is most commonly used and threads are not interrupted;
    • The lockInterruptibly() method is generally more expensive, and some impls may not implement lockInterruptibly(), but only if an effect interrupt is really needed. Check the description of impl before using it.
public class Demo1_GetLock {

    // fair lock
//    static Lock lock = new ReentrantLock(true);

    // Unfair Lock
    static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // Main thread gets lock
        lock.lock();

        Thread th = new Thread(() -> {
            /*
            // Subthreads acquire locks (just try it)
            boolean result = lock.tryLock();
            System.out.println("Acquire lock: "+ result";
             */

            /*
            // Subthreads acquire locks (out of date)
            try {
                System.out.println("Start acquiring lock ";
                boolean result = lock.tryLock(5, TimeUnit.SECONDS);
                System.out.println("Acquire lock: "+ result";
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
             */

            /*
            // Subthreads acquire locks (at their disposal)
            try {
                System.out.println("Start acquiring lock Interruptibly ";
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("The parent thread stopped me.");
            }
             */

            // Subthreads acquire locks (endless)
            System.out.println("Start acquiring locks");
            lock.lock();
            System.out.println("Lock acquired successfully");
        });
        th.start();

        Thread.sleep(7000);
        th.interrupt();
        System.out.println("th interrupted ...");

        Thread.sleep(15000);
        lock.unlock();
    }

}

3. Condition

  • The wait(), notify(), notifyAll() in Object can only be used with synchronized and can wake up one or all (a single wait set);
  • Condition s need to work with Lock to provide multiple waiting sets for more precise control.
Collaboration methods Deadlock mode 1 Deadlock Mode 2 (Wake Up, Suspend) Remarks
suspend/resume deadlock deadlock Discard
wait/notify Undeadlocked deadlock Used only for synchronized keywords
park/unpark deadlock Undeadlocked
condition Undeadlocked deadlock
  • Simulate condition deadlock

    public class Demo3_Condition {
        private static Lock lock = new ReentrantLock();
        private static Condition condition = lock.newCondition();
    
        public static void main(String[] args) throws InterruptedException {
            Thread th = new Thread(() -> {
                try {
                    // The simulation calls signal first, then await, and it is deadlocked
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
    
                try {
                    System.out.println("here I am ... 1");
                    condition.await();
                    System.out.println("here I am ... 2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            th.start();
    
            Thread.sleep(5000);
    
            lock.lock();
            condition.signal();
            lock.unlock();
        }
    }
    
  • Classic example: Blocking a queue through a condition

    public class Demo4_Condition3 {
        public static void main(String[] args) throws InterruptedException {
            MyBlockingQueue bb = new MyBlockingQueue(5);
    
            new Thread(() -> {
                for (int i = 0; i < 20; i ++) {
                    bb.put("x" + i);
                }
            }).start();
    
            Thread.sleep(3000);
            System.out.println("Start extracting elements from the queue...");
            for (int i = 0; i < 10; i ++) {
                bb.take();
                Thread.sleep(3000);
            }
        }
    }
    
    /**
     * Implement a blocking queue of your own and store only n elements
     * put When the queue is not full, put it directly
     *           If the queue is full, it will block until there is more space
     * get When there are elements in the queue, get the elements
     *           If there are no elements, wait for them
     */
    
    class MyBlockingQueue {
        List<Object> list = new ArrayList<>();
        private int length;
    
        private Lock lock = new ReentrantLock();
        private Condition putCondition = lock.newCondition();
        private Condition takeCondition = lock.newCondition();
    
        public MyBlockingQueue(int length) {
            this.length = length;
        }
    
        public void put(Object obj) {
            lock.lock();
            try {
                while (true) {
                    if (list.size() < this.length) {
                        list.add(obj);
                        takeCondition.signal();
                        System.out.println("put: " + obj);
                        return;
                    } else {
                        putCondition.await();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public Object take() {
            lock.lock();
            try {
                while (true) {
                    if (list.size() > 0) {
                        Object obj = list.remove(0);
                        putCondition.signal();
                        System.out.println("tack: " + obj);
                        return obj;
                    } else {
                        takeCondition.await();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                return null;
            }
        }
    }
    

4. ReentrantLock

public class Demo5_Reentrant {
    static Lock lock = new ReentrantLock(); // Re-lockable

    public static void main(String[] args) {
        System.out.println("get lock 1 ...");
        lock.lock();

        System.out.println("get lock 2 ...");
        lock.lock();

        // Print a message that the lock has been acquired again
        System.out.println("here I am ...");

        System.out.println("unlock 1 ...");
        lock.unlock();

        System.out.println("unlock 2 ...");
        lock.unlock();

        System.out.println("unlock 3 ...");
        lock.unlock(); // Third lock release, error
    }
}

  • Implement a ReentrantLock on your own
public class MyReentrantLock implements Lock {
    // Lock Owner
    AtomicReference<Thread> owner = new AtomicReference<>();

    // count recording the number of bursts
    AtomicInteger count = new AtomicInteger(0);

    // Waiting queue
    private LinkedBlockingDeque<Thread> waiters = new LinkedBlockingDeque<>();

    @Override
    public boolean tryLock() {
        int ct = count.get();
        // Determine if count is 0, if count!=0, the lock is occupied
        if (ct != 0) {
            if (Thread.currentThread() == owner.get()) {
                // Determine if the current thread is occupying or reentrant if the current thread is occupying
                count.set(ct + 1);
                return true;
            }
        } else {
            // If count=0, the lock is not occupied, use CAS to lock
            if (count.compareAndSet(ct, ct + 1)) {
                owner.set(Thread.currentThread());
                return true;
            }
        }
        return false;
    }

    @Override
    public void lock() {
        if (! tryLock()) {
            // No locks, join the waiting queue
            waiters.offer(Thread.currentThread());

            LockSupport.park();

            while (true) {
                // If the thread is the head of the queue, try locking
                Thread head = waiters.peek();
                if (head == Thread.currentThread()) {
                    if (!tryLock()) {
                        LockSupport.park();
                    } else {
                        waiters.poll();
                        return;
                    }
                } else {
                    // Thread Suspend
                    LockSupport.park();
                }
            }
        }
    }

    public boolean tryUnlock() {
        if (owner.get() != Thread.currentThread()) {
            throw new IllegalMonitorStateException();
        } else {
            // unlock is count-1
            int ct = count.get();
            int nextC = ct - 1;
            count.set(nextC);

            // Determines if count is 0, if 0, the lock is released successfully, otherwise the release fails
            if (nextC == 0) {
                owner.compareAndSet(Thread.currentThread(), null);
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    public void unlock() {
        if (tryUnlock()) {
            Thread head = waiters.peek();
            if (head != null) {
                LockSupport.unpark(head);
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }


    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

5. synchronized vs Lock

  • Synchronized

    • Advantage
      • Easy to use, clear semantics, where to point and where to go.
      • Provided by the JVM, various optimization schemes (lock coarsening, lock elimination, biased lock, lightweight lock) are provided.
      • The release of locks is done by virtual machines without manual intervention and reduces the likelihood of deadlocks.
    • shortcoming
      • Some lock advanced functions such as fair lock, interrupt lock, timeout lock, read-write lock, shared lock, etc. cannot be implemented.
  • Lock

    • Advantage
      • All the shortcomings of synchronized.
      • More functionality can be achieved to make synchronized more vulnerable.
    • shortcoming
      • The unlock needs to be released manually, and improper use by novices can cause deadlocks.

2. ReadWriteLock

1. Concepts

  • Maintains a pair of associated locks, one for read-only operations and one for write-only operations.
  • Read locks can be held simultaneously by multiple threads, write locks are exclusive, and at the same time, two locks cannot be held by different threads.

2. Demonstration of Lock's drawbacks in the case of more reading and less writing

public class Demo6_ReadWrite01 {
    volatile long i = 0;

    Lock lock = new ReentrantLock();

    public void read() {
        lock.lock();
        long a = i;
        lock.unlock();
    }

    public void write() {
        lock.lock();
        i ++;
        lock.unlock();
    }

    public static void main(String[] args) {
        final Demo6_ReadWrite01 demo = new Demo6_ReadWrite01();

        for (int i = 0; i <= 30; i ++) {
            int n = i;
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                for (int j = 0; j < 400000; j ++) {
                    if (n == 0) demo.write();
                    else demo.read();
                }
            }).start();
        }

        long endTime = System.currentTimeMillis();
    }
}
  • At this point, only the first time is a write operation, the other is a read operation.With Lock, only one thread can read at a time, which can be very inefficient.

  • The following is a demonstration of ReadWriteLock

public class Demo6_ReadWrite02 {
    volatile long i = 0;

    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void read() {
        rwLock.readLock().lock();
        long a = i;
        rwLock.readLock().unlock();
    }

    public void write() {
        rwLock.writeLock().lock();
        i ++;
        rwLock.writeLock().unlock();
    }

    public static void main(String[] args) {
        final Demo6_ReadWrite02 demo = new Demo6_ReadWrite02();

        for (int i = 0; i <= 30; i ++) {
            int n = i;
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                for (int j = 0; j < 400000; j ++) {
                    if (n == 0) demo.write();
                    else demo.read();
                }
            }).start();
        }

        long endTime = System.currentTimeMillis();
    }
}

3. Scenarios applicable

  • Suitable for scenarios where there are more read operations than write operations to improve mutex performance.Examples include security modifications for concurrent threads of collections, caching components.

  • Turn HashMap into thread-safe

// Turn hashmap into a concurrently secure
// This is an example given in the ReentrantReadWriteLock comment
public class Demo7_Map {
    private final Map<String, Object> m = new HashMap<>();

    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public Object get(String key) {
        r.lock();
        try {
            return m.get(key);
        } finally {
            r.unlock();
        }
    }

    public Object put(String key, Object value) {
        w.lock();
        try {
            return m.put(key, value);
        } finally {
            w.unlock();
        }
    }

    public void clear() {
        w.lock();
        try {
            m.clear();
        } finally {
            w.unlock();
        }
    }

}

4. Lock Degradation

  • Refers to the downgrade of a write lock to a read lock.The process of acquiring a read lock while holding a write lock and then releasing the write lock.
  • Write locks are thread exclusive and read locks are shared, so write->read is downgraded.(Read->Write is not possible)
public class Demo8_CacheData {
    public static void main(String[] args) {

    }
}

class TeacherInfoCache {
    static volatile boolean cacheValid;
    static final ReadWriteLock rwl = new ReentrantReadWriteLock();

    /**
     * Implement a way to use Cache data
     * Get data directly if the cache is available
     * Load data from DB to cache if cache is not available
     * Once you get the data, you need to use it, and it won't change during this time
     */
    static void processCacheData(String dataKey) {
        Object data = null;
        rwl.readLock().lock();
        try {
            if (cacheValid) {
                data = Redis.data.get(dataKey);
            } else {
                // Query the database for data
//                DataBase.queryUserInfo(); //Cache avalanche problem
                rwl.readLock().lock();
                rwl.writeLock().lock();
                try {
                    if (! cacheValid) {
                        data = DataBase.queryUserInfo();
                        Redis.data.put(dataKey, data);
                        cacheValid = true;
                    } else {
                        data = Redis.data.get(dataKey);
                    }
                } finally {
                    // Lock Degradation Threads acquire read locks while holding write locks and have both read and write locks
                    rwl.readLock().lock();
                    rwl.writeLock().unlock();
                }
            }
        } finally {
            rwl.readLock().unlock();
        }
    }
}

class DataBase {
    static String queryUserInfo() {
        System.out.println("query data base...");
        return "name:Kody,age:40,gender:true";
    }
}

class Redis {
    static Map<String, Object> data = new HashMap<>();
}

5. Internal Principles of Read-Write Locks

3. Read-write lock implementation

1. Implement a reentrant and read-write lock yourself

  • Common Code
public class MyAQS {

    AtomicInteger readCount = new AtomicInteger(0);
    AtomicInteger writeCount = new AtomicInteger(0);

    // Exclusive Lock, Owner
    AtomicReference<Thread> owner = new AtomicReference<>();

    // Waiting queue
    public volatile LinkedBlockingQueue<WaitNode> waiters = new LinkedBlockingQueue<>();
    public class WaitNode {
        int type = 0; // 0 for threads that want to acquire exclusive locks and 1 for threads that want to acquire shares
        Thread thread =  null;
        int arg = 0;

        public WaitNode(Thread thread, int type, int age) {
            this.thread = thread;
            this.type = type;
            this.arg = arg;
        }
    }

    public void lock() {
        int arg = 1;
        // Attempt to acquire an exclusive lock, if successful, exit the method, if failed...
        if (! tryLock(arg)) {
            // Mark as Exclusive
            WaitNode waitNode = new WaitNode(Thread.currentThread(), 0, arg);
            waiters.offer(waitNode); // Enter the waiting queue

            // Cycle Attempt to Hold Lock
            while (true) {
                // If the queue header is the current thread
                WaitNode head = waiters.peek();
                if (head != null && head.thread == Thread.currentThread()) {
                    if (! tryLock(arg)) { // Try to acquire an exclusive lock again
                        LockSupport.park(); // Suspend thread if failed
                    } else { // If successful
                        waiters.poll(); // Remove the current thread from the header of the queue
                        return; // And exit the method
                    }
                } else { // If it is not a queue header element
                    LockSupport.park(); // Suspend the current thread
                }
            }
        }
    }

    // Release exclusive lock
    public boolean unlock() {
        int arg = 1;

        // Attempt to release an exclusive lock, return true successfully, or fail...
        if (tryUnlock(arg)) {
            WaitNode head = waiters.peek();
            if (head != null) {
                Thread th = head.thread;
                LockSupport.unpark(th); // The thread that wakes up the head of the queue
            }
            return true; // Return true
        }
        return false;
    }

    // Acquire shared locks
    public void lockShared() {
        int arg = 1;

        if (tryLockShared(arg) < 0) { // If tryLockShared fails
            // Queue the current thread
            WaitNode node = new WaitNode(Thread.currentThread(), 1, arg);
            waiters.offer(node); // Join the queue

            while (true) {
                // If the element at the head of the queue is the current thread
                WaitNode head = waiters.peek();
                if (head != null && head.thread == Thread.currentThread()) {
                    if (tryLockShared(arg) >= 0) { // Attempt to acquire shared lock, successful
                        waiters.poll(); // Remove the current thread from the queue

                        WaitNode next = waiters.peek();
                        if (next != null && next.type == 1) { // If the next thread is also waiting for a shared lock
                            LockSupport.unpark(next.thread); // Wake it up
                        }
                        return; // Exit method
                    }
                } else { // If the attempt fails
                    LockSupport.park();
                }
            }
        }
    }

    // Unlock shared locks
    public boolean unLockShared() {
        int arg = 1;

        if (tryUnLockShared(arg)) { // When read count becomes zero, release share succeeds
            WaitNode next = waiters.peek();
            if (next != null) {
                LockSupport.unpark(next.thread);
            }
            return true;
        }
        return false;
    }

    // Attempt to acquire an exclusive lock
    public boolean tryLock(int acquires) {
        throw new UnsupportedOperationException();
    }

    // Attempt to release an exclusive lock
    public boolean tryUnlock(int releases) {
        throw new UnsupportedOperationException();
    }

    // Attempt to acquire shared lock
    public int tryLockShared(int acquires) {
        throw new UnsupportedOperationException();
    }

    // Attempt to unlock shared lock
    public boolean tryUnLockShared(int releases) {
        throw new UnsupportedOperationException();
    }

}
  • Reentry Lock Implementation
public class MyReentrantLock implements Lock {

    private boolean isFair;

    public MyReentrantLock(boolean isFair) {
        this.isFair = isFair;
    }

    MyAQS common = new MyAQS() {
        public boolean tryLock(int acquires) {
            if (isFair) {
                return tryFairLock(acquires);
            } else {
                return tryNotFairLock(acquires);
            }
        }

        // Unfair Lock
        public boolean tryNotFairLock(int acquires) {
            // If read count!= 0, returns false
            if (readCount.get() != 0) {
                return false;
            }

            int wct = writeCount.get(); // Get the current state of the exclusive lock

            if (wct == 0) {
                if (writeCount.compareAndSet(wct, wct + 1)) { // Lock by modifying state
                    owner.set(Thread.currentThread()); // Modify owner directly to current thread after lock is grabbed
                    return true;
                }
            } else if (owner.get() == Thread.currentThread()) {
                writeCount.set(wct + acquires); // Modify count value
                return true;
            }

            return false;
        }

        // fair lock
        public boolean tryFairLock(int acquires) {
            // If read count!= 0, returns false
            if (readCount.get() != 0) {
                return false;
            }

            int wct = writeCount.get(); // Get the current state of the exclusive lock

            if (wct == 0) {
                WaitNode head = waiters.peek();
                if (head != null && head.thread == Thread.currentThread() &&
                        writeCount.compareAndSet(wct, wct + 1)) { // Lock by modifying state
                    owner.set(Thread.currentThread()); // Modify owner directly to current thread after lock is grabbed
                    return true;
                }
            } else if (owner.get() == Thread.currentThread()) {
                writeCount.set(wct + acquires); // Modify count value
                return true;
            }

            return false;
        }

        // Attempt to release an exclusive lock
        public boolean tryUnlock(int releases) {
            // If the current thread does not hold an exclusive lock
            if (owner.get() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }

            int wc = writeCount.get();
            int nextc = wc - releases; // Calculate Exclusive Lock Remaining Occupancy
            writeCount.set(nextc);

            if (nextc == 0) { // Is it fully released
                owner.compareAndSet(Thread.currentThread(), null);
                return true;
            } else {
                return false;
            }
        }
    };

    public void lock() {
        common.lock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock() {
        return common.tryLock(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        common.unlock();
    }

    @Override
    public Condition newCondition() {
        return null;
    }

}
  • Read-write lock implementation
public class MyReadWriteLock implements ReadWriteLock {
    MyAQS common = new MyAQS() {
        // Attempt to acquire an exclusive lock
        public boolean tryLock(int acquires) {
            // If read count!= 0, returns false
            if (readCount.get() != 0) {
                return false;
            }

            int wct = writeCount.get(); // Get the current state of the exclusive lock

            if (wct == 0) {
                if (writeCount.compareAndSet(wct, wct + 1)) { // Lock by modifying state
                    owner.set(Thread.currentThread()); // Modify owner directly to current thread after lock is grabbed
                    return true;
                }
            } else if (owner.get() == Thread.currentThread()) {
                writeCount.set(wct + acquires); // Modify count value
                return true;
            }

            return false;
        }

        // Attempt to release an exclusive lock
        public boolean tryUnlock(int releases) {
            // If the current thread does not hold an exclusive lock
            if (owner.get() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }

            int wc = writeCount.get();
            int nextc = wc - releases; // Calculate Exclusive Lock Remaining Occupancy
            writeCount.set(nextc);

            if (nextc == 0) { // Is it fully released
                owner.compareAndSet(Thread.currentThread(), null);
                return true;
            } else {
                return false;
            }
        }

        // Attempt to acquire shared lock
        public int tryLockShared(int acquires) {
            while (true) {
                if (writeCount.get() != 0 && owner.get() != Thread.currentThread()) {
                    return -1;
                }

                int rct = readCount.get();
                if (readCount.compareAndSet(rct, rct + acquires)) {
                    return 1;
                }
            }
        }

        // Attempt to unlock shared lock
        public boolean tryUnLockShared(int releases) {
            while (true) {
                int rc = readCount.get();
                int nextc = rc - releases;
                if (readCount.compareAndSet(rc, nextc)) {
                    return nextc == 0;
                }
            }
        }
    };

    @Override
    public Lock readLock() {
        return new Lock() {
            @Override
            public void lock() {
                common.lock();
            }

            @Override
            public boolean tryLock() {
                return common.tryLock(1);
            }

            @Override
            public void unlock() {
                common.unlock();
            }

            @Override
            public void lockInterruptibly() throws InterruptedException {

            }

            @Override
            public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
                return false;
            }

            @Override
            public Condition newCondition() {
                return null;
            }
        };
    }

    @Override
    public Lock writeLock() {
        return new Lock() {
            @Override
            public void lock() {
                common.lockShared();
            }

            @Override
            public boolean tryLock() {
                return common.tryLockShared(1) == 1;
            }

            @Override
            public void unlock() {
                common.unLockShared();
            }

            @Override
            public void lockInterruptibly() throws InterruptedException {

            }

            @Override
            public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
                return false;
            }

            @Override
            public Condition newCondition() {
                return null;
            }
        };
    }
}
  • Reentry Lock Test
public class Test_ReentrantLock {

    volatile static int i = 0;

    static MyReentrantLock lc = new MyReentrantLock(true);

    public static void add() {
        lc.lock();
        i ++;
        lc.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i ++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j ++) {
                    add();
                }
                System.out.println("done...");
            }).start();
        }

        Thread.sleep(6000);
        System.out.println(i);
    }

}
  • Read-Write Lock Test
public class Test_ReadWriteLock {

    static MyReadWriteLock rwLock = new MyReadWriteLock();

    static volatile int i = 0;

    static void add() {
        i ++;
    }

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();

        for (int a = 0; a < 20000; a ++) {
            final int n = a;
            new Thread(() -> {
                if (n % 5 == 0) {
                    rwLock.writeLock().lock();
                    add();
                    rwLock.writeLock().unlock();
                } else {
                    rwLock.readLock().lock();
                    System.out.println("i = " + i);
                    int b = i;
                    rwLock.readLock().unlock();
                }
            }).start();
        }

        while (true) {
            System.out.println("Current Time-consuming: " + (System.currentTimeMillis() - startTime));
            Thread.sleep(1000);
            System.out.println("i = " + i);
        }
    }

}

2. AQS(AbstractQueuedSynchronizer) Abstract Queue Synchronizer

  • java.util.concurrent.locks.AbstractQueuedSynchronizer

  • Provides logic for resource occupancy, release, suspension and awakening of threads.
  • Reserved for various try methods to be implemented by the user.
  • It can be used in a variety of scenarios where you need to control resource contention.(ReentrantLock/CountDownLatch/Semphore)

3. ReadWriteLock stores two count values in an int

Posted by Smasher on Sun, 05 Apr 2020 17:27:36 -0700