关于后端:一文吃透Java并发高频面试题

53次阅读

共计 23079 个字符,预计需要花费 58 分钟才能阅读完成。

内容摘自我的学习网站:topjavaer.cn

分享 50 道 Java 并发高频面试题

线程池

线程池:一个治理线程的池子。

为什么平时都是应用线程池创立线程,间接 new 一个线程不好吗?

嗯,手动创立线程有两个毛病

  1. 不受控危险
  2. 频繁创立开销大

为什么不受控

系统资源无限,每个人针对不同业务都能够手动创立线程,并且创立线程没有统一标准,比方创立的线程有没有名字等。当零碎运行起来,所有线程都在抢占资源,毫无规定,凌乱局面可想而知,不好管控。最全面的 Java 面试网站

频繁手动创立线程为什么开销会大?跟 new Object() 有什么差异?

尽管 Java 中万物皆对象,然而 new Thread() 创立一个线程和 new Object()还是有区别的。

new Object()过程如下:

  1. JVM 调配一块内存 M
  2. 在内存 M 上初始化该对象
  3. 将内存 M 的地址赋值给援用变量 obj

创立线程的过程如下:

  1. JVM 为一个线程栈分配内存,该栈为每个线程办法调用保留一个栈帧
  2. 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
  3. 每个线程取得一个程序计数器,用于记录以后虚拟机正在执行的线程指令地址
  4. 零碎创立一个与 Java 线程对应的本机线程
  5. 将与线程相干的描述符增加到 JVM 外部数据结构中
  6. 线程共享堆和办法区域

创立一个线程大略须要 1M 左右的空间(Java8,机器规格 2c8G)。可见,频繁手动创立 / 销毁线程的代价是十分大的。

为什么应用线程池?

  • 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
  • 进步响应速度。当工作达到时,能够不须要等到线程创立就能立刻执行。
  • 进步线程的可管理性。对立治理线程,防止零碎创立大量同类线程而导致耗费完内存。

线程池执行原理?

  1. 当线程池里存活的线程数小于外围线程数 corePoolSize 时,这时对于一个新提交的工作,线程池会创立一个线程去解决工作。当线程池外面存活的线程数小于等于外围线程数 corePoolSize 时,线程池外面的线程会始终存活着,就算闲暇工夫超过了keepAliveTime,线程也不会被销毁,而是始终阻塞在那里始终期待工作队列的工作来执行。
  2. 当线程池外面存活的线程数曾经等于 corePoolSize 了,这是对于一个新提交的工作,会被放进工作队列 workQueue 排队期待执行。
  3. 当线程池外面存活的线程数曾经等于 corePoolSize 了,并且工作队列也满了,假如maximumPoolSize>corePoolSize,这时如果再来新的工作,线程池就会持续创立新的线程来解决新的工作,晓得线程数达到maximumPoolSize,就不会再创立了。
  4. 如果以后的线程数达到了maximumPoolSize,并且工作队列也满了,如果还有新的工作过去,那就间接采纳回绝策略进行解决。默认的回绝策略是抛出一个 RejectedExecutionException 异样。

本文曾经收录到 Github 仓库,该仓库蕴含 计算机根底、Java 根底、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享 等外围知识点,欢送 star~

Github 地址

如果拜访不了 Github,能够拜访 gitee 地址。

gitee 地址

线程池参数有哪些?

ThreadPoolExecutor 的通用构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

1、corePoolSize:当有新工作时,如果线程池中线程数没有达到线程池的根本大小,则会创立新的线程执行工作,否则将工作放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该思考调大 corePoolSize。

2、maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创立新的线程运行工作。否则依据回绝策略解决新工作。非核心线程相似于长期借来的资源,这些线程在闲暇工夫超过 keepAliveTime 之后,就应该退出,防止资源节约。

3、BlockingQueue:存储期待运行的工作。

4、keepAliveTime非核心线程 闲暇后,放弃存活的工夫,此参数只对非核心线程无效。设置为 0,示意多余的闲暇线程会被立刻终止。

5、TimeUnit:工夫单位

TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS

6、ThreadFactory:每当线程池创立一个新的线程时,都是通过线程工厂办法来实现的。在 ThreadFactory 中只定义了一个办法 newThread,每当线程池须要创立新线程就会调用它。

public class MyThreadFactory implements ThreadFactory {
    private final String poolName;
    
    public MyThreadFactory(String poolName) {this.poolName = poolName;}
    
    public Thread newThread(Runnable runnable) {return new MyAppThread(runnable, poolName);// 将线程池名字传递给构造函数,用于辨别不同线程池的线程
    }
}

7、RejectedExecutionHandler:当队列和线程池都满了的时候,依据回绝策略解决新工作。

AbortPolicy:默认的策略,间接抛出 RejectedExecutionException
DiscardPolicy:不解决,间接抛弃
DiscardOldestPolicy:将期待队列队首的工作抛弃,并执行当前任务
CallerRunsPolicy:由调用线程解决该工作

