多线程面试题

目录

1.僵尸进程和孤儿进程

1.1 孤儿进程定义

1.2 僵尸进程定义

1.2 怎样来清除僵尸进程:

1)kill杀死元凶父进程(一般不用)

2)父进程用wait或waitpid去回收资源(方案不好)

3)通过信号机制,在处理函数中调用wait,回收资源

2.线程池详解

2.1线程池的作用

2.2 常见线程池的创建方式

newFixedThreadPool()

newCachedThreadPool()

newSingleThreadExecutor()

newScheduledThreadPool()

总结:

2.3 工作队列

2.4 拒绝策略

2.5 基础、函数、复杂(Callable)创建线程池代码

3.线程池的几种状态:

4.线程池的底层实现原理?

4.1 首先了解一下生产者消费者模型

4.2 线程池的核心

5.线程安全是什么意思?

5.1 Servlet的线程安全性

5.2 SpringMVC的Controller是线程安全的吗?

6.乐观锁和悲观锁

7.volatile实现原理

7.1 volatile作用

7.2 volatile对应底层实现原理

7.3 相关代码

8.synchronized的底层原理

8.1 对代码块同步monitor表象原理;

8.2 Synchronized的使用

8.2.1 同步与异步

8.2.2 阻塞与非阻塞

8.2.3 补充

8.3 Synchronized的优化

8.3.1 普通对象内存结构

​编辑

8.3.2 锁升级

​编辑

8.3.3 锁粗化和锁消除

8.4 volatile和synchronized的区别:

9.ReentrantLock相关

9.1 底层原理

9.2 相关代码

9.3 ReentrantLock 和 Synchronized 区别

10.死锁的形成条件和避免方法:

10.1 形成条件:

10.2 避免方法:

11.红黑树和平衡二叉树的区别:

12.进程和线程的区别:

13.创建线程的方法

14.线程状态

15.集合相关内容

15.1 HashMap的底层实现原理

15.1.1 概述

15.1.2 HashMap和HashashTable的区别

两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全;

15.1.3 HashMap的底层存储

15.2 CocurrentHashMap的底层实现原理

15.2.1 JDK 1.7

15.2.2 JDK 1.8

16.进程通信方式

17.Scheduler.from

附录代码

1.ThreadLocal测试

2.invoke测试

3.ForkJoinPool测试


1.僵尸进程和孤儿进程

1.1 孤儿进程定义

孤儿进程:父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由init进程(进程号PID = 1)回收。

1.2 僵尸进程定义

子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,此时子进程将成为 一个僵尸进程。

1.2 怎样来清除僵尸进程:

1)kill杀死元凶父进程(一般不用)

严格的说,僵尸进程并不是问题的根源,罪魁祸首是产生大量僵死进程的父进程。因此,我们可以直接除掉元凶,通过kill发送SIGTERM或者SIGKILL信号。元凶死后,僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源。

或者运行:kill -9 父进程的pid值、

2)父进程用wait或waitpid去回收资源(方案不好)

父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。

3)通过信号机制,在处理函数中调用wait,回收资源

通过信号机制,子进程退出时向父进程发送SIGCHLD信号,父进程调用signal(SIGCHLD,sig_child)去处理SIGCHLD信号,在信号处理函数sig_child()中调用wait进行处理僵尸进程。什么时候得到子进程信号,什么时候进行信号处理,父进程可以继续干其他活,不用去阻塞等待。

ps -a -o pid,ppid,state,cmd

显示:(状态Z代表僵尸进程)

S PID PPID CMD

S 3213 2529 ./pid1

Z 3214 3213 [pid1]

2.线程池详解

2.1线程池的作用

  • 降低资源消耗,减少线程池的创建销毁开销;
  • 提高系统的响应速率,任务到达时,不需等待线程就能立即执行。
  • 提高线程可管理性;
  • 防止服务器过载。是针对内存溢出,CPU耗尽的一个比较好的解决方式,但是不能用newCachedThreadPool()。

2.2 常见线程池的创建方式

有4种类型的创建线程池的简单函数:

  • newFixedThreadPool()

  • 说明:初始化一个指定线程数的线程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 作为阻塞队列
  • 特点:即使当线程池没有可执行任务时,也不会释放线程。
  • newCachedThreadPool()

  • 说明:初始化一个可以缓存线程的线程池,核心线程数是0,默认缓存 60s,线程池的线程数可达到 Integer.MAX_VA LUE,即 2147483647,内部使用 SynchronousQueue 作为阻塞队列;
  • 特点:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。适合执行耗时小的异步连续任务;
  • newSingleThreadExecutor()

  • 说明:初始化只有一个线程的线程池,最大线程数也是1,内部使用 LinkedBlockingQueue 作为阻塞队列。
  • 特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行。
  • 与newFixedThreadPool(1)的区别在于,单一线程池大小在newSingleThreadExecutor方法中硬编码,不能再改变。
  • newScheduledThreadPool()

  • 特定:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使 用该线程池定期的同步数据。

总结:

除了 newScheduledThreadPool 的内部实现特殊一点之外,其它线程池内部都是基于 ThreadPoolExecutor 类(Executor 的子类)实现的。

2.3 工作队列

workQueue

用来保存等待被执行的任务的阻塞队列,且任务必须实现 Runable 接口,在 JDK 中提供了如下阻塞队列:
  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务;
  2. LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务,吞吐量通常要高于 Array BlockingQuene;
  3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除 操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene
  4. priorityBlockingQuene:具有优先级的无界阻塞队列; threadFactory 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。

2.4 拒绝策略

handler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
  2. CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并重新提交当前业务;
  4. DiscardPolicy:直接丢弃新来的任务不发出异常;

当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

2.5 基础、函数、复杂(Callable)创建线程池代码


import java.util.List;
import java.util.concurrent.*;

