我是靠谱客的博主 难过夏天,最近开发中收集的这篇文章主要介绍22-08-27 西安 JUC(01)synchronized、Lock锁、ThreadLocal、LockSupport、死锁问题排查线程安全问题线程通信死锁问题排查,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

线程安全问题

资源:服务器中的一个文件或者是某个接口

并行:多个请求同一个时间点访问不同的资源
并发:多个请求同一个时间点访问同一个资源(春运抢票、电商秒杀、限量抢购)

Lock锁 [jdk1.5新特性]

1、悲观锁与乐观锁

悲观锁

对于悲观锁来说,它总是认为每次访问共享资源时会发⽣冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同⼀时间只能有⼀个线程在执⾏。

只要添加了悲观锁,并发的多个线程只能等持有锁的线程释放锁才可以获取到锁使用,同时只能有一个线程使用锁。

悲观锁场景使用

悲观锁多⽤于”写多读少“的环境,避免频繁失败和重试影响性能

乐观锁

乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执⾏,⽆需加锁也⽆需等待。⽽⼀旦多个线程发⽣冲突,乐观锁通常是使⽤⼀种称为CAS的技术来保证线程执⾏的安全性

查询数据没有限制,但是查询数据时会一起查询它的版本号

如果需要更新数据 更新时先检查版本号是否一致 如果一致更新数据 不一致更新失败

乐观锁场景使用

乐观锁多⽤于“读多写少“的环境,避免频繁加锁影响性能


2、Lock接口

Lock锁是代码级别的锁 需要我们手动创建对象,调用方法完成加锁释放锁 使用复杂 但是灵活

lock()

lock:用来获取锁,如果锁被其他线程获取,处于等待状态

使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生

--------------------------------------------------------------------------------------

tryLock()

tryLock方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被
其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

--------------------------------------------------------------------------------------

unlock()

释放锁,一定要在finally块中释放


3、实现类ReentrantLock

Lock是一个接口,常用的实现类:可重入锁 ReentrantLock

ReentrantLock特点:

可重入的、可公平可不公平,可响应中断的,悲观的排他的锁,需要手动控制锁的添加、释放。使用灵活 代码层面的锁

ReentrantLock使用场景(适合写多读少),悲观锁的特征:你读我加锁,你写我也加锁

这些特点具体表现在:

1.需要自己创建对象调用方法加锁 释放锁

2.可重入性:持有锁的业务方法 可以调用其他的需要相同锁的方法

3.公平性:所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权

默认 new ReentrantLock() 是非公平锁

//默认底层初始化一个不公平的同步器
new ReentrantLock() 和new ReentrantLock(false)一样 


//创建一个公平锁对象,处理任务时 并发的线程按照排序顺序来执行
ReentrantLock lock = new ReentrantLock(true); 

4.可以响应中断[超时]::获取锁或者释放锁超过指定时间后 可以中断操作,一定程度上可以避免死锁

//返回获取锁是否成功
boolean b = lock.tryLock(2, TimeUnit.SECONDS);   

测试ReentrantLock是可重入锁

这部分我自己都觉得写的多余了,但又怕读者不能理解可重入锁。。就写上吧,万一以后我自己忘了啥玩意是可重入锁

class Ticket{

    private Integer number = 20;

    private ReentrantLock lock = new ReentrantLock();