线程池大小怎么设置?

如果线程池线程数量太小,当有大量申请须要解决,零碎响应比较慢,会影响用户体验,甚至会呈现工作队列大量沉积工作导致 OOM。

如果线程池线程数量过大,大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而减少线程的执行工夫,影响了执行效率。

CPU 密集型工作(N+1):这种工作耗费的次要是 CPU 资源,能够将线程数设置为 N(CPU 外围数)+1,多进去的一个线程是为了避免某些起因导致的线程阻塞(如 IO 操作,线程 sleep,期待锁)而带来的影响。一旦某个线程被阻塞,开释了 CPU 资源,而在这种状况下多进去的一个线程就能够充分利用 CPU 的闲暇工夫。

I/O 密集型工作 (2N):零碎的大部分工夫都在解决 IO 操作,此时线程可能会被阻塞,开释 CPU 资源,这时就能够将 CPU 交出给其它线程应用。因而在 IO 密集型工作的利用中,能够多配置一些线程,具体的计算方法: 最佳线程数 = CPU 外围数 * (1/CPU 利用率) = CPU 外围数 * (1 + (IO 耗时 /CPU 耗时)),个别可设置为 2N。

最全面的 Java 面试网站

线程池的类型有哪些?实用场景?

常见的线程池有 FixedThreadPoolSingleThreadExecutorCachedThreadPoolScheduledThreadPool。这几个都是 ExecutorService 线程池实例。

FixedThreadPool

固定线程数的线程池。任何工夫点,最多只有 nThreads 个线程处于活动状态执行工作。

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

应用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会回绝工作,即不会调用 RejectedExecutionHandler.rejectedExecution()办法。

maxThreadPoolSize 是有效参数,故将它的值设置为与 coreThreadPoolSize 统一。

keepAliveTime 也是有效参数,设置为 0L,因为此线程池里所有线程都是外围线程,外围线程不会被回收(除非设置了 executor.allowCoreThreadTimeOut(true))。

实用场景:实用于解决 CPU 密集型的工作,确保 CPU 在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。须要留神的是,FixedThreadPool 不会回绝工作,在工作比拟多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

public static ExecutionService newSingleThreadExecutor() {return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

应用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的工作放入工作队列,线程解决完工作就循环从队列里获取工作执行。保障程序的执行各个工作。

实用场景:实用于串行执行工作的场景,一个工作一个工作地执行。在工作比拟多的时候也是会导致 OOM。

CachedThreadPool

依据须要创立新线程的线程池。

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

如果主线程提交工作的速度高于线程解决工作的速度时,CachedThreadPool 会一直创立新的线程。极其状况下,这样会导致耗尽 cpu 和内存资源。

应用没有容量的 SynchronousQueue 作为线程池工作队列,当线程池有闲暇线程时,SynchronousQueue.offer(Runnable task)提交的工作会被闲暇线程解决,否则会创立新的线程解决工作。

实用场景:用于并发执行大量短期的小工作。CachedThreadPool容许创立的线程数量为 Integer.MAX_VALUE,可能会创立大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

在给定的提早后运行工作,或者定期执行工作。在理论我的项目中根本不会被用到,因为有其余计划抉择比方quartz

应用的工作队列 DelayQueue 封装了一个 PriorityQueuePriorityQueue 会对队列中的工作进行排序,工夫早的工作先被执行(即ScheduledFutureTasktime 变量小的先执行),如果 time 雷同则先提交的工作会被先执行(ScheduledFutureTasksquenceNumber 变量小的先执行)。

执行周期工作步骤:

  1. 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期工作是指 ScheduledFutureTask的 time 大于等于以后零碎的工夫;
  2. 执行这个 ScheduledFutureTask
  3. 批改 ScheduledFutureTask 的 time 变量为下次将要被执行的工夫;
  4. 把这个批改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

实用场景:周期性执行工作的场景,须要限度线程数量的场景。

一个我的项目应用多个线程池还是一个线程池?

我的项目中如果有多个场景须要应用线程池,那么最好的形式是:每一个业务场景应用独立的线程池。不要让所有的场景共用一个线程池。

1)独立的线城池之间相互不影响彼此的工作作业,更有利于保障本工作的独立性和完整性,更合乎低耦合的设计思维

2)如果所有的场景共用一个线程池,可能会呈现问题,比方有工作 A、工作 B、工作 C 这三个工作场景共用一个线程池。当工作 A 申请量激烈减少的时候就会导致工作 B 和工作 C,没有可用的线程,可能呈现迟迟获取不到资源的状况。比方工作 A 同时有 3000 个线程申请,此时就可能会导致 工作 B 和工作 C 调配不到资源或者调配到很少的线程资源。

注:

