Java locks in the matter

Java locks

Java to classify lock according to different characteristics about the following classification.

lock.png
Here we focus on optimistic and pessimistic locking and implement corresponding in Java.

For the same data concurrency, pessimistic locking consider themselves in the use of data, there will be other threads to modify the data, so before each operation data will add a lock to ensure that no other threads to modify the data. Java is synchronized locks and lock locks are pessimistic locking.
The optimistic locking think every time there will be no other threads to modify the data, so the data is not locked at the time of the operation, but when you modify the data to judge there is no other threads modify this data, if not modified, the update success, if it has been modified by another thread, then try again or failure. Java is the most commonly used is to achieve lock-free concurrent programming algorithms by CAS.

twoLock.png
According to the concept optimistic and pessimistic locking lock can be found:

  • Pessimistic locking for write once read many small scenes, because the first lock to ensure the correctness of the write operation.
  • Optimistic locking for reading and writing little scenes, because the read operation generally does not need to lock (not modify data), so no locking feature enables optimistic lock read performance is greatly improved (reduced waiting time lock) .

Pessimistic locking

Synchroniezd lock

Mutex is a synchronized, i.e. pessimistic locking, allows only one thread to enter a modified method or synchronized block of code. synchronized lock is reentrant, i.e., a thread can acquire the same lock object or class times.
synchronized, variable by using the built-in lock synchronization to ensure atomicity, ordering thread operations, visibility, ensuring safe operation under multiple threads.
Lock synchronized three different ways, namely to lock the object (modified conventional method, the object is to lock the current class), (non-class object lock current) block code lock, lock class (static modification method, the lock is the current class). More: synchronized lock
following is synchronized with a lock, with N threads 0 ~ M digital printing cycles.

public class SynchronizedTest implements Runnable {

	// 定义一个对象用来保持锁
	private static final Object LOCK = new Object();

	// 当前线程
	private int threadNum;

	// 线程总数
	private int threadSum;

	// 当前数字,从0开始打印
	private static int current = 0;

	// 要打印的最大值
	private int max;

	public SynchronizedTest(int threadNum, int threadSum, int max) {
		this.threadNum = threadNum;
		this.threadSum = threadSum;
		this.max = max;
	}