    public void sale(){
        lock.lock();

        if (number <= 0) {
            System.out.println("票已售罄!");
            lock.unlock();
            return;
        }

        try {
            Thread.sleep(200);
            // 调用check方法测试锁的可重入性
            this.check();
            number--;
            System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 为了测试可重入锁,添加检查余票方法
     */
    public void check(){
        lock.lock();
        System.out.println("网络支付安全...");
        lock.unlock();
    }
}

多线程测试类,我就不拿出来了,,,控制台运行结果,足以证明ReentrantLock是可重入锁

测试ReentrantLock是公平锁

公平就是谁排队时间最长谁先执行获取锁,上面也说了怎么去搞一个公平锁

创建一个公平锁对象,处理任务时 并发的线程按照排序顺序来执行

ReentrantLock lock = new ReentrantLock(true); 

把上面测试可重入锁的代码稍微改一下,改动方式如下

class Ticket {

    private Integer number = 20;

    private ReentrantLock lock = new ReentrantLock(true);

    public void sale() {
        lock.lock();
        try {
            if (number <= 0) {
                System.out.println("票已售罄!");
//                lock.unlock();
                return;
            }

            Thread.sleep(200);
            number--;
            System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

测试类,这里我就犯了一个毛病,,,,刚开始把For循环写外面了,难怪测试不出来 

public class LockDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    try {
                        ticket.sale();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, "AA").start();


            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    try {
                        ticket.sale();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, "BB").start();
    }
}

控制台测试结果:

测试ReentrantLock是可响应中断的

tryLock方法来实现,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果。

true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。

    public void sale() {
//        lock.lock();
        try {
            boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS);//尝试获取锁
            if (!b) {
                System.out.println(Thread.currentThread().getName() + "获取锁失败");
                return;
            }

            if (number <= 0) {
                System.out.println("票已售罄!");
                lock.unlock();
                return;
            }

            Thread.sleep(2000);
            number--;
            System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

这会报错,,,

原因就在那个finally上了,都获取锁失败了吗,但是finally还要求你释放锁,我都没锁怎么释放锁。。。

然后老师冷不丁的来了这么一句:刚开始觉得???,后来觉得还是能跟这个案例扯上关系的

Lock获取锁成功,底层通过一个int类型的state属性代表锁获取的次数
0代表空闲   >0代表锁使用的次数  值不能为负数

4、ReentrantReadWriteLock 可重入读写锁

ReentrantReadWirteLock实现了ReadWirteLock接口,并未实现Lock接口

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {

}


//ReadWriteLock接口只有这俩个方法
public interface ReadWriteLock {

    Lock readLock();//获取读锁 

    Lock writeLock();//获取写锁 
}

引入问题:synchronized或者ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。

现实中有这样一种场景:在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了

ReentrantReadWriteLock(读写锁)。读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞

针对并发较大 同时有部分写操作 和大量的读操作,希望只有读的时候不使用悲观锁,使用共享锁一旦有写的操作 所有的未执行的读都会阻塞 等所有的写操作执行结束后读才可以获取锁继续执行

常用的场景:并发的缓存读写管理

2个线程读读不互斥,读写和写写都是互斥的,也就是说一个线程读的时候,另外一个线程不能写

读写锁的注意点

重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

重入时降级支持:即持有写锁的情况下去获取读锁

演示不加读写锁时的问题

缓存管理类MyCache,一个写缓存的方法 一个是读缓存的方法

class MyCache{

    private volatile Map<String, String> cache= new HashMap<>();

    public void put(String key, String value){
        try {
            System.out.println(Thread.currentThread().getName() + " 开始写入!");
            Thread.sleep(300);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入成功!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
    }

    public void get(String key){
        try {
            System.out.println(Thread.currentThread().getName() + " 开始读出!");
            Thread.sleep(300);
            String value = cache.get(key);
            System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

public class ReentrantReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache cache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            String num = String.valueOf(i);
            // 开启5个写线程
            new Thread(()->{
                cache.put(num, num);
            }, num).start();
        }
        for (int i = 1; i <= 5; i++) {
            String num = String.valueOf(i);
            // 开启5个读线程
            new Thread(()->{
                cache.get(num);
            }, num).start();
        }
    }
}

控制台打印如下:

解决:加读写锁 ReentrantReadWriteLock

读的时候跟不加锁是一样的,那为什么加锁?

为了写的时候阻塞所有的读

class MyCache{

    private volatile Map<String, String> cache= new HashMap<>();

    // 加入读写锁
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public void put(String key, String value){
        // 加写锁
        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 开始写入!");
            Thread.sleep(300);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入成功!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            rwl.writeLock().unlock();
        }
    }

    public void get(String key){
        // 加入读锁
        rwl.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 开始读出!");
            Thread.sleep(300);
            String value = cache.get(key);
            System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            rwl.readLock().unlock();
        }
    }
}

运行结构不再有null值


5、Synchronized和ReentrantLock区别

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。

(4)synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

(5)性能上来说,在资源竞争不激烈的情形下,Lock性能稍微比synchronized差点(编译程序通常会尽可能的进行优化synchronized)。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。


ThreadLocal 线程本地变量

java.lang.ThreadLocal
public class ThreadLocal<T> {
}

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本,让每个线程绑定自己的值从而避免了线程安全问题

ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

多个线程如何才能不争抢
1.加入synchronized或者Lock控制资源的访问顺序
2.人手一份,大家各自安好,没必要抢夺

1、ThreadLocal的5个方法

ThreadLocal 类最常⽤的就是 set ⽅法和 get ⽅法。

ThreadLocal变量初始化

该变量是每个线程自带的。推荐使用withInitial()方法

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

如果使用initialValue()

protected T initialValue() {
        return null;
}

需要使用匿名内部类


2、ThreadLocal编码规范

ali编码规范中要求如下

那么我们做一个演示

使用完,不清理ThreadLocal的变量

class MyData {
    //一个ThreadLocal类型的成员变量
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);

    public void add() {
        //每调用一次该方法,给线程专属变量+1
        threadLocalField.set(1 + threadLocalField.get());
    }
}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadpool = Executors.newFixedThreadPool(3);
        //来10个任务,让该线程池去处理
        for (int i = 1; i <= 10; i++) {
            threadpool.submit(()->{
                Integer beforeINT = myData.threadLocalField.get();
                myData.add();
                Integer afterINT = myData.threadLocalField.get();
                System.out.println("当前处理任务的线程是"+Thread.currentThread().getName()+"t"+"beforeINT: "+beforeINT+"t afterINT: "+afterINT);
            });
        }
        threadpool.shutdown();
    }
}

