【2020-面试实战】-并发多线程

0 syncronized 和 Lock的异同

  1. 原始构成不同

    syncronized是关键字,属于JVM层面,底层是通过monitor对象来完成, 其实wait/notify等方法也依赖monitor对象,只有在同步块或方法中才能调用 wait/notify 等方法;
    Lock是具体类(java.util.concurrent.locks.Lock)是API层面的锁

  2. 使用方法不同

    syncronized不需要用户去手动释放锁,当syncronized代码执行完成后系统会自动让线程释放对锁的占用(就算出现异常,也会执行锁的释放)
    Lock则需要用户去手动管理锁; 就有可能会出现死锁现象; 需要lock()和unlock() 方法配合,try/finally 语句块来完成

  3. 等待是否可中断

    syncronized不可中断,除非抛出异常或者正常运行完成;
    Lock可中断:
    (1) 设置超时方法,tryLock(Long timeut, TimeUnit unit)
    (2) lockinterruptibly()放代码块中,调用interrupt()方法可中断

  4. 加锁是否公平

    syncronized非公平锁
    Lock两者都可以,默认是公平锁,构造方法可传入boolean值,true为公平锁, false为非公平锁

  5. 锁绑定多个条件 Condition

    syncronized没有
    Lock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像syncronized要么随机唤醒一个,要么全部唤醒

1 谈谈volatile

volatile是Java虚拟机提供的轻量级的同步机制
它具有

  • 可见性
  • 有序性

volatile不能保证原子性

1.1 什么是可见性

首先我们需要知道
JMM(Java内存模型)规定:

  • 所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问
  • 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存

volatile能够保证可见性,是因为它在本地线程修改变量副本之后,会立刻将修改后的值刷新回主内存

1.2 什么是原子性

原子性指, 不可分割,完整性,即某个线程在做某个业务时,中间不可被加塞或者被分割.需要整体完整,要么同时成功,要么同时失败;

volatile不能保证原子性
举例:

我们都知道 i++ 这个操作是非原子操作, 就算变量 i 使用了volatile修饰,也是不能保证的

因为 i++ 运算在 JVM 中其实是被拆分成了三个指令, 分别是:

  • 执行 getfield拿到原始 i;
  • 执行 iadd 进行加 1 操作;
  • 执行 putfield写,把累加后的值写回;

说白了, volatile不是锁,无法保证操作的原子性

至于如果保证原子性,就是 加锁

1.3 什么是有序性

有序性就是禁止指令重排序, 通过 内存屏障(Memory Barrier) 实现的.
内存屏障是一个CPU指令,其作用有:

  • 保证特定操作的执行顺序;
  • 保证某些变量的内存可见性;

2 CAS工作原理

CAS:Compare And Swap, 它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值, 如果是则更改为新值,这个过程是原子的.

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

3 JUC锁

JUC锁

4 线程池 ThreadPoolExecutor? 项目中使用哪个线程池? 七大参数了解吗?

线程池做的工作主要是控制运行的线程的数量, 处理过程中将任务放入队列,然后在线程创建后启动这些任务, 如果线程数量超过了最大数量,超出数量的线程排队等候, 等其它线程执行完毕, 再从队列中取出任务来执行;

线程池的主要特点为: 线程复用, 控制最大并发数, 管理线程;

线程池的主要优势是:

  1. 降低资源消耗.

    通过重复利用已创建的线程降低线程创建和销毁造成的消耗;

  2. 提高响应速度.

    当任务到达时, 任务可以不需要等线程创建就能立即执行;

  3. 提高线程的可管理性;

    线程是稀缺资源, 如果无限制的创建, 不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一的分配, 调优和监控;

线程池的方式创建多线程,代码实现(3种方式)

	/**
		创建的为一池固定数量线程
		int 为线程个数
		用于执行长期的任务,性能好很多
	*/	
	Executors.newFixedThreadPool(int);
	// 创建一池一线程
	// 一个任务一个任务执行的场景
    Executors.newSingleThreadExecutor();
    // 创建一池多线程
    // 适用于执行很多短期异步的小程序或者负载较轻的服务器
    Executors.newCachedThreadPool();

工作中一般是使用自定义线程
因为 阿里开发手册中 建议我们:
线程资源必须通过线程池提供,不允许在应用中自行显示创建线程;

使用线程池的好处是减少在创建和销毁线程上锁消耗的时间以及系统资源的开销, 解决资源不足的问题. 如果不使用线程池, 有可能造成系统创建大量同类线程而导致消耗完内存或者"过度切换"的问题;

线程池不允许使用 Executors去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则, 规避资源耗尽的风险;
Executors返回的线程池对象的弊端如下:

1 FixedThreadPoolSingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE, 可能会堆积大量的请求, 从而导致 OOM

