美文网首页
JUC并发编程(三)

JUC并发编程(三)

作者: 勿念及时雨 | 来源:发表于2020-03-11 18:23 被阅读0次

    常用辅助类

    CountDownLatch

    CountDownLatch,是一种减法计数器。
    CountDownLatch主要有两个方法:

    • await()会阻塞线程,等待计时器归零。
    • countDown()会令计数器减1。

    例如,创建6个线程,需要等待这6个线程执行完再在主线程中输出“main End”。代码如下:

    package com.wunian.juc.help;
    
    import java.util.concurrent.CountDownLatch;
    /**
     * CountDownLatch 减法计数器
     * 创建六个线程,等待这六个线程跑完再执行主线程的End
     */
    public class CountDownLatchDemo {
    
        public static void main(String[] args) throws InterruptedException {
            //CountDownLatch默认的计数器初始值设置为6
            CountDownLatch countDownLatch =new CountDownLatch(6);
    
            for(int i=1;i<=6;i++){
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+" Start");
                    countDownLatch.countDown();//计数器-1
                },String.valueOf(i)).start();
            }
            countDownLatch.await();//只要计数器没有归零,这里就会一直阻塞
            //等待上面六个线程跑完再执行主线程的End
            System.out.println(Thread.currentThread().getName()+" End");
        }
    }
    

    CyclicBarrier

    CyclicBarrier,作用和CountDownLatch相反,是一种加法计数器。主要是通过判断线程数来控制线程的阻塞。
    例如,模拟集齐7颗龙珠召唤神龙。代码如下:

    package com.wunian.juc.help;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    /**
     * 加法计数器
     * 集齐七颗龙珠召唤神龙
     */
    public class CyclicBarrierDemo {
        public static void main(String[] args) {
            //CyclicBarrier 篱栅  7是最多放入的线程数
            CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
                System.out.println("召唤神龙成功!");
            });
            for(int i=1;i<=7;i++){
                final int temp=i;
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"收集了第"+temp+"颗龙珠");
                    //线程阻塞 1 2 3 4 5 6 7
                    try {
                        cyclicBarrier.await();//阻塞
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    }
    

    Semaphore

    Semaphore,信号量,主要用于两种情况:

    • 多线程共享资源互斥。
    • 并发线程的控制。

    主要方法

    • acquire()
      1.当一个线程调用acquire()操作时,就是获取到了一个信号量减1。
      2.如果当前信号量为0,就会一直等待。
    • release()
      信号量加1,唤醒等待的线程。

    例如,模拟停车场有3个位置,现在有6辆车要进来的场景,代码如下:

    package com.wunian.juc.help;
    
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    /**
     * Semaphore信号量
     * 主要用在两种地方:多线程共享资源互斥!并发线程的控制!
     * 模拟场景:停车场有3个位置,现在有6辆车要进来
     */
    public class SemaphoreDemo {
        public static void main(String[] args) {
            //模拟3个车位
            Semaphore semaphore=new Semaphore(3);
            //模拟6辆车
            for(int i=1;i<=6;i++){
                new Thread(()->{
                    try {
                        semaphore.acquire();//得到车位,如果信号量为0,就会一直等待
                        System.out.println(Thread.currentThread().getName()+"抢到了车位");
                        TimeUnit.SECONDS.sleep(3);
                        System.out.println(Thread.currentThread().getName()+"离开了车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();//释放车位,唤醒等待的车
                    }
                },String.valueOf(i)).start();
            }
        }
    }
    

    JMM

    JMM,即Java内存模型(Java Memory Mode),是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。
    线程的八大操作
    内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

    • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
    • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
    • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
    • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
    • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
    • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
    • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。


      线程工作原理

    JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存 (可见)。
    • 不允许一个线程将没有assign的数据从工作内存同步回主内存。
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作。
    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存。

    volatile

    volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。
    volatile的特性

    • 保证可见性。
    • 不保证原子性。
    • 禁止指令重排。

    volatile保证可见性的论证,代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.TimeUnit;
    /**
     * volatile可见性论证
     */
    public class VolatileDemo {
    
        //private  static int num=0;
        private volatile static int num=0;
    
        public static void main(String[] args) throws InterruptedException {//有3个线程 main、gc、我们定义的线程
            new Thread(()->{
                while(num==0){
                    //没加volatile时,这个对象不可见,执行过程中会一直卡住
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
            num=1;//没加volatile时,虽然main线程修改了这个值,但上面的线程并不知道
            System.out.println(num);
        }
    }
    

    volatile不保证原子性(不可分割)的论证,代码如下:

    package com.wunian.juc.jmm;
    
    /**
     *volatile不保证原子性(不可分割)的论证
     */
    public class VolatileDemo2 {
    
        private volatile static int num = 0;
    
        // synchronized
        public static void add(){
            num++;
        }
    
        public static void main(String[] args) {
            // 期望 num 最终是 2 万
            for (int i = 1; i <=20 ; i++) {
                new Thread(()->{
                    for (int j = 1; j <= 1000; j++) {
                        add();
                    }
                },String.valueOf(i)).start();
            }
            // 判断活着的线程
            while (Thread.activeCount()>2){ // mian  gc
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + " " + num);
        }
    }
    

    从运行结果可以知道,最终输出的结果并不总是20000,进入class文件目录,打开DOS窗口,输入命令javap -c VolatileDemo2.class可以查看该类的Java字节码。

    java字节码

    显然,num++并不是一个原子性操作。
    如果不使用synchronized和lock,应该如何解决int类型变量自增的原子性问题呢?
    这个时候就要使用AtomicInteger了。
    AtomicInteger
    AtomicInteger类提供了int类型变量运算的原子性操作,代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.atomic.AtomicInteger;
    /**
     * volatile不保证原子性验证
     * AtomicInteger是原子性的
     * 保证变量原子性的方法:
     * 1.在方法中加synchronized关键字
     * 2.使用AtomicInteger
     */
    public class AtomicIntegerDemo {
    
        //private volatile static int num=0;//int 不是原子性的
        /*public synchronized static void add(){
            num++;//num++不是原子性的操作
        }*/
        private volatile static AtomicInteger num=new AtomicInteger();
    
        public static void add(){
            num.getAndIncrement();//等价于num++,是原子性操作
            //Java不能直接操作内存! native c++=>操作内存
            //Unsafe类:后门,可以用它来直接操作内存!
        }
    
        public static void main(String[] args){
            //期望num的最终结果是20000,当使用volatile时,num的实际结果并不准确
            for(int i=1;i<=20;i++){
                new Thread(()->{
                    for(int j=1;j<=1000;j++){
                        add();
                    }
                },String.valueOf(i)).start();
            }
            //判断活着的线程
            while(Thread.activeCount()>2){//除了main、 gc还有其他线程运行时,先运行其他线程
                Thread.yield();//线程让步
            }
            System.out.println(Thread.currentThread().getName()+" "+num);
        }
    }
    

    禁止指令重排
    指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。
    Java代码的执行过程:
    源代码->编译器(优化重排)->指令并行重排-> 内存系统的重排-> 最终执行的结果
    虽然单线程一定是安全的,但是也无法避免指令重排。
    处理器在进行重排的时候会考虑指令之间的依赖性。
    尝试理解多线程下的指令重排问题,如下所示:

    int x,y,a,b = 0;
    
    线程1                       线程2
    x = a;                     y = b;
    b = 1;                     a = 2;
    理想的结果: x=0  y = 0
    
    指令重排:
    线程1                       线程2
    b = 1;                     a = 2;
    x = a;                     y = b;
    
    重排后的结果: x=2  y = 1          
    

    指令重排小结

    • volatile可以禁止指令重排。
    • 内存屏障(Memory Barrier)是CPU指令,它有两个作用:
      1.保证特定的执行顺序。
      2.保证某些变量的内存可见性(volatile就是用这个特性来实现的)。


      内存屏障原理

    单例模式

    单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例。
    单例模式一般分为两种:饿汉式懒汉式
    饿汉式,代码如下:

    package com.wunian.juc.single;
    /**
     * 单例模式:饿汉式
     * 单例思想:构造器私有
     */
    public class Hungry {
    
        //浪费空间 不是我们需要的
        private byte[] data=new byte[10*1024*1024];
    
        private Hungry(){}
    
        private final static Hungry HUNGRY=new Hungry();
    
        public static Hungry getInstance(){
            return HUNGRY;
        }
    }
    

    懒汉式,基础版,代码如下:

    package com.wunian.juc.single;
    
    public class Lazy {
    
        private Lazy(){
            System.out.println(Thread.currentThread().getName() + " start");
        }
    
        private static Lazy lazy;
    
        public static Lazy getInstance() {
            if (lazy == null){
                lazy = new Lazy();
            }
            return lazyMan;
        }
    
        public static void main(String[] args) {
            // 多线程下单例失效 
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    Lazy.getInstance();
                }).start();
            }
        }
    }
    

    DCL(双重校验锁)懒汉式,代码如下:

    package com.wunian.juc.single;
    
    public class Lazy {
    
        private Lazy(){
            System.out.println(Thread.currentThread().getName() + " start");
        }
    
        private volatile static Lazy lazy;
    
        public static Lazy getInstance() {
           if (lazy == null){
                synchronized (Lazy.class){
                    if (lazy == null){
                        lazy = new Lazy(); // 请你谈谈这个操作!它不是原子性的
                        // java创建一个对象
                        // 1、分配内存空间
                        // 2、执行构造方法,创建对象
                        // 3、将对象指向空间
    
                        // A  先执行13,这个时候对象还没有完成初始化!
                        // B  发现对象为空,B线程拿到的对象就不是完成的
                    }
                }
            }
            return lazy;
        }
    
        public static void main(String[] args) {
            // 多线程下单例失效 
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    Lazy.getInstance();
                }).start();
            }
        }
    }
    

    单例之所以安全,是因为构造器私有的。但是构造器私有也不安全,使用反射就可以绕过构造器直接创建对象,代码如下:

    package com.wunian.juc.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * 单例模式:DCL(Double Check Lock 双重校验锁)懒汉式
     */
    public class Lazy {
    
        private static boolean protectedCode=false;//标记参数,防止被反编译破坏
    
        private Lazy(){
            synchronized (Lazy.class){
                if(protectedCode==false){
                    protectedCode=true;
                }else{
                    //病毒代码、文件无限扩容
                    throw  new RuntimeException("不要试图破坏我的单例模式");
                }
            }
        }
    
        private volatile static Lazy lazy;
    
        public static Lazy getInstance(){
            //双重校验锁
            if(lazy==null){
                synchronized (Lazy.class){
                    if(lazy==null) {
                        lazy=new Lazy();//创建对象不是原子性的,还是存在不安全
                        //java创建一个对象
                        //1.分配内存空间
                        //2.执行构造方法,创建对象
                        //3.将对象指向空间
                        //如果A先执行1 3,这个时候对象还没完成初始化!B发现对象为空,B线程拿到的对象就不是完成的
                    }
                }
            }
            return lazy;
        }
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    
            //反射安全吗?,官方推荐我们单例真的是DCL懒汉式吗?
            //Lazy lazy1=Lazy.getInstance();
            //得到无参构造器
    //        Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
    //        Lazy lazy2=declaredConstructors.newInstance();//创建对象
    //        Lazy lazy3=declaredConstructors.newInstance();//创建对象
    //        //hashcode不一样,所以还是不安全  反射根本不需要通过构造器
    //        //System.out.println(lazy1.hashCode());
    //        System.out.println(lazy2.hashCode());
    //        System.out.println(lazy3.hashCode());
    
            //如何破坏在反编译过程中的保护参数
            Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
            declaredConstructors.setAccessible(true);
            Lazy lazy4=declaredConstructors.newInstance();//创建对象
            //获取参数对象,必须是在知道参数名称的情况下
            Field protectedCode=Lazy.class.getDeclaredField("protectedCode");
            //重新将参数值设置为false
            protectedCode.setAccessible(true);
            protectedCode.set(lazy4,false);
            Lazy lazy5=declaredConstructors.newInstance();//创建对象
            System.out.println(lazy4.hashCode());
            System.out.println(lazy5.hashCode());
        }
    }
    

    这个时候就要使用枚举类了。枚举是一个类,实现了枚举的接口,反射无法破坏枚举。代码如下:

    package com.wunian.juc.single;
    
    import com.sun.org.apache.bcel.internal.generic.INSTANCEOF;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * 单例模式:使用枚举类
     * 枚举是一个类,实现了枚举的接口
     * 反射 无法破坏枚举
     */
    public enum SingleEnum {
    
        INSTANCE;
    
        public SingleEnum getInstance(){
            return INSTANCE;
        }
    }
    //至少在做一个普通的jvm的时候,jdk源码没有被修改的时候,枚举就是安全的
    //可以通过修改 jdk/jre/lib/rt.jar中java.lang.reflect.Constructor.class来破坏枚举(见Constructor类)
    class Demo{
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            //通过jad.exe反编译可知,SingleEnum类只有一个有参构造器
            //Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
            Constructor<SingleEnum> declaredConstructor=SingleEnum.class.getDeclaredConstructor(String.class,int.class);
            declaredConstructor.setAccessible(true);
            // throw new IllegalArgumentException("Cannot reflectively create enum objects");
            SingleEnum singleEnum1=declaredConstructor.newInstance();
            SingleEnum singleEnum2=declaredConstructor.newInstance();
            System.out.println(singleEnum1.hashCode());
            System.out.println(singleEnum2.hashCode());
            //这里面没有无参构造!JVM才是王道
            //java.lang.NoSuchMethodException: com.wunian.juc.single.SingleEnum.<init>()
        }
    }
    

    运行代码会发现,报了一个异常:Cannot reflectively create enum objects,因此无法通过反射破坏枚举。但是如果修改jdk源码,枚举也可能变得不安全,但至少一般情况下枚举还是安全的。

    CAS

    CAS,CompareAndSet,比较并交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
    示例代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * CAS:比较并交换
     */
    public class CompareAndSetDemo {
    
        public static void main(String[] args) {
            //AtomicInteger默认为0
            AtomicInteger atomicInteger=new AtomicInteger(5);
            //compareAndSet CAS 比较并交换
            //如果这个值是期望的值,则更新为指定的值,交换成功返回true,否则返回false
            System.out.println(atomicInteger.compareAndSet(5,20));
            System.out.println(atomicInteger.get());//输出当前的值
            System.out.println(atomicInteger.compareAndSet(20, 5));
        }
    }
    

    分析AtomicInteger类的getAndIncrement方法
    getAndIncrement方法实现了int++的原子性操作,它底层是如何实现的呢?来看看它的源码:

    // unsafe可以直接操作内存
    public final int getAndIncrement() {
        // this 调用的对象
        // valueOffset 当前这个对象的值的内存地址偏移值
        // 1
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5; // ? 
        do { // 自旋锁(就是一直判断!)
            // var5 = 获得当前对象的内存地址中的值!
            var5 = this.getIntVolatile(this, valueOffset); // 1000万
            // compareAndSwapInt 比较并交换
            // 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
            // 如果是期望的值,就交换,否则就不交换!
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    

    可以看出,getAndIncrement的底层是通过自旋锁和CAS算法来实现的。
    CAS的缺点

    • 循环开销很大。
    • 内存操作,每次只能保证一个共享变量的原子性。
    • 可能出现ABA问题。

    原子引用

    什么是ABA问题?
    简单来说就是狸猫换太子,例如有两个线程T1和T2,T1线程希望通过CAS算法将一个变量的值由100更新为1,结果在更新过程中睡眠了3秒,在这三秒中T2线程也通过CAS算法先将该变量值更新由100更新为1,然后又将该变量值由1再次更新为100,整个过程看起来似乎该变量的值没有改变,但是对于T1线程来说,数据已经改动了。
    如何解决ABA问题?
    可以使用原子类,通过增加一个版本号来解决,原理和乐观锁一样。例如,小明和小花同时更新一个数据,小明先睡了三秒,结果数据先被小花更新了,这时版本号发生变化,小明再去更新就会失败,代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * ABA问题 1-100-1
     * 通过version字段加1来实现数据的原子性
     */
    public class ABADemo {
    
        //version =1
        static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
    
        public static void main(String[] args) {
    
            //其他人员 小花 需要每次执行完毕+1
            new Thread(()->{
                int stamp=atomicStampedReference.getStamp();//获得版本号
    
                System.out.println("T1 stamp01=>"+stamp);
    
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedReference.compareAndSet(100,101,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp()+1);
    
                System.out.println("T1 stamp02=>"+atomicStampedReference.getStamp());
    
                atomicStampedReference.compareAndSet(101,100,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp()+1);
                System.out.println("T1 stamp03=>"+atomicStampedReference.getStamp());
    
            },"T1").start();
    
            //乐观的小明,sleep过程中数据被小花改过,版本号发生变化,无法完成更新
            new Thread(()->{
                int stamp=atomicStampedReference.getStamp();//获得版本号
                System.out.println("T2 stamp01=>"+stamp);
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean result=atomicStampedReference.compareAndSet(100,1,stamp,stamp+1);
                System.out.println("T2是否修改成功:"+result);
                System.out.println("T2 stamp02=>"+atomicStampedReference.getStamp());
                System.out.println("T2 当前获取得最新的值=>"+atomicStampedReference.getReference());
            },"T2").start();
        }
    }
    

    自旋锁

    自旋锁是为实现保护共享资源而提出一种锁机制。是为了解决对某项资源的互斥使用。在任何时刻,最多只能有一个执行单元获得锁。如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
    Unsafe类源码中的getAndAddInt方法中就使用了自旋锁,源码如下:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5; 
        do { // 自旋锁(就是一直判断!)
            // var5 = 获得当前对象的内存地址中的值!
            var5 = this.getIntVolatile(this, valueOffset); // 1000万
            // compareAndSwapInt 比较并交换
            // 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
            // 如果是期望的值,就交换,否则就不交换!
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    

    我们可以使用自旋锁的方式定义一把锁,代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.atomic.AtomicReference;
    /**
     * 自己定义一把自旋锁
     */
    public class CodingLock {
    
        //AtomicInteger默认是0
        //AtomicReference默认是null
        //锁线程
        AtomicReference<Thread> atomicReference=new AtomicReference<>();
    
        //加锁
        public void lock(){
            Thread thread=Thread.currentThread();
            System.out.println(thread.getName()+"==>lock");
            //上锁 自旋(不停的循环)
            while(!atomicReference.compareAndSet(null,thread)){
    
            }
        }
    
        //解锁
        public void unlock(){
            Thread thread=Thread.currentThread();
            atomicReference.compareAndSet(thread,null);
            System.out.println(thread.getName()+"==>unlock");
        }
    }
    

    然后编写测试类测试一下,代码如下:

    package com.wunian.juc.jmm;
    
    import java.util.concurrent.TimeUnit;
    /**
     * 测试自旋锁
     */
    public class CodingLockTest {
    
        public static void main(String[] args) {
            CodingLock lock=new CodingLock();
    
            //1 一定要先拿到锁,1解锁后2才可以拿到锁
            new Thread(()->{
                lock.lock();
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            },"T1").start();
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2
            new Thread(()->{
                lock.lock();
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            },"T2").start();
        }
    }
    

    测试结果为T1线程先拿到锁,然后等T1释放锁之后T2线程才能拿到锁。这说明我们自己定义的锁实现了锁的功能。

    死锁排查

    死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
    产生死锁的四个条件
    互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。

    死锁原理
    模拟死锁,代码如下:
    package com.wunian.juc.jmm;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 死锁
     * 死锁条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件
     * 死锁处理:查看堆栈信息:JVM知识
     * //1.获取当前运行的java进程号 jps -l
     * //2.查看进程信息 jstack 进程号
     * //3.jconsole查看对应的信息(可视化工具)
     */
    public class DeadLockDemo {
    
        public static void main(String[] args) {
    
            String lockA="lockA";
            String lockB="lockB";
    
            new Thread(new MyLockThread(lockA,lockB),"T1").start();
            new Thread(new MyLockThread(lockB,lockA),"T2").start();
        }
    }
    
    class MyLockThread implements Runnable{
    
        private String lockA;
        private String lockB;
    
        public MyLockThread(String lockA,String lockB) {
            this.lockA=lockA;
            this.lockB=lockB;
        }
    
        @Override
        public void run() {
            synchronized (lockA){
                System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get:"+lockB);
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB){
                    System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get:"+lockA);
    
                }
            }
        }
    }
    

    遇到死锁问题怎么处理?
    1.获取当前运行的java进程号,命令:jps -l
    2.查看进程信息,命令:jstack 进程号
    3.使用jconsole查看对应的信息(可视化工具)。
    4.修改代码,破坏死锁产生的其中一个条件即可。

    几个jdk命令

    查看字节码:javap -c xxx.class
    查看class源码:javap -p xxx.class
    将class文件反编译成java文件(需先将jad.exe拷贝至jdk的bin目录下):jad -sjava xxx.class

    相关文章

      网友评论

          本文标题:JUC并发编程(三)

          本文链接:https://www.haomeiwen.com/subject/aemcjhtx.html