新版Java面试专题视频教程——多线程篇②

新版Java面试专题视频教程——多线程篇②

      • 0. 问题汇总
        • 0.1 线程的基础知识
        • 0.2 线程中并发安全
        • 0.3 线程池
        • 0.4 使用场景
      • 1.线程的基础知识
      • 2.线程中并发锁
      • 3.线程池
        • 3.1 说一下线程池的核心参数(线程池的执行原理知道嘛)
        • 3.2 线程池中有哪些常见的阻塞队列
          • ArrayBlockingQueue的LinkedBlockingQueue区别
        • 3.3 如何确定核心线程数
        • 3.4 线程池的种类有哪些
        • 3.5 为什么不建议用Executors创建线程池
      • 4.线程使用场景问题
        • 4.1 线程池使用场景CountDownLatch、Future(你们项目哪里用到了多线程)
          • 4.1.1 CountDownLatch
          • 4.1.2 案例一(es数据批量导入)
          • 4.1.3 案例二(数据汇总)
          • 4.1.4 案例二(异步调用)
        • 4.2 如何控制某个方法允许并发访问线程的数量?
          • Semaphore两个重要的方法
      • 5.其他
        • 5.1 谈谈你对ThreadLocal的理解
          • 5.1.1 概述
          • 5.1.2 ThreadLocal基本使用
            • 三个主要方法:
          • 5.1.3 ThreadLocal的实现原理&源码解析
          • 5.1.4 ThreadLocal-内存泄露问题
            • 强引用、软引用、弱引用的区别和解析
            • 内存泄漏问题
      • 6 真实面试还原
        • 6.1 线程的基础知识
        • 6.2 线程中并发锁
        • 6.3 线程池
        • 6.4 线程使用场景问题
        • 6.5 其他

在这里插入图片描述

在这里插入图片描述

0. 问题汇总

0.1 线程的基础知识

线程与进程的区别
并行与并发的区别
线程创建的方式有哪些
runnable和callable有什么区别
线程包括哪些状态
状态之间是如何变化的
在java中wait和sleep方法的不同
新建三个线程,如何保证它们按顺序执行
notify和notifyAll有什么区别
线程的run()和start()有什么区别
如何停止一个正在运行的线程

0.2 线程中并发安全

synchronized关键字的底层原理
你谈谈JMM (Java 内存模型)
CAS你知道吗
什么是AQS
ReentrantLock的实现原理
synchronized和Lock有什么区别
死锁产生的条件是什么
如何进行死锁诊断
请谈谈你对volatile的理解
聊一下ConcurrentHashMap
导致并发程序出现问题的根本原因是什么

0.3 线程池

说一下线程池的核心参数(线程池的执行原理知道嘛)

线程池中有哪些常见的阻塞队列

如何确定核心线程数

线程池的种类有哪些

为什么不建议用Executors创建线程池

0.4 使用场景

线程池使用场景(你们项目中哪坠用到了线程池)

如何控制某个方法允许并发访问线程的数量

谈谈你对ThreadLocal的理解

在这里插入图片描述

1.线程的基础知识

新版Java面试专题视频教程——多线程篇①

2.线程中并发锁

新版Java面试专题视频教程——多线程篇①
在这里插入图片描述

3.线程池

3.1 说一下线程池的核心参数(线程池的执行原理知道嘛)

难易程度:☆☆☆
出现频率:☆☆☆☆

线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数

在这里插入图片描述

  • corePoolSize 核心线程数目

  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

  • workQueue 阻塞队列 - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue放满时,会触发拒绝策略

工作流程

在这里插入图片描述

1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略

拒绝策略

1.AbortPolicy:直接抛出异常,默认策略

2.CallerRunsPolicy:用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

4.DiscardPolicy:直接丢弃任务

参考代码:


public class TestThreadPoolExecutor {

    static class MyTask implements Runnable {
        private final String name;
        private final long duration;

        public MyTask(String name) {
            this(name, 0);
        }

        public MyTask(String name, long duration) {
            this.name = name;
            this.duration = duration;
        }