class taskrunnable implements Runnable{
    int n=0;
    public taskrunnable(int n){
        this.n=n;
    }
    @Override
    public void run(){
        System.out.println("开始执行"+n);
        try {
            Thread.sleep(3000L);
            //System.out.println("runnable con:"+n);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            //e.printStackTrace();
            System.err.println("异常:"+e.getMessage());
        }
        System.out.println("执行结束"+n);

    }
}
public class ThreadPoolExecutorTest {
    //线程不是越多越好
    /*
     * 线程在java中是一个对象,更是操作系统的资源,
     * 线程创建、销毁需要时间。
     * 如果创建时间+销毁时间>执行任务时间就很不合算。
     *
     * java对象占用堆内存,操作系统线程占用系统内存。
     * 根据jvm规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存分配的;
     * 线程很多,会消耗很多的内存。
     *
     * 操作系统需要频繁切换线程上下文(大家都想被运行),影响性能;
     *
     * 线程池就是为了控制线程数量

     * 线程池接口定义和实现类
     * 接口:Executor:最上层的接口,定义执行任务的方法execute,这个没有具体实现;
     * 接口:ExecutorService:继承了Executor接口,拓展了Callable,Future,关闭方法。
     * 接口:ScheduledExecutorService:继承了ExecutorService,增加了定时任务相关的方法;
     * 实现类:ThreadPoolExecutor:基础、标准的线程池实现;
     * 实现类:ScheduledThreadPoolExecutor:
     * 继承了上面的实现类,实现了接口三中相关定时任务的方法;
     */
    ThreadPoolExecutor tpe=null;
    public void testCommon(ThreadPoolExecutor tpe){
        //提交15个执行时间为3s的任务,看超过大小的2个,对应的处理情况;
        for(int i=0;i<15;i++){
            int n=i;//必须有这个赋值过程,才能利用i中的值;
            tpe.submit(new taskrunnable(i));
            System.out.println("任务提交成功"+i);
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("当前线程池核心线程数量为:"+tpe.getPoolSize());
        //超出核心线程数量的任务将会被缓冲到等待队列;
        /*
         * 原因:
         * 执行->线程数量是否超过核心线程数量-是->判断工作队列即等待队列是否被任务占满了?--是->剩的任务数量是否超过最大线程数量和核心线程数量的差?-是->执行拒绝策略
         *             |                             |                                            |
         *             否                            否                                            否
         *             |                             |                                            |
         *         创建新核心线程,                将任务丢到等待队列,                     剩下的任务创建新的线程执行,若5s内线程没任务执行,则线程被销毁;
         *          处理任务;                      不创建新线程;                              工作队列不在线程池里,是在外面排队;
         *         即使此时有空闲线程,
         *        只要不满足coresize,就新创建线程;
         * */
        System.out.println("当前线程池等待状态的线程数量为:"+tpe.getQueue().size());

        //理论上,15s后,超出核心线程数量的线程若5s内没被调用会被自动销毁;
        //但test1显然用不到;
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("当前线程池线程数量为:"+tpe.getPoolSize());
        System.out.println("当前线程池等待状态的线程数量为:"+tpe.getQueue().size());
    }

    /*
     * 线程池信息:核心线程数量5,最大线程数量10,
     * 无界队列,即意味着可以缓冲无限数量的任务
     * 超出核心线程数量的【线程】存活时间:5s,
     * keyi指定拒绝策略,这里没有指定;
     */

    private void threadPoolExecutorTest1() throws Exception{
        tpe=new ThreadPoolExecutor(5,10,5, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
        /*
         * workQueue - //只容纳由执行方法提交的尚未执行的接口任务
         * the queue to use for holding tasks before they are executed.
         * This queue will hold only the Runnable tasks submitted by the execute method.
         *
         *handler - the handler to use when execution is blocked
         *because the thread bounds and queue capacities are reached
         *
         *threadFactory
         *- the factory to use when the executor creates a new thread
         *
         *
         * 预计的结果:线程池线程数量的任务为5,
         * 超出数量的任务,除去超时未完成自动销毁的,其他的进入队列中等待被执行;
         */
    }

    public void dealtask(){
        try {
            threadPoolExecutorTest4();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            //System.out.println("有任务被拒绝执行了");
        }
        testCommon(tpe);
        try {
            Thread.sleep(60000L);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("60s后:");
        System.out.println("当前线程池线程数量为:"+tpe.getPoolSize());
        System.out.println("当前线程池等待状态的线程数量为:"+tpe.getQueue().size());
    }

    private void threadPoolExecutorTest2 ()throws Exception{
        //这里等待队列初始大小为3,所以可以最大容纳13个任务;
        //默认的策略是抛出RejectedExecutionException异常,java.util.cocurrent.ThreadPoolExecutor.AbortPolicy
        tpe=new ThreadPoolExecutor(5,10,5,TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),new  rejectedexehandler());
    }
    /* 上述是自己实例化线程池,
     *
     * 也可以用Executors创建线程池的工厂类。
     * 常用方法如下:
     *
     * new FixedThreadPool(int nThreads)
     * 创建一个固定大小、任务队列容量无界的线程池。
     * 核心线程数=最大线程数
     *
     * new CachedThreadPool()
     * 创建的是一个线程数量大小无界的缓冲线程池。
     * 它的任务队列是一个同步队列。任务加入到池中:
     * 如果池中有空闲线程,则用空闲线程执行。
     * 如果没有,则创建新线程执行;
     * 池中的线程空闲超过60s,将被销毁释放。
     * 线程数随任务的多少变化。
     * ******适用于执行耗时小的异步任务********。
     * 池的核心线程数=0,最大线程数=Integer.MAX_VALUE;
     *
     * new SingleThreadExecutor():
     * 只有一个线程来执行无界任务队列的单一线程池。
     * 该线程池确保任务按加入的顺序一个个依次执行。
     * 当唯一的线程因任务异常终止时,将创建一个新的线程来继续执行后续的任务。
     * 与newFixedThreadPool(1)的区别在于,
     * 单一线程池大小在newSingleThreadExecutor方法中硬编码,不能再改变。
     *
     * new ScheduledThreadPool(int corePoolSize):
     * *********能定时执行任务的线程池***********。
     * 该池的核心线程数由参数指定,最大线程数=Integer.MAX_VALUE;
     *
     */

    private void threadPoolExecutorTest3 ()throws Exception{
        tpe=new ThreadPoolExecutor(5,5,0,TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

    /*
     * SynchronousQueue是同步队列,
     * 但它实际上不是一个真正的队列,
     * 因为它不会为队列中元素维护存储空间。
     * 与其他队列不同,它维护一组线程,
     * 这些线程在等待着把元素加入或移出队列;
     * 在用SynchronousQueue时,
     * 如果客户端代码向线程池提交任务,
     * 而线程池没有空闲的线程从SynchronousQueue队列实例中取一个任务,
     * 那么相应的offer方法就会失效,即任务没有被存入工作队列SynchronousQueue;
     * 此时,线程池执行器会新建一个新的工作线程用于处理这个入队列失败任务,
     * 假设此时线程池大小还没到MaximumPoolSize;
     * 和Executors.newCachedThreadPool()一样的;
     * 这种方法适用于不确定需要多少线程的情况,参数二不建议这么大;
     */
    //缓存线程池,没法预估线程数量,线程数量时多时少的情况下;
    //使用需谨慎;
    private void threadPoolExecutorTest4 ()throws Exception{
        tpe=new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

    /*
     * 定时执行线程池信息,3s后执行,一次性任务,到点就执行;
     * 核心线程数量为5,最大数量Integer.MAX_VALUE,
     * DelayedWorkQueue延时队列,
     * 超出核心线程数量的存活时间:0s;
     */
    private void threadPoolExecutorTest5()throws Exception{
        ScheduledThreadPoolExecutor tpe=new ScheduledThreadPoolExecutor(5);
        tpe.schedule(new Runnable(){
            @Override
            public void run(){
                System.out.println("任务被执行,现在时间:"+System.currentTimeMillis());
            }
        }, 3000, TimeUnit.MILLISECONDS);
        //3s后开始执行任务,如果3s后要处理的任务特别多,
        //所以要设大一点核心线程数量;
        //所谓存储定时任务其实就是将任务存储到延时队列里;
        System.out.println("定时任务,提交成功,时间是:"+System.currentTimeMillis()+",当前线程池数量是:"+tpe.getPoolSize());
    }

    /*
     * 适用场景:每隔一天需要做什么一类。。。
     * 周期性执行一个任务,线程池提供了两种调度方式:
     * 测试场景:提交的任务需要3s后才能执行完毕,看两种不同调度方式的区别;
     * 效果1:提交后,2s后开始第一次执行,然后每间隔1s,固定执行一次
     * (如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
     */
    private void threadPoolExecutorTest6()throws Exception{
        ScheduledThreadPoolExecutor tpe=new ScheduledThreadPoolExecutor(5);
        tpe.scheduleAtFixedRate(new Runnable(){
            @Override
            public void run(){
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                System.out.println("任务1被执行,现在时间:"+System.currentTimeMillis());
            }
        }, 2000, 1000,TimeUnit.MILLISECONDS);
        /*
         * 创建并执行一个周期性操作,
         * 该操作将在给定的初始延迟后首先启用,然后在给定的周期内启用;
         * 也就是说,执行将在initialDelay,initialDelay + period,
         * initialDelay + 2 * period等之后开始,
         * 如果任务的任何执行遇到异常,则后续执行将被禁止,
         * 否则任务将仅通过执行者的取消或终止而终止 。
         * 如果此任务的任何执行花费的时间超过其周期,
         * 则后续执行可能会延迟开始,但不会同时执行。
         * period-连续执行之间的时间间隔
         * unit-initialDelay和period参数的时间单位
         * 返回:ScheduledFuture,表示任务即将完成,
         * 其get()方法将在取消时引发异常
         */

        System.out.println("定时任务,提交成功,时间是:"+System.currentTimeMillis()+",当前线程池数量是:"+tpe.getPoolSize());
    }

    private void threadPoolExecutorTest7()throws Exception{
        //任务周期3s,执行间隔1s,任务周期大于执行间隔;
        //上面的是忽略执行间隔,这个是不忽略时间间隔;
        //上面的:3s 开始执行 3s 开始执行;
        //这个: 3s 1s 开始执行  3s 1s 开始执行
        ScheduledThreadPoolExecutor tpe=new ScheduledThreadPoolExecutor(5);
        tpe.scheduleWithFixedDelay(new Runnable(){
            @Override
            public void run(){
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                System.out.println("任务2被执行,现在时间:"+System.currentTimeMillis());
            }
        }, 2000, 1000,TimeUnit.MILLISECONDS);

        System.out.println("定时任务,提交成功,时间是:"+System.currentTimeMillis()+",当前线程池数量是:"+tpe.getPoolSize());
    }

    private void threadPoolExecutorTest8() throws Exception{
        //13个容量;
        ThreadPoolExecutor/*执行者*/ tpe=new ThreadPoolExecutor(5,10,5,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(3),new RejectedExecutionHandler(){
            @Override
            public void rejectedExecution(Runnable r,ThreadPoolExecutor exec){
                System.err.println("有任务被拒绝执行了");
            }
        });
        for(int i=0;i<15;i++){
            int n=i;
            tpe.submit(new taskrunnable(n));
            System.out.println("任务提交成功:"+i);
        }
        Thread.sleep(5000L);
        tpe.shutdown();
        /*
         * shutdown的终止是终止接收新任务,
         * 但是会把接收过的任务执行完
         * */

        tpe.submit(new Runnable(){
            @Override
            public void run(){
                System.out.println("追加一个任务");
            }
        });
    }

    private void threadPoolExecutorTest9() throws Exception{
        ThreadPoolExecutor/*执行者*/ tpe=new ThreadPoolExecutor(5,10,5,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(3),new RejectedExecutionHandler(){
            @Override
            public void rejectedExecution(Runnable r,ThreadPoolExecutor exec){
                System.err.println("有任务被拒绝执行了");
            }
        });
        for(int i=0;i<15;i++){
            int n=i;
            tpe.submit(new taskrunnable(n));
            System.out.println("任务提交成功:"+i);
        }
        Thread.sleep(1000L);
        //返回的是处于等待队列中的任务;
        List<Runnable> shutdown=tpe.shutdownNow();
        /*
         * shutdown的终止是终止接收新任务,
         * 但是会把接收过的任务执行完
         * */

        tpe.submit(new Runnable(){
            @Override
            public void run(){
                System.out.println("追加一个任务");
            }
        });


        System.out.println("未结束的任务有:"+shutdown.size());

    }
    public static void main(String[]args){
        ThreadPoolExecutorTest t1=new ThreadPoolExecutorTest();
//		t1.dealtask();
        try {
            t1.threadPoolExecutorTest6();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    class rejectedexehandler implements RejectedExecutionHandler{
        @Override
        public void rejectedExecution(Runnable r,ThreadPoolExecutor exec){
            System.err.println("有任务被拒绝执行了");
        }
    }

    /*
     * 如何确定合适数量的线程?
     *
     * 计算型任务:CPU核数量的1-2倍;
     * CPU密集型程序:最佳线程数=CPU核数+1;
     *
     * I/0型任务:相对比计算型任务,需多一些线程,
     * 要根据具体的I/O阻塞时长进行考量决定;
     * 如TOMCAT中默认的最大线程数为200;
     * 也可考虑根据需要在一个最小数量和最大数量间自动增减线程数;
     *
     * I/O密集型程序:
     * 最佳线程数=(1/cpu利用率)*CPU核心数=(1+(I/0耗时/CPU耗时))*CPU核心数;
     * 可以用APM工具如SkyWalking或者CAT或者ZIPKIN去计算CPU利用率;
     *
     * CPU利用率得到80%才可以说明CPU合理利用;
     * 或者用new CachedThreadPool()进行线程池创建处理;
     */
}


import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;

class TaskCallable implements Callable<List<String>> {
    private int serverPort;
    private String command;

    public TaskCallable(String serverPort,String command){
        this.serverPort=Integer.parseInt(serverPort);
        this.command=command;
    }
    @Override
    public List<String> call() throws Exception {
        List<String>result=new LinkedList<>();
        for(int i=0;i<5;i++){
            String s=Thread.currentThread().getName()+i+"";
            result.add(s);
        }
        return result;
    }
}

public class CallableTest {
    public void CallableSubmitTest(){
        ExecutorService pool= Executors.newFixedThreadPool(2);
        List<Future<List<String>>> futures=new ArrayList<>();

        for(int i=0;i<5;i++){
            Callable<List<String>>callable=new TaskCallable(i+"","123");
            Future<List<String>> future = pool.submit(callable);
            futures.add(future);
        }
        pool.shutdown();
        for(int i=0;i<futures.size();i++){
            if(futures.get(i).isDone()) {
                try {
                    System.out.println(futures.get(i).get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args){
        CallableTest callableTest=new CallableTest();
        callableTest.CallableSubmitTest();
    }
}

3.线程池的几种状态:

  • RUNNING 自然是运行状态,指可以接受任务且可以执行队列里的任务;
  • SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕。
  • STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务(中断这个不一定能彻底中断)。
  • TIDYING 所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
  • TERMINATED 终止状态,当TIDYING 时执行 terminated() 后会更新为这个状态,表示回调函数已经完成。

4.线程池的底层实现原理?

4.1 首先了解一下生产者消费者模型

import java.util.LinkedList;
import java.util.List;

/*
 * 临界资源池问题;
 *
 * 生产者生产逻辑:
 * 通过一个生产标记判断是否需要生产产品;
 * 需要生产,生产,并通知消费者消费;
 * 不需要生产,等待;
 *
 * 消费者逻辑:
 * 通过资源池情况判断是否有产品可以消费;
 * 有的话,获取并消费;
 * 不可以消费,等待;
 */
public class T08_ProduceAndCosume {
    public static void main(String[]args){
        //非常经典的多线程并发存在的问题;
        //之所以存在并发问题,是因为生产者类实例和消费者类实例共同用一个资源池实例对象,
        //资源池实例对象是被二者
        ProductPool pool=new ProductPool(15);
        Productor p=new Productor(pool);
        p.start();
        Consumer c=new Consumer(pool);
        c.start();


    }
}

class Product{
    private String name;

    public String getname(){
        return name;
    }
    public void setname(String name){
        this.name=name;
    }
    public Product(String name){
        this.name=name;
    }
}

class ProductPool{
    private List<Product>productlist;

    private int maxsize=0;

    public ProductPool(int maxsize){
        this.productlist=new LinkedList<Product>();
        this.maxsize=maxsize;
    }
    public synchronized int getpoolsize(){
        return this.productlist.size();
    }
    public void push(Product product){
        synchronized("A") {
            if(this.productlist.size()==maxsize){
                try {
                    "A".wait();
                    //wait的是push函数代码;
                    //所以就可以理解为什么是用同步方法即锁对象是this了,因为被挂起的是这个类的对象即统一的pool;
                    System.out.println("this.push.name="+this.getClass());
                    //将产品添加到集合中
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            this.productlist.add(product);


            "A".notifyAll();
        }

    }


    public Product pop(){
        synchronized("A") {
            while(this.productlist.size()==0){
                try {//为了防止伪唤醒,即不是notifyall唤醒的情况,最好把if改为while。
                    "A".wait();
                    System.out.println("this.pop.name="+this.getClass());
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                return null;
            }
            Product product=this.productlist.remove(0);

            "A".notifyAll();

            return product;
        }
    }
}

class Productor extends Thread{

    private ProductPool pool;

    public Productor(ProductPool pool){
        this.pool=pool;
    }

    @Override
    public void run(){
        int i=0;
        while(true){
            String name=(i++)+"产品";
            System.out.println("生产了一件产品:"+name);
            Product product =new Product(name);
            this.pool.push(product);
            if(i==100){
                break;
            }
        }
    }
}
class Consumer extends Thread{

    private ProductPool pool;

    public Consumer(ProductPool pool){
        this.pool=pool;
    }

    @Override
    public void run(){
        while(true){
            Product product=this.pool.pop();

            if(product!=null){
                System.out.println("消费者消费了产品"+product.getname());
            }else{
                try {
                    this.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if(this.pool.getpoolsize()==0){
                    break;
                }
            }
        }
    }
}

4.2 线程池的核心

简单看了一下源码,线程池的状态是通过Atomic Integer实现的,所谓线程池就是一个HashSet,HashSet里面存储了一堆Worker对象,Worker是一个实现了Runnbale接口的类,每次来一个任务,如果需要增加线程,就要调用ReentrantLock的对象mainlock进行该线程池ThreadPoolExecutor对象的加锁和解锁,来向hashset对象里面添加这个对应的工作线程worker。具体原理待补充。

execute –> addWorker –>runworker (getTask)
线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。
从Woker类的构造方法实现可以发现:线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。
firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;

Java线程池实现原理与源码解析(jdk1.8)_猪杂汤饭的博客-CSDN博客_java线程池源码

四种线程池拒绝策略_onEars的博客-CSDN博客_线程池拒绝策略

5.线程安全是什么意思?

线程安全是指某个函数函数库在多线程环境中被调用时,可以正确的处理多个线程之间的共享变量,让程序功能正确完成。

5.1 Servlet的线程安全性

Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,是不能保证共享变量的线程安全性的。

5.2 SpringMVC的Controller是线程安全的吗?

同样不是,和Servlet类似,但是和前者一样都可以用ThreadLocal来处理问题。

6.乐观锁和悲观锁

乐观锁:对于并发间操作产生的线程安全状态抱有乐观态度。乐观锁认为竞争不总是会发生,因此它不需要持有锁,一般是通过CAS操作解决并发出现的问题。

悲观锁:假设并发操作会出现线程安全问题,即总是有多个线程竞争资源,所以会有一个独占的锁,像synchronized。

7.volatile实现原理

7.1 volatile作用

  • 保证线程的可见性;
  • 禁止指令重排序;

7.2 volatile对应底层实现原理

  • 通过lock前缀指令在多核处理器实现以下两点:

(1)将当前处理器缓存的数据写回到系统内存;

对volatile变量进行写操作,JVM会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存;

(2)这个写回操作会引起其他CPU里缓存了该内存地址的数据无效;

通过缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期了。当处理器发现自己缓存行对应的内存地址内容被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

  • volatile通过内存屏障防止指令重排

主要是借助JMM在编译期和处理器层面限制指令重排序,java的内存屏障插入策略是保守策略,策略如下:

普通写-storestore-volatile写-storeload-volatile读-loadload-普通读-loadstore-普通读-普通写。。。

7.3 相关代码


public class 单例模式 {
}
//懒汉式单例;
class Singleton1 {
    private static volatile Singleton1 singleton=null;
    /**
     * 构造函数私有,禁止外部实例化
     * DCL双重检查;
     */
    private Singleton1() {};
    public static Singleton1 getInstance() {
        if (singleton == null) {
            synchronized (Singleton1.class) {
                if (singleton == null) {
                    singleton = new Singleton1();
                }
            }
        }
        return singleton;
    }
}
//饿汉式单例;本身是线程安全的,不需要像懒汉式单例一样考虑线程安全问题;
class Singleton2{
    private static final Singleton2 singleton=new Singleton2();
    private Singleton2(){}
    public static Singleton2 getInstance(){
        return singleton;
    }
}

8.synchronized的底层原理

8.1 对代码块同步monitor表象原理;

synchronized每个对象都有一个监视器锁monitor,当monitor被占用时就会处于锁定状态。

线程所谓的加锁其实就是执行monitorenter指令尝试获取monitor的使用权,过程如下:

如果monitor进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

如果线程已经占有monitor,则重新进入,则进入monitor的数量加一。

如果其他线程已经占用了该monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

所谓对象其实就是括号里的对象。上述描述可以推断出:

synchronized是可重入锁;

8.2 Synchronized的使用

调用指令将会检查方法的ACC_SYNCHRONIZED访问标识是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。

  • 修饰实例方法,对当前实例对象加锁;
  • 修饰静态方法,对当前类的Class对象加锁;
  • 修饰代码块,对synchronized括号内的对象加锁,里面如果是类.class或者类的静态对象,则对当前类加锁,若是普通对象(包括字符串)就是对实例对象加锁。
  • 个人猜测这个对象的内存结构有关。
  • 相关代码:

import lombok.SneakyThrows;

public class SynchronizedTest {
    //锁标志是字符串,锁住的是对象,是对象锁;
    public static void main(String[] args){
        Runnable A1=new Runnable(){
            @SneakyThrows
            @Override
            public void run() {

            }
        };
//        Thread t1=new Thread(A,"t1");
//        Thread t2=new Thread(A,"t2");
//        t1.start();
//        t2.start();
        Shared s=new Shared();
        A a=new A(s);
        B b=new B(s);
        a.start();
        b.start();

    }
}
class A extends Thread{
    Shared s1;
    public A(Shared s){
        s1=s;
    }
    @SneakyThrows
    public void run(){
        s1.setAup();
    }
}

class B extends Thread{
    Shared s2;

    public B(Shared s){
        s2=s;
    }

    @SneakyThrows
    public void run(){
        s2.setAup();
    }
}
class Shared{
    int a=0;
    public void setAup() throws InterruptedException {
        synchronized ("a"){
            a++;
            System.out.println(Thread.currentThread().getName());
            System.out.println(a);
            System.out.println(Thread.currentThread().getName());
        }
    }
}

synchronized和lock属于同步阻塞。

使用CAS属于同步非阻塞;

8.2.1 同步与异步

同步就是不停的查询等待结果;

异步就是等服务端通知就好,不用主动查询获取结果。

8.2.2 阻塞与非阻塞

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的 accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前 线程会被挂起,直到得到结果之后才会返回。

非阻塞就是可以继续去做别的事情;

8.2.3 补充

网络中获取数据的读操作步骤:
等待数据准备。
数据从内核空间拷贝到用户空间。

同步与异步:
同步与异步是针对应用程序与内核的交互而言。也就是上图的read操作,从缓存中读取数据,如果缓存中数据还没有准备好,如果是同步操作,它会一直等待,直到操作完成。如果是异步操作,那么它会去做别的事情,等待数据准备好,内核通知它,它再去读取数据。

同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。
异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO操作交给内核来处理,完成后内核通知进程IO完成。
同步和异步是相对于操作结果来说,会不会等待结果返回。
阻塞与非阻塞:
应用进程请求IO操作时,如果数据未准备好,如果请求立即返回就是非阻塞,不立即返回就是阻塞。简单来说,就是做一件事如果不能立即获得返回,需要等待,就是阻塞,否则可以理解为非阻塞。

阻塞和非阻塞是相对于线程是否被阻塞。
异步/同步和阻塞/非阻塞的区别:
其实,这两者存在本质区别,他们的修饰对象是不同的。 阻塞和非阻塞是指进程访问的数据如果尚未准备就绪,进程是否需要等待,简单来说这相当于函数内部的实现区别 ,也就是未就绪时是直接返回还是等待就绪。
而同步和异步是指访问数据的机制 ,同步一般指主动请求并等待IO操作完毕的方式 ,当数据就绪后再读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知,这可以使进程再数据读写时也不阻塞。

同步/异步 与 阻塞/非阻塞的组合方式
故事:老王烧开水
出场人物:老王,两把水壶(水壶,响水壶)

同步阻塞: 效率是最低的,实际程序中,就是fd未设置O_NONBLOCK 标志位的read/write操作。
老王用水壶烧水,并且站在那里(阻塞),不管水开没开,每隔一定时间看看水开了没(同步->轮询)。
同步非阻塞: 实际上效率是低下的,注意对fd设置O_NONBLOCK 标志位。
老王用水壶烧水,不再傻傻的站在那里,跑去做别的事情(非阻塞),但是还是会每个一段时间过来看看水开了没,没开就继续去做的事情(同步->轮询)。
异步阻塞: 异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息时被阻塞。比如select函数,假如传入的最后一个timeout函数为NULL,那么如果所关注的事件没有一个被触发,程序就会一直阻塞在select调用处。
老王用响水壶烧水,站在那里(阻塞),但是不再去看水开了没,而是等水开了,水壶会自动通知它(异步,内核通知进程)。
异步非阻塞: 效率更高,注册一个回调函数,就可以去做别的事情。
老王用响水壶烧水。跑去做别的事情(非阻塞),等待响水壶烧开水自动通知它(异步,内核通知进程)
socket的fd是什么?
fd是(file descriptor/文件描述符) ,这种一般是BSD Socket的用法,用在Unix/linux系统上。在Unix/linux系统下,一个Socket句柄,可以看做是一个文件,在socket上收发数据,相当于读一个文件进行读写,所以一个socket句柄,通常也用表示文件句柄的fd来表示。

缓存IO
缓存IO又被称为标准IO,大多数文件系统的默认IO操作都是缓存IO。 在linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存IO缺点:
数据在传输过程中需要在应用程序地址空间和内核空间进行多次数据拷贝操作,这些数据拷贝带来的CPU以及内存开销是非常大的。

8.3 Synchronized的优化

8.3.1 普通对象内存结构

  • 对象头(markword[关于synchronized的所有信息都在这部分,8个字节];
  • 类型指针class pointer[属于哪个类,存储大小4个字节]) ;
  • 实例数据instance data[成员变量所在的存储,存储初始化为0字节] ;
  • 对齐padding[补足八个字节的倍数]

可以通过

Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

查看对象的内存结构。

输出结果为:

OFFSET  SIZE   TYPE DESCRIPTION   VALUE
0     4        (object header)    01 00 00 00 (00000001 00000000 00000000 00000000) (1)

4     4        (object header)    00 00 00 00 (00000000 00000000 00000000 00000000) (0)
               前两部分markword 
8     4        (object header)    e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)    这是classpointer           
12     4        (loss due to the next object alignment)这是补足8的倍数;

//Instance size: 16 bytes

//所以Object o=new Object();在内存中占16个字节;

//java -XX:+PrintCommandLineFlags -version
//D:\install\MavenRepository>java -XX:+PrintCommandLineFlags -version
//-XX:InitialHeapSize=131599232 -XX:MaxHeapSize=2105587712
//-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers
//-XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
//java version "1.8.0_221"
//Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
//Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
//现在电脑都是64位,java也是64位,指针长度也应该是64位,
//但是默认开启了compressedclasspointer,指针长度压缩成了4个字节的大小;
//普通对象指针默认也是压缩,因为-XX:+UseCompressedOops,所以也是4个字节;

锁的信息记录在对象里,其实就是在对象的Markword里;内含指向栈中所记录的指针或者指向重量级锁的指针。对象头的MarkWord中的LockRecord指向monitor的起始地址。下图为markword内部关于锁的信息的存储:

8.3.2 锁升级

锁升级其实就是锁的优化。

锁升级包括无锁-偏向锁-轻量锁-重量级锁几步。

synchronized (o){
//            //执行大括号里的代码的时候锁定o这个对象,
//            //锁的信息记录在对象里,其实就是在对象的Markword里;内含指向栈中所记录的指针或 //            //者指向重量级锁的指针。对象头的MarkWord中的LockWord指向monitor的起始地址。
//            //这部分代码只是变成了互斥区;
//            //互斥区和临界区的区别是不是唯一性?
}

偏向锁:当前没有线程争抢的情况,没有线程争抢,所以本线程就将当前线程指针指向占有该对象的线程。

轻量级锁、自旋锁:有线程争抢情况。

如果有线程要争抢这个对象的锁,偏向锁升级为这种锁。线程释放偏向锁,每个线程都有自己的线程栈,每个线程栈都可以生成自己的Lock Record,线程争抢看哪个线程能让指针指向自己线程栈中的Lock Record。因为自旋锁很占CPU,所以当有线程自旋超过10次或者自旋线程数超过CPU核数的一半的时候, java高版本中加入自适应自选adaptive self spinning,JVM自己控制是否升级为重量级锁;

重量级锁:指向互斥量(重量级锁)的指针; 升级为重量级锁,向操作系统申请资源,linux mutex,CPU从3级到0级系统调用, 线程挂起,进入等待队列,阻塞等待操作系统的调度,此时不占用CPU,然后再映射回用户空间;GC标记信息(CMS过程用到的标记信息)。

8.3.3 锁粗化和锁消除

  • 锁粗化:遇到不停加锁减锁的情况,就把锁的代码范围扩大,这样就不用频繁加锁解锁了。
  • 锁消除:如果操作的stringbuffer对象没被其他线程争抢引用,所以锁就不需要了,所以系统自动进行锁消除;stringbuffer之所以是线程安全的,是因为它的关键方法都是被synchronized修饰过的。

8.4 volatile和synchronized的区别:

锁具有两种特性:互斥性和可见性;

  • volatile不加锁,synchronized加锁;
  • volatile仅能作用于变量级别,synchronized则可以作用于变量、代码段、方法级别。
  • volatile仅能保证变量的可见性,synchronized则可以同时保证变量的可见性和原子性。例如volatile变量的自增操作就没有原子性。
  • volatile不会造成线程的阻塞,synchronized可能会。

9.ReentrantLock相关

9.1 底层原理

以ReentrantLock为例理解AQS原理_weixin_42196085的博客-CSDN博客_cas和aqs

9.2 相关代码


import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
就是一个方法里,外层方法占用了锁,但是里面还有方法要获得锁,
如果不是重入锁,程序无法继续运行,陷入死锁,是重入锁就继续执行。
我的理解是,同一把锁,外层方法获得锁后,内层方法还可以获得锁;
看生产环境,没有什么好的坏的。
 */
public class ReenttrantlockTest {
    public void AchieveBasicFunctionOfLock(){
        Runnable t1=new MyThread1();
        new Thread(t1,"t1").start();
        new Thread(t1,"t2").start();
    }
    public void FairLockTest(){
        //公平锁模式开启
        Runnable t1=new MyThread1(true);
        new Thread(t1,"t1").start();
        new Thread(t1,"t2").start();
        new Thread(t1,"t3").start();
        new Thread(t1,"t4").start();
        new Thread(t1,"t5").start();
        /*输出:
        上锁t1
        解锁t1
        上锁t3
        解锁t3
        上锁t2
        解锁t2
        上锁t4
        解锁t4
        上锁t5
        解锁t5
        上锁t1
        解锁t1
        上锁t3
        解锁t3
        上锁t2
        解锁t2
        上锁t4
        解锁t4
        上锁t5
        解锁t5
        上锁t1
        解锁t1
        上锁t3
        解锁t3
        上锁t2
        解锁t2
        上锁t4
        解锁t4
        上锁t5
        解锁t5
         */
        /*
        AQS虚拟同步队列+CAS比较交换;
         */
    }

    /*
    死锁解决方案一:
    响应中断就是一个线程获取不到锁,不会傻傻的一直等下去,ReentrantLock会给予一个中断回应。
    因此可以通过Interrupt来解除死锁;
     */
    public void ReactToInterruptTest(){
        Lock lock1=new ReentrantLock();
        Lock lock2=new ReentrantLock();
        Thread thread1=new Thread(
                new ThreadDemo(lock1,lock2)
        );
        Thread thread2=new Thread(
                new ThreadDemo(lock2,lock1)
        );
        thread1.start();
        thread2.start();
        thread1.interrupt();
    }

    /*死锁解决方案二:
    限时等待是通过我们的tryLock方法来实现,
    可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:
    true表示获取锁成功,false表示获取锁失败。
    我们可以将这种方法用来解决死锁问题。
     */
    public void LimitTimeWaitTest(){
        Lock lock1=new ReentrantLock();
        Lock lock2=new ReentrantLock();
        Thread thread1=new Thread(
                new ThreadDemo1(lock1,lock2)
        );
        Thread thread2=new Thread(
                new ThreadDemo1(lock2,lock1)
        );
        thread1.start();
        thread2.start();
    }
    public static void main(String[] arg){
        ReenttrantlockTest reenttrantlockTest=new ReenttrantlockTest();
        reenttrantlockTest.LimitTimeWaitTest();
    }
}

class MyThread1 implements Runnable {

    private Lock lock=new ReentrantLock();

    public MyThread1(boolean fairornow){
        lock=new ReentrantLock();
    }
    public MyThread1(){}

    public void run() {
        for(int i=0;i<5;i++){
            try{
                lock.lock();
                System.out.println("上锁"+Thread.currentThread().getName());
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                lock.unlock();
                System.out.println("解锁"+Thread.currentThread().getName());
            }
        }
    }
}

class ThreadDemo implements Runnable{
    Lock lock1;
    Lock lock2;

    public ThreadDemo(Lock firstLock,Lock secondLock){
        this.lock1=firstLock;
        this.lock2=secondLock;
    }
    @Override
    public void run() {
        try {
            lock1.lockInterruptibly();
            TimeUnit.MILLISECONDS.sleep(50);
            lock2.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock1.unlock();
            lock2.unlock();
            System.out.println("解锁:"+Thread.currentThread().getName());
        }
    }

}

class ThreadDemo1 implements Runnable{
    Lock lock1;
    Lock lock2;

    public ThreadDemo1(Lock firstLock,Lock secondLock){
        this.lock1=firstLock;
        this.lock2=secondLock;
    }
    @Override
    public void run() {
        /*限时等待通过我们的tryLock方法来实现,
        可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:
        true表示获取锁成功,false表示获取锁失败。
         */
        try {
            /*
            可以设置tryLock的超时等待时间tryLock(long timeout,TimeUnit unit),
            也就是说一个线程在指定的时间内没有获取锁,那就会返回false,就可以再去做其他事了。
             */
            if(!lock1.tryLock(1,TimeUnit.SECONDS)){
                TimeUnit.MILLISECONDS.sleep(10);
                System.out.println("加锁1:"+Thread.currentThread().getName());
            }
            //一会儿可以将参数改成100,就不会死锁;
            //现在输出结果是:
            /*
            加锁2:Thread-0
            加锁2:Thread-1
            Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
                at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
                at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
                at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
                at com.javabasis.面试.多线程.ThreadDemo1.run(ReenttrantlockTest.java:187)
                at java.lang.Thread.run(Thread.java:748)
            java.lang.IllegalMonitorStateException
                at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
                at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
                at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
                at com.javabasis.面试.多线程.ThreadDemo1.run(ReenttrantlockTest.java:187)
                at java.lang.Thread.run(Thread.java:748)
             */
            if(!lock2.tryLock(1000,TimeUnit.MILLISECONDS)){
                TimeUnit.MILLISECONDS.sleep(10);
                System.out.println("加锁2:"+Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock1.unlock();
            lock2.unlock();
            System.out.println("解锁:"+Thread.currentThread().getName());
        }
    }

}

import com.sun.xml.internal.ws.spi.db.RepeatedElementBridge;
import lombok.SneakyThrows;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//问题:
//        三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现

public class LockConditionTest {
    static volatile Lock lock=new ReentrantLock();//默认不公平;
    Lock lock1=new ReentrantLock(true);//默认公平;
    static volatile Condition condition1=lock.newCondition();
    static volatile Condition condition2=lock.newCondition();
    static volatile Condition condition0=lock.newCondition();
    public static volatile int times=0;

    class RunnableA implements Runnable{

        @Override
        public void run() {
            while(true) {
                try {
                    lock.lock();
                    times=0;
                    Thread.sleep(1000);
                    System.out.println("A");
                    times++;
                    condition1.signal();
                    condition2.signal();
                    condition0.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    class RunnableB implements Runnable{

        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    condition1.await();
                    Thread.sleep(1000);
                    System.out.println("B");
                    times++;
                    if(times==2){
                        condition0.signal();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    class RunnableC implements Runnable{

        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    condition2.await();
                    Thread.sleep(1000);
                    System.out.println("C");
                    times++;
                    if(times==2){
                        condition0.signal();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args){
        //因为不是静态内部类,也没有传一个共同的参数,所以只能加lock,condition,times全设为static volatile了,切记;
        Thread A=new Thread(new LockConditionTest().new RunnableA());
        Thread B=new Thread(new LockConditionTest().new RunnableB());
        Thread C=new Thread(new LockConditionTest().new RunnableC());
//        A.setPriority(5);
//        B.setPriority(7);
//        C.setPriority(7)
//        不用它是因为优先级不会起到决定性作用,只是辅助性作用而已;
        B.start();
        C.start();//因为没有condition的判断条件,所以B,C必须在A前面启动,要不然会发生阻塞;
        A.start();
    }
}

9.3 ReentrantLock 和 Synchronized 区别

  • ReentrantLock可以实现公平锁、锁中断、锁超时;
  • ReentrantLock可以通过condition变量实现明确唤醒某个线程,而不是只能全部唤醒然后通过其他方法限制争抢。
  • ReentrantLock和Synchronized的底层实现原理不一样。

10.死锁的形成条件和避免方法:

10.1 形成条件:

进程对资源的独占性,不可抢占性,请求与等待性,循环等待;这将导致不同进程竞争资源时出现僵持状态。

10.2 避免方法:

(1)控制死锁的加锁顺序,银行家算法,银行家算法需要事先知道自己需要多少资源和系统可以给多少资源,这是问题;

(2)设置获取锁的超时时间;

(3)强制中断线程,interrupt(java),(有对应sql命令)数据库;

11.红黑树和平衡二叉树的区别:

红黑树和AVL树(平衡二叉树)区别_Charles_yy的博客-CSDN博客_红黑树和平衡二叉树区别

一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树

12.进程和线程的区别:

进程时操作系统分配资源的最小单位,线程是CPU进行调度的最小单位。

线程是进程的一部分,线程的资源来自于进程,因此进程切换的开销(程序上下文)要远比线程(程序计数器+虚拟机栈+本地方法栈)大。

系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

13.创建线程的方法

有 以下几种方式可以用来创建线程:

  • 继承 Thread
  • 实现 Runnable 接口
  • 应用程序可以使用 Executor 框架来创建线程池
  • 实现 Callable 接口

实现 Runnable 接口比继承 Thread 类所具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免 java 中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类
  • runnable 实现线程可以对线程进行复用,因为 runnable 是轻量级的对象,重复 new 不会耗费太大资源,而 Thread 则不然,它是重量级对象,而且线程执行完就结束了,无法再次利用。

不可以通过调用Thread的run()方法启动线程,原因:

如果我们调用了 Thread 的 run()方法,它的行为就会和普通的方 法一样,会在当前线程中执行。为了在新的线程中执行我们的代码,必须使用 Thread.start()方法。

14.线程状态

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。 该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。

3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行 sleep()join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁,sleep 必须捕获异常,而 waitnotify notifyAll 不需要捕获异常)

5、死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

15.集合相关内容

15.1 HashMap的底层实现原理

15.1.1 概述

HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 键和null值,因为key不允许重复,因此只能有一个键为null。另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。HashMap的扩容操作是一项很耗时的任务,所以如果能估算Map的容量,最好给它一个默认初始值,避免进行多次扩容。HashMap的线程是不安全的,多线程环境中推荐是ConcurrentHashMap。

15.1.2 HashMap和HashashTable的区别

  • 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全;

  • 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很麻烦。注意HashMap以null作为key时,总是存储在table数组的第一个节点上;
  • HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类;
  • Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模;HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模;

15.1.3 HashMap的底层存储

1)JDK1.7 版本

HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。

数组存储区间是连续的,占用内存严重,故空间复杂度较大。但数组的查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表存储区间离散,占用内存比较宽松,故空间复杂度较小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

Entry数组初始容量为16,扩容的容量是2的次幂;

2)JDK 1.8的改变

HashMap变化为数组+链表+红黑树的存储方式,当链表长度超过阈值8时,将链表转换为红黑树,加上其他一些优化让HashMap在性能上进一步得到提升。

红黑树:平衡二叉查找树。当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树

3)HashMap的判断键值是否相等的原理

containsKey方法是先计算hash然后使用hash和table.length取摸得到index值,遍历table[index]元素查找是否包含key相同的值。即先通过hashcode()确定在数组中的下标,再通过equals确定链表中的key值是否相等。有就覆盖,没有就创建新的链表节点,1.8以后好像是Node节点,不是Entry节点了。

equals判断的默认实现函数是:

public boolean equals(Object obj) {
        return (this == obj);
}

所以对于基本数据类型判断值是否相等,对于包装类Integer有特殊情况,其他对象类里String也有特殊情况,因为String重写了hashcode函数和equals函数,对于自定义的类对象,也要自定义一下hashcode函数和equals函数。

4)扩容机制的对比

Java 1.7中是hashmap达到阈值(容量*负载因子0.75)便进行扩容,扩容总是二倍容量扩容。扩容方法是用一个容量更大的数组代替已有的容量小的数组,transfer()方法将原有的Entry数组中的元素拷贝到新的Entry数组中,然后重新计算每个元素在数组中的位置。

Java 8中扩容前提条件一样,但是扩容机制出现变化,因为容量是二倍扩展,所以只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变为“原索引+oldCap”,就是原位置再移动二次幂的位置。

15.2 CocurrentHashMap的底层实现原理

15.2.1 JDK 1.7

采用锁分段技术,有一个segment数组,容量默认设置为16。Segment 继承了 ReentrantLock,表明每个 segment 都可以当做一个锁。这样对每个 segment 中的数据需要同步操作的话都是使用每个 segment 容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的 segment。HashTable之所以效率低,是因为所有访问HashTable的线程都必须竞争一把锁,而这里有16把锁。

15.2.2 JDK 1.8

  • 抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
  • 当 table[i]下面的链表长度大于8时就转化为红黑树结构。

16.进程通信方式

管道、有名管道、信号量、消息队列、共享内存、套接字。

共享内存:共享内存可以说是最有用的进程间通信方式,也是最快的 IPC 形式。两个不同进程 AB共享内存的意思是,同一块物理内存被映射到进程 AB 各自的进程地址空间。进程 A 可以即时看到进程 B 对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

17.Scheduler.from

RxJava学习笔记(三)--- 线程调度Scheduler_编程的小蚂蚁的博客-CSDN博客

附录代码

1.ThreadLocal测试

package com.javabasis.面试.多线程;

import java.util.concurrent.TimeUnit;

public class ThreadLocal1_SerialNum {
    //既有共享数据又有各自隔离数据的一个普通类;
    // The next serial number to be assigned
    // static确保只加载一次;
    public static int nextSerialNum = 0;
    private static ThreadLocal serialNum = new ThreadLocal() {
        protected Object initialValue() {
            return new Integer(nextSerialNum++);
            //对每个调用该类的线程对应的ThreadLocal内容初始化;
            //可以看出来可以用来统计线程数量,用在网络中可以统计会话session数量;
        }
    };
    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ThreadLocal1_SerialNum.get());
                    System.out.println("test"+ThreadLocal1_SerialNum.nextSerialNum);
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ThreadLocal1_SerialNum.get());
                    System.out.println("test"+ThreadLocal1_SerialNum.nextSerialNum);
                }
            }
        }).start();
        //输出:
        /*
        1
        0
        test2
        test2
         */
    }
}

2.invoke测试


import jdk.nashorn.internal.codegen.CompilerConstants;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;

public class InvokeTest {
    ExecutorService pool= Executors.newSingleThreadExecutor();
    LinkedHashSet<Callable<String>> callableSet=new LinkedHashSet<>();
    public void InvokeAnyTest() throws ExecutionException, InterruptedException {
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task1";
            }
        });
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task2";
            }
        });
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task3";
            }
        });
        String result=pool.invokeAny(callableSet);
        //虽说是随机执行某一个,但是其实是默认执行第一个;
        System.out.println(result);
        pool.shutdown();
    }

    public void InvokeAllTest() throws ExecutionException, InterruptedException {
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task1";
            }
        });
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task2";
            }
        });
        callableSet.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Task3";
            }
        });
        List<Future<String>> futureList=pool.invokeAll(callableSet);
        Thread.sleep(1000);
        for(Future<String>future:futureList){
            System.out.println(future.get());
        }

        pool.shutdown();
        /*
        shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
        shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。
        之所以是大部分,而不是全部,是因为 interrupt()方法能力有限。
         */
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        InvokeTest invokeTest=new InvokeTest();
        invokeTest.InvokeAllTest();
    }

}

