百木园-与人分享,
就是让自己快乐。

Java面试之JCU部分

JCU

  • 1. JUC多线程以及高并发
    • 1.1. 一些概念
    • 1.2. 卖票案例
    • 1.3. 生产者-消费者问题
    • 1.4. 线程8锁
    • 1.5. list和map线程不安全问题
    • 1.6. Callable接口
    • 1.7. CountDownLatch
    • 1.8. CyclicBarrier
    • 1.9. BlockingQueue
    • 1.10. ReadWriteLock和Semaphore
    • 1.11. 线程池
      • 1. 线程池的工作原理
      • 2. 特点
      • 3. 优势
      • 4. 创建线程池
      • 5. 线程池的7大参数
      • 6. 线程池的工作原理
      • 7. 线程池如何设置参数

1. JUC多线程以及高并发

java.util.concurrent在并发编程中使用的工具类:并发包,并发原子包,并发lock包

请添加图片描述

1.1. 一些概念

进程:运行在后台的某个程序

线程:概念性的东西就不重复了,,,可参考OS书上的解释

并行:在某一个时刻只有一个线程在工作

并发:多个线程同一时间点抢争同一个资源

线程的6个状态:NEW,RUNNABLE就绪,WAITING等待,BLOCKED阻塞,TIMED_WAITING等待超时, TERMINATED

线程的sleepwait会导致线程进入到BLOCKED,但是sleep并不释放锁,进入锁池或者等待池,唤醒之后可以主动竞争锁;而wait会释放锁,需要唤醒

1.2. 卖票案例

在高内聚低耦合的前提下,线程操纵(通过资源类对外暴露的方法供线程使用)资源类

package com.hz.juc;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 定义资源类
class Ticket {

    private int number = 100;
    private Lock lock = new ReentrantLock(); // 可重入的互斥锁

