Java EE之线程编(进阶版)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

        这些锁策略能适用于很多中语言博主是学Java的所以下面的代码会用Java去写请大家见谅但是处理的方法是大差不差的。 

一、常见锁和锁策略

(一)、乐观锁和悲观锁

1、何为乐观锁和悲观锁呢

答乐观锁对应于生活中乐观的人总是想着事情往好的方向发展而悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种态度各有优缺点不能不以场景而定去说一种好于另外一种。乐观锁和悲观锁是两种思想主要用于解决并发场景下的数据竞争问题。

乐观锁乐观锁在进行操作数据时非常乐观认为别人不会同时修改数据因此乐观锁不会上锁只是在执行更新的时候判断一下在此期间别人是否修改了数据如果别人修改了数据则放弃操作否则执行操作。

悲观锁在执行操作数据时比较悲观认为别人会同时修改数据因此操作数据时直接把数据锁住直到操作完成后才会释放锁而在上锁期间其他人不能修改数据。

2、乐观锁和悲观锁的实现式

2.1、乐观锁的实现机制主要有两种CAS机制和版本号机制

2.1.1、何为CAS呢

答CAS全称Compare and swap翻译过来就是比较并且交换从这里就可以明白,一个CAS涉及一下三个操作步骤

假设内存的原始数据是a旧的预期值是b需要修改的新值是c
第一步比较b和a的值是否相等
第二步如果返回相等就把c的值写入a中
第三步返回操作成功

下面这个图片是CAS伪代码仅提供参考用于去理解CAS的执行流程

看完上述代码可以大致明白CAS的执行过程但是这里要注意CAS的伪代码并不是原子的是典型的check and set(判定后设定值)当多个线程同时使用CAS操作的时候如果不做处理明显会造成线程不安全的操作但是大佬们在设计CAS的已经充分考虑了这一点了多个线程使用CAS时候只允许有一个线程操作成功其他线程虽然不会阻塞但是会接收到操作失败的返回值结果。

2.1.2、CAS是如何实现的呢

答java的CAS利用的是unsafe类提供的CAS操作而unsafe的CAS依赖于JVM针对不同操作系统实现的Atomic::cmpxch实现而Atomic::cmpxch的实现使用了汇编的CAS操作并且使用CPU硬件提供的lock机制从而保证原子性。

2.1.3、CAS的应用

(1)、原子类的使用(位于java.util.concurr.atomic)

这里只举例AtomicInteger类直接上代码具体的使用可以自行探索

public class AtomicCounter {

    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
//直接中主内存中读取变量的值
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
//执行CAS操作成功返回true失败返回false
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

(2)、实现自旋锁

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class Demo
{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }
    public void unLock()
    {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t"+"----task over,unLock...");
    }
    public static void main(String[] args)
    {
        Demo spinLockDemo = new Demo();
        new Thread(() -> {
            spinLockDemo.lock();
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        },"A").start();
        //暂停500毫秒,线程A先于B启动
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            spinLockDemo.lock();
            spinLockDemo.unLock();
        },"B").start();
    }
}

 java中自旋锁是一种轻量级锁的实现其优缺点如下

优点没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁

缺点如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源导致CPU做无用功

2.1.4、为什么会提出版本号机制

给一个例子

假设t1线程工作时间10秒t2线程工作时间为2秒由于t2线程的工作时间很短那么在t1线程工作的时间之内主内存的共享变量A已经被t2线程修改了多次了只是恰好最后一次修改的值是共享变量A的初始值此时用CAS机制判定出来的结果共享变量A虽然是期望值但是A已经不再是原来的A了这就是ABA问题。有些业务可能不需要关心中间过程只要前后值一样就行但是有些业务却要求变量在中间过程中不能发生改变显然CAS就无法解决这个问题了此时就要进行优化了

2.1.5、何为版本号机制

版本号的机制是给要进行修改的数据中增加一个版本号信息用于表示当前数据的版本号每次数据被修改成功的时候版本号+1。
操作步骤的跟新
当某个线程查询数据时将该数据的版本号一起查出来。
当该线程更新数据时判断当前版本号与之前读取的版本号是否一致如果一致才进行操作。

下面举两个例子

(1)、考虑下面这个场景某款游戏的系统要进行更新玩家的金币数而金币的跟新结果取决于当前玩家的金币数量因此就要查询当前玩家的金币数量

