Java多线程:Java中15种锁的介绍

1、公平锁/非公平锁

1)、公平锁:公平锁是指多个线程申请锁的顺序来获取锁

2)、非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁。有可能,又造成优先级反转或者饥饿现象

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大

对于Synchronized而言,是一种非公平锁。由于其不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁

2、可重入锁/不可重入锁

1)、重入锁:重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提是同一对象或class),这样的锁就叫做可重入锁。ReentrantLock和Synchronized都是可重入锁

	synchronized void setA() throws InterruptedException {
		TimeUnit.SECONDS.sleep(1);
		setB();
	}

	synchronized void setB() throws InterruptedException {
		TimeUnit.SECONDS.sleep(1);
	}

上面的代码就是可重入锁的一个特点,如果不是可重入锁的话,setB()可能不会被当前线程执行,可能造成死锁

2)、不可重入锁:不可重入锁不可递归调用,递归调用就发生死锁

// 使用自旋锁来模拟一个不可重入锁
public class UnreentrantLock {
	private AtomicReference<Thread> owner = new AtomicReference<>();

	public void lock() {
		Thread current = Thread.currentThread();
		for (;;) {
			if (owner.compareAndSet(null, current)) {
				return;
			}
		}
	}

	public void unlock() {
		Thread current = Thread.currentThread();
		owner.compareAndSet(current, null);
	}
}

ReentrantLock中可重入锁实现:

非公平锁的锁获取方法:

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

在AQS中维护了一个volatile修饰的state成员变量来计数重入次数,避免了频繁的持有释放操作,这样提升了效率,又避免了死锁

3、排它锁/共享锁

1)、排它锁:排它锁在同一时刻只允许一个线程进行访问

2)、共享锁:共享锁在同一时刻可以允许多个线程访问,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确实每次只能被独占

排它锁和共享锁也是通过AQS来实现的

4、互斥锁/读写锁

1)、互斥锁(排它锁)

2)、读写锁:既是互斥锁,又是共享锁,read模式是共享,write模式是互斥的

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

5、乐观锁/悲观锁

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

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

6、分段锁

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

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

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

一般有三种方式降低锁的竞争程序:

  • 减少锁的持有时间
  • 降低锁的请求频率
  • 使用带有协调机制的独占锁,这些机制允许更高的并发性

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

容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是JDK1.7中ConcurrentHashMap使用的锁分段技术

HashMap和ConcurrentHashMap源码分析(基于JDK1.7和1.8):https://blog.csdn.net/qq_40378034/article/details/87256635

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

锁的状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

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

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

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

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

8、自旋锁

CAS算法是乐观锁的一种实现方式,CAS算法中有涉及到自旋锁

1)、CAS算法

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

  • 需要读写的内存值V
  • 进行比较的值A
  • 写入的新值B

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

2)、自旋锁

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

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

3)、Java如何实现自旋锁?

public class SpinLock {
	private AtomicReference<Thread> cas = new AtomicReference<>();

	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()方法释放了该锁

4)、自旋锁存在的问题

A.如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高

B.上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在线程饥饿问题

5)、自旋锁的优点

A.自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

B.非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

6)、自旋锁与互斥锁

  • 自旋锁与互斥锁都是为了实现保护资源共享的机制
  • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者
  • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放

7)、自旋锁总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁
  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)
  • 自旋锁如果持有锁的时间太长,则会导致其他等待获取锁的线程耗尽CPU
  • 自旋锁本身无法保证公平性,同时也无法保证可重入性
  • 基于自旋锁,可以实现具备公平性和可重入性的锁

参考文章:https://mp.weixin.qq.com/s/D3Lg-wzqRP_efV9kNZN4dw

参考书籍:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/87606625
今日推荐