看的出来,线程1的ThreadLocal变量值一直在增加,这回造成业务逻辑问题和内存泄漏

使用完,清理ThreadLocal的变量

public static void main(String[] args) {
    MyData myData = new MyData();
    ExecutorService threadpool = Executors.newFixedThreadPool(3);
    //来10个任务,让该线程池去处理
    for (int i = 1; i <= 10; i++) {
        threadpool.submit(() -> {
            try {
                Integer beforeINT = myData.threadLocalField.get();
                myData.add();
                Integer afterINT = myData.threadLocalField.get();
                System.out.println("当前处理任务的线程是" + Thread.currentThread().getName() + "t" + "beforeINT: " + beforeINT + "t afterINT: " + afterINT);
            } finally {
                //每个线程处理完任务之后,都要回收ThreadLocal变量,避免造成内存泄漏
                myData.threadLocalField.remove();
            }
        });

    }
    threadpool.shutdown();
}

ThreadLocal变量的值不再累加,符合效果 

====

建议ThreadLocal对象使用static修饰

ThreadLocali能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap。所以,ThreadLocali可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。

阿里手册


3、ThreadLocalMap

为什么TheadLocal线程安全?

Thread类中有一个ThreadLocalMap类型的变量,每个线程人手一份,不和别人共用,该map

只被持有它的线程访问,故不存在线程安全以及锁的问题

ThreadLocalMapThreadLocal的静态内部类

ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

以当前ThreadLocal对象作为键,给定的数据作为值设置到当前线程对象的ThreadLocalMap


4、set、get、remove方法

 set方法

public void set(T value) {
    Thread t = Thread.currentThread();//获取当前线程对象
    ThreadLocalMap map = getMap(t);//得到线程的Map,也就是这个属性threadLocals
    if (map != null)
        map.set(this, value);//将当前的threadLocal对象作为键  传入的参数作为值存入
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

=========  后续通过ThreadLocal获取属性值

get方法 

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

====================  移除数据

 remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

5、ThreadLocalMap与WeakReference

内存泄漏:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

只被弱引用关联的对象只能生存到下一次垃圾收集发生为止;在系统GC时,只要发现弱引用,
无论内存是否足够都会回收掉只被弱引用关联的对象。
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
)


线程通信

线程间通信是通过 线程竞争关系+ 线程等待释放锁+唤醒等待 来完成线程的通信

synchornized和Lock加锁的方式 锁定资源都不同 所以不能通用

synchronized线程间通信

  • synchronized 保证多个线程访问的资源有竞争关系
  • wait/noityAll+ 业务中的判断 保证满足条件的线程才会执行

多线程模板

1.线程操作资源类

2.线程间的协作(通信)

         判断、干活、通知(唤醒)


1、线程通信简单案例

两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮

这个案例之所以能成功,原因在于AA线程和BB线程都在争抢同一个对象shareDataOne ,存在竞争关系。