        @Override
        public void run() {
            try {
                LoggerUtils.get("myThread").debug("running..." + this);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "MyTask(" + name + ")";
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger c = new AtomicInteger(1);
        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                3,
                0,
                TimeUnit.MILLISECONDS,
                queue,
                r -> new Thread(r, "myThread" + c.getAndIncrement()),
                new ThreadPoolExecutor.AbortPolicy());
        showState(queue, threadPool);
        threadPool.submit(new MyTask("1", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("2", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("3"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("4"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("5",3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("6"));
        showState(queue, threadPool);
    }

    private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<Object> tasks = new ArrayList<>();
        for (Runnable runnable : queue) {
            try {
                Field callable = FutureTask.class.getDeclaredField("callable");
                callable.setAccessible(true);
                Object adapter = callable.get(runnable);
                Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
                Field task = clazz.getDeclaredField("task");
                task.setAccessible(true);
                Object o = task.get(adapter);
                tasks.add(o);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        LoggerUtils.main.debug("pool size: {}, queue: {}", threadPool.getPoolSize(), tasks);
    }

}

在这里插入图片描述

3.2 线程池中有哪些常见的阻塞队列

难易程度:☆☆☆
出现频率:☆☆☆

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

比较常见的有4个,用的最多是ArrayBlockingQueue和LinkedBlockingQueue

  1. ArrayBlockingQueue:基于数组结构的有界(可指定容量大小)阻塞队列,FIFO(先进先出)。
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
  3. DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层是链表底层是数组
是懒惰的,创建节点的时候添加数据提前初始化Node数组
入队会生成新NodeNode需要是提前创建好的
两把锁(头尾)一把锁

左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式

  • LinkedBlockingQueue读和写各有一把锁,性能相对较好
  • ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些

在这里插入图片描述

public class FixedThreadPoolCase {

    static class FixedThreadDemo implements Runnable{
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 2; i++) {
                System.out.println(name + ":" + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建一个固定大小的线程池,核心线程数和最大线程数都是3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(new FixedThreadDemo());
            Thread.sleep(10);
        }

        executorService.shutdown();
    }

}

在这里插入图片描述

3.3 如何确定核心线程数

难易程度:☆☆☆☆
出现频率:☆☆☆

在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型

  • IO密集型任务

一般来说:文件读写、DB读写、网络请求

推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)

  • CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)

java代码查看CPU核数

参考回答:

高并发、任务执行时间 -->( CPU核数+1 ),减少线程上下文的切换

并发不高、任务执行时间

  • IO密集型的任务 --> (CPU核数 * 2 + 1)
  • 计算密集型任务 --> ( CPU核数+1

并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

3.4 线程池的种类有哪些

难易程度:☆☆☆
出现频率:☆☆☆

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种

  1. 创建使用固定线程数的线程池
    在这里插入图片描述
  • 核心线程数与最大线程数一样,没有救急线程 那0L和unit就没意义
  • 阻塞队列是LinkedBlockingQueue,最大容量Integer.MAX_VALUE
  • 适用场景:适用于任务量已知,相对耗时的任务
  • 案例:
public class FixedThreadPoolCase {

    static class FixedThreadDemo implements Runnable{
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 2; i++) {
                System.out.println(name + ":" + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建一个固定大小的线程池,核心线程数和最大线程数都是3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(new FixedThreadDemo());
            Thread.sleep(10);
        }

        executorService.shutdown();
    }
}
  1. 单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
    在这里插入图片描述
  • 核心线程数和最大线程数都是1
  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
  • 适用场景:适用于按照顺序执行的任务
  • 案例:
public class NewSingleThreadCase {
    static int count = 0;

    static class Demo implements Runnable {
        @Override
        public void run() {
            count++;
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //单个线程池,核心线程数和最大线程数都是1
        ExecutorService exec = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(5);
        }
        exec.shutdown();
    }
}

在这里插入图片描述

  1. 可缓存线程池
    在这里插入图片描述
  • 核心线程数为0
  • 最大线程数是Integer.MAX_VALUE
  • 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
  • 适用场景:适合任务数比较密集,但每个任务执行时间较短的情况
  • 案例:
public class CachedThreadPoolCase {

    static class Demo implements Runnable {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            try {
                //修改睡眠时间,模拟线程执行需要花费的时间
                Thread.sleep(100);

                System.out.println(name + "执行完了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(1);
        }
        exec.shutdown();
    }
}

在这里插入图片描述

  1. 提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。
    在这里插入图片描述
  • 适用场景:有定时和延迟执行的任务
  • 案例
public class ScheduledThreadPoolCase {

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                String name = Thread.currentThread().getName();

                System.out.println(name + ", 开始:" + new Date());
                Thread.sleep(1000);
                System.out.println(name + ", 结束:" + new Date());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        System.out.println("程序开始:" + new Date());

        /**
         * schedule 提交任务到线程池中
         * 第一个参数:提交的任务
         * 第二个参数:任务执行的延迟时间
         * 第三个参数:时间单位
         */
        scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);

        Thread.sleep(5000);

        // 关闭线程池
        scheduledThreadPool.shutdown();
    }
}

在这里插入图片描述

3.5 为什么不建议用Executors创建线程池

难易程度:☆☆☆
出现频率:☆☆☆

参考阿里开发手册《Java开发手册-嵩山版》

在这里插入图片描述
在这里插入图片描述

4.线程使用场景问题

4.1 线程池使用场景CountDownLatch、Future(你们项目哪里用到了多线程)

难易程度:☆☆☆
出现频率:☆☆☆☆

4.1.1 CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await() 用来等待计数归零
  • countDown() 用来让计数减一

在这里插入图片描述

案例代码:

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        //初始化了一个倒计时锁 参数为 3
        CountDownLatch latch = new CountDownLatch(3);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        String name = Thread.currentThread().getName();
        System.out.println(name + "-waiting...");
        //等待其他线程完成
        latch.await();
        System.out.println(name + "-wait end...");
    }
    
}
4.1.2 案例一(es数据批量导入)

在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出

整体流程就是通过CountDownLatch+线程池配合去执行

在这里插入图片描述

详细实现流程:

在这里插入图片描述

详细实现代码,请查看当天代码

package com.itheima.cdl.service.impl;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;

@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl implements ApArticleService {

    @Autowired
    private ApArticleMapper apArticleMapper;
    @Autowired
    private RestHighLevelClient client;

    @Autowired
    private ExecutorService executorService;

    private static final String ARTICLE_ES_INDEX = "app_info_article";

    private static final int PAGE_SIZE = 2000;

    /**
     * 批量导入
     */
    @SneakyThrows
    @Override
    public void importAll() {

        //总条数
        int count = apArticleMapper.selectCount();
        //总页数
        int totalPageSize = count % PAGE_SIZE == 0 ? count / PAGE_SIZE : count / PAGE_SIZE + 1;
        //开始执行时间
        long startTime = System.currentTimeMillis();
        //一共有多少页,就创建多少个CountDownLatch的计数
        CountDownLatch countDownLatch = new CountDownLatch(totalPageSize);

        int fromIndex;
        List<SearchArticleVo> articleList = null;

        for (int i = 0; i < totalPageSize; i++) {
            //起始分页条数
            fromIndex = i * PAGE_SIZE;
            //查询文章
            articleList = apArticleMapper.loadArticleList(fromIndex, PAGE_SIZE);
            //创建线程,做批量插入es数据操作
            TaskThread taskThread = new TaskThread(articleList, countDownLatch);
            //执行线程
            executorService.execute(taskThread);
        }

        //调用await()方法,用来等待计数归零
        countDownLatch.await();

        long endTime = System.currentTimeMillis();
        log.info("es索引数据批量导入共:{}条,共消耗时间:{}秒", count, (endTime - startTime) / 1000);
    }

    class TaskThread implements Runnable {

        List<SearchArticleVo> articleList;
        CountDownLatch cdl;

        public TaskThread(List<SearchArticleVo> articleList, CountDownLatch cdl) {
            this.articleList = articleList;
            this.cdl = cdl;
        }

        @SneakyThrows
        @Override
        public void run() {
            //批量导入
            BulkRequest bulkRequest = new BulkRequest(ARTICLE_ES_INDEX);

            for (SearchArticleVo searchArticleVo : articleList) {
                bulkRequest.add(new IndexRequest().id(searchArticleVo.getId().toString())
                        .source(JSON.toJSONString(searchArticleVo), XContentType.JSON));
            }
            //发送请求,批量添加数据到es索引库中
            client.bulk(bulkRequest, RequestOptions.DEFAULT);

            //让计数减一
            cdl.countDown();
        }
    }
}
4.1.3 案例二(数据汇总)

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?

在这里插入图片描述

MQ跟多线程异步改写的区别:MQ主要解决跨进程之间的消息同步问题,将其改写成了异步 侧重于服务间通讯 而多线程主要解决的是当前进程快速响应

package com.itheima.cdl.controller;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

@RestController
@RequestMapping("/order_detail")
@Slf4j
public class OrderDetailController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ExecutorService executorService;

    @SneakyThrows
    @GetMapping("/get/detail_new/{id}")
    public Map<String, Object> getOrderDetailNew() {

        long startTime = System.currentTimeMillis();

        Future<Map<String, Object>> f1 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);
            return r;
        });
        Future<Map<String, Object>> f2 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);
            return r;
        });

        Future<Map<String, Object>> f3 = executorService.submit(() -> {
            Map<String, Object> r =
                    restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);
            return r;
        });

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("order", f1.get());
        resultMap.put("product", f2.get());
        resultMap.put("logistics", f3.get());

        long endTime = System.currentTimeMillis();

        log.info("接口调用共耗时:{}毫秒",endTime-startTime);
        return resultMap;
    }

    @SneakyThrows
    @GetMapping("/get/detail/{id}")
    public Map<String, Object> getOrderDetail() {

        long startTime = System.currentTimeMillis();

        Map<String, Object> order = restTemplate.getForObject
            ("http://localhost:9991/order/get/{id}", Map.class, 1);

        Map<String, Object> product = restTemplate.getForObject
            ("http://localhost:9991/product/get/{id}", Map.class, 1);

        Map<String, Object> logistics = restTemplate.getForObject
            ("http://localhost:9991/logistics/get/{id}", Map.class, 1);

        long endTime = System.currentTimeMillis();

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("order", order);
        resultMap.put("product", product);
        resultMap.put("logistics", logistics);

        log.info("接口调用共耗时:{}毫秒",endTime-startTime);
        return resultMap;
    }
}
  • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
  • 报表汇总