//代码仅仅是例子  
public void updateCoins(Integer playerId){
    //根据player_id查询玩家信息
    Player player = query("select coins, level from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息计算新的金币数
    Long newCoins = ……;
    //更新金币数
    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}

这段代码很明显在涉及多个线程操作的时候就会涉及线程安全的问题很可能会影响玩家的金币数量但是当我们引入一个版本号的时候就会解决这样的问题了看代码

代码仅仅是例子
public void updateCoins(Integer playerId){
    //根据player_id查询玩家信息包含version信息
    Player player = query("select coins, level, version from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息计算新的金币数
    Long newCoins = ……;
    //更新金币数条件中增加对version的校验
    update("update player set coins = {0} where player_id = {1} and version = {2}", newCoins, playerId, player.version);
}

(2)、假设有三个线程t1、t2、t3三者共享的变量A的初始值是200

假设在执行减100的是操作时候出现了卡顿(t1)导致多创建一个减100操作(t2)cpu调度t1线程操作执行的时候正常执行A的是变为了100是预期值但是在执行t2之前t3线程给A又加了100A又变回了200轮到t2线程执行的时候发现A的值又变成200了那我就减100执行成功A的值又变成了100。但是大家想一想t2线程应该减100嘛

 解决引入版本号

对比上面的没有引入版本号的理解

对比可以发现引入版本号之后就可以很好的解决CAS中潜在的ABA问题

2.2悲观锁的实现机制主要有synchronized 关键字和 Lock 接口相关类

Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等我们以 Lock 接口为例例如 Lock 的实现类 ReentrantLock类中的 lock() 等方法就是执行加锁而 unlock()方法是执行解锁。处理资源之前必须要先加锁并拿到锁等到处理完了之后再解开锁这就是非常典型的悲观锁思想

3、悲观锁和乐观锁使用场景

3.1、从功能方面来说与悲观锁相比乐观锁的使用受到了更多的限制不管是CAS还是版本号机制

例如CAS只能保证单个变量操作的原子性当涉及到多个变量时CAS是无能为力的而synchronized则可以通过对整个代码块加锁来处理再比如版本号机制如果query的时候是表1而而update的时候是针对表2也很难通过简单的版本号来实现乐观锁此时悲观锁就可以使用了

3.2、从锁竞争的激烈程度来说使用哪一种锁要根据锁竞争的激烈程度来考虑
当竞争不激烈 (出现并发冲突的概率小)时乐观锁更有优势因为悲观锁会锁住代码块或数据其他线程无法同时访问影响并发而且而且加锁和释放锁都需要消耗额外的资源。当竞争激烈(出
现并发冲突的概率大)时悲观锁更有优势因为乐观锁在执行更新时频繁失败需要不断重试浪费CPU资源。

(二)、读写锁

1、何为读写锁

答Java读写锁也就是ReentrantReadWriteLock其包含了读锁和写锁其中读锁是可以多线程共享的即共享锁而写锁是排他锁在更改时候不允许其他线程操作。读写锁底层是同一把锁基于同一个AQS所以会有同一时刻不允许读写锁共存的限制。

代码演示

public static void main(String[] args) {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    Thread t1 = new Thread(() -> {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " read lock ok");
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        readLock.unlock();
    });

    Thread t2 = new Thread(() -> {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " read lock ok");
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        readLock.unlock();
    });

    Thread t3 = new Thread(() -> {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + " write lock ok");
        writeLock.unlock();
    });

    t1.start();
    t2.start();
    t3.start();
}

结果由此可见读写锁适用于频繁读不频繁写的场景

2、java中实现读写锁接口的类ReentrantReadWriteLock

2.1、ReentrantReadWriteLock类的特点

(1)具有与ReentrantLock类似的公平锁和非公平锁的实现默认的支持非公平锁对于二者而言非公平锁的吞吐量由于公平锁

(2)支持重入读线程获取读锁之后能够再次获取读锁写线程获取写锁之后能再次获取写锁也可以获取读锁

(3)锁能降级遵循获取写锁、获取读锁在释放写锁的顺序即写锁能够降级为读锁读锁不能升级为写锁