AA获取到后,就去给number+1,假设此时number=0,AA会把number+1,因为synchronized是非公平锁,所以AA可能又抢到了,此时number=1,AA就在this.wait那等着,并且释放锁,BB拿到锁之后对number-1,并且唤醒所有在对象shareDataOne等待的线程,AA就醒了,继续往下走。。。把number再+1.。。。

public class ShareDataOne {
    private Integer number = 0;
    /**
     *  增加1
     */
    public synchronized void increment() throws InterruptedException {
        //判断
        if(number!=0){
            //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
            this.wait();
        }
        //干活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        //通知:唤醒此对象上等待的其他线程
        this.notifyAll();
    }
    /**
     *  减少1
     */
    public synchronized void decrement() throws InterruptedException {
        //判断
        if(number!=1){
            //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
            this.wait();
        }
        //干活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        //通知:唤醒此对象上等待的其他线程
        this.notifyAll();
    }
}

测试类如下:

public class NotifyWaitDemo {

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

    }
}

测试结果也是对的,交替打印

2、虚假唤醒问题案例

资源类不改动,我们改变测试类代码、

再加CC和DD线程

public class NotifyWaitDemo {

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
        
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();
    }
}

打印结果,依然会有概率是,10101010...。

但是,多执行几次,也会出现错乱的现象,也就是虚假唤醒问题

分析为什么会出现虚假唤醒问题

消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

解决线程唤醒问题很简单

判断时,不能用if,改用while

if只判断一次,while是只要唤醒就要拉回来再判断一次。

//判断
while(number!=0){
    //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
    this.wait();
}

 //判断
 while(number!=1){
     //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
     this.wait();
 }

测试,控制台打印结果,就会交替打印1和0了


ReentrantLock线程通信

ReentrantLock: 它是通过同一个锁对象的state属性值 判断锁是否被使用,

多个线程如果希望通过Lock来加锁保证竞争关系,他们需要使用同一个lock对象,哪一个能够将lock对象的state属性值从0改为1 哪一个就可以获取锁成功

注:只能有一个线程修改state成功,会把等待的线程放在队列(AQS)

此时线程间的通信需要使用的方法:跟上面那一套(synchronized)不一样

线程间通信需要使用Lock的newCondition创建的Condition对象的:

await()、signal()/signalAll() 控制线程间的通信

Condition对象每个线程拥有自己单独的一个,才可以精确控制当前线程释放锁等待 或者被唤醒

public class ShareDataOne {
    private Integer number = 0;

    Lock lock = new ReentrantLock(); // 初始化lock锁

    //必须使用正在加锁的锁对象创建Condition对象
    Condition c_add = lock.newCondition();
    Condition c_sub = lock.newCondition();

    /**
     * 打印0
     */
    public void print0() {
        lock.lock(); // 加锁
        try {
            // 1. 判断
            while (number != 0) {
                // c_add让当前线程等待
                c_add.await();
            }

            // 2. 干活
            System.out.print(number++ + "t");

            // 3. 通知 唤醒的是使用c_sub等待的线程
            //condition对象 每个线程需要自己单独的一个,才可以精确控制当前线程释放锁等待  或者被唤醒
            c_sub.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 打印1
     */
    public void print1() {
        lock.lock();
        try {
            // 1. 判断
            while (number != 1) {
                c_sub.await();
            }

            // 2. 干活
            System.out.print(number-- + "t");

            // 3. 通知
            //condition对象 每个线程需要自己单独的一个,才可以精确控制当前线程释放锁等待  或者被唤醒
            c_add.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

测试类:

public class NotifyWaitDemo {

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                shareDataOne.print0();
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                shareDataOne.print1();
            }
        },"BB").start();
    }
}

控制台打印效果 


LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

1、LockSupport工具类

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

8个方法


2、park()和unpark()

LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit且permit最多只有一个,重复调用unpark也不会积累凭证

==========

park()

permit许可证默认设有,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程的发放permit,park方法才会被唤醒。

调用了UnSafe类

public static void park() {
    UNSAFE.park(false, 0L);
}

==========

unpark()

调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。

调用了UnSafe类

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

=======

为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。


3、LockSupport实现线程间通信

实现线程间等待唤醒的3种方式

方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

不管是方式1还是方式2都有如下2个限制