在这里插入图片描述

4.1.4 案例二(异步调用)

在这里插入图片描述

在进行搜索的时候,需要保存用户的搜索记录,而搜索记录不能影响用户的正常搜索,我们通常会开启一个线程去执行历史记录的保存,在新开启的线程在执行的过程中,可以利用线程提交任务

package com.itheima.cdl.service.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {

    @Autowired
    private RestHighLevelClient client;
    private static final String ARTICLE_ES_INDEX = "app_info_article";

    private int userId = 1102;

    @Autowired
    private ApUserSearchService apUserSearchService;
    /**
     * 文章搜索
     * @return
     */
    @Override
    public List<Map> search(String keyword) {

        try {
            SearchRequest request = new SearchRequest(ARTICLE_ES_INDEX);
            //设置查询条件
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            //第一个条件
            if(null == keyword || "".equals(keyword)){
                request.source().query(QueryBuilders.matchAllQuery());
            }else {
                request.source().query(QueryBuilders.queryStringQuery(keyword).field("title").defaultOperator(Operator.OR));
                //保存搜索历史
                apUserSearchService.insert(userId,keyword);
            }
            //分页
            request.source().from(0);
            request.source().size(20);

            //按照时间倒序排序
            request.source().sort("publishTime", SortOrder.DESC);
            //搜索
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);

            //解析结果
            SearchHits searchHits = response.getHits();
            //获取具体文档数据
            SearchHit[] hits = searchHits.getHits();
            List<Map> resultList = new ArrayList<>();
            for (SearchHit hit : hits) {
                //文档数据
                Map map = JSON.parseObject(hit.getSourceAsString(), Map.class);
                resultList.add(map);
            }
            return resultList;
        } catch (IOException e) {
            throw new RuntimeException("搜索失败");
        }
    }
}
package com.itheima.cdl.service.impl;
import com.itheima.cdl.service.ApUserSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {

    /**
     * 保存搜索历史记录
     * @param userId
     * @param keyword
     */
    @Async("taskExecutor")//异步调用
    @Override
    public void insert(Integer userId, String keyword) {

        //保存用户记录  mongodb或mysql
        //执行业务
        log.info("用户搜索记录保存成功,用户id:{},关键字:{}",userId,keyword);
    }
}

