Java species introduced 15 kinds of locks: fair locks, reentrant lock, exclusive lock, mutex, etc.

Java's introduction of 15 locks

In many concurrent reading the article will mention a wide variety of locks such as fair locks, optimistic locks, and so on, this article describes the classification of various locks. Introduction of the following:

  • Lock fair / unfair Lock

  • Reentrant lock / non-lock reentrant

  • Exclusive lock / shared lock

  • Mutex / read-write lock

  • Lock optimistic / pessimistic locking

  • Segmented lock

  • Biased locking / lock lightweight / heavyweight lock

  • Spinlocks

The above is a lot of locks term, these categories are not all referring to the lock state, some refer to characteristics of the lock, the lock refers to some of the design, content is summarized below for each lock a certain interpretation of the term.

Lock fair / unfair Lock

Fair locks

  • Fair lock means that multiple threads in order to apply the lock to get the lock.


Unfair lock

  • Unfair lock refers to the order of multiple threads to obtain the lock is not in order to apply the lock, it is possible to apply the thread after thread priority to acquire the lock than prior application. Possible cause priority inversion or hunger.


For Java ReentrantLock, the constructor specify whether the lock by lock is fair, non-default fair locks. Unfair advantage that the lock is larger than the throughput fairness lock. For Synchronized, it is also an unfair lock. Because it is not as ReentrantLock be achieved by AQS thread scheduling, and so there is no way that it becomes fair locks.

Reentrant lock / non-lock reentrant

Reentrant lock

Reentrant lock in a broad sense refers to the lock may be called recursively repeated, after use of the outer lock, still may be used in the inner layer, and deadlock does not occur (provided that have the same object or class), such a lock called reentrant lock. ReentrantLock are synchronized and locked reentrant

synchronized void setA() throws Exception{
 Thread.sleep(1000);
 setB();
}
synchronized void setB() throws Exception{
 Thread.sleep(1000);
}

The code above is a re-lock into a characteristic, if not reentrant lock, then, setB may not be current thread of execution, may cause a deadlock.

Non-reentrant lock

Not reentrant lock, the lock opposite the reentrant, non-recursive call, the recursive call to deadlock. See explanation of a classic, spinlock used to simulate a non-reentrant lock, as follows

import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
 private AtomicReference<Thread> owner = new AtomicReference<Thread>();
 public void lock() {
 Thread current = Thread.currentThread();
 // The message is very classic "spin" syntax, AtomicInteger also has
 for (;;) {
 if (!owner.compareAndSet(null, current)) {
 return;
 }
 }
 }
 public void unlock() {
 Thread current = Thread.currentThread();
 owner.compareAndSet(current, null);
 }
}

The code is relatively simple to use references to store atomic threads, the same thread two calls lock () method, if you do not perform the unlock () to release the lock, then the second call will spin when the deadlock, the lock is not reentrant, and in fact do not always have the same thread releases the lock again to acquire the lock, such scheduling switch is a waste of resources.

Turn it into a reentrant lock  :

import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
 private AtomicReference<Thread> owner = new AtomicReference<Thread>();
 private int state = 0;
 public void lock() {
 Thread current = Thread.currentThread();
 if (current == owner.get()) {
 state++;
 return;
 }
 // The message is very classic "spin" grammar, AtomicInteger also has
 for (;;) {
 if (!owner.compareAndSet(null, current)) {
 return;
 }
 }
 }
 public void unlock() {
 Thread current = Thread.currentThread();
 if (current == owner.get()) {
 if (state != 0) {
 state--;
 } else {
 owner.compareAndSet(current, null);
 }
 }
 }
}

Before performing each operation, it is judged whether the current lock owner is the current object using the counting state, do not always go to release the lock.

ReentrantLock locks for the reentrant

Here to see the unfair lock lock acquisition method:

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	//right here
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

AQS maintenance in a private volatile int state to count the number of re-entry, to avoid the frequent holding release operation, so that not only improves efficiency, but also avoid the deadlock.

