Java面试之JCU部分
2022/3/3 22:18:41
本文主要是介绍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
线程的sleep和wait会导致线程进入到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(); } }
-
标准访问,先打印邮件再执行发微信
-
邮件先暂停4秒,先打印邮件再打印微信
以上两种锁较为简单,一个类里面如果有多个synchronized普通方法,某一个时刻内,只能有一个线程去调用其中的一个synchronized方法,其他的线程只能等待;锁的是当前访问对象this(调用者),被锁定后,其他实例对象的线程都不能进入到当前对象的其他synchronized方法
-
新增一个普通方法,邮件先暂停4秒,先打印看电影再打印发邮件:由于新增加的方法并没有加上synchronized上锁,并不需要和发邮件方法去抢占资源,所以可以直接调用watchMovie方法
-
两个资源类,两个同步普通方法,邮件暂停4秒,先执行发微信再执行发邮件: sendEmail方法锁的是当前对象phone1,而phone2是另一个对象,不是同一把锁,各用各的,所以phone2的方法先执行
-
两个静态同步方法,同一个资源,邮件暂停4秒,先打印邮件再打印微信
-
两个静态同步方法,两个资源,邮件暂停4秒,先打印邮件再打印微信
由于静态同步方法锁的是所在的整个类模板(类锁),加锁的对象从实例对象变成类,无论有多少资源,同一个时刻只能有一个线程去调用方法,与情况4相反
- 1个普通同步方法1个静态同步方法,1个资源,邮件暂停4秒,先执行发微信再执行发邮件
- 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底层就是HashMap,add方法就是调用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接口
多线程中,获得多线程的方式:
- 继承Thread类,重写run
- 方法实现Runnable接口,重写run方法:
创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
callable和Runnable接口的区别:
- callable接口需要重写的方法名为call,后者为run;
- callable接口需要重写的方法有返回值,后者没有;
- callable接口需要重写的方法会抛异常,后者没有
- 通过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)
- 线程池(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. 优势
- 降低资源消耗 2. 提高响应速度 3. 提高线程的可管理性
4. 创建线程池
线程池通过Executor框架实现的,用到Executor,Executors(线程池工具类,类似Arrays和Collections),
ExecutorService和ThreadPoolExecutor
使用线程池工具类来创建线程池的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
这篇关于Java面试之JCU部分的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27本地多文件上传的简单教程
- 2024-11-27低代码开发:初学者的简单教程
- 2024-11-27如何轻松掌握拖动排序功能
- 2024-11-27JWT入门教程:从零开始理解与实现
- 2024-11-27安能物流 All in TiDB 背后的故事与成果
- 2024-11-27低代码开发入门教程:轻松上手指南
- 2024-11-27如何轻松入门低代码应用开发
- 2024-11-27ESLint开发入门教程:从零开始使用ESLint
- 2024-11-27Npm 发布和配置入门指南
- 2024-11-27低代码应用课程:新手入门指南