	@Override
	public void run() {
		// 实现N个线程循环打印数字
		while (true) {
			// 对代码块加锁,保证每次只有一个线程进入代码块
			synchronized (LOCK) {
				// 当前值 / 线程总数 = 当前线程
				// 这里一定要用while,而不能用if。因为当线程被唤醒时,监视条件可能还没有满足(线程唤醒后是从wait后面开始执行)。
				while (current % threadSum != threadNum) {
					// 打印完了,跳出循环
					if (current >= max) {
						break;
					}
					// 不满足打印条件,则让出锁,进入等待队列
					try {
						LOCK.wait();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				// 这里还要做一次判断
				if (current >= max) {
					break;
				}
				System.out.println(Thread.currentThread().getName() + " 打印 " + current);
				++current;
				// 当前线程打印完了,通知所有等待的线程进入阻塞队列,然后一起去争抢锁
				LOCK.notifyAll();
			}
		}
	}

	public static void main(String[] args) {
		// 开启N个线程
		int N = 3;
		// 打印M个数字
		int M = 15;
		for (int i = 0; i < N; ++i) {
			new Thread(new SynchronizedTest(i, N, M)).start();
		}
	}
}
复制代码

Lock Lock

Lock is also a mutex lock, while pessimistic locking. It displays a lock, the lock releasing operation of the lock requires manual implementation, synchronized release of the lock is automatic.
ReentrantLock define the internal lock lock fair and unfair lock. For fair locks, internal thread maintains a FIFO queue to hold incoming and ensure first in the thread can be executed first. For non-fair locks, if a thread releases the lock, all other threads can grab the lock, which would lead some people might starve to death, it may never get executed. But in order to achieve absolute fairness lock on the order of time, requiring frequent context switching, rather than a fair lock will reduce certain context switch overhead is reduced. So ReentrantLock non-default fair locks used to improve performance.
reentrantLock visibility is achieved by using volatile AQS modified state to achieve, to analyze the principle (non-fair locks, for example) below.
reentrantLock first display lock, call lock method.

final void lock() {
    // 先尝试获取锁,也就是将state更新为1(这里用了CAS),如果获取成功,则将当前线程设置为独占模式同步的当前所有者
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    // 如果获取失败,则进入acquire()方法
    else
        acquire(1);
}
复制代码

Following entry acquire () method:

public final void acquire(int arg) {
    // 调用tryAcquire尝试获取锁
    // 如果获取锁失败,则用当前线程创建一个独占结点加到等待队列的尾部,并继续尝试获取锁
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

Here only enter tryAcquire a look:

protected final boolean tryAcquire(int acquires) {
    // 内部又调用了一个非公平的尝试获取锁方法
    return nonfairTryAcquire(acquires);
}
复制代码

Enter look down:

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 重点!首先从主存中获取state(state是个volatile修饰的变量)
    int c = getState();
    // 如果state为0,说明没有获取过锁
    if (c == 0) {
        // 尝试获取锁
        if (compareAndSetState(0, acquires)) {
            // 将当前线程设置为独占模式当前所有者
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果state不为0,说明之前获取过锁
    else if (current == getExclusiveOwnerThread()) {
        // 将锁的数量叠加
        int nextc = c + acquires;
        if (nextc < 0) // 溢出(超过最大锁的数量)则抛出异常
            throw new Error("Maximum lock count exceeded");
        // 因为当前线程已经获取了锁,在这一步不会有其它线程来干扰,所以不需要用CAS来设置state
        setState(nextc);
        return true;
    }
    return false;
}
复制代码

The above is to get major code lock, if the acquisition fails, it will be added to the wait queue continue to try to acquire the lock. (This step is not analysis)
The following look at the process of releasing the lock:

public void unlock() {
    // 通过内部类调用父类AbstractQueuedSynchronizer的release方法
    sync.release(1);
}
复制代码

The following release into the method:

public final boolean release(int arg) {
    // 调用tryRelease方法来尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 如果头节点不为空且等待状态非0
        if (h != null && h.waitStatus != 0)
            // 如果头节点的后继节点存在,则唤醒它
            unparkSuccessor(h);
        return true;
    }
    return false;
}
复制代码

reentrantLock internal sync class overrides the method tryRelease:

protected final boolean tryRelease(int releases) {
    // 重点!也是首先获取state,并减去要释放的锁的数量
    int c = getState() - releases;
    // 如果当前线程不等于当前独占模式拥有者线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        // 抛出一个非法监视器状态异常
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果持有锁的数量为0
    if (c == 0) {
        // 设置锁为可释放
        free = true;
        // 把当前独占线程清空
        setExclusiveOwnerThread(null);
    }
    // 设置state
    setState(c);
    return free;
}
复制代码

These are the key to release the lock codes

Through the above analysis shows that, in each lock and release the lock when the time will enter first obtain state method, and finally to set the state end.
Since the state variables are modified by volatile, so the state is visible to all threads, and because the volatile variables at each time to force a refresh to the main memory when the non-volatile variables will also refresh back to main memory.
In the code locked in, certainly the first call lock (due to the operation of the volatile state (read before write), will force a refresh main memory), and finally call unlock (also operating state, it will be forced once again to refresh main memory), according happens-before rule, write volatile variable is read next is visible to the. So it ensures the synchronization code shared variable is visible.

Here is an alternating cycle reentrantLock achieve printing ABC, which also locks the variable conditions of use condition.

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

public class ReentrantLockTest {

	// 定义一个显示锁
	private static ReentrantLock lock = new ReentrantLock();
	// 监控a的条件变量
	private static Condition a = lock.newCondition();
	// 监控b的条件变量
	private static Condition b = lock.newCondition();
	// 监控c的条件变量
	private static Condition c = lock.newCondition();
	// 控制要打印的值
	private static int flag = 0;

	public static void printA() {
		for (int i = 0; i < 5; i++) {
			// 显示加锁
			lock.lock();
			try {
				try {
					while (flag != 0) {
						// 不满足监视条件则等待
						a.await();
					}
					System.out.println(Thread.currentThread().getName() + "我是A");
					flag = 1;
					// 通知b线程去打印
					b.signal();
				} catch (Exception e) {
					e.printStackTrace();
				}
			} finally {
				// 释放锁
				lock.unlock();
			}
		}
	}

	public static void printB() {
		for (int i = 0; i < 5; i++) {
			lock.lock();
			try {
				try {
					while (flag != 1) {
						b.await();
					}
					System.out.println(Thread.currentThread().getName() + "我是B");
					flag = 2;
					c.signal();
				} catch (Exception e) {
					e.printStackTrace();
				}
			} finally {
				lock.unlock();
			}
		}
	}

	public static void printC() {
		for (int i = 0; i < 5; i++) {
			lock.lock();
			try {
				try {
					while (flag != 2) {
						c.await();
					}
					System.out.println(Thread.currentThread().getName() + "我是C");
					flag = 0;
					a.signal();
				} catch (Exception e) {
					e.printStackTrace();
				}
			} finally {
				lock.unlock();
			}
		}

	}

	public static void main(String[] args) {

		new Thread(() -> {
			printA();
		}).start();

		new Thread(() -> {
			printB();
		}).start();

		new Thread(() -> {
			printC();
		}).start();
	}
}
复制代码

synchronized locks and lock reentrantLock

Same point

  • Can for resource locking to ensure access synchronization between threads.
  • They are reentrant lock, that is, one thread can have more resources repeatedly locked.
  • Are guaranteed a multi-threaded atomicity, orderly operation, visibility (this can only be guaranteed in the shared variable locking visibility in the operation, while locking visibility outside the operation is not an absolute guarantee, because the outer lock We can not guarantee that has been acquiring data from main memory, working memory may be out of sync)

difference

  • Different mechanisms to achieve synchronization
    • synchronized java object is achieved by the associated monitor monitors
    • reentrantLock through AQS, CAS and so achieve
  • Different mechanisms to achieve visibility
    • synchronized by java memory model to ensure visibility
    • reentrantLock to ensure visibility through the AQS state (volatile modified)
  • Different monitoring conditions
    • synchronized by a java object monitoring conditions
    • reentrantLock by Condition (providing await, signal or the like) as the monitoring condition
  • Use different ways
    • Examples of the method used to modify synchronized (locked instance of an object), the static method (locked class object), the synchronization code block (locked objects specified)
    • Call lock reentrantLock need to show the lock, and finally the need to release the lock
  • Different feature-rich program
    • synchronized simply locking
    • ReentrantLock provides timing to acquire the lock, the lock may be interrupted acquire, for condition Condition (provided await, signal or the like) and other features.
  • Different types of locks
    • synchronized only support fee fair locks.
    • reentrantLock support fair locks and lock unfair, but non-higher efficiency fair locks

Before synchronized optimization, it is relatively heavyweight, its performance is much worse than ReentrantLock, but since the introduction of synchronized biased locking, lightweight lock (spin lock), locks removed, lock coarsening techniques, both performance it is almost the same phase.

Optimistic locking

CAS algorithm

I.e., compare and swap (compare and swap) is a well-known lock-free algorithms. Lock-free programming, that is, when not in use lock variable synchronization between multiple threads, which is synchronized variable in the absence of the thread is blocked, it is also called non-blocking synchronization (Non-blocking Synchronization).
CAS algorithm involves three operands:

  • The need to read and write memory value V (values ​​in memory)
  • Value A (the input value) is compared
  • The new value B (to be updated value) to be written

If and only if the value of V is equal to the value A, CAS atomically value V is updated to B (comparison and substitution is an atomic operation, the unsafe bottom to guarantee the atomicity of the operating system), if V is not equal to A, then a failure or Retry. There is no concern to the lock operation, it is very efficient.
But it has three problems:

  • Large loop overhead. If CAS is not successful for a long time (greater than concurrent write), it will lead to a long spin, resulting in a waste of CPU resources.
  • Atomic operation can only guarantee a variables. Providing AtomicReference but starting JDK1.5 classes to guarantee atomicity references between objects may be variable in the plurality of objects in a CAS operation is performed.
  • ABA problem. If the CAS first value to B, and back to A. In CAS it seems that the value is not changed, but actually changed conditions. The most typical is the money from the ATM issue: Balance 100, I took out 50, then ATM opened two threads, but a thread temporarily hung up, a thread successfully update the balance is 50, and my friend gave me turn 50 in this case amounted to 100, but just take money out of that thread back to life, just continue the operation, 100 attempts to update the 50, emmm, it appears in the CAS can be successful, but this is not logical ( my friend forwarded to me it's 50 going ???). General Solutions ABA problem is to add a variable before the version number, this update becomes 1A-> 2B-> 3A, so that CAS will think they are not the same. JDK1.5 began offering AtomicStampedReference introduced flag, the current flag and the flag compareAndSet expected of this class () method is required to update the same success (this flag will be updated with each update).

Here CountDownLatch combination AtomicStampedReference and implement an example of the ABA (the problem can be solved by version number)

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CasTest {

    // 定义一个原子类型变量
	private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(1, 0);

    // 定义一个线程计数器
	private static CountDownLatch countDownLatch = new CountDownLatch(1);

	public static void main(String[] args) {
		new Thread(() -> {
			// 打印当前线程的值
				System.out.println("线程 " + Thread.currentThread().getName()
						+ " value" + asr.getReference());
				// 最开始的版本
				int stamp = asr.getStamp();
				try {
					// 等待其它线程全部执行完毕(这里只需等待线程2运行结束)
					countDownLatch.await();
				} catch (Exception e) {
					e.printStackTrace();
				}
				// 将1改为2,又改为1后,再次尝试将最开始的版本的1修改为2
				// 操作结果应该是失败的,因为当前版本(0)与预期版本(2)不同
				// 如果将取版本号的操作放在当前,操作结果肯定是成功的(因为这里修改的1不是最开始版本的1)
				System.out.println(Thread.currentThread().getName()
						+ " CAS操作结果 "
						+ asr.compareAndSet(1, 2, stamp, stamp + 1));
			}, "线程1").start();

		new Thread(() -> {
			// 把值修改为2
				asr.compareAndSet(1, 2, asr.getStamp(), asr.getStamp() + 1);
				System.out.println("线程 " + Thread.currentThread().getName()
						+ " value" + asr.getReference());
				// 把值修改为1
				asr.compareAndSet(2, 1, asr.getStamp(), asr.getStamp() + 1);
				System.out.println("线程 " + Thread.currentThread().getName()
						+ " value" + asr.getReference());
				// 当前任务执行完毕,等待线程数减1
				countDownLatch.countDown();
			}, "线程2").start();
	}
}
复制代码

Guess you like

Origin juejin.im/post/5df84b7e6fb9a016323d7ec3