美文网首页
juc和多线程并发相关面试题

juc和多线程并发相关面试题

作者: mundane | 来源:发表于2022-03-04 15:27 被阅读0次

    线程池7大参数介绍

    • corePoolSize:线程池中的常驻核心线程数
      在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
      当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
    • maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
    • keepAliveTime:多余的空闲线程的存活时间。
      当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
    • unit:keepAliveTime的单位。
    • workQueue:任务队列,被提交但尚未被执行的任务。
    • threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
    • handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)

    线程池底层工作原理


    • 在创建了线程池后,等待提交过来的任务请求。

    • 当调用execute()方法添加一个请求任务时,线程池会做如下判断:

      • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
      • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
      • 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
      • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
    • 当一个线程完成任务时,它会从队列中取下一个任务来执行。

    • 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
      如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。

    线程有哪几种状态

    https://blog.csdn.net/pange1991/article/details/53860651

    1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
    2. 运行(RUNNABLE)
      Java线程中将就绪(ready)和运行中(running)称为“运行”。
      调用了该对象的start()方法之后,该状态的线程等待被线程调度选中获取CPU的使用权,此时处于就绪状态(ready)。
      就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
    3. 阻塞(BLOCKED):表示线程阻塞于锁。
    4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    5. 超时等待(TIMED_WAITING):不同于WAITING,它可以在指定的时间后自行返回。
    6. 终止(TERMINATED):表示该线程已经执行完毕。
      这6种状态定义在Thread类的State枚举中

    请你谈谈JMM

    JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
    JMM关于同步的规定:

    1. 线程解锁前,必须把共享变量的值刷新回主内存
    2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
    3. 加锁解锁是同一把锁

    由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

    可见性问题

    通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
    这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
    可见性的代码验证:

    package com.mundane.interviewdemo.volatiledemo;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 假设是主物理内存
     */
    class MyData {
    
    //    volatile int number = 0;
        int number = 0;
    
        public void addTo60() {
            this.number = 60;
        }
    }
    
    /**
     * 验证volatile的可见性
     * 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
     */
    public class VolatileDemo {
    
        public static void main(String args []) {
    
            // 资源类
            MyData myData = new MyData();
    
            // AAA线程 实现了Runnable接口的,lambda表达式
            new Thread(() -> {
    
                System.out.println(Thread.currentThread().getName() + "\t come in");
    
                // 线程睡眠3秒,假设在进行运算
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 修改number的值
                myData.addTo60();
    
                // 输出修改后的值
                System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
    
            }, "AAA").start();
    
            // main线程就一直在这里等待循环,直到number的值不等于零
            while(myData.number == 0) {}
    
            // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
            // 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
            System.out.println(Thread.currentThread().getName() + "\t mission is over");
    
        }
    }
    

    结果是程序卡在while(myData.number == 0) {}那一行,MyData中的number换成用volatile修饰就能顺利走完整个程序。

    volatile关键字的作用

    volatile是JVM提供的轻量级的同步机制

    • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,所以对其他线程是可见的,当有其他线程需要读取时,它会去内存中读取新值。
    • 不保证原子性
    • 禁止进行指令重排序

    volatile不保证原子性

    原子性指的是什么意思?

    不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。

    volatile不保证原子性案例演示:

    package com.mundane.interviewdemo.volatiledemo;
    
    class MyData2 {
        /**
         * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
         */
        volatile int number = 0;
    
    
        public void addPlusPlus() {
            number ++;
        }
    }
    
    public class VolatileAtomicityDemo {
    
        public static void main(String[] args) {
            MyData2 myData = new MyData2();
    
            // 创建10个线程,线程里面进行1000次循环
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    // 里面
                    for (int j = 0; j < 1000; j++) {
                        myData.addPlusPlus();
                    }
                }, String.valueOf(i)).start();
            }
    
            // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
            // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
            while(Thread.activeCount() > 2) {
                // yield表示不执行
                Thread.yield();
            }
    
            // 查看最终的值
            // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
            System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
    
        }
    
    }
    

    输出结果:

    main     finally number value: 18192
    

    volatile不保证原子性理论解释

    number++在多线程下是非线程安全的。

    我们可以将代码编译成字节码,可看出number++被编译成3条指令。


    假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。

    上面的解释可能不太清楚,再解释一遍:
    1、线程读取i
    2、temp = i + 1
    3、i = temp

    • 当 i=5 的时候A、B两个线程同时读取了 i 的值。
    • A线程执行了 temp = i + 1的操作, 然后B线程也执行了 temp = i + 1的操作。此时A,B两个线程保存的 i 的值都是5,temp 的值都是6。
    • A线程执行了 i = temp的操作,此时i的值变成6。随后B线程执行 i = temp ,i还是6。所以导致了计算结果比预期少了1。

    https://blog.csdn.net/qq_31442743/article/details/107930684

    多线程卖票

    public class SellTicket implements Runnable {
    
        private int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    // 通过sleep模拟出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }
    
    public class SellTicketDemo {
        public static void main(String[] args) {
            SellTicket st = new SellTicket();
            Thread t1 = new Thread(st, "窗口1");
            Thread t2 = new Thread(st, "窗口2");
            Thread t3 = new Thread(st, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    

    输出结果:

    窗口2正在出售第100张票
    窗口3正在出售第100张票
    窗口1正在出售第100张票
    窗口1正在出售第97张票
    窗口2正在出售第97张票
    窗口3正在出售第97张票
    窗口1正在出售第94张票
    窗口2正在出售第94张票
    窗口3正在出售第94张票
    窗口1正在出售第91张票
    窗口3正在出售第91张票
    窗口2正在出售第91张票
    窗口1正在出售第88张票
    窗口3正在出售第88张票
    窗口2正在出售第88张票
    窗口3正在出售第85张票
    窗口2正在出售第85张票
    窗口1正在出售第85张票
    窗口2正在出售第82张票
    窗口3正在出售第82张票
    窗口1正在出售第82张票
    窗口1正在出售第79张票
    窗口3正在出售第79张票
    窗口2正在出售第79张票
    ...
    窗口2正在出售第4张票
    窗口3正在出售第2张票
    窗口1正在出售第1张票
    窗口2正在出售第0张票
    窗口3正在出售第-1张票
    

    如何解决?使用同步代码块

    public class SellTicket implements Runnable {
    
        private int tickets = 100;
        private Object obj = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) {
                    if (tickets > 0) {
                        // 通过sleep模拟出票时间
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                    }
                }
            }
        }
    }
    

    https://www.bilibili.com/video/BV1Ci4y1u74K?p=334

    关于同步方法,同步方法的锁对象是this, 同步静态方法的锁对象是类名.class

    volatile不保证原子性问题解决

    可加给addPlusPlus方法加synchronized解决

        public synchronized void addPlusPlus() {
            number++;
        }
    

    但它是重量级同步机制,性能上有所顾虑。

    如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。

    import java.util.concurrent.atomic.AtomicInteger;
    
    class MyData2 {
        /**
         * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
         */
        volatile int number = 0;
        AtomicInteger number2 = new AtomicInteger();
    
        public void addPlusPlus() {
            number ++;
        }
        
        public void addPlusPlus2() {
            number2.getAndIncrement();
        }
    }
    
    public class VolatileAtomicityDemo {
    
        public static void main(String[] args) {
            MyData2 myData = new MyData2();
    
            // 创建10个线程,线程里面进行1000次循环
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    // 里面
                    for (int j = 0; j < 1000; j++) {
                        myData.addPlusPlus();
                        myData.addPlusPlus2();
                    }
                }, String.valueOf(i)).start();
            }
    
            // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
            // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
            while(Thread.activeCount() > 2) {
                // yield表示不执行
                Thread.yield();
            }
    
            // 查看最终的值
            // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
            System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
            System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
        }
    }
    

    输出结果为:

    main     finally number value: 18766
    main     finally number2 value: 20000
    

    volatile指令重排案例1

    为了提高性能,编译器和处理器常常会对指令做重排,一般分以下3种:



    单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
    处理器在进行重排序时必须要考虑指令之间的数据依赖性
    多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

    重排案例

    public void mySort{
        int x = 11;//语句1
        int y = 12;//语句2
        × = × + 5;//语句3
        y = x * x;//语句4
    }
    

    可重排序列:

    • 1234
    • 2134
    • 1324
      问题:请问语句4可以重排后变成第一个条吗?答:不能,因为数据有依赖性。

    重排案例2
    int a,b,x,y = 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指令重排案例2

    禁止指令重排小总结
    volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

    先了解一个概念,内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令,它的作用有两个:

    1. 保证特定操作的执行顺序,
    2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

    由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

    对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。



    对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。


    关于volatile的底层原理:

    https://www.cnblogs.com/yaowen/p/11240540.html
    https://zhuanlan.zhihu.com/p/133851347

    线程安全性获得保证

    1. 工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

    2. 对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

    单例模式在多线程环境下可能存在安全问题

    单例模式volatile分析

    DCL(Double Check Lock双端检锁机制)

    public class SingletonDemo{
        private SingletonDemo(){}
        
        private volatile static SingletonDemo instance = null;
    
        public static SingletonDemo getInstance() {
            if(instance == null) {
                synchronized(SingletonDemo.class){
                    if(instance == null){
                        instance = new SingletonDemo();       
                    }
                }
            }
            return instance;
        }
    }
    

    DCL中volatile解析
    原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化instance = new SingletonDemo();可以分为以下3步完成(伪代码):

    memory = allocate(); //1.分配对象内存空间
    instance(memory); //2.初始化对象
    instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
    

    步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

    memory = allocate(); //1.分配对象内存空间
    instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
    instance(memory);//2.初始化对象
    

    但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
    所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

    CAS是什么

    Compare And Set
    示例程序

    public class CASDemo{
        public static void main(String[] args){
            AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
            System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
            System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());
        }
    }
    

    输出结果为

    true    2019
    false   2019
    

    CAS底层原理-上

    CAS底层原理?如果知道,谈谈你对UnSafe的理解
    atomiclnteger.getAndIncrement();源码

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;
        
        /**
         * Creates a new AtomicInteger with the given initial value.
         *
         * @param initialValue the initial value
         */
        public AtomicInteger(int initialValue) {
            value = initialValue;
        }
    
        /**
         * Creates a new AtomicInteger with initial value {@code 0}.
         */
        public AtomicInteger() {
        }
        
        ...
                
        /**
         * Atomically increments by one the current value.
         *
         * @return the previous value
         */
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
        
        ...
    }
    

    UnSafe
    Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
    注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

    1. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
        /**
         * Atomically increments by one the current value.
         *
         * @return the previous value
         */
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    
    1. 变量value用volatile修饰,保证了多线程之间的内存可见性。

    CAS是什么
    CAS的全称为Compare-And-Swap,它是一条CPU并发原语。

    它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

    CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

    CAS底层原理-下

        public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
            return var5;
        }
    

    UnSafe.getAndAddInt()源码解释:

    • var1 AtomicInteger对象本身。
    • var2 该对象值的引用地址。
    • var4 需要变动的数量。
    • var5是用var1,var2找出的主内存中真实的值。
    • 用该对象当前的值与var5比较:
      a. 如果相同,更新var5+var4并且返回true,
      b. 如果不同,继续取值然后再比较,直到更新完成。

    this.compareAndSwapInt(var1, var2, var5, var5 + var4)的意思就是在var1对象的当前引用地址var2下的值如果和var5一样,就更新为var5+var4,并且返回true,否则就无法更新,并且返回false。

    假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

    1. Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
    2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
    3. 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
    4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了(while中返回了true)。
    5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

    底层汇编
    Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
    UnsafeWrapper("Unsafe_CompareAndSwaplnt");
    oop p = JNlHandles::resolve(obj);
    jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
    return (jint)(Atomic::cmpxchg(x, addr, e))== e;
    UNSAFE_END
    //先想办法拿到变量value在内存中的地址。
    //通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
    

    小结
    CAS(CompareAndSwap)
    比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止

    CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
    当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    CAS缺点

    循环时间长开销很大

    // ursafe.getAndAddInt
    public final int getAndAddInt(Object var1, long var2, int var4){
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
        return var5;
    }
    

    我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

    只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
    引出来ABA问题

    ABA问题

    ABA问题怎么产生的
    CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化

    比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

    1. one取出了A
    2. two设置了将A设置成了B
    3. two设置了将B设置成了A
    4. one进行CAS操作,发现仍然是A

    尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

    AtomicReference原子引用

    import java.util.concurrent.atomic.AtomicReference;
    
    class User{
        
        String userName;
        
        int age;
        
        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return String.format("User [userName=%s, age=%s]", userName, age);
        }
        
    }
    
    public class AtomicReferenceDemo {
        public static void main(String[] args){
            User z3 = new User( "z3",22);
            User li4 = new User("li4" ,25);
            AtomicReference<User> atomicReference = new AtomicReference<>();
            atomicReference.set(z3);
            System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
            System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
        }
    }
    

    输出结果:

    true    User [userName=li4, age=25]
    false   User [userName=li4, age=25]
    

    AtomicStampedReference版本号原子引用

    原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。

    ABA问题的解决

    ABA问题代码演示:

    public class ABADemo {
    
        static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    
        public static void main(String[] args) {
    
            new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }, "t1").start();
    
            new Thread(() -> {
                // 暂停一秒钟,保证上面的t1完成一次ABA操作
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
            }, "t2").start();
        }
    }
    

    输出结果:

    true    2019
    

    解决方式:

    /**
     * ABA问题的解决
     */
    public class ABADemo2 {
    
        static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    
        public static void main(String[] args) {
    
            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t第1次版本号:" + stamp);
                // 暂停1秒, 保证和t4取得的第1次版本号是一样的
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedReference.compareAndSet(100,
                        101,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName()+"\t第2次版本号:" + atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(101,
                        100,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName()+"\t第3次版本号:" + atomicStampedReference.getStamp());
            }, "t3").start();
    
            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t第1次版本号:" + stamp);
                // 暂停3秒钟,保证上面的t1完成一次ABA操作, 并且保证和t3取得的第1次版本号是一样的
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
                System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:" + atomicStampedReference.getReference());
            }, "t4").start();
        }
    

    输出结果:

    t3  第1次版本号:1
    t4  第1次版本号:1
    t3  第2次版本号:2
    t3  第3次版本号:3
    t4  修改成功否:false 当前最新实际版本号:3
    t4  当前实际最新值:100
    

    集合类不安全之并发修改异常

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.UUID;
    import java.util.Vector;
    
    public class ArrayListNotSafeDemo {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            //List<String> list = new Vector<>();
            //List<String> list = Collections.synchronizedList(new ArrayList<>());
    
            for (int i = 0; i < 30; i++) {
                new Thread(() -> {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    上述程序会抛java.util.ConcurrentModificationException

    解决方法之一:Vector

    解决方法之二:Collections.synchronizedList()

    原因:
    System.out.println(list);会调用list的toString()方法。在toString()方法中,会调用iterator(),这个方法会new一个Iterator。这个Iterator的expectedModCount在对象初始化的时候是和modCount一样的。注意这里expectedModCount是属于Iterator对象的,modCount是属于list对象的。假如A线程执行完了这步,expectedModCount和modCount都是3,然后cpu切到了B线程,B线程调用了add()方法,list的modCount变成4。然后A线程继续往下执行,在it.next()方法里调用了checkForComodification()方法,在这个方法里会对modCount和expectedModCount进行比较,不同就抛出异常。

        public String toString() {
            Iterator<E> it = iterator();
            if (! it.hasNext())
                return "[]";
    
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (;;) {
                E e = it.next();
                sb.append(e == this ? "(this Collection)" : e);
                if (! it.hasNext())
                    return sb.append(']').toString();
                sb.append(',').append(' ');
            }
        }
    
    }
    

    集合类不安全之写时复制

    上一节程序导致抛java.util.ConcurrentModificationException的原因解析

    java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
        at java.util.ArrayList$Itr.next(ArrayList.java:859)
        at java.util.AbstractCollection.toString(AbstractCollection.java:461)
        at java.lang.String.valueOf(String.java:2994)
        at java.io.PrintStream.println(PrintStream.java:821)
        at com.lun.collection.ArrayListNotSafeDemo.lambda$0(ArrayListNotSafeDemo.java:20)
        at java.lang.Thread.run(Thread.java:748)
    

    假设线程A将通过迭代器next()获取下一元素时,从而将其打印出来。但之前,其他某线程添加新元素至list,结构发生了改变,modCount自增。当线程A运行到checkForComodification(),expectedModCount是modCount之前自增的值,判定modCount != expectedModCount为真,继而抛出ConcurrentModificationException。

    解决方法之三:CopyOnWriteArrayList(推荐)

    public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
        /** The array, accessed only via getArray/setArray. */
        private transient volatile Object[] array;
        
        final Object[] getArray() {
            return array;
        }
    
        final void setArray(Object[] a) {
            array = a;
        }
        
        ...
        
        public boolean add(E e) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
        
        ...
        
        public String toString() {
            return Arrays.toString(getArray());
        }
        
        ...
    }
    

    CopyOnWrite容器即写时复制的容器。

    • 先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements(长度是原容器长度+1)。
    • 然后往新的容器Object[] newELements里添加元素
    • 添加完元素之后,再将原容器的引用指向新的容器setArray(newElements)。

    这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    集合类不安全之Set

    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.UUID;
    import java.util.concurrent.CopyOnWriteArraySet;
    
    public class SetNotSafeDemo {
        
        public static void main(String[] args) {
            
            Set<String> set = new HashSet<>();
            //Set<String> set = Collections.synchronizedSet(new HashSet<>());
            //Set<String> set = new CopyOnWriteArraySet<String>();
            
            for (int i = 0; i < 30; i++) {
                new Thread(() -> {
                    set.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(set);
                }, String.valueOf(i)).start();
            }
        }   
    }
    

    解决方法:

    1. Collections.synchronizedSet(new HashSet<>())
    2. CopyOnWriteArraySet<>()(推荐)

    CopyOnWriteArraySet源码一览:

    public class CopyOnWriteArraySet<E> extends AbstractSet<E>
            implements java.io.Serializable {
        private static final long serialVersionUID = 5457747651344034263L;
    
        private final CopyOnWriteArrayList<E> al;
    
        /**
         * Creates an empty set.
         */
        public CopyOnWriteArraySet() {
            al = new CopyOnWriteArrayList<E>();
        }
    
        public CopyOnWriteArraySet(Collection<? extends E> c) {
            if (c.getClass() == CopyOnWriteArraySet.class) {
                @SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
                    (CopyOnWriteArraySet<E>)c;
                al = new CopyOnWriteArrayList<E>(cc.al);
            }
            else {
                al = new CopyOnWriteArrayList<E>();
                al.addAllAbsent(c);
            }
        }
     
        //可看出CopyOnWriteArraySet包装了一个CopyOnWriteArrayList
        
        ...
        
        public boolean add(E e) {
            return al.addIfAbsent(e);
        }
        
        public boolean addIfAbsent(E e) {
            Object[] snapshot = getArray();
            return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
                addIfAbsent(e, snapshot);
        }
        
        //暴力查找
        private static int indexOf(Object o, Object[] elements,
                                   int index, int fence) {
            if (o == null) {
                for (int i = index; i < fence; i++)
                    if (elements[i] == null)
                        return i;
            } else {
                for (int i = index; i < fence; i++)
                    if (o.equals(elements[i]))
                        return i;
            }
            return -1;
        }
    
        private boolean addIfAbsent(E e, Object[] snapshot) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] current = getArray();
                int len = current.length;
                if (snapshot != current) {//还要检查多一次元素存在性,生怕别的线程已经插入了
                    // Optimize for lost race to another addXXX operation
                    int common = Math.min(snapshot.length, len);
                    for (int i = 0; i < common; i++)
                        if (current[i] != snapshot[i] && eq(e, current[i]))
                            return false;
                    if (indexOf(e, current, common, len) >= 0)
                            return false;
                }
                Object[] newElements = Arrays.copyOf(current, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
        
        ...
            
    }
    

    hashSet底层原理:
    底层是一个hashmap。
    add的时候,添加进去的元素作为map的key, value是一个Object类型的常量。、

    集合类不安全之Map

    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Hashtable;
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class MapNotSafeDemo {
    
        public static void main(String[] args) {
            Map<String, String> map = new HashMap<>();
    //        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    //      Map<String, String> map = new ConcurrentHashMap<>();
    //      Map<String, String> map = new Hashtable<>();
            for (int i = 0; i < 30; i++) {
                new Thread(() -> {
                    map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(map);
                }, String.valueOf(i)).start();
            }
    
        }
    
    }
    

    解决方法:

    1. HashTable
    2. Collections.synchronizedMap(new HashMap<>())
    3. ConcurrentMap<>()(推荐)

    TransferValue醒脑小练习

    class Person {
        private Integer id;
        private String personName;
    
        public Person(String personName) {
            this.personName = personName;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getPersonName() {
            return personName;
        }
    
        public void setPersonName(String personName) {
            this.personName = personName;
        }
    }
    
    public class TransferValueDemo {
        public void changeValue1(int age) {
            age = 30;
        }
    
        public void changeValue2(Person person) {
            person.setPersonName("XXXX");
        }
        public void changeValue3(String str) {
            str = "XXX";
        }
    
        public static void main(String[] args) {
            TransferValueDemo test = new TransferValueDemo();
    
            // 定义基本数据类型
            int age = 20;
            test.changeValue1(age);
            System.out.println("age ----" + age);
    
            // 实例化person类
            Person person = new Person("abc");
            test.changeValue2(person);
            System.out.println("personName-----" + person.getPersonName());
    
            // String
            String str = "abc";
            test.changeValue3(str);
            System.out.println("string-----" + str);
    
        }
    }
    

    输出结果:

    age ----20
    personName-----XXXX
    string-----abc
    

    记住一个要点:
    局部变量的作用域在方法体内,也就是说程序走出了这个方法(这个方法的栈帧弹栈)以后,这个局部变量就相当于销毁了。记住这句话就行。

    java锁之公平和非公平锁

    是什么

    • 公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
    • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象

    两者区别

    • 公平锁
      • Threads acquire a fair lock in the order in which they requested it.
      • 公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
    • 非公平锁
      • a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
      • 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

    题外话
    Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
    非公平锁的优点在于吞吐量比公平锁大。
    对于Synchronized而言,也是一种非公平锁。

    可重入锁理论知识

    可重入锁(也叫做递归锁)

    指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

    也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

    ReentrantLock/synchronized就是一个典型的可重入锁。

    可重入锁最大的作用是避免死锁。

    java锁之可重入锁和递归锁代码验证

    Synchronized可入锁演示程序

    class Phone {
    
        public synchronized void sendSMS() throws Exception{
            System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
    
            // 在同步方法中,调用另外一个同步方法
            sendEmail();
        }
    
    
        public synchronized void sendEmail() throws Exception{
            System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
        }
    }
    
    public class SynchronizedReentrantLockDemo {
    
        public static void main(String[] args) {
            Phone phone = new Phone();
    
            // 两个线程操作资源列
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "t2").start();
        }
    
    }
    

    输出结果:

    t1   invoked sendSMS()
    t1   invoked sendEmail()
    t2   invoked sendSMS()
    t2   invoked sendEmail()
    

    ReentrantLock可重入锁演示程序

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class Phone2 implements Runnable{
    
        Lock lock = new ReentrantLock();
    
        /**
         * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
         */
        public void getLock() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t get Lock");
                setLock();
            } finally {
                lock.unlock();
            }
        }
    
        public void setLock() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t set Lock");
            } finally {
                lock.unlock();
            }
        }
    
        @Override
        public void run() {
            getLock();
        }
    }
    
    public class ReentrantLockDemo {
    
    
        public static void main(String[] args) {
            Phone2 phone = new Phone2();
    
            /**
             * 因为Phone实现了Runnable接口
             */
            Thread t3 = new Thread(phone, "t3");
            Thread t4 = new Thread(phone, "t4");
            t3.start();
            t4.start();
        }
    }
    

    输出结果

    t3   get Lock
    t3   set Lock
    t4   get Lock   
    t4   set Lock
    

    自旋锁理论知识

    自旋锁(Spin Lock)

    是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

    提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁
    -- 《深入理解JVM.2nd》Page 398

    Unsafe#getAndAddInt

        public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
            return var5;
        }
    

    关于自旋和阻塞的比喻:
    阳哥在这里打电话,张强上来问问题。
    阻塞: 张强就一直杵在阳哥边上,阳哥什么时候电话打完了,他就什么时候问问题
    自旋: 张强看见阳哥在打电话,先回座位,然后隔一会儿上来看阳哥打完电话没有,隔一会儿上来看阳哥打完电话没,打完了就问问题

    java锁之自旋锁代码验证

    问题:请手写一个自旋锁

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    public class SpinLockDemo {
        // 现在的泛型装的是Thread,原子引用线程
        AtomicReference<Thread>  atomicReference = new AtomicReference<>();
    
        public void myLock() {
            // 获取当前进来的线程
            Thread thread = Thread.currentThread();
            System.out.println(Thread.currentThread().getName() + "\t come in ");
    
            // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
            while(!atomicReference.compareAndSet(null, thread)) {
                //摸鱼
            }
        }
    
        public void myUnLock() {
            // 获取当前进来的线程
            Thread thread = Thread.currentThread();
    
            // 自己用完了后,把atomicReference变成null
            atomicReference.compareAndSet(thread, null);
    
            System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
        }
        
        public static void main(String[] args) {
            SpinLockDemo spinLockDemo = new SpinLockDemo();
    
            // 启动t1线程,开始操作
            new Thread(() -> {
    
                // 开始占有锁
                spinLockDemo.myLock();
    
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                // 开始释放锁
                spinLockDemo.myUnLock();
    
            }, "t1").start();
    
    
            // 让main线程暂停1秒,使得t1线程,先执行
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // 1秒后,启动t2线程,开始占用这个锁
            new Thread(() -> {
    
                // 开始占有锁
                spinLockDemo.myLock();
                // 开始释放锁
                spinLockDemo.myUnLock();
    
            }, "t2").start();
        }
    }
    

    输出结果:

    t1   come in 
    t2   come in // t2进来了,但是一直在while循环,等着t1结束
    t1   invoked myUnlock()
    t2   invoked myUnlock()
    

    自旋锁
    好处:不用阻塞
    坏处:长时间会消耗性能

    java锁之读写锁理论知识

    独占锁:指该锁一次只能被一个线程所持有。
    ReentrantLock和Synchronized而言都是独占锁

    共享锁:指该锁可被多个线程所持有。
    多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源可以同时进行。
    但如果有一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。

    ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
    读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

    java锁之读写锁代码验证

    实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况。
    注意这里需要保证写操作是原子+独占,整个过程必须是一个完整的统一体,中间不许被分隔,被打断。也就是说正在写入和写入完成之间不能夹杂其他操作。

    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    class MyCache {
    
        private volatile Map<String, Object> map = new HashMap<>();
    
        public void put(String key, Object value) {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        }
    
        public void get(String key) {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
        }
    }
    
    public class ReadWriteWithoutLockDemo {
    
        public static void main(String[] args) {
            MyCache myCache = new MyCache();
            // 线程操作资源类,5个线程写
            for (int i = 0; i < 5; i++) {
                final int tempInt = i;
                new Thread(() -> {
                    myCache.put(tempInt + "", tempInt +  "");
                }, String.valueOf(i)).start();
            }
            
            // 线程操作资源类, 5个线程读
            for (int i = 0; i < 5; i++) {
                final int tempInt = i;
                new Thread(() -> {
                    myCache.get(tempInt + "");
                }, String.valueOf(i)).start();
            }
    
        }
    
    }
    

    输出结果:

    0    正在写入:0
    1    正在写入:1
    3    正在写入:3
    2    正在写入:2
    4    正在写入:4
    0    正在读取:
    1    正在读取:
    2    正在读取:
    4    正在读取:
    3    正在读取:
    1    写入完成
    4    写入完成
    0    写入完成
    2    写入完成
    3    写入完成
    3    读取完成:3
    0    读取完成:0
    2    读取完成:2
    1    读取完成:null
    4    读取完成:null
    

    可以看到有些线程读取到null,并且写操作也不是原子性的。

    可用ReentrantReadWriteLock解决

    package com.lun.concurrency;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    class MyCache2 {
    
        private volatile Map<String, Object> map = new HashMap<>();
    
        private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
        public void put(String key, Object value) {
    
            // 创建一个写锁
            rwLock.writeLock().lock();
    
            try {
    
                System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
    
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                map.put(key, value);
    
                System.out.println(Thread.currentThread().getName() + "\t 写入完成");
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 写锁 释放
                rwLock.writeLock().unlock();
            }
        }
    
        public void get(String key) {
    
            // 读锁
            rwLock.readLock().lock();
            try {
    
                System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
    
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                Object value = map.get(key);
    
                System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 读锁释放
                rwLock.readLock().unlock();
            }
        }
    
        public void clean() {
            map.clear();
        }
    
    
    }
    
    public class ReadWriteWithLockDemo {
        public static void main(String[] args) {
    
            MyCache2 myCache = new MyCache2();
    
            // 线程操作资源类,5个线程写
            for (int i = 1; i <= 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.put(tempInt + "", tempInt +  "");
                }, String.valueOf(i)).start();
            }
    
            // 线程操作资源类, 5个线程读
            for (int i = 1; i <= 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.get(tempInt + "");
                }, String.valueOf(i)).start();
            }
        }
    }
    

    输出结果:

    1    正在写入:1
    1    写入完成
    2    正在写入:2
    2    写入完成
    3    正在写入:3
    3    写入完成
    5    正在写入:5
    5    写入完成
    4    正在写入:4
    4    写入完成
    2    正在读取:
    3    正在读取:
    1    正在读取:
    5    正在读取:
    4    正在读取:
    3    读取完成:3
    2    读取完成:2
    1    读取完成:1
    5    读取完成:5
    4    读取完成:4
    

    可以看到写入操作是原子性的,写的过程中不允许别的写和读操作。而读的过程可以看到是并发的,提高了性能。

    CountDownLatch

    让一线程阻塞直到另一些线程完成一系列操作才被唤醒。

    CountDownLatch主要有两个方法(await(),countDown())。

    当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行

    假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的。

    在使用CountDownLatch之前:

    public class CountDownLatchDemo {
    
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                }, String.valueOf(i)).start();
            }
    
    
            System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
        }
    }
    

    输出结果:

    0    上完自习,离开教室
    3    上完自习,离开教室
    2    上完自习,离开教室
    1    上完自习,离开教室
    4    上完自习,离开教室
    main     班长最后关门
    5    上完自习,离开教室
    6    上完自习,离开教室
    

    使用了CountDownLatch以后:

    public class CountDownLatchDemo2 {
    
        public static void main(String[] args) throws InterruptedException {
    
            CountDownLatch countDownLatch = new CountDownLatch(6);
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                    countDownLatch.countDown();
                }, String.valueOf(i)).start();
            }
    
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
        }
    }
    

    输出结果:

    1    上完自习,离开教室
    5    上完自习,离开教室
    4    上完自习,离开教室
    3    上完自习,离开教室
    2    上完自习,离开教室
    6    上完自习,离开教室
    main     班长最后关门
    

    CyclicBarrier

    CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法。

    CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。

    程序演示集齐7个龙珠,召唤神龙:

    public class CyclicBarrierDemo {
    
        public static void main(String[] args) {
            /**
             * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
             */
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
                System.out.println("召唤神龙");
            });
    
            for (int i = 1; i <= 7; i++) {
                final Integer tempInt = i;
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
    
                    try {
                        // 先到的被阻塞,等全部线程完成后,才能执行方法
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "线程执行结束");
                }, String.valueOf(i)).start();
            }
        }
    }
    

    输出结果:

    1    收集到 第1颗龙珠
    2    收集到 第2颗龙珠
    3    收集到 第3颗龙珠
    4    收集到 第4颗龙珠
    5    收集到 第5颗龙珠
    6    收集到 第6颗龙珠
    7    收集到 第7颗龙珠
    召唤神龙
    7线程执行结束
    1线程执行结束
    3线程执行结束
    6线程执行结束
    5线程执行结束
    4线程执行结束
    2线程执行结束
    

    来自《Java编程思想》的例子,展现CyclicBarrier的可循环性:

    class Horse implements Runnable {
        private static int counter = 0;
        private final int id = counter++;
        private int strides = 0;
        private static Random rand = new Random(47);
        private static CyclicBarrier barrier;
    
        public Horse(CyclicBarrier b) {
            barrier = b;
        }
    
        public synchronized int getStrides() {
            return strides;
        }
    
        @Override
        public void run() {
            try {
                //没有中断,就不断循环
                while (!Thread.interrupted()) {
                    synchronized (this) {
                        //模拟马单位时间的移动距离
                        // Produces 0, 1 or 2
                        strides += rand.nextInt(3);
                    }
                    //<---等待其他马到齐到循环屏障
                    barrier.await();
                }
            } catch (InterruptedException e) {
                // A legitimate way to exit
            } catch (BrokenBarrierException e) {
                // This one we want to know about
                throw new RuntimeException(e);
            }
        }
    
        @Override
        public String toString() {
            return "Horse " + id + " ";
        }
    
        public String tracks() {
            StringBuilder s = new StringBuilder();
            for (int i = 0; i < getStrides(); i++) {
                s.append("*");
            }
            s.append(id);
            return s.toString();
        }
    }
    
    public class HorseRace {
        static final int FINISH_LINE = 75;
        private List<Horse> horses = new ArrayList<>();
        private ExecutorService exec = Executors.newCachedThreadPool();
        private CyclicBarrier barrier;
    
        public HorseRace(int nHorses, final int pause) {
            // 初始化循环屏障
            // 循环多次执行的任务
            barrier = new CyclicBarrier(nHorses, () -> {
    
                // The fence on the racetrack
                StringBuilder s = new StringBuilder();
                for (int i = 0; i < FINISH_LINE; i++) {
                    s.append("=");
                }
                System.out.println(s);
    
                //打印马移动距离
                for (Horse horse : horses) {
                    System.out.println(horse.tracks());
                }
    
                //判断有没有马到终点了
                for (Horse horse : horses) {
                    if (horse.getStrides() >= FINISH_LINE) {
                        System.out.println(horse + "won!");
                        exec.shutdownNow();// 有只马跑赢了,所有任务都结束了
                        return;
                    }
                }
    
                try {
                    TimeUnit.MILLISECONDS.sleep(pause);
                } catch (InterruptedException e) {
                    System.out.println("barrier-action sleep interrupted");
                }
            });
            // 开跑!
            for (int i = 0; i < nHorses; i++) {
                Horse horse = new Horse(barrier);
                horses.add(horse);
                exec.execute(horse);
            }
        }
    
        public static void main(String[] args) {
            int nHorses = 7;
            int pause = 200;
            new HorseRace(nHorses, pause);
        }
    }
    

    输出结果:

    ...省略一些...
    ===========================================================================
    **********************************************************0
    ************************************************************1
    ******************************************************2
    ***********************************************************************3
    *************************************************************************4
    *****************************************************************5
    *****************************************************************6
    ===========================================================================
    **********************************************************0
    ************************************************************1
    *******************************************************2
    ***********************************************************************3
    **************************************************************************4
    *****************************************************************5
    *******************************************************************6
    ===========================================================================
    ***********************************************************0
    *************************************************************1
    *******************************************************2
    ***********************************************************************3
    ****************************************************************************4
    *******************************************************************5
    ********************************************************************6
    Horse 4 won!
    

    说一下大致流程,首先调用HorseRace的构造方法。在这个构造方法中有一个CyclicBarrier。首先调用CyclicBarrier下面的代码,把马丢进线程池中(马是实现了Runnable接口的),7匹马开跑。然后去看马的run方法。在一个while循环中,每匹马移动一定的距离,然后达到循环屏障。然后调用CyclicBarrier中runnable中的run方法。在barrier的run方法中,先打印分隔线,然后打印马的移动距离,后面拼上马的id。接着判断是否有马到达了终点,如果有程序结束。如果没有,线程睡200毫秒,run方法结束。run方法一结束,会继续执行马中barrier.await();后面的代码,也就是7匹马再次开跑一定的距离,然后再次到达循环屏障。以此类推。

    Semaphore

    信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

    正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源。

    模拟一个抢车位的场景,假设一共有6个车,3个停车位

    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    
    public class SemaphoreDemo {
        public static void main(String[] args) {
    
            /**
             * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
             */
            Semaphore semaphore = new Semaphore(3, false);
    
            // 模拟6部车
            for (int i = 0; i < 6; i++) {
                new Thread(() -> {
                    try {
                        // 代表一辆车,已经占用了该车位
                        semaphore.acquire(); // 抢占
    
                        System.out.println(Thread.currentThread().getName() + "\t 抢到车位");
    
                        // 每个车停3秒
                        try {
                            TimeUnit.SECONDS.sleep(3);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread().getName() + "\t 离开车位");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放停车位
                        semaphore.release();
                    }
                }, String.valueOf(i)).start();
            }
        }
    }
    

    输出结果:

    1    抢到车位
    2    抢到车位
    0    抢到车位
    0    离开车位
    2    离开车位
    1    离开车位
    5    抢到车位
    4    抢到车位
    3    抢到车位
    5    离开车位
    4    离开车位
    3    离开车位
    

    相关文章

      网友评论

          本文标题:juc和多线程并发相关面试题

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