1.JDK 自带的类应用了很多的线程池;
2. 很多开源框架应用了大量的线程池;
3. 本人的利用也会创立多个线程池;
4. 多少个线程池, 每个线程池提供多少线程,必须通过具体的测试

过程线程

过程是指一个内存中运行的应用程序,每个过程都有本人独立的一块内存空间。

线程是比过程更小的执行单位,它是在一个过程中独立的控制流,一个过程能够启动多个线程,每条线程并行执行不同的工作。

线程的生命周期

初始(NEW):线程被构建,还没有调用 start()。

运行(RUNNABLE):包含操作系统的就绪和运行两种状态。

阻塞(BLOCKED):个别是被动的,在抢占资源中得不到资源,被动的挂起在内存,期待资源开释将其唤醒。线程被阻塞会开释 CPU,不开释内存。

期待(WAITING):进入该状态的线程须要期待其余线程做出一些特定动作(告诉或中断)。

超时期待(TIMED_WAITING):该状态不同于 WAITING,它能够在指定的工夫后自行返回。

终止(TERMINATED):示意该线程曾经执行结束。

图片起源:Java 并发编程的艺术

讲讲线程中断?

线程中断即线程运行过程中被其余线程给打断了,它与 stop 最大的区别是:stop 是由零碎强制终止线程,而线程中断则是给指标线程发送一个中断信号,如果指标线程没有接管线程中断的信号并完结线程,线程则不会终止,具体是否退出或者执行其余逻辑取决于指标线程。

线程中断三个重要的办法:

1、java.lang.Thread#interrupt

调用指标线程的 interrupt() 办法,给指标线程发一个中断信号,线程被打上中断标记。

2、java.lang.Thread#isInterrupted()

判断指标线程是否被中断,不会革除中断标记。

3、java.lang.Thread#interrupted

判断指标线程是否被中断,会革除中断标记。

private static void test2() {Thread thread = new Thread(() -> {while (true) {Thread.yield();

            // 响应中断
            if (Thread.currentThread().isInterrupted()) {System.out.println("Java 技术栈线程被中断,程序退出。");
                return;
            }
        }
    });
    thread.start();
    thread.interrupt();}

创立线程有哪几种形式?

  • 通过扩大 Thread 类来创立多线程
  • 通过实现 Runnable 接口来创立多线程
  • 实现 Callable 接口,通过 FutureTask 接口创立线程。
  • 应用 Executor 框架来创立线程池。

继承 Thread 创立线程 代码如下。run()办法是由 jvm 创立完操作系统级线程后回调的办法,不能够手动调用,手动调用相当于调用一般办法。

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:15
 */
public class MyThread extends Thread {public MyThread() { }

    @Override
    public void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread() + ":" + i);
        }
    }

    public static void main(String[] args) {MyThread mThread1 = new MyThread();
        MyThread mThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();}
}

Runnable 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:04
 */
public class RunnableTest {public static  void main(String[] args){Runnable1 r = new Runnable1();
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("主线程:["+Thread.currentThread().getName()+"]");
    }
}

class Runnable1 implements Runnable{
    @Override
    public void run() {System.out.println("以后线程:"+Thread.currentThread().getName());
    }
}

实现 Runnable 接口比继承 Thread 类所具备的劣势:

  1. 能够防止 java 中的单继承的限度
  2. 线程池只能放入实现 Runable 或 Callable 类线程,不能间接放入继承 Thread 的类

Callable 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:21
 */
public class CallableTest {public static void main(String[] args) {Callable1 c = new Callable1();

        // 异步计算的后果
        FutureTask<Integer> result = new FutureTask<>(c);

        new Thread(result).start();

        try {
            // 期待工作实现,返回后果
            int sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {e.printStackTrace();
        }
    }

}

class Callable1 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {sum += i;}
        return sum;
    }
}

应用 Executor 创立线程代码

/**
 * @author: 程序员大彬
 * @time: 2021-09-11 10:44
 */
public class ExecutorsTest {public static void main(String[] args) {
        // 获取 ExecutorService 实例,生产禁用,须要手动创立线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 提交工作
        executorService.submit(new RunnableDemo());
    }
}

class RunnableDemo implements Runnable {
    @Override
    public void run() {System.out.println("大彬");
    }
}

什么是线程死锁?

线程死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种相互期待的景象。若无外力作用,它们都将无奈推动上来。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方持有的资源,所以这两个线程就会相互期待而进入死锁状态。

上面通过例子阐明线程死锁,代码来自并发编程之美。

public class DeadLockDemo {private static Object resource1 = new Object();// 资源 1
    private static Object resource2 = new Object();// 资源 2

    public static void main(String[] args) {new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();}
}

代码输入如下:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 取得 resource1 的监视器锁,而后通过 Thread.sleep(1000)。让线程 A 休眠 1s 为的是让线程 B 失去执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠完结了都开始希图申请获取对方的资源,而后这两个线程就会陷入相互期待的状态,这也就产生了死锁。