2 CachedThreadPoolScheduledThreadPool:
允许的创建线程数量为 INteger.MAX_VALUE, 可能会创建大量的线程, 从而导致 OOM

7大参数
1 corePoolSize
线程池中的常驻核心线程数

1 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务, 近似理解为今日当值线程
2 当线程池中的线程数目达到corePoolSize后, 就会把到达的任务放到缓存队列当中

2 maximumPoolSize
线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3 keepAliveTime
多与的空闲线程的存活时间, 当前线程池数量超过 corePoolSize时, 当空闲时间达到keepAliveTime值时,多与空闲线程会被销毁直到只剩下corePoolSize个线程为止

4 unit
keepAliveTime的单位,分,秒,毫秒

5 workQueue
任务队列, 被提交但尚未被执行的任务

6 threadFactory
表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可

7 handler
拒绝策略, 表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝

4.1 线程池的拒绝策略

1 是什么?

等待队列也已经排满了, 再也塞不下新任务了, 同时,线程池中的max线程也达到了, 无法继续为新任务服务. 这时候我们就需要拒绝策略机制合理的处理这个问题

2 JDK内置的拒绝策略有哪些?
AbortPolicy(默认)

直接抛出 RejectedExecutionException异常, 阻止系统正常运行

CallerRunsPolicy

"调用者运行"一种调节机制, 该策略既不会抛弃任务,也不会抛出异常, 而是将某些任务回退到调用者,从而降低新的任务流量;

DiscardOldestPolicy

抛弃队列中等待最久的任务, 然后把当前任务加入队列中尝试再次提交

DiscardPolicy

直接丢弃任务, 不予任何处理也不抛出异常. 如果允许任务丢失, 这是最好的一种方案.

以上内置拒绝策略均实现了RejectedExecutionHandler接口

5 原子类AtomicInteger的ABA问题谈谈, 原子更新引用知道吗?

AtomicIntegerInteger的原子类,在多线程环境中,我们通常使用它来替换++操作;
其底层原理是: Unsafe+自旋锁

Unsafe类

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

自旋锁
在这里插入图片描述
CAS的缺点

  1. 循环时间长,给CPU 带来很大的开销

  2. 只能保证一个共享变量的原子操作

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

  3. 引来ABA问题
    ABA问题
    定义

CAS 会导致"ABA问题."
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换. 那么在这个时间差类会导致数据的变化.
比如说一个线程1 从内存位置 V 处取出A, 这时候另一个线程2 也从内存中取出A, 并且线程2 进行了一些操作将值变成了B, 然后线程2 又将V位置的数据变成A, 这时候线程1 进行 CAS操作发现内存中任然是A, 然后线程1 操作成功.
尽管线程1 的CAS操作成功了, 但是不代表这个过程就是没有问题的. 当然如果有些业务只关心头尾,即对ABA问题不关心,那没所谓

如何解决
使用带时间戳的原子引用AtomicStampedReference
看一个小Demo,了解下ABA操作具体是如何通过AtomicStampedReference解决的

/**
 * @author:xukai
 * @date:2020/3/17,16:58
   假设两个线程,线程t1模拟ABA问题,
   在线程t1完成ABA操作之后,线程t2通过和线程t1几乎同时拿到的
   最初的版本号stmap去做自己的CAS操作
   因为ABA问题,如果线程t2单单只是compare 要修改的值的话,是没有问题的
   但是t1线程已经对值进行过了修改,而此时假设我们的业务要求我们
   不能够出现ABA问题,这个时候我们使用的是带有时间戳(版本号)的原子引用类
   AtomicStampedReference,
   两个线程几乎同时获取到最初的版本号都是1
   此时我们用线程t1模拟的ABA操作,致使时间戳从1变成了3
   而线程t2在比对的时候,由于线程t1做了ABA操作,原子引用类的版本也发生了改变
   但是线程t2并不知道,在做CAS操作的时候使用的时间戳还是最初和t1几乎同时获得的
   初始时间戳1, 此时由于时间戳与预期的不同,CAS操作失败👇以下示例代码
 */
public class CASDemo {
    //参数 initialRef 初始值, initialStamp 初始版本号
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        //线程t1完成一次ABA问题
        new Thread(() -> {
                int t1Stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t 线程t1获取到的最初始版本号:"+t1Stamp);
                stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
                stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
        },"t1").start();