3.ForkJoinPool测试


import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.RecursiveTask;

public class ForkJoinPoolTest {
    public static void main(String[] args){
        ForkJoinPool forkJoinPool=new ForkJoinPool(5);

        ForkJoinPoolTest forkJoinPoolTest=new ForkJoinPoolTest();

        RecursiveActionTest recursiveActionTest=new RecursiveActionTest(20);

        forkJoinPool.invoke(recursiveActionTest);

        RecursiveTaskTest recursiveTaskTest=new RecursiveTaskTest(125);

        long result=forkJoinPool.invoke(recursiveTaskTest);

        System.out.println(result);

    }
}
class RecursiveActionTest extends RecursiveAction{

    private long workload=0;

    public RecursiveActionTest(long workload){
        this.workload=workload;
    }

    @Override
    protected void compute() {
        if(this.workload>16){
            System.out.println("splitting workload:"+this.workload);

            List<RecursiveActionTest>subtasks=new ArrayList<>();

            subtasks.addAll(createSubtasks());

            for(RecursiveAction subtask:subtasks){
                subtask.fork();
            }

        }else {
            System.out.println("doing workload myself:"+this.workload);
        }
    }
    public List<RecursiveActionTest>createSubtasks(){
        List<RecursiveActionTest>subtasks=new ArrayList<>();

        RecursiveActionTest subtask1=new RecursiveActionTest(this.workload/2);
        RecursiveActionTest subtask2=new RecursiveActionTest(this.workload/2);

        subtasks.add(subtask1);
        subtasks.add(subtask2);

        return subtasks;
    }
}

class RecursiveTaskTest extends  RecursiveTask<Long>{

    private long workload=0;

    public RecursiveTaskTest(long workload){
        this.workload=workload;
    }


    @Override
    protected Long compute() {
        if(this.workload>16){
            System.out.println("here");

            List<RecursiveTaskTest>subtasks=new ArrayList<>();
            subtasks.addAll(createSubTasks());

            for(RecursiveTaskTest subtask:subtasks){
                subtask.fork();
            }
            long result=0;
            for(RecursiveTaskTest subtask:subtasks){
                result=result+subtask.join();
            }
            return result;
        }else {
            //叶子结点的业务处理在这里;
            System.out.println("doing workload on its own"+this.workload);
            return workload*3;
        }
    }

    private List<RecursiveTaskTest>createSubTasks(){
        List<RecursiveTaskTest>subtasks=new ArrayList<>();

        RecursiveTaskTest subtask1=new RecursiveTaskTest(this.workload/2);
        RecursiveTaskTest subtask2=new RecursiveTaskTest(this.workload/2);

        subtasks.add(subtask1);
        subtasks.add(subtask2);

        return subtasks;
    }

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值