线程死锁怎么产生?怎么防止?

死锁产生的四个必要条件

  • 互斥:一个资源每次只能被一个过程应用
  • 申请与放弃:一个过程因申请资源而阻塞时,不开释取得的资源
  • 不剥夺:过程已取得的资源,在未应用之前,不能强行剥夺
  • 循环期待:过程之间循环期待着资源

防止死锁的办法

  • 互斥条件不能毁坏,因为加锁就是为了保障互斥
  • 一次性申请所有的资源,防止线程占有资源而且在期待其余资源
  • 占有局部资源的线程进一步申请其余资源时,如果申请不到,被动开释它占有的资源
  • 按序申请资源

线程 run 和 start 的区别?

  • 当程序调用 start() 办法,将会创立一个新线程去执行 run() 办法中的代码。run()就像一个一般办法一样,间接调用 run() 的话,不会创立新线程。
  • 一个线程的 start() 办法只能调用一次,屡次调用会抛出 java.lang.IllegalThreadStateException 异样。run() 办法则没有限度。

线程都有哪些办法?

start

用于启动线程。

getPriority

获取线程优先级,默认是 5,线程默认优先级为 5,如果不手动指定,那么线程优先级具备继承性,比方线程 A 启动线程 B,那么线程 B 的优先级和线程 A 的优先级雷同

setPriority

设置线程优先级。CPU 会尽量将执行资源让给优先级比拟高的线程。

interrupt

通知线程,你应该中断了,具体到底中断还是持续运行,由被告诉的线程本人解决。

当对一个线程调用 interrupt() 时,有两种状况:

  1. 如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),那么线程将立刻退出被阻塞状态,并抛出一个 InterruptedException 异样。
  2. 如果线程处于失常活动状态,那么会将该线程的中断标记设置为 true。不过,被设置中断标记的线程能够持续失常运行,不受影响。

interrupt() 并不能真正的中断线程,须要被调用的线程本人进行配合才行。

join

期待其余线程终止。在以后线程中调用另一个线程的 join()办法,则以后线程转入阻塞状态,直到另一个过程运行完结,以后线程再由阻塞转为就绪状态。

yield

暂停以后正在执行的线程对象,把执行机会让给雷同或者更高优先级的线程。

sleep

使线程转到阻塞状态。millis 参数设定睡眠的工夫,以毫秒为单位。当睡眠完结后,线程主动转为 Runnable 状态。

volatile 底层原理

volatile是轻量级的同步机制,volatile保障变量对所有线程的可见性,不保障原子性。

  1. 当对 volatile 变量进行写操作的时候,JVM 会向处理器发送一条 LOCK 前缀的指令,将该变量所在缓存行的数据写回零碎内存。
  2. 因为缓存一致性协定,每个处理器通过嗅探在总线上流传的数据来查看本人的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被批改,就会将以后处理器的缓存行置为有效状态,当处理器对这个数据进行批改操作的时候,会从新从零碎内存中把数据读到处理器缓存中。

来看看缓存一致性协定是什么。

缓存一致性协定:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其余 CPU 中也存在该变量的正本,会发出信号告诉其余 CPU 将该变量的缓存行置为有效状态,因而当其余 CPU 须要读取这个变量时,就会从内存从新读取。

volatile关键字的两个作用:

  1. 保障了不同线程对共享变量进行操作时的 可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。
  2. 禁止进行 指令重排序

指令重排序是 JVM 为了优化指令,进步程序运行效率,在不影响单线程程序执行后果的前提下,尽可能地进步并行度。Java 编译器会在生成指令系列时在适当的地位会插入 内存屏障 指令来禁止处理器重排序。插入一个内存屏障,相当于通知 CPU 和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个 volatile 字段进行写操作,Java 内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

synchronized 的用法有哪些?

  1. 润饰一般办法:作用于以后对象实例,进入同步代码前要取得以后对象实例的锁
  2. 润饰静态方法 :作用于以后类,进入同步代码前要取得以后类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁
  3. 润饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁

synchronized 的作用有哪些?

原子性:确保线程互斥的拜访同步代码;

可见性:保障共享变量的批改可能及时可见;

有序性:无效解决重排序问题。

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始地位,monitorexit 指令则指明同步代码块的完结地位。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种形式获取锁的,也是为什么 Java 中任意对象能够作为锁的起因)。

其外部蕴含一个计数器,当计数器为 0 则能够胜利获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0
,表明锁被开释。如果获取对象锁失败,那以后线程就要阻塞期待,直到锁被另外一个线程开释为止

synchronized 润饰的办法并没有 monitorenter 指令和 monitorexit 指令,获得代之的的确是ACC_SYNCHRONIZED 标识,该标识指明了该办法是一个同步办法,JVM 通过该 ACC_SYNCHRONIZED 拜访标记来分别一个办法是否申明为同步办法,从而执行相应的同步调用。

