Java中关于“锁”的那些事

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这章详细讲解

相关推荐
pearlthriving1 小时前
c++当中的泛型思想以及c++11部分新特性
java·开发语言·c++
梦魇星虹2 小时前
idea Cannot find declaration to go to
java·ide·intellij-idea
小雅痞2 小时前
[Java][Leetcode hard] 42. 接雨水
java·开发语言·leetcode
xfcoding2 小时前
关于代码注释的思考
java
虹梦未来2 小时前
【开发心得】在SpringBoot体系中正确使用redisConfig
java·spring boot·spring
skiy2 小时前
Spring Framework 中文官方文档
java·后端·spring
xifangge20252 小时前
【故障排查】IDEA 打开 Java 文件没有运行按钮(Run)?深度解析项目标识与环境配置的 3 大底层坑点
java·ide·intellij-idea
麻辣璐璐2 小时前
EditText属性运用之适配RTL语言和LTR语言的输入习惯
android·xml·java·开发语言·安卓
weisian1513 小时前
Java并发编程--33-Redis分布式缓存三大核心架构:主从、哨兵、分片,落地实战与选型
java·redis·缓存·主从架构·哨兵架构·分片架构