4.2 如何控制某个方法允许并发访问线程的数量?

难易程度:☆☆☆
出现频率:☆☆

Semaphore [ˈsɛməˌfɔr] 信号量,是JUC包下的一个工具类底层是AQS,我们可以通过其限制执行的线程数量,达到限流的效果

当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束

Semaphore两个重要的方法

lsemaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)

lsemaphore.release():释放一个信号量,此时信号量个数+1

线程任务类:

public class SemaphoreCase {
    public static void main(String[] args) {
        // 1. 创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3); //容量是3
        // 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {

                try {
                    // 3. 获取许可
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    System.out.println("running...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("end...");
                } finally {
                    // 4. 释放许可
                    semaphore.release();
                }
            }).start();
        }
    }
}

5.其他

5.1 谈谈你对ThreadLocal的理解

难易程度:☆☆☆
出现频率:☆☆☆☆

5.1.1 概述

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

在这里插入图片描述

5.1.2 ThreadLocal基本使用
三个主要方法:
  • set(value) 设置值
  • get() 获取值
  • remove() 清除值
public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itcast");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t1").start();
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itheima");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t2").start();
    }

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();
    }
}

5.1.3 ThreadLocal的实现原理&源码解析

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

在ThreadLocal中有一个内部类叫做ThreadLocalMap,类似于HashMap