volatile 和 synchronized 的区别是什么?

  1. volatile只能应用在变量上;而 synchronized 能够在类,变量,办法和代码块上。
  2. volatile至保障可见性;synchronized保障原子性与可见性。
  3. volatile禁用指令重排序;synchronized不会。
  4. volatile不会造成阻塞;synchronized会。

ReentrantLock 和 synchronized 区别

  1. 应用 synchronized 关键字实现同步,线程执行完同步代码块会 主动开释锁,而 ReentrantLock 须要手动开释锁。
  2. synchronized 是 非偏心锁,ReentrantLock 能够设置为偏心锁。
  3. ReentrantLock 上期待获取锁的线程是 可中断的,线程能够放弃期待锁。而 synchonized 会无限期期待上来。
  4. ReentrantLock 能够设置超时获取锁。在指定的截止工夫之前获取锁,如果截止工夫到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 办法 能够尝试非阻塞的获取锁,调用该办法后立即返回,如果可能获取则返回 true,否则返回 false。

wait()和 sleep()的异同点?

相同点

  1. 它们都能够使以后线程暂停运行,把机会交给其余线程
  2. 任何线程在调用 wait()和 sleep()之后,在期待期间被中断都会抛出InterruptedException

不同点

  1. wait()是 Object 超类中的办法;而 sleep() 是线程 Thread 类中的办法
  2. 对锁的持有不同,wait()会开释锁,而 sleep() 并不开释锁
  3. 唤醒办法不完全相同,wait()依附 notify 或者 notifyAll 、中断、达到指定工夫来唤醒;而sleep() 达到指定工夫被唤醒
  4. 调用 wait() 须要先获取对象的锁,而 Thread.sleep() 不必

Runnable 和 Callable 有什么区别?

  • Callable 接口办法是call(),Runnable 的办法是run()
  • Callable 接口 call 办法有返回值,反对泛型,Runnable 接口 run 办法无返回值。
  • Callable 接口 call() 办法容许抛出异样;而 Runnable 接口 run() 办法不能持续上抛异样。

线程执行程序怎么管制?

假如有 T1、T2、T3 三个线程,你怎么保障 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

能够应用 join 办法 解决这个问题。比方在线程 A 中,调用线程 B 的 join 办法示意的意思就是:A 期待 B 线程执行结束后(开释 CPU 执行权),在继续执行。

代码如下:

public class ThreadTest {public static void main(String[] args) {Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            // 春天线程先启动
            spring.start();
            // 主线程期待线程 spring 执行完,再往下执行
            spring.join();
            // 夏天线程再启动
            summer.start();
            // 主线程期待线程 summer 执行完,再往下执行
            summer.join();
            // 秋天线程最初启动
            autumn.start();
            // 主线程期待线程 autumn 执行完,再往下执行
            autumn.join();} catch (InterruptedException e)
        {e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){this.name = name;}

