Bota5ky

Java笔记(六):锁

Synchronized ReentrantLock
Java中的一个关键字 JDK提供的一个类
自动加锁与释放锁 需要手动加锁与释放锁
JVM层面的锁 API层面的锁
非公平锁 公平锁或非公平锁
锁的是对象,锁信息保存在对象头中 int类型的state标识来标识锁的状态
底层有锁升级过程 没有锁升级过程

传统的Synchronized锁

Synchronized 通过为方法或代码块添加互斥锁,来保证线程安全性。 持有相同锁的多个线程,同一时间只有一个线程能够拿到锁并执行锁定的代码块或方法。

public synchronized void run(){ // do something }
synchronized (this){// do something}
synchronized (A.class){// do something}

synchronized锁的升级过程

无锁状态(No lock):初始状态下,共享资源没有被任何线程锁定,可以被任意线程访问。

偏向锁状态(Biased lock):当第一个线程访问同步代码块时,JVM会将对象头中的标记位设置为偏向锁。这表示该对象偏向于第一个获取锁的线程。在这个阶段,锁是非竞争的,其他线程可以直接进入临界区。这个阶段旨在优化只有一个线程访问同步代码块的情况。

轻量级锁状态(Lightweight lock):如果多个线程尝试获取偏向锁失败,JVM会将锁升级为轻量级锁。在轻量级锁状态下,JVM会将对象头中的一部分空间用于存储锁记录(Lock Record)的指针。每个线程在进入临界区之前,会通过CAS(Compare and Swap)操作尝试将锁记录指针替换为自己的线程ID。如果替换成功,表示获取到锁;否则,表示锁被其他线程占用,需要进行锁膨胀。

重量级锁状态(Heavyweight lock):如果轻量级锁获取失败,JVM会进行锁膨胀,将锁升级为重量级锁。在重量级锁状态下,JVM会使用操作系统的互斥量来实现锁。这样,如果一个线程持有重量级锁,其他线程需要等待,直到持有锁的线程释放。

需要注意的是,锁的升级过程是逐步发生的。当线程竞争激烈或临界区执行时间较长时,锁可能会直接升级到重量级锁,跳过偏向锁和轻量级锁的阶段。

锁的升级过程是JVM自动完成的,开发者无需显式操作。JVM会根据线程竞争情况和锁的使用方式来选择最适合的锁级别,以平衡并发性能和线程安全性。

Lock锁

接口Lock的实现类:

  • ReentractLock
  • ReentractReadWriteLock.ReadLock
  • ReentractReadWriteLock.WriteLock
public void run(){
  lock.lock();
  try{
    // do something 
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

非公平锁

非公平锁(Unfair Lock)是一种线程同步机制,与公平锁(Fair Lock)相对应。在多线程环境中,公平锁会按照线程的申请顺序来获取锁资源,即先到先得的原则。而非公平锁则不考虑线程的申请顺序,允许新来的线程插队获取锁资源,从而可能导致已经在等待的线程长期等待。

非公平锁的设计主要是为了提高系统的整体吞吐量和性能。由于公平锁要求按照申请顺序获取锁资源,如果一个线程获取锁资源的时间较长,那么其他已经准备好并且在等待的线程就必须一直等待。这样会导致线程频繁地从用户态和内核态之间切换,增加了上下文切换的开销,降低了系统的吞吐量。

相比之下,非公平锁允许新来的线程插队获取锁资源,避免了等待时间过长的情况,减少了线程的等待时间和上下文切换的开销,从而提高了系统的整体性能和吞吐量。然而,由于非公平锁的设计特点,可能会导致某些线程一直无法获取到锁资源,造成不公平现象。

选择使用公平锁还是非公平锁需要根据具体的场景和需求来决定。如果对线程的公平性要求较高,并且能够容忍一定的性能损失,可以选择公平锁。如果追求系统的整体性能和吞吐量,并且能够接受一些线程的不公平性,可以选择非公平锁。

ReentrantLock 公平锁和非公平锁的实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:

  • 如果是公平锁,会先检查AQS(AbstractQueuedSynchronizer队列同步器)队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
  • 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁

另外,不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

Synchronized 和 Lock 的区别

https://xie.infoq.cn/article/4e370ded27e4419d2a94a44b3

传统的生产者消费者问题,防止虚假唤醒

线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚促唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并目如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:

synchronized (obj) {
  while (<condition does not hold>)
		obi.wait(timeout);
	...// Perform action appropriate to condition
}

如果当前线程interrupted任何线程之前或在等待时,那么InterruptedException被抛出。 如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。

Lock版的生产者消费者问题

Condition可以实现精准通知唤醒

class BoundedBuffer<E> {
	final Lock lock = new ReentrantLock();
	final Condition notFull  = lock.newCondition(); 
	final Condition notEmpty = lock.newCondition(); 

	final Object[] items = new Object[100];
	int putptr, takeptr, count;

	public void put(E x) throws InterruptedException{
		lock.lock();
		try {
			while (count == items.length)
				notFull.await();
			items[putptr] = x;
			if (++putptr == items.length) putptr = 0;
			++count;
			notEmpty.signal();
		} finally {
			lock.unlock();
		}
	}

	public E take() throws InterruptedException {
		lock.lock();
		try {
			while (count == 0)
				notEmpty.await();
			E x = (E) items[takeptr];
			if (++takeptr == items.length) takeptr = 0;
			--count;
			notFull.signal();
			return x;
		} finally {
			lock.unlock();
		}
	}
}

八锁现象

  • syncMethod 方法使用 synchronized 关键字修饰,锁定对象为当前实例(this),nonSyncMethod 方法没有使用同步关键字,不会进行锁定
  • static 修饰的方法属于Class模版,只有一个

Java死锁如何避免?

造成死锁的几个原因:

  1. 一个资源每次只能被一个线程使用

  2. 一个线程在阻塞等待某个资源时,不释放已占有资源

  3. 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺

  4. 若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置一个超时时间
  3. 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
posted @ 2023-05-22 14:49  Bota5ky  阅读(9)  评论(0编辑  收藏  举报