  • 线程先要获得并持有锁,等待唤醒方法必须在锁块(synchronized或Iock)中
  • 必须要先等待后唤醒,线程才能够被唤醒
public class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"t----come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"t----被唤醒");
        },"t1");
        t1.start();
        //休眠1s
       TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"t----发出通知");
        },"t2").start();
    }
}


死锁问题排查

1、死锁产生的原因

死锁:两个或者两个以上线程在执行过程中,因为争夺资源而造成一种互相等待的观象,如果没有外力干涉,他们无法再执行下去

 A持有A锁,B持有B锁,都试图获取对方的锁

造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。

1.一个资源每次只能被一个线程使用
2.一个线程在阻塞等待某个资源时,不释放已占有资源
3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4.若干线程形成头尾相接的循环等待资源关系

而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。


2、死锁的几种情况

1、使用synchronized

两个线程在持有锁的情况下去获取对方的锁(业务中没有合理的使用锁、释放锁)

/*
死锁:
    A线程和B线程 互相持有锁
    各自在加锁的代码中有需要获取对方的锁导致死锁

 */
public class JucDemo04DeadLock {
    Object obj1 = new Object();
    Object obj2 = new Object();

    public static void main(String[] args) {
        JucDemo04DeadLock deadLock = new JucDemo04DeadLock();
        new Thread(()->{
            deadLock.a();
        },"AA").start();
        new Thread(()->{
            deadLock.b();
        },"BB").start();
    }
    public void a(){
        synchronized (obj1){
            System.out.println(Thread.currentThread().getName()+"获取到obj1的锁 正在执行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName()+"获取到obj2的锁 正在执行");
            }

        }
    }
    public void b(){
        synchronized (obj2){
            System.out.println(Thread.currentThread().getName()+"获取到obj2的锁 正在执行");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj1){
                System.out.println(Thread.currentThread().getName()+"获取到obj1的锁 正在执行");
            }

        }
    }
}

控制台打印效果如下:停不下来了。。。直是红色方块 

2、使用reentrantLock时:

为了防止虚假唤醒使用了while死循环判断某个flag变量的状态值

    - 如果一个线程执行完任务后没有修改flag变量的值 会导致死循环
    - 如果忘了通过signal或者使用错了condition对象signal都会导致死锁
    - 如果lock忘记unlock也会导致死锁


3、如何查看死锁

  1. 可以通过jstack命令来进行查看,jstack/jvisualvm命令中会显示发生了死锁的线程
  2. 或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

判断是否出现死锁

如果你打不开terminal窗口,就试着改一下这个。。深受其害

jps : 查看所有的java进程   

jps -l   //定位进程号

 注意是:小L

jstack:查看某个java进程的线程堆栈信息+线程的状态

jstack 进程号  //找到死锁的问题

可是可是在实际开发报错是这样的。。。程序就是卡住在那一块了,用jstack也是照样卡住。。。

线程的状态

  • NEW 未启动的。

  • RUNNABLE 运行中。

  • BLOCKED 受阻塞并等待监视器锁。

  • WATING 等待另一个线程执行特定操作。

  • TIMED_WATING 有时限的等待另一个线程的特定操作。

  • TERMINATED 已退出的。


4、死锁的解决方案

如果是synchronized

jps+jstack 可以查看到死锁的线程 排查代码问题

如果是reentrantLock

需要我们自己判断长时间等待的线程,到代码中找业务问题


5、开发中如何避免死锁

1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁
2.要注意加锁时限,可以针对锁设置一个超时时间
3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

===============

使用synchronized时:

因为它不可响应中断,一定不要写复杂的嵌套锁的获取

使用ReentrantLock

每次加锁必须伴随一次释放锁,释放锁建议写在finally中,还可以使用tryLock尝试获取锁,可以响应中断,获取失败超时

最后

以上就是难过夏天为你收集整理的22-08-27 西安 JUC(01)synchronized、Lock锁、ThreadLocal、LockSupport、死锁问题排查线程安全问题线程通信死锁问题排查的全部内容,希望文章能够帮你解决22-08-27 西安 JUC(01)synchronized、Lock锁、ThreadLocal、LockSupport、死锁问题排查线程安全问题线程通信死锁问题排查所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部