JAVA中JUC多线程并发编程
第二章 Java中关于"锁"的那些事
文章目录
一、乐观锁和悲观锁
悲观锁 :认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized 关键字和Lock 的实现类都是悲观锁
适合写操作多的场景
乐观锁 :认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
Java中是通过使用无锁编程来实现,最常采用的是CAS(Compare-and-Swap,即比较并替换)算法 ,如:Java原子类中的递增操作就通过CAS自旋实现的。
适合读操作多的场景
伪代码
java
//=============悲观锁的调用方式
public synchronized void writeXX()
{
//加锁后的业务逻辑......
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void writeXX() {
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}
//乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
二、公平锁和非公平锁
⽣活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间 的,其实就是 FIFO,否则视为不公平
AQS中体现最详细,后期会详细聊一聊这块详细内容
synchronized:默认是非公平锁,并且无法配置为公平锁
ReentrantLock:默认是非公平锁,并可以配置为公平锁
java
class Ticket
{
private int number = 30;
//默认为非公平
ReentrantLock lock = new ReentrantLock();
//改为true为公平
//ReentrantLock lock = new ReentrantLock(true);
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class SaleTicketDemo
{
public static void main(String[] args)
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"c").start();
}
}
三、可重入锁
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
synchronized:默认可重入锁
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
ReentrantLock:可重入锁。
java
public class ReEntryLock
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("外层调用");
synchronized (objectLockA)
{
System.out.println("中层调用");
synchronized (objectLockA)
{
System.out.println("内层调用");
}
}
}
},"a").start();
}
}
四、死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生原因:系统资源不足,进程运行推进的顺序不合适,资源分配不当
如何排查死锁?jps -l jstack 进程编号
java
public class DeadLockDemo
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
final Object objectLockB = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
}
}
},"B").start();
}
}
五、自旋锁
自旋锁(spinlock)是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

六、读写锁
读写锁:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
在java中读写锁代表ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现。
ReentrantReadWriteLock有锁降级机制策略

锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)。锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性。但是,线程获取读锁是不能直接升级为写入锁的。
在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。
所以,需要释放所有读锁,才可获取写锁,如果需要的读线程特别多,写锁会一直堵塞,导致锁饥饿
分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:读锁全完,写锁有望;写锁独占,读写全堵;
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,人家还在读着那,你先别去写,省的数据乱。
解决上面问题读的:也允许读锁获取写锁介入,引入乐观锁StampedLock 。(下面有代码示例)这样会导致我们读的数据就可能不一致!
所以,需要额外的方法来判断读的过程中是否有写入
StampedLock的特点:
- 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
- StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式
- Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
缺点: - 不支持重入,没有Re开头,而且不支持任何形式的重入,无论是读锁(悲观读)还是写锁,说白了就是完全不支持重入
- 悲观读锁和写锁都不支持条件变量(Condition):
java
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
notFull.await(); // 自动释放锁,线程挂起
// 通知读取线程
notFull.signalAll();
//这个他不支持
- 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法:StampedLock 在处理中断时,没有采用标准的"抛出异常"机制,而是采用了"疯狂自旋"的策略。
如果你对一个正在阻塞等待 StampedLock(无论是读锁还是写锁)的线程调用 interrupt(),该线程不会抛出 InterruptedException 退出,而是会陷入一个死循环(自旋),导致该线程所在的 CPU 核心占用率瞬间飙升到 100%。
StampedLock 提供了专门的可中断方法:
readLockInterruptibly():可中断的悲观读锁。
writeLockInterruptibly():可中断的写锁。
读写锁Demo
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyResource
{
Map<String,String> map = new HashMap<>();
//=====ReentrantLock 等价于 =====synchronized
Lock lock = new ReentrantLock();
//=====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void write(String key,String value)
{
rwLock.writeLock().lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"---正在写入");
map.put(key,value);
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"---完成写入");
}finally {
rwLock.writeLock().unlock();
}
}
public void read(String key)
{
rwLock.readLock().lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"---正在读取");
String result = map.get(key);
//后续开启注释修改为2000,演示一体两面,读写互斥,读读共享,读没有完成时候写锁无法获得
//try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"---完成读取result:"+result);
}finally {
rwLock.readLock().unlock();
}
}
}
public class ReentrantReadWriteLockDemo
{
public static void main(String[] args)
{
MyResource myResource = new MyResource();
for (int i = 1; i <=10; i++) {
int finalI = i;
new Thread(() -> {
myResource.write(finalI +"", finalI +"");
},String.valueOf(i)).start();
}
for (int i = 1; i <=10; i++) {
int finalI = i;
new Thread(() -> {
myResource.read(finalI +"");
},String.valueOf(i)).start();
}
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
//读全部over才可以继续写
for (int i = 1; i <=3; i++) {
int finalI = i;
new Thread(() -> {
myResource.write(finalI +"", finalI +"");
},"newWriteThread==="+String.valueOf(i)).start();
}
}
}
锁降级Demo
java
//锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
//如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
public class LockDownGradingDemo
{
public static void main(String[] args)
{
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
writeLock.lock();
try{
System.out.println("-------正在写入");
readLock.lock();
}finally{
writeLock.unlock();
}
try{
System.out.println("-------正在读取");
}finally{
readLock.unlock();
}
}
}
stampedLock
java
public class StampedLockDemo
{
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public void write()
{
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
try
{
number = number + 13;
}catch (Exception e){
e.printStackTrace();
}finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
}
//悲观读
public void read()
{
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
//暂停几秒钟线程
for (int i = 0; i <4 ; i++) {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
}
try
{
int result = number;
System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
}catch (Exception e){
e.printStackTrace();
}finally {
stampedLock.unlockRead(stamp);
}
}
//乐观读
public void tryOptimisticRead()
{
long stamp = stampedLock.tryOptimisticRead();
int result = number;
//间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
for (int i = 1; i <=4 ; i++) {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
"秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
+stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)) {
System.out.println("有人动过--------存在写操作!");
stamp = stampedLock.readLock();
try {
System.out.println("从乐观读 升级为 悲观读");
result = number;
System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
}catch (Exception e){
e.printStackTrace();
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
}
public static void main(String[] args)
{
StampedLockDemo resource = new StampedLockDemo();
new Thread(() -> {
resource.read();
//resource.tryOptimisticRead();
},"readThread").start();
// 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
//try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
resource.write();
},"writeThread").start();
}
}
七、无锁→偏向锁→轻量锁→重量锁
这个是Synchronized锁升级过程,后期会在Synchronized这章详细讲解