菜鸟笔记
提升您的技术认知

锁的分类及详解

锁的分类都是从不同的纬度由人去划分的,基本上都是成对的出现。

1.乐观锁/悲观锁
乐观锁顾名思义,很乐观的认为每次读取数据的时候总是认为没人动过,所以不去加锁。但是在更新的时候回去对比一下原来的值,看有没有被别人更改过。适用于读多写少的场景。
java中的atomic就属于乐观锁的表现。就是CAS
悲观锁在每次读取数据的时候都认为其他人会修改数据,所以读取数据的时候也加锁,这样别人想拿的时候就会阻塞,直到这个线程释放锁,这就影响了并发性能。适合写操作比较多的场景。

for select xxx for update; update update xx set a = aaa

synchronized实现也是悲观锁,悲观锁如果使用不当的,性能影响会很大。
2.独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有,而共享锁是指该锁可被多个线程所持有。
案列1:ReentrantLock就是独享锁

public class Test {
  
		Lock lock=	new ReentrantLock();
	long start =	System.currentTimeMillis();
	void read() {
  
		lock.lock();
		try {
  
			Thread.sleep(100);
		} catch (InterruptedException e) {
  
			e.printStackTrace();
		}finally {
  
			lock.unlock();
		}
		System.out.println("read time = "+(System.currentTimeMillis()-start));
	}

	public static void main(String[] args) {
  
		Test test = new Test();
		for (int i = 0; i < 10; i++) {
  
				new Thread(test::read).start();
		}
	}
}
read time = 146
read time = 247
read time = 347
read time = 448
read time = 548
read time = 649
read time = 749
read time = 850
read time = 951
read time = 1052

结果分析:每个线程结束的时间点逐个上升,锁被独享,一个用完下一个,依次获取锁。

案例2:ReadWriteLock,read共享,write独享

public class SharedLock {
  
			ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
			Lock lock = readWriteLock.readLock();
			long start = System.currentTimeMillis();
			void read() {
  
			lock.lock();
			try {
  
			Thread.sleep(100);
			} catch (InterruptedException e) {
  
			e.printStackTrace();
			}finally {
  
			lock.unlock();
			}
			System.out.println("end time = "+(System.currentTimeMillis()-start));
			}

	public static void main(String[] args) {
  
		final SharedLock lock = new SharedLock();
		for (int i = 0; i < 10; i++) {
  
			new Thread(lock::read).start();
		}
	}
}
end time = 147
end time = 148
end time = 148
end time = 148
end time = 148
end time = 148
end time = 148
end time = 148
end time = 148
end time = 148

结果分析:每个线程独自跑,各在100ms左右,证明是共享的。
如果换成write锁,会变成顺序时间,证明为独享锁。

3.可重入锁
可重入锁指的获取到锁后,如果同步块内需要再次获取同一把锁的时候,直接放行,而不是等待。其意义在于防止死锁。前面使用的synchronized 和ReentrantLock 都是可重入锁。
实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。如果同一个线程再次请求这个锁,计数器将递增,线程退出同步块,计数器值将递减。直到计数器为0锁被释放。

public class SharedLock {
  
	byte[] lock = new byte[0];
	public void vone(){
  
		synchronized (lock){
  
			System.out.println("f1 from parent");
		}
	}
}
public class SonLock extends SharedLock {
  

	public void vone(){
  
		synchronized (super.lock){
  
			super.vone();
			System.out.println("f1 from son");
		}

	}

	public static void main(String[] args) {
  
		SonLock sonLock = new SonLock();
		sonLock.vone();
	}
}

//打印结果
//f1 from parent
//f1 from son

4.公平锁/非公平锁

常见于AQS,公平锁就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,直到按照FIFO的规则从队列中取到自己。
非公平锁与公平锁基本类似,只是在放入队列前先判断当前锁是否被线程持有。如果锁空闲,那么他可以直接抢占,而不需要判断当前队列中是否有等待线程。只有锁被占用的话,才会进入排队。
优缺点:
公平锁的优点是等待锁的线程不会饿死,进入队列规规矩矩的排队,迟早会轮到。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁的性能要高于公平锁,因为线程有几率不阻塞直接获得锁。ReentrantLock默认使用非公平锁就是基于性能考量。但是非公平锁的缺点是可能引发队列中的线程始终拿不到锁,一直排队被饿死。

5.锁升级
java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。偏向锁、轻量锁、重量锁就是围绕如何使得cpu的占用更划算而展开的。
如何理解呢?
A占了锁,B就要阻塞等。但是,在操作系统中,阻塞就要存储当前线程状态,唤醒就要再恢复,这个过程是要消耗时间的…
如果A使用锁的时间远远小于B被阻塞和挂起的执行时间,那么我们将B挂起阻塞就相当的不合算。
那有没有办法呢?
于是出现自旋:自旋指的是锁已经被其他线程占用时,当前线程不会被挂起,而是在不停的试图获取锁(可以理解为不停的循环),每循环一次表示一次自旋过程。显然这种操作会消耗CPU时间,但是相比线程下文切换时间要少的时候,自旋划算。

如果自旋的线程过多呢?
再上重量级锁阻塞和挂起就行了。

举个例子:公司有一个会议室(共享资源)
偏向锁:
前期公司只有1个团队,那么什么时候开会都能满足,就不需要预约,OA里直接默认设定为使用者A。A在会议室门口挂了个牌子,写着A专用(ThreadID=A)
轻量级锁:
随着业务发展,扩充为2个团队,B团队肯定不会同意A无法无天,于是当AB同时需要开会时,两者在OA抢占,谁抢到谁算谁的。偏向锁升级为轻量级锁,但是未抢到者在门口会不停敲门询问(自旋,循环),开完没有?开完没有?
重量级锁:
后来随着团队规模继续扩充,发现,这种不停敲门的方式很烦,BCDEF……都在门口站着一直问。于是锁再次升级。
如果会议室被A占用,那么其他团队直接闭嘴等着(wait进入阻塞),直到A用完后会通知其他人(notifyAll)。

注意点:
上面几种锁都是jvm自己内部实现,我们不需要干预,但是可以配置jvm参数开启/关闭自旋锁、偏向锁。
锁可以升级,但是不能反向降级:偏向锁→轻量级锁→重量级锁
无锁争用的时候使用偏向锁,第二个线程到了升级为轻量级锁进行竞争,更多线程时,进入重量级锁阻塞

6.互斥锁/读写锁
典型的互斥锁:synchronized,ReentrantLock,读写锁:ReadWriteLock
互斥锁属于独享锁,读写锁里的写锁属于独享锁,而读锁属于共享锁

7.AQS
除了java自带的synchronized关键字之外,jdk提供的另外一种锁机制。如果需要自己实现锁的逻辑,可以考虑使用AQS,非常的便捷。

需要子类继承AQS,并实现的方法(protected):

protected boolean tryAcquire(int arg) //独占式获取同步状态
protected boolean tryRelease(int arg) //独占式释放同步状态
protected int tryAcquireShared(int arg) //共享式获取同步状态
protected boolean tryReleaseShared(int arg) //共享式释放同步状态

使用时,调用的是父类的方法(public)

public final void acquire(int arg) //独享锁获取
public final boolean release(int arg) //独享锁释放
public final void acquireShared(int arg) //共享锁获取
public final boolean releaseShared(int arg) //共享锁释放