        new Thread(() -> {
                int t2Stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 线程t2获取到的最初始版本号:"+t2Stamp);
            try {
                //线程t2休眠3秒 保证线程t1能够完成ABA的操作
                TimeUnit.SECONDS.sleep(3);
                //取出stampedReference的reference看看是不是100,如果是说明ABA成功
                System.out.println(Thread.currentThread().getName()+"\t 当前对象的值:"+ stampedReference.getReference());
                //然后再次进行CAS操作看看能否成功
                boolean result = stampedReference.compareAndSet(100,2019,
                        t2Stamp,t2Stamp+1);
                System.out.println(Thread.currentThread().getName()+"\t 线程t2CAS结局:"+result);
                System.out.println(Thread.currentThread().getName()+"\t 线程t2在线程1执行完ABA操作之后的真正版本号"+stampedReference.getStamp());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t2").start();
    }
}

6 我们知道 ArrayList 是线程不安全的, 请编写一个不安全的案例并给出解决方案

7 CountDownLatch/CyclicBarrier/Semphore使用过吗?

JUC常用三大类

8 阻塞队列知道吗?

阻塞队列

9 线程池处理任务的流程是怎样的?

  1. 在创建了线程池后, 等待提交过来的任务请求;
  2. 当调用 execute() 方法添加一个请求任务时, 线程池会做如下判断:
    2.1 如果正在运行的线程数量小于corePoolSize, 那么马上创建线程运行这个任务;
    2.2 如果正在运行的线程数量大于或等于 corePoolSize, 那么将这个任务放入队列;
    2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize, 那么还是要创建非核心线程立刻运行这个任务;
    2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize, 那么线程池会启动和饱和拒绝策略来执行;
  3. 当一个线程完成任务时, 它会从队列中取 下一个任务来执行;
  4. 当一个线程无事可做超过一定的时间(keepAliveTime) 时, 线程池会判断:
    如果当前运行的线程数大于corePoolSize, 那么这个线程就被停掉;
    所以线程池的所有任务完成后它最终会收缩到 corePoolSize的大小;

10 死锁编码及定位分析

死锁

11 如何实现一个生产者与消费者模式?

可实现的方式有, 锁, 信号量, 线程通信, 阻塞队列等
实现生产者消费者的四种方式
syncronized

class Stock {
    private String name;
    // 标记库存是否有内容
    private boolean hasComputer = false;

    public synchronized void putOne(String name) {
        // 若库存中已有内容,则生产线程阻塞等待
        while (hasComputer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        System.out.println("生产者...生产了 " + name);
        // 更新标记
        this.hasComputer = true;
        // 这里用notify的话,假设p0执行完毕,此时c0,c1都在wait, 同时唤醒另一个provider:p1,
        // p1判断标记后休眠,造成所有线程都wait的局面,即死锁;
        // 因此使用notifyAll解决死锁问题
        this.notifyAll();
    }

    public synchronized void takeOne() {
        // 若库存中没有内容,则消费线程阻塞等待生产完毕后继续
        while (!hasComputer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者...消费了 " + name);
        this.hasComputer = false;
        this.notifyAll();
    }
}

public static void main(String[] args) {
    // 用于通信的库存类
    Stock computer = new Stock();
    // 定义两个生产者和两个消费者
    Thread p1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.putOne("Dell");
            }
        }
    });
    Thread p2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.putOne("Mac");
            }
        }
    });
    
    Thread c1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.takeOne();
            }
        }
    });
    Thread c2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.takeOne();
            }
        }
    });
    p1.start();
    p2.start();
    c1.start();
    c2.start();
}

阻塞队列

class MyResource{
    // 默认开启, 进行生产+消费
    private volatile boolean FLAG = true;
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = null;

    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    public void myProd() throws Exception{
        String data = null;
        boolean retValue;
        while (FLAG){
            data = atomicInteger.incrementAndGet()+"";
            retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
            if (retValue){
                System.out.println(Thread.currentThread().getName()+"生产\t"+data+"成功");
            }else {
                System.out.println(Thread.currentThread().getName()+"生产\t"+data+"失败");
            }
            //睡一秒 是因为生产速度太快了, 控制台数据不太好看
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName()+"叫停该活动,FLAG 为 flase 不再生产");
    }

    public void myConsumer() throws Exception{
        String result = null;
        while (FLAG){
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if (null == result || result.equalsIgnoreCase("")){
                FLAG = false;
                System.out.println(Thread.currentThread().getName()+"\t 超过2秒没有消费到, 退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName()+"\t 消费 "+result+"成功");
        }
    }

    public void stop(){
        FLAG = false;
    }

}

public class ProdConsumer_BlockQUeueDemo {
    public static void main(String[] args) throws InterruptedException {
        MyResource mr = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t 生产线程启动");
            try {
                mr.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"prod").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
            try {
                mr.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"comsumer").start();

        //生产消费10秒,之后主线程叫停该活动
        TimeUnit.SECONDS.sleep(5);

        mr.stop();
        System.out.println("5秒时间到,main线程叫停,活动结束");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值