    public void sale(){
        lock.lock();
        try{
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + \" 卖出第:\" + (number--) + \"还剩下:\" + number);
            }else{
                System.out.println(\"票已抢完...\");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

/**
 * 在高内聚低耦合的前提下,线程操纵(对外暴露调用方法供其他线程使用)调用资源类
 *
 */
public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // Thread(Runnable target, String name)
        // 匿名内部类
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 110; i++) {
//                   ticket.sale();
//                }
//            }
//        }, \"t1\").start();
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 110; i++) {
//                    ticket.sale();
//                }
//            }
//        }, \"t2\").start();
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 110; i++) {
//                    ticket.sale();
//                }
//            }
//        }, \"t3\").start();
        // lambda表达式
        new Thread(() -> { for (int i = 0; i < 110; i++) { ticket.sale(); }}, \"t1\").start();
        new Thread(() -> { for (int i = 0; i < 110; i++) { ticket.sale(); }}, \"t2\").start();
        new Thread(() -> { for (int i = 0; i < 110; i++) { ticket.sale(); }}, \"t3\").start();
    }
}

1.3. 生产者-消费者问题

两个线程,可以操作初始值为0的一个变量,实现一个线程对该变量加1,一个线程对该变量-1。实现交替,重复10轮;
判断资源是否还有 -> 操作资源的方法 -> 通知线程

class Mutex{
    private int number = 0;

    public synchronized void product() throws InterruptedException {
        // 判断当前互斥变量是否为0 第一次初始值为0 则跳过if 如果不为0 则当前线程进行等待
        while(number != 0) {
            this.wait();
        }
        // 使用资源
        number++;
        System.out.println(Thread.currentThread().getName() + \" 生产资源:\" + number);
        // 通知 由于使用wait方法等待会释放锁 所以使用资源结束需要唤醒进程(判断条件不能用if 只能用while)
        this.notifyAll();
    }

    public synchronized void consumer() throws InterruptedException {
        while(number == 0){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + \" 消费资源:\" + number);
        this.notifyAll();
    }
}

public class ProducerAndCustomer {

    public static void main(String[] args) {
        Mutex mutex = new Mutex();
        new Thread(() -> {
            for (int i = 1; i <=10; i++) {
                try{
                    mutex.product();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }, \"t1\").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try{
                    mutex.consumer();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }, \"t2\").start();
    }
}

控制台打印输出:
请添加图片描述
可以正确的输出1,0,1,0…证明使用了synchronized+wait+notify(/notifyAll)可以解决线程并发问题。

在JDK1.8版本之后,JCU中出现了新的方法来解决JCU中的lock替代之前的synchronized,Condition中的方法await和signal替换之前的wait和notify(或notifyAll)

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Mutex2{
    private int number = 0;
    final Lock lock = new ReentrantLock();
    final Condition condition = lock.newCondition();

    public void producer(){
        lock.lock();
        try{
            while(number != 0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + \" 生产资源:\" + number);
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void consumer(){
        lock.lock();
        try{
            while(number == 0){
                condition.await();
            }
            number --;
            System.out.println(Thread.currentThread().getName() + \" 消费资源:\" + number);
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

public class ProducerAndCustomer_2 {

    public static void main(String[] args) {
        Mutex2 mutex = new Mutex2();

        new Thread(() -> {
            for (int i = 1; i <=10; i++) {
                try{
                    mutex.producer();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }, \"t1\").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try{
                    mutex.consumer();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }, \"t2\").start();
    }
}

但是新方法的特性可以解决以前多线程交互中的虚假唤醒问题,如果想要实现:
多线程之间按顺序调用实现 A -> B -> C
lock配合Condition使用:精确通知 精准唤醒

class ShareResources{
    private int mutex = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5(){
        lock.lock();
        try{
            while(mutex != 1){
               condition1.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + \" 第\" + i + \"次\");
            }
            // 通知 精确唤醒2 修改标志位
            mutex = 2;
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try{
            while(mutex != 2){
                condition2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + \" 第\" + i + \"次\");
            }
            // 通知 精确唤醒1 修改标志位
            mutex = 3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try{
            while(mutex != 3){
                condition3.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + \" 第\" + i + \"次\");
            }
            // 通知 精确唤醒 修改标志位
            mutex = 1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
public class ProducerAndCustomer_3 {

    public static void main(String[] args) {

        ShareResources resources = new ShareResources();

        new Thread( () -> {
            for (int i = 0; i < 10; i++) {
                resources.print5();
            }
        }, \"t1\").start();
        new Thread( () -> {
            for (int i = 0; i < 10; i++) {
                resources.print10();
            }
        }, \"t2\").start();
        new Thread( () -> {
            for (int i = 0; i < 10; i++) {
                resources.print15();
            }
        }, \"t3\").start();
    }
}

这里对于三个方法设置了3个不同的condition相当于三把钥匙,对于打印方法1,判断条件为当前互斥变量mutex是否等于1,不等于1则当前线程进入到while条件里,进项等待,反之进行打印输出,并且通知下一个需要工作的线程(也就是修改标志位),并且唤醒下一个线程(做到了精确唤醒);2和3的流程类似,形成按照顺序A->B->C->A的执行链。

1.4. 线程8锁

class Phone{

    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(4); // 暂停4秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(\"发送邮件...\");
    }

    public static synchronized void sendWechat(){
        System.out.println(\"发微信...\");
    }

    public void watchMovie(){
        System.out.println(\"看电影...\");
    }

}

public class Lock8 {

    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
//        Phone phone2 = new Phone();

        new Thread(() -> {
            try{
                phone1.sendEmail();
            }catch (Exception e){
                e.printStackTrace();
            }
        }, \"t1\").start();
        Thread.sleep(100);

        new Thread(() -> {
            try{
              phone1.sendWechat();
//              phone1.watchMovie();
//                phone2.sendWechat();
            }catch (Exception e){
                e.printStackTrace();
            }
        }, \"t2\").start();

    }
}
  1. 标准访问,先打印邮件再执行发微信

  2. 邮件先暂停4秒,先打印邮件再打印微信

以上两种锁较为简单,一个类里面如果有多个synchronized普通方法,某一个时刻内,只能有一个线程去调用其中的一个synchronized方法,其他的线程只能等待;锁的是当前访问对象this(调用者),被锁定后,其他实例对象的线程都不能进入到当前对象的其他synchronized方法

  1. 新增一个普通方法,邮件先暂停4秒,先打印看电影再打印发邮件:由于新增加的方法并没有加上synchronized上锁,并不需要和发邮件方法去抢占资源,所以可以直接调用watchMovie方法

  2. 两个资源类,两个同步普通方法,邮件暂停4秒,先执行发微信再执行发邮件: sendEmail方法锁的是当前对象phone1,而phone2是另一个对象,不是同一把锁,各用各的,所以phone2的方法先执行

  3. 两个静态同步方法,同一个资源,邮件暂停4秒,先打印邮件再打印微信

  4. 两个静态同步方法,两个资源,邮件暂停4秒,先打印邮件再打印微信

由于静态同步方法锁的是所在的整个类模板(类锁),加锁的对象从实例对象变成类,无论有多少资源,同一个时刻只能有一个线程去调用方法,与情况4相反

  1. 1个普通同步方法1个静态同步方法,1个资源,邮件暂停4秒,先执行发微信再执行发邮件
  2. 1个普通同步方法1个静态同步方法,2个资源,邮件暂停4秒,先执行发微信再执行发邮件

普通同步方法锁的是当前new出来的实例对象(this),而静态同步方法锁的是这个类模板(.class),一个是对象锁一个是类锁,互不影响,所以微信方法执行;对于两个资源,一个实例对象也是一个新的对象锁,与类锁也不会竞争

1.5. list和map线程不安全问题

举例说明list是线程不安全的:线程数增加时,出现java.util.ConcurrentModificationException也就是常说的并发修改异常

public class ListAndMap {

    public static void main(String[] args) {
        // 当线程数增多时会出现异常:java.util.ConcurrentModificationException并发修改异常
        // 导致原因:add方法并没有加线程安全
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

出现的原因是:对于ArrayList的源码中的add方法并没有加锁导致,同一时刻可以有多个线程进行调用
请添加图片描述
解决方案:

  • 使用List的前身Vector,由于Vector的add方法是加上synchronized关键字的,即加锁,可防止多个线程同时调用;但是同一时刻只能有一个对象调用,因此使用vector的访问效率较低
  • 使用Collections工具类中的synchronizedList方法将ArrayList加锁;
  • 使用CopyOnWriteArrayList写时复制容器,底层所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。通过源码分析:
    请添加图片描述
    在容器中添加元素时,不直接在当前容器中Object[]添加,而是先将当前容器Object[]进行copy(使用Arrays工具类中的copyOf方法),复制出一个新的容器Object[] newElements;然后在的容器Object[] newElements中添加元素,添加元素之后,将原容器的引用指向新的容器setArray(newElements)。可以对CopyOrWrite容器进行并发的读,不需要加锁,因为当前容器并不会添加新的元素,所以CopyOrWrite容器也是一种读写分离的思想

测试打印输出:程序可以正常运行
请添加图片描述


关于HashSet也是同理解决线程不安全的问题,HashSet底层就是HashMapadd方法就是调用HashMap中的put方法,而对于value是一个固定的常量PRESENT


同理,使用使用Collections工具类中的synchronizedSet方法将HashSet加锁;或者CopyOnWriteArraySet

Collections.synchronizedMap()或者ConcurrentHashMap来解决map的线程不安全问题。

HashMap的底层是数组+链表+红黑树,默认初始值16,负载因子0.75(可修改),当size达到12时,map会进行扩容(翻倍),插入数据时,比较hash地址,如果地址相同,并且插入的key与之前在该位置的key相同,则替换之前的value,如果不相同,该hash地址则进行链表存储(链地址法);当size>8时单向链表变成红黑树存储

1.6. Callable接口

多线程中,获得多线程的方式:

  1. 继承Thread类,重写run
  2. 方法实现Runnable接口,重写run方法

创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

callable和Runnable接口的区别:

  • callable接口需要重写的方法名为call,后者为run;
  • callable接口需要重写的方法有返回值,后者没有;
  • callable接口需要重写的方法会抛异常,后者没有
  1. 通过Callable接口和FutureTask:创建Callable接口的实现类 ,并实现Call方法。创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值,使用FutureTask对象作为Thread对象的target创建并启动线程,调用FutureTask对象的get()来获取子线程执行结束的返回值。通过Callable接口来创建线程的细节:
    • 实现callable接口的线程可以防止线程阻塞,但是注意该线程中task的get方法需要在放在最后,防止主线程被阻塞
    • 在创建线程时,如果传入的futureTask对象是同一个,只会执行一次线程中的方法

由于Thread的构造方法中没有以Callable接口为参数的,而Callable接口中存在一个子接口RunnableFuture,该子接口的实现类有一个FutureTask,并且在该类的构造方法中存在FutureTask(Callable callable)。因此可以使用该类去包装Callable接口 才能作为Thread的target来创建线程:

Thread(Runnable target, String name) ->
Thread(RunnableFuture target, String name) ->
Thread(FutureTask(Callable) target, String name)

  1. 线程池(Thread Pool)
class MyThread1 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println(\"call....\");
        return 999;
    }
}
class MyThread2 implements Runnable{

    @Override
    public void run() {

    }
}

public class CallableDemo {

    public static void main(String[] args) throws Exception {
        MyThread1 thread1 = new MyThread1();
        FutureTask task = new FutureTask(thread1);
        
        new Thread(task, \"t1\").start();
        new Thread(task, \"t2\").start();
        System.out.println(Thread.currentThread().getName() + \"====完成====\");
        System.out.println(\"返回值:\" + task.get()); // 获取callable中call方法的返回值

    }
}

1.7. CountDownLatch

1.8. CyclicBarrier

1.9. BlockingQueue

1.10. ReadWriteLock和Semaphore

1.11. 线程池

1. 线程池的工作原理

控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务;
如果线程数量超过最大数量,超出数量的线程排队等候,等待其他线程处理完毕,再从队列中取出任务来执行特点

2. 特点

线程复用,控制最大并发数,管理线程

3. 优势

  1. 降低资源消耗 2. 提高响应速度 3. 提高线程的可管理性

4. 创建线程池

线程池通过Executor框架实现的,用到Executor,Executors(线程池工具类,类似Arrays和Collections),
ExecutorServiceThreadPoolExecutor

使用线程池工具类来创建线程池的3大方法:

  • Executors.newFixedThreadPool(int N)
    执行长期任务性能较好,创建一个线程池有N个固定数目的线程
  • Executors.newSingleThreadExecutor()
    创建一个线程数目的ThreadPool
  • Executors.newCachedThreadPool()
    执行很多短期异步任务,线程池根据需要创建新线程(可扩容),但先前构建的线程可用时将其重用

5. 线程池的7大参数

三个方法的源码实现都是new一个<ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);

而ThreadPoolExecutor构造函数中除了上述5个参数外,还包括threadFactory, defaultHandler

  • corePoolSize:线程池中的常驻核心线程数
  • maximumPoolSize:能够同时执行的最大线程数 >=1
  • keepAliveTime:多余的空闲线程的存活时间,当池中线程数量超过corePoolSize时,空闲时间得到keepAliveTime;多余线程会被销毁,直到剩下corePoolSize个线程为止
  • unit:keepAliveTime的时间单位
  • workQueue:任务队列,被提交但还没执行的任务
  • threadFactory:创建线程的线程工厂(一般默认)
  • defaultHandler:拒绝策略,当队列满时,并且工作线程>=线程池的最大线程数,如何拒绝请求执行的runnable策略

6. 线程池的工作原理

使用者提交任务 -> 线程池首先判断corePoolSize是否已满 -> 否 -> 创建线程执行任务;

反之查询阻塞队列workQueue是否已满 -> 是 -> 线程池进行扩容能够满足需求 -> 否 -> 按照拒绝策略处理无法执行的任务;

阻塞队列没有满,则将任务添加到队列中进行等待;

线程池扩容能够满足需要,则创建线程执行任务

7. 线程池如何设置参数

线程池的创建方法?

线程池不允许使用Executors去创建,而是使用ThreadPoolExecutor来创建,由于Executors返回线程池对象的缺点有:

  • FixedThreadPool和SingleThreadExecutor,允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM;
  • CachedThreadPool和ScheduledThreadPool:允许创建的线程数数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

如何使用线程池?

import java.util.concurrent.*;

public class ThreadPool_2 {

    public static void main(String[] args) {

        ExecutorService pool = new ThreadPoolExecutor(
                2,
                5,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            for (int i = 1; i <= 9; i++) {
                pool.execute( () -> {
                    System.out.println(Thread.currentThread().getName() + \"====执行====\");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }

    public static void initPool(){
        
    }
}

这里再创建线程池使用的是默认拒绝策略AbortPolicy:当任务数 > maximumPoolSIze + 阻塞Queue的size时直接会抛出异常

请添加图片描述
CallerRunsPolicy:调用者运行一种机制,既不会抛出任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量

请添加图片描述
DiscardPolicy:默认丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许丢失任务,是最好的一种策略
DiscardOldestPolicy:抛弃任务队列中等待最久的任务,然后将当前任务加入队列中,再次提交当前任务
请添加图片描述
如果是CPU密集型,设置maximumPoolSize为当前主机上的核数+1


来源:https://blog.csdn.net/m0_45971439/article/details/123255838
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » Java面试之JCU部分

相关推荐

  • 暂无文章