提示锁降级是指如果当先线程是写锁的持有者并保持获得写锁的状态同时又获取到读锁然后释放写锁的过程看如下代码演示

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDemo {
    private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();

    public static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "获取读锁开始执行");
            Thread.sleep(20);
            System.out.println(Thread.currentThread().getName()+ "尝试升级读锁为写锁");
            //读锁升级为写锁(失败)
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() +"读锁升级为写锁成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
        }
    }

    public static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "获取写锁开始执行");
            Thread.sleep(40);
            System.out.println(Thread.currentThread().getName() +"尝试降级写锁为读锁");
            //写锁降级为读锁成功
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+ "写锁降级为读锁成功");
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
            readLock.unlock();
        }
    }
    public static void main(String[] args) {
        new Thread(() -> write(), "Thread1").start();
        new Thread(() -> read(), "Thread2").start();
    }
}

(三)、公平锁和非公平锁

1、何为公平锁和非公平锁

答公平锁多个线程按照申请锁的顺序去获得锁线程会直接进入队列去排队永远都是队列的第一位才能得到锁非公平锁多个线程去获取锁的时候会直接去尝试获取获取不到再去进入等待队列如果能获取到就直接获取到锁

上图理解公平vs非公平

2、二者的优缺点

(四)、可重入锁和不可重入锁

1、何为可重入锁和不可重入锁

答可重入锁当线程获取某个锁后还可以继续获取它可以递归调用而不会发生死锁不可重入锁获取锁后不能重复获取否则会造成死锁。

2.代码演示不可重入锁

public class Demo {

    private Thread owner;// 持有锁的线程为null表示无人占有

    /**
     * 获取锁锁被占用时阻塞直到锁被释放
     * @throws InterruptedException 等待锁时线程被中断
     */
    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread();
        // wait()方法一般和while一起使用防止因其它原因唤醒而实际没达到期望的条件
        while (owner != null) {
            System.out.println(String.format("%s 等待 %s 释放锁",
                    thread.getName(), owner.getName()));
            wait(); // 阻塞直到被唤起
        }
        System.out.println(thread.getName() + " 获得了锁");
        owner = thread;//成功上位
    }

    public synchronized void unlock() {
        //只有持有锁的线程才有资格释放锁别的线程不去调用它
        if (Thread.currentThread() != owner) {
            throw new IllegalMonitorStateException();
        }
        System.out.println(owner.getName() + " 释放了持有的锁");
        owner = null;
        notify();//唤醒一个等待锁的线程也可以用notifyAll()
    }

    public static void main(String[] args) throws InterruptedException {
        Demo lock = new Demo();
        lock.lock(); // 获取锁
        lock.lock(); // 再次获取锁造成死锁
    }
}

有上述代码执行的效果的锁是不可重入的锁

3.代码演示可重入锁 

3.1使用synchronized演示

public class Demo {

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            lock(5);
        }).start();
        Thread.sleep(1000);
        System.out.println("我是主线程我也要来");
        lock(2);
    }

    //可重入锁也被称为递归锁自己锁自己而不会造成死锁
    private static synchronized void lock(int count) {
        if (count == 0) {
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " " + count);
        lock(count - 1);
    }

}

3.2使用ReentrantLock演示

import java.util.concurrent.locks.ReentrantLock;
public class Demo {

    public static void main(String[] args) throws Exception {
        // 构造函数可传入一个布尔表示是否使用公平锁(什么是公平锁看上面的讲解)
        ReentrantLock lock = new ReentrantLock(false);
        new Thread(() -> {
            lock.lock();
            System.out.println("A 获取了锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A 释放了锁");
            lock.unlock();
        }).start();
        new Thread(() -> {
            System.out.println("B 等待锁");
            lock.lock();
            System.out.println("B 获取了锁");
            lock.unlock();
            System.out.println("B 释放了锁");
        }).start();
    }
}

注这里重量级锁和轻量级锁我没有进行讲解会在后面的synchronized中涉及到不必担心

二、synchronized讲解

总结从上述的锁策略中可以得出java中的synchronized的情况(JDK1.8)

1、synchronized特点

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁

2、锁的加锁过程

从上图看出JVM将synchronized锁分为无状态、偏向锁、轻量级锁、重量级锁四个状态根据情况会有升级的情况下面讲一讲升级的大致原理和过程

2.1、无锁到偏向锁

在讲这个之前要讲一下相关的东西对象中的对象头

什么是对象头呢