ThreadLocalMap中有一个属性table数组,这个是真正存储数据的位置

set方法

在这里插入图片描述

get方法/remove方法

在这里插入图片描述

5.1.4 ThreadLocal-内存泄露问题

Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用

强引用、软引用、弱引用的区别和解析
  • 强引用:

最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收

在这里插入图片描述

  • 弱引用:

表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收

在这里插入图片描述

内存泄漏问题

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例value为线程变量的副本

在这里插入图片描述

在使用ThreadLocal的时候,强烈建议:务必手动remove 防止内存泄漏

6 真实面试还原

6.1 线程的基础知识

聊一下并行和并发有什么区别?

候选人:
是这样的~~
现在都是多核CPU,在多核CPU下
并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

说一下线程和进程的区别?

候选人:
嗯,好~
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

如果在java中创建线程有哪些方式?

候选人:
在java中一共有四种常见的创建方式,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。通常情况下,我们项目中都会采用线程池的方式创建线程。

好的,刚才你说的runnable 和 callable 两个接口创建线程有什么不同呢?

候选人:
是这样的~
最主要的两个线程一个是有返回值,一个是没有返回值的。
Runnable 接口run方法无返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
还有一个就是,他们异常处理也不一样。Runnable接口run方法只能抛出运行时异常,也无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息
在实际开发中,如果需要拿到执行的结果,需要使用Callalbe接口创建线程,调用FutureTask.get()得到可以得到返回值,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程包括哪些状态,状态之间是如何变化的?

候选人:
在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。
关于线程的状态切换情况比较多。我分别介绍一下
当一个线程对象被创建,但还未调用 start 方法时处于 新建状态,调用了 start 方法,就会由 新建进入 可运行状态。如果线程内代码已经执行完毕,由 可运行进入 终结状态。当然这些是一个线程正常执行情况。
如果线程获取锁失败后,由 可运行进入 Monitor 的阻塞队列 阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的 阻塞线程,唤醒后的线程进入 可运行状态
如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从 可运行状态释放锁 等待状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为 可运行状态
还有一种情况是调用 sleep(long) 方法也会从 可运行状态进入 有时限等待状态,不需要主动唤醒,超时时间到自然恢复为 可运行状态

嗯,好的,刚才你说的线程中的 wait 和 sleep方法有什么不同呢?

候选人:
它们两个的相同点是都可以让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
不同点主要有三个方面:
第一:方法归属不同
sleep(long) 是 Thread 的静态方法。而 wait(),是 Object 的成员方法,每个对象都有
第二:线程醒来时机不同
线程执行 sleep(long) 会在等待相应毫秒后醒来,而 wait() 需要被 notify 唤醒,wait() 如果不唤醒就一直等下去
第三:锁特性不同
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(相当于我放弃 cpu,但你们还可以用)
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(相当于我放弃 cpu,你们也用不了)

好的,我现在举一个场景,你来分析一下怎么做,新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

候选人:
嗯~~,我思考一下 (适当的思考或想一下属于正常情况,脱口而出反而太假[背诵痕迹])
可以这么做,在多线程中有多种方法让线程按特定顺序执行,可以用线程类的 join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
比如说:
使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成

在我们使用线程的过程中,有两个方法。线程的 run()和 start()有什么区别?

候选人:
start方法用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。run方法封装了要被线程执行的代码,可以被调用多次。

那如何停止一个正在运行的线程呢?

候选人
有三种方式可以停止线程
第一:可以使用退出标志,使线程正常退出,也就是当run方法完成后线程终止,一般我们加一个标记
第二:可以使用线程的stop方法强行终止,不过一般不推荐,这个方法已作废
第三:可以使用线程的interrupt方法中断线程,内部其实也是使用中断标志来中断线程
我们项目中使用的话,建议使用第一种或第三种方式中断线程

6.2 线程中并发锁

讲一下synchronized关键字的底层原理?

候选人
嗯~~好的,
synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。
synchronized 因为需要依赖于JVM级别的Monitor ,相对性能也比较低。

好的,你能具体说下Monitor 吗?