    @Override
    public void run() {for (int i = 1; i <4; i++) {System.out.println(this.name + "来了:" + i + "次");
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

运行后果:

春天来了: 1 次
春天来了: 2 次
春天来了: 3 次
夏天来了: 1 次
夏天来了: 2 次
夏天来了: 3 次
秋天来了: 1 次
秋天来了: 2 次
秋天来了: 3 次

守护线程是什么?

守护线程是 运行在后盾的一种非凡过程。它独立于管制终端并且周期性地执行某种工作或期待解决某些产生的事件。在 Java 中垃圾回收线程就是非凡的守护线程。

线程间通信形式

1、应用 Object 类的 wait()/notify()。Object 类提供了线程间通信的办法:wait()notify()notifyAll(),它们是多线程通信的根底。其中,wait/notify 必须配合 synchronized 应用,wait 办法开释锁,notify 办法不开释锁。wait 是指在一个曾经进入了同步锁的线程内,让本人临时让出同步锁,以便其余正在期待此锁的线程能够失去同步锁并运行,只有其余线程调用了 notify(),notify 并不开释锁,只是通知调用过wait() 的线程能够去参加取得锁的竞争了,但不是马上失去锁,因为锁还在他人手里,他人还没开释,调用 wait() 的一个或多个线程就会解除 wait 状态,从新参加竞争对象锁,程序如果能够再次失去锁,就能够持续向下运行。

2、应用 volatile 关键字。基于 volatile 关键字实现线程间互相通信,其底层应用了共享内存。简略来说,就是多个线程同时监听一个变量,当这个变量发生变化的时候,线程可能感知并执行相应的业务。

3、应用 JUC 工具类 CountDownLatch。jdk1.5 之后在 java.util.concurrent 包下提供了很多并发编程相干的工具类,简化了并发编程开发,CountDownLatch 基于 AQS 框架,相当于也是保护了一个线程间共享变量 state。

4、基于 LockSupport 实现线程间的阻塞和唤醒。LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,应用它不必关注是期待线程先进行还是唤醒线程先运行,然而得晓得线程的名字。

ThreadLocal

线程本地变量。当应用 ThreadLocal 保护变量时,ThreadLocal为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立地扭转本人的正本,而不会影响其它线程。

ThreadLocal 原理

每个线程都有一个 ThreadLocalMapThreadLocal 外部类),Map 中元素的键为ThreadLocal,而值对应线程的变量正本。

调用 threadLocal.set()–> 调用getMap(Thread)–> 返回以后线程的ThreadLocalMap<ThreadLocal, value>–>map.set(this, value),this 是threadLocal 自身。源码如下:

public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

调用get()–> 调用getMap(Thread)–> 返回以后线程的ThreadLocalMap<ThreadLocal, value>–>map.getEntry(this),返回value。源码如下:

    public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {@SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();}

threadLocals的类型 ThreadLocalMap 的键为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,如 longLocalstringLocal

public class ThreadLocalDemo {ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal并不是用来解决共享资源的多线程拜访问题,因为每个线程中的资源只是正本,不会共享。因而 ThreadLocal 适宜作为线程上下文变量,简化线程内传参。

ThreadLocal 内存透露的起因?

每个线程都有⼀个 ThreadLocalMap 的外部属性,map 的 key 是ThreaLocal,定义为弱援用,value 是强援用类型。垃圾回收的时候会⾃动回收 key,而 value 的回收取决于 Thread 对象的生命周期。个别会通过线程池的形式复用线程节俭资源,这也就导致了线程对象的生命周期比拟长,这样便始终存在一条强援用链的关系:Thread –> ThreadLocalMap–>Entry–>Value,随着工作的执行,value 就有可能越来越多且无奈开释,最终导致内存透露。

解决⽅法:每次使⽤完 ThreadLocal 就调⽤它的 remove() ⽅法,手动将对应的键值对删除,从⽽防止内存透露。

ThreadLocal 应用场景有哪些?

场景 1

ThreadLocal 用作保留每个线程独享的对象,为每个线程都创立一个正本,这样每个线程都能够批改本人所领有的正本, 而不会影响其余线程的正本,确保了线程平安。

这种场景通常用于保留线程不平安的工具类,典型的应用的类就是 SimpleDateFormat。

如果需要为 500 个线程都要用到 SimpleDateFormat,应用线程池来实现线程的复用,否则会耗费过多的内存等资源,如果咱们每个工作都创立了一个 simpleDateFormat 对象,也就是说,500 个工作对应 500 个 simpleDateFormat 对象。然而这么多对象的创立是有开销的,而且这么多对象同时存在在内存中也是一种内存的节约。能够将 simpleDateFormat 对象给提取了进去,变成动态变量,然而这样一来就会有线程不平安的问题。咱们想要的成果是,既不节约过多的内存,同时又想保障线程平安。此时,能够应用 ThreadLocal 来达到这个目标,每个线程都领有一个本人的 simpleDateFormat 对象。

场景 2

ThreadLocal 用作每个线程内须要独立保存信息,以便供其余办法更不便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,后面执行的办法保留了信息后,后续办法能够通过 ThreadLocal 间接获取到,防止了传参,相似于全局变量的概念。

比方 Java web 利用中,每个线程有本人独自的 Session 实例,就能够应用 ThreadLocal 来实现。

AQS 原理

AQS,AbstractQueuedSynchronizer,形象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如罕用的ReentrantLock/Semaphore/CountDownLatch

AQS 应用一个 volatile 的 int 类型的成员变量 state 来示意同步状态,通过 CAS 批改同步状态的值。当线程调用 lock 办法时,如果 state=0,阐明没有任何线程占有共享资源的锁,能够取得锁并将 state加 1。如果 state不为 0,则阐明有线程目前正在应用共享变量,其余线程必须退出同步队列进行期待。

private volatile int state;// 共享变量,应用 volatile 润饰保障线程可见性

同步器依赖外部的同步队列(一个 FIFO 双向队列)来实现同步状态的治理,以后线程获取同步状态失败时,同步器会将以后线程以及期待状态(独占或共享)结构成为一个节点(Node)并将其退出同步队列并进行自旋,当同步状态开释时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

ReentrantLock 是如何实现可重入性的?

ReentrantLock外部自定义了同步器 sync,在加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,查看以后保护的那个线程 ID 和以后申请的线程 ID 是否 统一,如果统一,同步状态加 1,示意锁被以后线程获取了屡次。

源码如下:

final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

锁的分类

偏心锁与非偏心锁

依照 线程拜访程序 获取对象锁。synchronized是非偏心锁,Lock默认是非偏心锁,能够设置为偏心锁,偏心锁会影响性能。

public ReentrantLock() {sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

共享式与独占式锁

共享式与独占式的最次要 区别 在于:同一时刻独占式只能有 一个线程 获取同步状态,而共享式在同一时刻能够有多个线程获取同步状态。例如读操作能够有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其余操作都会被阻塞。

乐观锁与乐观锁

乐观锁,每次拜访资源都会加锁 ,执行完同步代码开释锁,synchronizedReentrantLock属于乐观锁。

乐观锁,不会锁定资源,所有的线程都能拜访并批改同一个资源,如果没有抵触就批改胜利并退出,否则就会持续循环尝试。乐观锁最常见的实现就是CAS

实用场景:

  • 乐观锁适宜 写操作多 的场景。
  • 乐观锁适宜 读操作多 的场景,不加锁能够晋升读操作的性能。

乐观锁有什么问题?

乐观锁防止了乐观锁独占对象的问题,进步了并发性能,但它也有毛病:

  • 乐观锁只能保障 一个共享变量 的原子操作。
  • 长时间自旋可能导致 开销大。如果 CAS 长时间不胜利而始终自旋,会给 CPU 带来很大的开销。
  • ABA 问题。CAS 的原理是通过比对内存值与预期值是否一样而判断内存值是否被改过,然而会有以下问题:如果内存值原来是 A,起初被一条线程改为 B,最初又被改成了 A,则 CAS 认为此内存值并没有产生扭转。能够引入版本号解决这个问题,每次变量更新都把版本号加一。

什么是 CAS?

CAS 全称 Compare And Swap,比拟与替换,是乐观锁的次要实现形式。CAS 在不应用锁的状况下实现多线程之间的变量同步。ReentrantLock 外部的 AQS 和原子类外部都应用了 CAS。

CAS 算法波及到三个操作数:

  • 须要读写的内存值 V。
  • 进行比拟的值 A。
  • 要写入的新值 B。

只有当 V 的值等于 A 时,才会应用原子形式用新值 B 来更新 V 的值,否则会持续重试直到胜利更新值。

AtomicInteger 为例,AtomicIntegergetAndIncrement() 办法底层就是 CAS 实现,要害代码是 compareAndSwapInt(obj, offset, expect, update),其含意就是,如果 obj 内的 valueexpect相等,就证实没有其余线程扭转过这个变量,那么就更新它为update,如果不相等,那就会持续重试直到胜利更新值。

CAS 存在的问题?

CAS 三大问题:

  1. ABA 问题 。CAS 须要在操作值的时候查看内存值是否发生变化,没有发生变化才会更新内存值。然而如果内存值原来是 A,起初变成了 B,而后又变成了 A,那么 CAS 进行查看时会发现值没有发生变化,然而实际上是有变动的。ABA 问题的解决思路就是在变量后面增加版本号,每次变量更新的时候都把版本号加一,这样变动过程就从A-B-A 变成了1A-2B-3A

    JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,原子更新带有版本号的援用类型。

  2. 循环工夫长开销大。CAS 操作如果长时间不胜利,会导致其始终自旋,给 CPU 带来十分大的开销。
  3. 只能保障一个共享变量的原子操作。对一个共享变量执行操作时,CAS 可能保障原子操作,然而对多个共享变量操作时,CAS 是无奈保障操作的原子性的。

    Java 从 1.5 开始 JDK 提供了 AtomicReference 类来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行 CAS 操作。

原子类

根本类型原子类

应用原子的形式更新根本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

AtomicInteger 类罕用的办法:

public final int get() // 获取以后的值
public final int getAndSet(int newValue)// 获取以后的值,并设置新的值
public final int getAndIncrement()// 获取以后的值,并自增
public final int getAndDecrement() // 获取以后的值,并自减
public final int getAndAdd(int delta) // 获取以后的值,并加上预期的值
boolean compareAndSet(int expect, int update) // 如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update)public final void lazySet(int newValue)// 最终设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

AtomicInteger 类次要利用 CAS (compare and swap) 保障原子操作,从而防止加锁的高开销。

数组类型原子类

应用原子的形式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:援用类型数组原子类

AtomicIntegerArray 类罕用办法:

public final int get(int i) // 获取 index=i 地位元素的值
public final int getAndSet(int i, int newValue)// 返回 index=i 地位的以后的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)// 获取 index=i 地位元素的值,并让该地位的元素自增
public final int getAndDecrement(int i) // 获取 index=i 地位元素的值,并让该地位的元素自减
public final int getAndAdd(int i, int delta) // 获取 index=i 地位元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) // 如果输出的数值等于预期值,则以原子形式将 index=i 地位的元素值设置为输出值(update)public final void lazySet(int i, int newValue)// 最终 将 index=i 地位的元素设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

援用类型原子类

  • AtomicReference:援用类型原子类
  • AtomicStampedReference:带有版本号的援用类型原子类。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。
  • AtomicMarkableReference:原子更新带有标记的援用类型。该类将 boolean 标记与援用关联起来

为什么要应用 Executor 线程池框架呢?

  • 每次执行工作都通过 new Thread()去创立线程,比拟耗费性能,创立一个线程是比拟耗时、耗资源的
  • 调用 new Thread()创立的线程不足治理,能够无限度的创立,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪
  • 间接应用 new Thread()启动的线程不利于扩大,比方定时执行、定期执行、定时定期执行、线程中断等都不好实现

如何进行一个正在运行的线程?

  1. 应用共享变量的形式。共享变量能够被多个执行雷同工作的线程用来作为是否进行的信号,告诉进行线程的执行。
  2. 应用 interrupt 办法终止线程。当一个线程被阻塞,处于不可运行状态时,即便主程序中将该线程的共享变量设置为 true,但该线程此时根本无法查看循环标记,当然也就无奈立刻中断。这时候能够应用 Thread 提供的 interrupt()办法,因为该办法尽管不会中断一个正在运行的线程,然而它能够使一个被阻塞的线程抛出一个中断异样,从而使线程提前结束阻塞状态。

什么是 Daemon 线程?

后盾 (daemon) 线程,是指在程序运行的时候在后盾提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的局部。因而,当所有的非后盾线程完结时,程序也就终止了,同时会杀死过程中的所有后盾线程。反过来说,只有有任何非后盾线程还在运行,程序就不会终止。必须在线程启动之前调用 setDaemon()办法,能力把它设置为后盾线程。

留神:后盾过程在不执行 finally 子句的状况下就会终止其 run()办法。

比方:JVM 的垃圾回收线程就是 Daemon 线程,Finalizer 也是守护线程。

SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保障线程平安,所以每次只能有一个线程来拜访 map。

JDK1.8 ConcurrentHashMap 采纳 CAS 和 synchronized 来保障并发平安。数据结构采纳数组 + 链表 / 红黑二叉树。synchronized 只锁定以后链表或红黑二叉树的首节点,反对并发拜访、批改。
另外 ConcurrentHashMap 应用了一种不同的迭代形式。当 iterator 被创立后汇合再产生扭转就不再是抛出 ConcurrentModificationException,取而代之的是在扭转时 new 新的数据从而不影响原有的数据,iterator 实现后再将头指针替换为新的数据,这样 iterator 线程能够应用原来老的数据,而写线程也能够并发的实现扭转。

怎么判断线程池的工作是不是执行完了?

有几种办法:

1、应用线程池的原生函数isTerminated();

executor 提供一个原生函数 isTerminated()来判断线程池中的工作是否全副实现。如果全副实现返回 true,否则返回 false。

2、应用重入锁,维持一个公共计数

所有的一般工作维持一个计数器,当工作实现时计数器加一(这里要加锁),当计数器的值等于工作数时,这时所有的工作曾经执行结束了。

3、应用 CountDownLatch

它的原理跟第二种办法相似,给 CountDownLatch 一个计数值,工作执行结束后,调用 countDown()执行计数值减一。最初执行的工作在调用办法的开始调用 await()办法,这样整个工作会阻塞,直到这个计数值为零,才会继续执行。

这种形式的 毛病 就是须要提前晓得工作的数量。

4、submit 向线程池提交工作,应用 Future 判断工作执行状态

应用 submit 向线程池提交工作与 execute 提交不同,submit 会有 Future 类型的返回值。通过 future.isDone()办法能够晓得工作是否执行实现。

什么是 Future?

在并发编程中,不论是继承 thread 类还是实现 runnable 接口,都无奈保障获取到之前的执行后果。通过实现 Callback 接口,并用 Future 能够来接管多线程的执行后果。

Future 示意一个可能还没有实现的异步工作的后果,针对这个后果能够增加 Callback 以便在工作执行胜利或失败后作出相应的操作。

举个例子:比方去吃早点时,点了包子和凉菜,包子须要等 3 分钟,凉菜只需 1 分钟,如果是串行的一个执行,在吃上早点的时候须要期待 4 分钟,然而因为你在等包子的时候,能够同时筹备凉菜,所以在筹备凉菜的过程中,能够同时筹备包子,这样只须要期待 3 分钟。Future 就是前面这种执行模式。

Future 接口次要包含 5 个办法:

  1. get()办法能够当工作完结后返回一个后果,如果调用时,工作还没有完结,则会阻塞线程,直到工作执行结束
  2. get(long timeout,TimeUnit unit)做多期待 timeout 的工夫就会返回后果
  3. cancel(boolean mayInterruptIfRunning)办法能够用来进行一个工作,如果工作能够进行(通过 mayInterruptIfRunning 来进行判断),则能够返回 true,如果工作曾经实现或者曾经进行,或者这个工作无奈进行,则会返回 false。
  4. isDone()办法判断以后办法是否实现
  5. isCancel()办法判断以后办法是否勾销

最初给大家分享一个 Github 仓库,下面有大彬整顿的 300 多本经典的计算机书籍 PDF,包含 C 语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生 等,能够 star 一下,下次找书间接在下面搜寻,仓库继续更新中~

Github 地址

正文完
 0