答对象头(Object Header)包括两部分信息第一部分用于存储对象自身的运行时数据 如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等官方称它为“Mark Word”对象头的另外一部分是类型指针即是对象指向它的类的元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。

偏向锁不是真正的加锁举个例子一段同步的代码一直只被线程A去访问也没有其他的线程来访问线程A每次访问一次就去获取锁那岂不是浪费了很多资源锁的创建和销毁是很消耗资源的所以这种情况下就会进入偏向锁状态如果后续没有其他线程来竞争该锁, 其他同步操作了就不用进行避免了加锁解锁的开销偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销。

2.2、偏向锁到轻量级锁

在偏向锁的情况下一旦有第二个线程参与竞争就会立即膨胀为轻量级锁企图去获取锁的线程一开始会使用自旋的方式去获取锁如果循环几次其他的线程释放了锁就不需要进行用户态到内核态的切换但是如果一直获取不到锁我就一直自旋吗显然不可能自旋会占用很多CPU的资源。JDK1.7以后就对自旋锁做了一定的优化自适应自旋锁的自旋次数不在固定而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的如果超过了次数就会继续膨胀。

2.3、轻量级锁到重量级锁

如果锁之间的竞争进一步激烈就会转变为重量级锁此处重量级锁就是指用到内核提供的mutex线程获取到锁就会执行加锁状态从用户态到核心态的转换没有获取到锁的线程会阻塞等待CPU的调度。

3、其他的锁优化操作

3.1、锁消除

Java的JIT机制会通过逃逸分析去分析加锁的代码段/共享资源他们是否被一个或者多个线程使用或者等待被使用如果通过分析证实只被一个线程访问在编译这个代码段的时候就不生成 Synchronized 关键字仅仅生成代码对应的机器码换句话说即使在代码段上加上了synchronized锁只要JIT发现这个代码段只有一个线程在进行访问就会去掉synchronized从而提高访问速率。

3.2、锁粗化

锁粗化是JIT 编译器对内部锁具体实现的优化假设有几个在程序上相邻的同步块代码段上每个同步块使用的是同一个锁实例那么 JIT 会在编译的时候将这些同步块合并成一个大同步块并且使用同一个锁实例。这样避免一个线程反复申请/释放锁减少资源的消耗。

三、JUC中的ReentrantLock

1、什么是ReentrantLock

答ReentrantLock是Java中常用的锁属于乐观锁类型多线程并发情况下。能保证共享数据安全性线程间有序性ReentrantLock通过原子操作和阻塞实现锁原理一般使用lock获取锁unlock释放锁

2、ReentrantLock原理

ReentrantLock主要用到unsafe的CAS和PARK两个功能实现锁CAS + park

多个线程同时操作一个数N使用原子CAS操作原子操作能保证同一时间只能被一个线程修改而修改数N成功后返回true其他线程修改失败返回false这个原子操作可以判断线程是否拿到锁返回true代表获取锁返回false代表为没有拿到锁。拿到锁的线程自然是继续执行后续逻辑代码而没有拿到锁的线程则调用park将线程自己阻塞而线程阻塞需要其他线程唤醒ReentrantLock中用到了链表用于存放等待或者阻塞的线程每次线程阻塞先将自己的线程信息放入链表尾部再阻塞自己之后需要拿到锁的线程在调用unlock 释放锁时从链表中获取阻塞线程调用unpark 唤醒线程

3、ReentrantLock和synchronized的区别

3.1、从底层上来讲

synchronized 的实现涉及到锁的升级具体为无锁、偏向锁、自旋锁、向OS申请重量级锁ReentrantLock实现则是通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能

3.2、从锁的释放来讲

synchronized 不需要用户去手动释放锁synchronized 代码执行完后系统会自动让线程释放对锁的占用而ReentrantLock则需要用户去手动释放锁如果没有手动释放锁就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成免得忘记释放造成死锁

3.3、从中断上来讲

ynchronized是不可中断类型的锁除非加锁的代码中出现异常或正常执行完成 ReentrantLock则可以中断可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中调用interrupt方法进行中断。

3.4、从是否能实现公平锁来讲

synchronized默认为非公平锁 而ReentrantLock即可以实现公平锁也可以实现非公平锁通过构造方法new ReentrantLock时传入boolean值进行选择为空默认false非公平锁true为公平锁。

3.5、从能否指定唤醒线程来讲

synchronized不能指定唤醒而ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒阻塞线程而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