候选人
monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
monitor内部维护了三个变量
WaitSet:保存处于Waiting状态的线程
EntryList:保存处于Blocked状态的线程
Owner:持有锁的线程
只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner
在上锁的过程中,如果有其他线程也来抢锁,则进入EntryList 进行阻塞,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的。

好的,那关于synchronized 的锁升级的情况了解吗?

候选人
嗯,知道一些(要谦虚)
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
一旦锁发生了竞争,都会升级为重量级锁

好的,刚才你说了synchronized它在高并发量的情况下,性能不高,在项目该如何控制使用锁呢?

候选人
嗯,其实,在高并发下,我们可以采用ReentrantLock来加锁。

嗯,那你说下ReentrantLock的使用方式和底层原理?

候选人
好的,
ReentrantLock是一个可重入锁:,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞,内部直接增加重入次数 就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。
ReentrantLock是属于juc报下的类,属于api层面的锁,跟synchronized一样,都是悲观锁。通过lock()用来获取锁,unlock()释放锁。
它的底层实现原理主要利用 CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数( 默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。

好的,刚才你说了CAS和AQS,你能介绍一下吗?

候选人
好的。
CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
CAS使用到的地方很多:AQS框架、AtomicXXX类
在操作共享变量的时候使用的自旋锁,效率上更高一些
CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
AQS的话,其实就一个jdk提供的类AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
内部有一个属性 state 属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态
在它的内部还提供了基于 FIFO 的等待队列,是一个双向列表,其中
tail 指向队列最后一个元素
head 指向队列中最久的一个元素
其中我们刚刚聊的ReentrantLock底层的实现就是一个AQS。

synchronized和Lock有什么区别 ?

候选人
嗯,好的,主要有三个方面不太一样
第一,语法层面
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
第二,功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock
第三,性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock 的实现通常会提供更好的性能
统合来看,需要根据不同的场景来选择不同的锁的使用。

死锁产生的条件是什么?

候选人
嗯,是这样的,一个线程需要同时获取多把锁,这时就容易发生死锁,举个例子来说:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁

那如果产出了这样的,如何进行死锁诊断?

候选人
这个也很容易,我们只需要通过jdk自动的工具就能搞定
我们可以先通过jps来查看当前java程序运行的进程id
然后通过jstack来查看这个进程id,就能展示出来死锁的问题,并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。

请谈谈你对 volatile 的理解

候选人
嗯~~
volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能
第一:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
第二: 禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个 内存屏障,通过插入内存屏障禁止在内存屏障 前后的指令执行重排序优化

本文作者:接《集合相关面试题》

那你能聊一下ConcurrentHashMap的原理吗?

候选人
嗯好的,
ConcurrentHashMap 是一种线程安全的高效Map集合,jdk1.7和1.8也做了很多调整。
JDK1.7的底层采用是 分段的数组+ 链表 实现
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
在jdk1.7中 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元 素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁
在jdk1.8中的ConcurrentHashMap 做了较大的优化,性能提升了不少。首先是它的数据结构与jdk1.8的hashMap数据结构完全一致。其次是放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升

6.3 线程池

线程池的种类有哪些?

候选人
嗯!是这样
在jdk中默认提供了4中方式创建线程池
第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回 收空闲线程,若无可回收,则新建线程。
第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。
第三个是:newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
第四个是:newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池的核心参数有哪些?

候选人
在线程池中一共有7个核心参数:
corePoolSize 核心线程数目 - 池中会保留的最多线程数
maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
在拒绝策略中又有4中拒绝策略
当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。

如何确定核心线程池呢?

候选人
是这样的,我们公司当时有一些规范,为了减少线程上下文的切换,要根据当时部署的服务器的CPU核数来决定,我们规则是:CPU核数+1就是最终的核心线程数。

线程池的执行原理知道吗?

候选人
嗯~,它是这样的
首先判断线程池里的核心线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队 列里。如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给拒绝策略来处理这个任务。

为什么不建议使用Executors创建线程池呢?

候选人
好的,其实这个事情在阿里提供的最新开发手册《Java开发手册-嵩山版》中也提到了
主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。

6.4 线程使用场景问题

如果控制某一个方法允许并发访问线程的数量?

候选人
嗯~~,我想一下
在jdk中提供了一个Semaphore[seməfɔːr]类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了
第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1

好的,那该如何保证Java程序在多线程的情况下执行安全呢?

候选人
嗯,刚才讲过了导致线程安全的原因,如果解决的话,jdk中也提供了很多的类帮助我们解决多线程安全的问题,比如:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题

你在项目中哪里用了多线程?

候选人
嗯~~,我想一下当时的场景[根据自己简历上的模块设计多线程场景]
参考场景一:
es数据批量导入
在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以,当时我就想到可以使用线程池的方式导入,利用CountDownLatch+Future来控制,就能大大提升导入的时间。
参考场景二:
在我做那个xx电商网站的时候,里面有一个数据汇总的功能,在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终再汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行
参考场景三:
《黑马头条》项目中使用的
我当时做了一个文章搜索的功能,用户输入关键字要搜索文章,同时需要保存用户的搜索记录(搜索历史),这块我设计的时候,为了不影响用户的正常搜索,我们采用的异步的方式进行保存的,为了提升性能,我们加入了线程池,也就说在调用异步方法的时候,直接从线程池中获取线程使用

6.5 其他

谈谈你对ThreadLocal的理解

候选人
嗯,是这样的~~
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享

好的,那你知道ThreadLocal的底层原理实现吗?

候选人
嗯,知道一些~
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

好的,那关于ThreadLocal会导致内存溢出这个事情,了解吗?

候选人
嗯,我之前看过源码,我想一下~~
是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/400767.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java Web演化史:从Servlet到SpringBoot的技术进程及未来趋势

引言 在快速演进的IT世界里&#xff0c;Java Web开发始终屹立不倒&#xff0c;它不仅承担着历史的厚重&#xff0c;也始终面向未来。 自诞生之日起&#xff0c;Java Web技术就在不断地进化&#xff0c;以适应不同时代的需求。 本文将回顾Java Web开发的重要里程碑&#xff0c;…

django rest framework 学习笔记-实战商城2

01收货地址模型类和视图定义_哔哩哔哩_bilibili 本博客借鉴至大佬的视频学习笔记 地址信息的管理&#xff1a;增删改查的实现 # 序列化器配置 class AddrSerializer(serializers.ModelSerializer):"""收货地址的模型序列化器"""class Meta:mo…

喀秋莎画中画怎么设置 喀秋莎画中画视频怎么导出 喀秋莎什么意思 camtasia studio下载

画中画视频&#xff0c;顾名思义&#xff0c;就是在一个视频中有两个画面&#xff0c;游戏解说、微课等类型的视频常常就以画中画的形式出现。作为一款专业的视频编辑软件&#xff0c;使用camtasia可以轻松地制作画中画视频并导出。接下来我将为大家介绍&#xff1a;喀秋莎画中…

HEVC视频编解码标准学习笔记-1

视频编解码标准H.265/HEVC&#xff08;High Efficiency Video Coding&#xff09;通过将视频数据编码为更高效格式&#xff0c;大幅改善了视频流的压缩效率。这里主要介绍Tile、Slice和CTU的定义&#xff0c;以及介绍这些技术组件之间的相互关系。 CTU&#xff08;编码树单元&…

Chrome插件精选 — 缓存清理

Chrome实现同一功能的插件往往有多款产品&#xff0c;逐一去安装试用耗时又费力&#xff0c;在此为某一类型插件挑选出比较好用的一款或几款&#xff0c;尽量满足界面精致、功能齐全、设置选项丰富的使用要求&#xff0c;便于节省一个个去尝试的时间和精力。 1. Chrome清理大师…

基于深度学习的红肉新鲜过期判决系统matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 系统构成与流程 4.2 模型训练与优化 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ...............................................…

phar反序列化原理及利用

phar是什么&#xff1f; phar 是 PHP 的一种归档文件格式&#xff0c;类似于 ZIP 或 TAR 文件&#xff0c;它可以包含多个文件和目录&#xff0c;并且可以像访问普通文件系统一样在 PHP 中进行访问。在php 5.3 或更高版本中默认开启 在php.ini中配置如下时&#xff0c;才能生成…

线性代数:线性方程组解的结构

目录 齐次/非齐次方程组的解 Ax 0 的解的性质 定理 Ax b 的解的性质 相关证明 例1 例2 例3 齐次/非齐次方程组的解 Ax 0 的解的性质 定理 Ax b 的解的性质 相关证明 例1 例2 例3

docker容器常见操作

目录 一、认识容器 1.1、docker用到的内核技术 1.2、namespace 1.3、Control Group 1.4、LXC与docker区别 二、docker环境准备 2.1、安装docker 2.2、docker daemon环境管理 三、镜像、容器和仓库 3.1、镜像常见操作 3.2、配置镜像加速器 命名空间 3.3、非官方镜像仓…

亿道丨防爆工业平板哪家好丨防爆平板电脑pad:防什么?

在爆炸性环境中&#xff0c;工作安全是至关重要的。防爆工业平板作为一种特殊设计的设备&#xff0c;不仅能够抵御爆炸风险&#xff0c;还提供高效的工作性能。本文将介绍防爆工业平板的防护功能以及其在各个行业中的应用。 在许多行业&#xff0c;如石油、化工、矿山和制药等领…

【RT-DETR有效改进】Best Paper | DAttention (DAT)可变形注意力机制和动态采样点

一、本文介绍 本文给大家带来的是RT-DETR改进DAT(Vision Transformer with Deformable Attention)的教程&#xff0c;其发布于2022年CVPR2022上同时被评选为Best Paper&#xff0c;由此可以证明其是一种十分有效的改进机制&#xff0c;其主要的核心思想是&#xff1a;引入可变…

考研高数(高阶导数的计算)

1.归纳法 常见高阶导数 2.泰勒展开式 3.莱布尼兹公式 4.用导数定义证明导函数在某一点连续的例题

亿道丨三防平板电脑厂商哪家好丨麒麟系统三防平板PAD

随着科技的飞速发展&#xff0c;人们对于移动设备的需求越来越高。然而&#xff0c;在不同的行业应用场景下&#xff0c;常规的智能平板往往无法满足特殊的工作要求。&#xff0c;亿道三防平板&#xff0c;将高可靠性与卓越性能高度结合&#xff0c;为各行各业提供卓越的移动解…

stm32——hal库学习笔记(ADC)

这里写目录标题 一、ADC简介&#xff08;了解&#xff09;1.1&#xff0c;什么是ADC&#xff1f;1.2&#xff0c;常见的ADC类型1.3&#xff0c;并联比较型工作示意图1.4&#xff0c;逐次逼近型工作示意图1.5&#xff0c;ADC的特性参数1.6&#xff0c;STM32各系列ADC的主要特性 …

华为OD机试真题-用连续自然数之和来表达整数-2023年OD统一考试(C卷)---python代码免费

题目&#xff1a; 代码 """ 题目分析&#xff1a; 一个整数 连续的自然数之和表示(非负整数&#xff09;输入&#xff1a; 一个整数T[1,1000] 输出&#xff1a; 输出多个表达式&#xff0c;自然数个数最少优先输出 最后一行&#xff0c; 输出“Result : 个数…

[计网底层小探索]:实现并部署多线程并发Tcp服务器框架(基于生产者消费者模型的线程池结构)

文章目录 一.网络层与传输层协议sockaddr结构体继承体系(Linux体系)贯穿计算机系统的网络通信架构图示: 二.实现并部署多线程并发Tcp服务器框架线程池模块序列化反序列化工具模块通信信道建立模块服务器主体模块任务回调模块(根据具体应用场景可重构)Tips:DebugC代码过程中遇到…

4.Spring MVC入门

文章目录 1. HTTP协议2. Spring MVC2.1. 三层架构2.2. MVC&#xff08;解决表现层的问题&#xff09;2.3. 核心组件 3. Thymeleaf3.1. 模板引擎3.2. Thymeleaf3.3. 常用语法 代码 1. HTTP协议 网址&#xff1a;https://www.ietf.org/ &#xff08;官网网址&#xff09; https:…

oppo手机如何录屏?解锁录屏新功能!

“最近换了一款oppo手机&#xff0c;感觉它的拍照功能真的很强大。但除此之外&#xff0c;我发现oppo还有许多隐藏功能&#xff0c;比如录屏。但我尝试了很久&#xff0c;都没找到录屏的开关在哪里。有没有哪位oppo用户知道怎么打开这个功能呢&#xff1f;” 随着科技的不断发…

几个常见的C/C++语言冷知识

当涉及到C/C语言时&#xff0c;有一些冷知识可能并不为人所熟知&#xff0c;但却可以让你更深入地理解这门古老而强大的编程语言。以下是一些有趣的C/C语言冷知识。 1. 数组的下标可以是负数 在我们日常的C语言编程中&#xff0c;数组是一个非常常见的数据结构。我们习惯性地使…

opencv python投影变换效果

变换原理&#xff1a; https://www.cnblogs.com/txwtech/p/18024547 python示范代码&#xff1a; src2原图&#xff0c;4个坐标点 dst2转换后&#xff0c;4个坐标点 p_touyin cv2.getPerspectiveTransform(src2,dst2) #计算投影变换矩阵 #利用矩阵值进行图像投影变换 r…