Java EE之线程编(进阶版)

news2024/9/27 9:30:03

        这些锁策略能适用于很多中语言,博主是学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又加了100,A又变回了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去查看哪个线程造成了阻塞,在根据代码逐步分析,这里的检测方法只是我现在用到的方法,大家可以试一试,我的方法仅仅提供参考,大家可以在评论区发表自己的看法和意见。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/175463.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux服务器常见运维性能测试(3)CPU测试super_pi、sysbench

Linux服务器常见运维性能测试&#xff08;3&#xff09;CPU测试常见性能测试软件CPU测试&#xff1a;super_pi &#xff08;计算圆周率&#xff09;CPU测试&#xff1a;sysbench&#xff08;CPU功能测试部分&#xff09;下载安装sysbench综合测试功能执行CPU测试最近需要测试一…

Java面试题含答案,最新面试题(1)

Java 中 InvokeDynamic指令是干什么的&#xff1f; JVM字节码指令集一直比较稳定&#xff0c;一直到Java7中才增加了一个InvokeDynamic 指令&#xff0c;这是JAVA为了实现『动态类型语言』支持而做的一种改进&#xff1b;但是在Java7中并没有提供直接生成InvokeDynamic 指令的…

自定义类型:结构体,枚举,联合

目录一、结构体内存对齐二、位段2.1 什么是位段2.2 位段内存分配规则2.3 位段的跨平台问题三、枚举四、联合体4.1 联合类型的定义4.2联合的特点4.3 联合大小的计算4.4 练习一、结构体内存对齐 struct s {char c1;int i;char c2; }; int main() {printf("%d\n", size…

【Hadoop】HDFS体系结构分析

文章目录1. NameNode2. Secondary NameNode3. DataNodeHDFS主要包含NameNode、Secondary NameNode和DataNode三部分&#xff0c;且这三部分在分布式文件系统中分属不同的机器&#xff0c;其中Secondary NameNode不是必须的&#xff0c;在HA架构中Standby NameNode可以替代它。 …

【深度学习】详解 SimCLR

目录 摘要 一、引言 二、方法 2.1 The Contrastive Learning Framework 2.2. Training with Large Batch Size 2.3. Evaluation Protocol 三、用于对比表示学习的数据增广 3.1 Composition of data augmentation operations is crucial for learning good representa…

5-2中央处理器-指令周期的数据流

文章目录一.指令周期二.数据流向1.取指周期2.间址周期3.执行周期4.中断周期三.指令执行方案1.单指令周期2.多指令周期3.流水线方案一.指令周期 指令周期&#xff1a;CPU从主存中每取出并执行一条指令所需的全部时间。 此处&#xff1a;取指周期取指令指令译码 指令周期常用若…

SSM整合(Spring + SpringMVC + MyBatis)

SSM Spring SpringMVC MyBatis 准备数据库 SET FOREIGN_KEY_CHECKS0; DROP TABLE IF EXISTS user; CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT,username varchar(20) NOT NULL COMMENT 用户名,password varchar(255) NOT NULL COMMENT 密码,real_name varchar(…

Linux常用命令——startx命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) startx 用来启动X Window 补充说明 startx命令用来启动X Window&#xff0c;实际上启动X Window的程序为xinit。 语法 startx(参数)参数 客户端及选项&#xff1a;X客户端及选项&#xff1b;服务器及选项&a…

[LeetCode周赛复盘] 第 329 场周赛20230122

[LeetCode周赛复盘] 第 329 场周赛20230122 一、本周周赛总结二、 [Easy] 6296. 交替数字和1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6297. 根据第 K 场考试的分数排序1. 题目描述2. 思路分析3. 代码实现四、[Medium] 6298. 执行逐位运算使字符串相等1. 题目描述2. 思路…

深入理解 OpenMP 线程同步机制

深入理解 OpenMP 线程同步机制 前言 在本篇文章当中主要给大家介绍 OpenMP 当中线程的同步和互斥机制&#xff0c;在 OpenMP 当中主要有三种不同的线程之间的互斥方式&#xff1a; 使用 critical 子句&#xff0c;使用这个子句主要是用于创建临界区和 OpenMP 提供的运行时库…

连续系统的数字PID控制仿真-1

被控对象为一电机模型传递函数&#xff1a;式中&#xff0c;J 0.0067;B0.10。采用M函数的形式&#xff0c;利用ODE45的方法求解连续对象方程&#xff0c;输入指令信号为yd(k)0.50sin(2*3.14*t)&#xff0c;采用PID控制方法设计控制器&#xff0c;其中kp20.0 ,kd0.50。PID正弦跟…

12个开源的后台管理系统

1. D2admin 开源地址&#xff1a;https://github.com/d2-projects/d2-admin 文档地址&#xff1a;https://d2.pub/zh/doc/d2-admin/ 效果预览&#xff1a;https://d2.pub/d2-admin/preview/#/index 开源协议&#xff1a;MIT 2. vue-element-admin 开源地址&#xff1a;htt…

Kettle(3):快速入门

1 需求 有一个txt文件&#xff0c;内容如下&#xff1a; id,name,age,gender,province,city,region,phone,birthday,hobby,register_date 392456197008193000,张三,20,0,北京市,昌平区,回龙观,18589407692,1970-8-19,美食;篮球;足球,2018-8-6 9:44 267456198006210000,李四,2…

Vue3 – Composition API

1、认识CompositionAPI 1.1、Options API的弊端 在Vue2中&#xff0c;我们编写组件的方式是Options API&#xff1a; Options API的一大特点就是在对应的属性中编写对应的功能模块&#xff1b;比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性…

【快速简单登录认证】SpringBoot使用Sa-Token-Quick-Login插件快速登录认证

一、解决的问题 Sa-Token-Quick-Login 可以为一个系统快速的、零代码 注入一个登录页面 试想一下&#xff0c;假如我们开发了一个非常简单的小系统&#xff0c;比如说&#xff1a;服务器性能监控页面&#xff0c; 我们将它部署在服务器上&#xff0c;通过访问这个页面&#xf…

学习字符串函数和内存函数必看

字符串函数 1.strlen函数 strlen库函数 #include<stdio.h> #include<string.h> int main() {char arr[] "abc";char arr1[] { a,b,c };int len strlen(arr);int len1 strlen(arr1);//没有\0就无法停止printf("%d\n",len);printf("%…

VUE中的provide和inject用法

一、Vue中 常见的组件通信方式可分为三类 父子通信 父向子传递数据是通过 props&#xff0c;子向父是通过 events&#xff08;$emit&#xff09;&#xff1b; 通过父链 / 子链也可以通信&#xff08;$parent / $children&#xff09;&#xff1b; ref 也可以访问组件实例&…

XLSReadWriteII 写了一个DBGrdiEh创建EXCEL表的函数

XLSReadWriteII 写了一个DBGrdiEh创建EXCEL表的函数 自己通过XLSReadWriteII&#xff0c;写了一个由DBGridEh数据集&#xff0c;通过参数调用&#xff0c;创建EXCEL表格的函数&#xff0c;通过调用的参数设置&#xff0c;可以较为方便地&#xff0c;创建指定数据集的常用EXCEL表…

自动化和Selenium

作者&#xff1a;~小明学编程 文章专栏&#xff1a;测试开发 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 什么是自动化&#xff0c;我们为什么需要自动化的测试&#xff1f; 为什么选择selenium来作为我们的web自动化测试的工具&#xff1f; 定位元素…

【C进阶】找单身狗

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C语言进阶 ⭐代码仓库&#xff1a;C Advanced 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们…