Exclusive lock / shared lock

Exclusive locks and shared locks you will find in ReeReentrantLock and ReentrantReadWriteLock under you read CUT package, Talia is exclusive one is a shared lock.

  • Exclusive lock  : The lock can only be held once every thread.

  • Shared locks  : The locks can be more threads there are, in typical is ReentrantReadWriteLock read lock, it read locks can be shared, but it really can only write locks are exclusive.

Also shared read lock ensures concurrent read is very efficient, but read and write and write, read-write are mutually exclusive.

Exclusive lock and a shared lock is achieved through the AQS, by implementing different methods to achieve exclusive or shared. For Synchronized, of course, it is the exclusive lock.

Mutex / read-write lock

Mutex

在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源

读写锁

读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

读写锁有三种状态 :读加锁状态、写加锁状态和不加锁状态

读写锁在Java中的具体实现就是 ReadWriteLock

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。 只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。

乐观锁 / 悲观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁( 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程 )。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐 观锁适用于多读的应用类型,这样可以提高吞吐量 ,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的 原子变量类就是使用了乐观锁的一种实现方式CAS实现的 

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。

在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能。在锁上发生竞争时将通水导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。

我们一般有三种方式降低锁的竞争程度 : 1、减少锁的持有时间 2、降低锁的请求频率 3、使用带有协调机制的独占锁,这些机制允许更高的并发性。

在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。

其实说的简单一点就是 

容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。

偏向锁 / 轻量级锁 / 重量级锁

锁的状态 

  • 无锁状态

  • 偏向锁状态

  • 轻量级锁状态

  • 重量级锁状态

锁的状态是通过对象监视器在对象头中的字段来表明的。 四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。 这四种状态都不是Java语言中的锁 ,而是Jvm为了提高锁的获取与释放效率而做的优化( 使用synchronized时 )。

偏向锁

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。


轻量级

  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。


重量级锁

  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。


自旋锁

我们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,所以这里给大家讲一下什么是自旋锁。

简单回顾一下CAS算法

CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V

  • 进行比较的值 A

  • 拟写入的新值 B

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。

什么是自旋锁?

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环 

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。 无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁 。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

Java如何实现自旋锁?

下面是个简单的例子:

public class SpinLock {
 private AtomicReference<Thread> cas = new AtomicReference<Thread>();
 public void lock() {
 Thread current = Thread.currentThread();
 // 利用CAS
 while (!cas.compareAndSet(null, current)) {
 // DO nothing
 }
 }
 public void unlock() {
 Thread current = Thread.currentThread();
 cas.compareAndSet(current, null);
 }
}

lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

自旋锁存在的问题

1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。 2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

可重入的自旋锁和不可重入的自旋锁

文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

public class ReentrantSpinLock {
 private AtomicReference<Thread> cas = new AtomicReference<Thread>();
 private int count;
 public void lock() {
 Thread current = Thread.currentThread();
 if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
 count++;
 return;
 }
 // 如果没获取到锁,则通过CAS自旋
 while (!cas.compareAndSet(null, current)) {
 // DO nothing
 }
 }
 public void unlock() {
 Thread cur = Thread.currentThread();
 if (cur == cas.get()) {
 if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
 count--;
 } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
 cas.compareAndSet(cur, null);
 }
 }
 }
}

自旋锁与互斥锁

  • 自旋锁与互斥锁都是为了实现保护资源共享的机制。

  • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。

  • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

  • 这里推荐一下我的 Java后端技术圈子: 867857579,群里有(分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、等学习资源)进群免费送给每一位Java小伙伴,不管你是转行,还是工作中想提升自己能力都可以,欢迎进群一起深入交流学习!

自旋锁总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。

  • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。

  • 自旋锁本身无法保证公平性,同时也无法保证可重入性。

  • 基于自旋锁,可以实现具备公平性和可重入性质的锁。


Guess you like

Origin blog.51cto.com/13689432/2409292