四、线程安全的集合类的推荐

(一)、ArrayList

1.自己在线程不安全的地方使用synchronized或者reentrantLock

2.使用Collections.synchronizedList(new ArrayList)创建线程安全的ArraList

3.使用CopyOnWriteArrayList

(二)、Queue

1、ArrayBlockingQueue(基于数组实现的阻塞队列 )

2、LinkedBlockingQueue(基于链表实现的阻塞队列)

3、PriorityBlockingQueue(基于堆实现的带优先级的阻塞队列 )

4、TransferQueue(最多只包含一个元素的阻塞队列 )

(三)、哈希表

1、Hashtable类

1.1、多个线程访问同一个Hashtable会造锁冲突

1.2、关键方法都使用synchronized加锁

1.3、一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 会导致该线程的执行效率变得很低.

1.4、key值不允许为null

2、ConcurrentHashMap类

2.1、读操作没有加锁但是使用了 volatile 保证从内存读取结果, 只对写操作进行加锁.加锁的方式是用 synchronized, 但是不是锁整个对象, 而是用每个链表的头结点作为锁对象, 这样做降低了锁冲突的概率

2.2、充分利用 CAS 特性size 属性通过 CAS 来更新. 避免出现重量级锁的情况

2.3、扩容需要把旧数组上的全部节点转移到扩容之后的新数组上节点的转移是从数组的最后一个索引位置开始一个索引一个索引进行的。每个线程一轮处理有限个数的哈希桶。当旧数组上的全部节点转移到扩容之后的新数组后ConcurrentHashMap 的 table 成员变量指向扩容之后的新数组扩容操作完成

五、死锁

1、死锁如何产生的

死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的一种互相等待的现象若无外力作用它们会一直僵持下去造成死等的情况。

举个例子某计算机系统中只有一台打印机和一台输入 设备进程P1正占用输入设备同时又提出使用打印机的请求但此时打印机正被进程P2 所占用而P2在未释放打印机之前又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去均无法继续执行此时两个进程陷入死锁状态。

2、死锁产生的必要条件

1、互斥等待进程要求对所分配的资源如打印机进行排他性控制即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源则请求进程只能等待。

2、不剥夺条件进程所获得的资源在未使用完毕之前不能被其他进程强行夺走即只能由获得该资源的进程自己来释放只能是主动释放)。

3、请求和保持条件进程已经保持了至少一个资源但又提出了新的资源请求而该资源已被其他进程占有此时请求进程被阻塞但对自己已获得的资源保持不放。

4、循环等待条件存在一种进程资源的循环等待链链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn}其中Pi等 待的资源被P(i+1)占有i=0, 1, ..., n-1)Pn等待的资源被P0占有看图

这就是循环等待

代码举例

class DeadLock implements Runnable{

    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private boolean flag;

    public DeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + "运行");

        if(flag){
            synchronized(obj1){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(obj2){
                    // 执行不到这里
                    System.out.println("1秒钟后"+Thread.currentThread().getName()
                            + "锁住obj2");
                }
            }
        }else{
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(obj1){
                    // 执行不到这里
                    System.out.println("1秒钟后"+Thread.currentThread().getName()
                            + "锁住obj1");
                }
            }
        }
    }
}
public class Demo {

    public static void main(String[] args) {

        Thread t1 = new Thread(new DeadLock(true), "线程1");
        Thread t2 = new Thread(new DeadLock(false), "线程2");

        t1.start();
        t2.start();
    }
}

2、死锁的避免

1、加锁顺序

//参考案例
Thread 1:
  lock A
  lock B
Thread 2:
   wait for A
   lock C (when A locked)
Thread 3:
   wait for A
   wait for B
   wait for C

可以观察发现线程2和线程3只有在获取了锁A之后才能尝试获取锁C换句话说获取锁A是获取锁C的必要条件。因为线程1已经拥有了锁A所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前必须成功地对A加了锁 。

2、加锁时限

给尝试获取锁的线程加一个超时时间这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁则会进行回退并释放所有已经获得的锁然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁。

3、死锁检测

使用jdk自带的工具jconsole去查看哪个线程造成了阻塞在根据代码逐步分析这里的检测方法只是我现在用到的方法大家可以试一试我的方法仅仅提供参考大家可以在评论区发表自己的看法和意见。

                                  最后祝福大家新年快乐天天向上。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java