Java并发

并行和并发的区别?

  • 并行是指两个或多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生;
  • 并行是在不同实体上的多个事件;并发是在同一实体上的多个事件;
  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

线程和进程的区别?

  • 进程是程序的一次执行过程,是系统运行程序的基本单位
  • 线程与进程类似,但线程是一个比进程更小的执行单位。一个进程执行过程中可以产生多个线程,

在Java中,启用一个main方法就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称为主线程。

从JVM角度分析进程和线程的关系

根据JVM的内存划分,对于线程而言:多个线程共享进程的堆、方法区资源,但每个线程又有自己的程序计数器、虚拟机栈、本地方法栈。

也就是说,在一个 JVM 进程中,可以存在多个线程,每个线程都共享了这个 JVM 进程的方法区、堆;并且每个线程又都具有自己的虚拟机栈、本地方法栈、程序计数器等。

为什么方法区和堆是线程共享区?

  • 方法区(Method Area) 存储已被虚拟机加载的类信息、常量、静态变量等数据。方法区中又包含 运行时常量池 ,这部分区域储存Class文件信息和编译期生成的各种字面量和符号引用。
  • 堆(Heap) 堆内存储存了对象实例(比如new关键字创建的实例对象),它是JVM中内存区最大的一块区域。

所以,一个进程的启动可能包含了多个线程,而这个进程中的静态变量等都是随着类加载而加载的,他应该不属于某个线程独有,所以将其存储于方法区中。对象实例都储存在Java堆内存中,作为Java最大的一块内存区域,肯定不能是某个线程独占的。

为什么虚拟机栈和本地方法栈是线程独占区?

  • 虚拟机栈: 每个Java方法执行的同时都会创建一个栈帧储存局部变量表、操作数栈、方法出口等。从方法的执行到结束,对应将栈帧压入Java虚拟机栈和从虚拟机栈中弹出的过程。
  • 本地方法栈: 本地方法栈类似Java虚拟机栈,只不过Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。

    程序计数器是什么?

程序计数器(Program Counter Register):当前线程执行的字节码的行号指示器。每个线程都有独立的程序计数器。此内存区域是Java虚拟机中唯一一个没有任何 OutOfMemoryError 情况的区域。

Java 多线程

使用多线程可能带来什么问题?

并发编程的目的就是提高程序的执行效率,但并发编程可能造成:内存泄漏、上下文切换、死锁等问题

关于线程状态

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

Java 多线程规定了以下六种状态:

  • 新建状态(New):刚创建线程对象,并没有调用该对象的 start 方法,这是线程处于新建状态。
  • 就绪状态(Runnable):当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态(等待线程调度)。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 阻塞状态(Blocked):正在等待监视器锁进入同步代码块/方法,或者调用了 Object.wait() 后重新进入同步代码块/方法;
  • 等待状态(Waiting):由于调用了 Object.wait()、Thread.join()、LockSupport.park() 方法之一,线程处于等待状态;处于等待状态的线程正在等待另一个线程执行特定操作唤醒:wait 等待 notify,join 等待指定的线程终止;
  • 计时等待(Timed_Waiting):由于调用了 Thread.sleep(timeout)、Object.wait(timeout)、Thread.join(timeout)、LockSupport.parkNanos(,)/ParkUntil(,) 方法之一,线程进入该状态;
  • 终止状态:线程已经完成执行;

    什么是上下文切换?

简单来说,并发编程中实际线程的数量都可能大于 CPU 核心的个数,而 CPU 一个核心在任意时刻只能被一个线程使用,CPU 为了保证并发的线程都有被执行,采用随机分配时间片并轮转的方式;而一个线程的时间片用户将保存并进入就绪状态直到下次分配时间片再执行,这个 任务从保存到再加载的过程就是一次上下文切换

说说sleep()方法和wait()方法的区别?

两者都可以暂停线程的执行。

两者的区别在于:

  1. sleep 方法没有释放锁,而 wait 方法释放了锁
  2. 来自不同的类,Object.wait(); Thread.sleep();
  3. 使用范围不同,wait 只能用在同步代码块中, sleep 可以在任意地方使用;
  4. wait()通常用于线程间交互/通信,sleep()通常用于暂停执行
  5. wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。

什么是死锁?如何避免?

举例:线程A持有资源2,线程B持有资源1,在线程A、B都没有释放自己所持有资源的情况下(锁未释放),他们都想同时获取对方的资源,因为资源1、2都被锁定,两个线程都会进入相互等待的情况,这种情况称为死锁。

栗子:

  1. public class DeadLockDemo {
  2. private static Object resource1 = new Object();//资源 1
  3. private static Object resource2 = new Object();//资源 2
  4. public static void main(String[] args) {
  5. new Thread(() -> {
  6. synchronized (resource1) {
  7. System.out.println(Thread.currentThread() + "get resource1");
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread() + "waiting get resource2");
  14. synchronized (resource2) {
  15. System.out.println(Thread.currentThread() + "get resource2");
  16. }
  17. }
  18. }, "线程 1").start();
  19. new Thread(() -> {
  20. synchronized (resource2) {
  21. System.out.println(Thread.currentThread() + "get resource2");
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. System.out.println(Thread.currentThread() + "waiting get resource1");
  28. synchronized (resource1) {
  29. System.out.println(Thread.currentThread() + "get resource1");
  30. }
  31. }
  32. }, "线程 2").start();
  33. }
  34. }

Output:

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

线程1以resource1作为同步监视器,即可以轻松获取resource1同时也锁定了resource1,此时调用sleep让线程1等待1秒钟;此时线程2开始执行,他以resource2作为同步监视器同时也锁定了resource2,此时调用sleep让线程2等待1秒钟;而此时线程1等待1秒已经结束了,当他想要获取resource2时发现resource2已经被线程2锁定了,同理线程2结束等待后想要获取resource1时发现resource1已经被线程1锁定了。那么两者都无法同时获取对方的线程,便进入死锁状态。

因此产生死锁需要具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只能由一个线程占用
  2. 请求和保持条件:一个线程因请求资源而阻塞时,对已获取的资源保持不放
  3. 不剥夺条件:线程已获取的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才使用资源
  4. 循环等待条件:若干进程之前形成一种头尾相接的循环等待资源关系。

避免死锁就要破坏这四个条件中任意一个:

  1. 破坏互斥条件:这个条件我们无法破坏,因为我们用锁的目的就是想让他们互斥
  2. 破坏请求与保持条件:一次性申请所有资源
  3. 破坏循环等待条件:按照一定顺序申请资源,避免资源的循环使用

解决方案: 修改线程2

  1. new Thread(() -> {
  2. synchronized (resource1) {
  3. System.out.println(Thread.currentThread() + "get resource1");
  4. try {
  5. Thread.sleep(1000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. System.out.println(Thread.currentThread() + "waiting get resource2");
  10. synchronized (resource2) {
  11. System.out.println(Thread.currentThread() + "get resource2");
  12. }
  13. }
  14. }, "线程 2").start();

Outout:

  1. Thread[线程 1,5,main]get resource1
  2. Thread[线程 1,5,main]waiting get resource2
  3. Thread[线程 1,5,main]get resource2
  4. Thread[线程 2,5,main]get resource1
  5. Thread[线程 2,5,main]waiting get resource2
  6. Thread[线程 2,5,main]get resource2
  7. Process finished with exit code 0

调用start()方法会执行run()方法,为什么不能直接调用run()方法?

new一个Thread,线程进入了新建状态;调用start()方法,会启用一个线程并使线程进入就绪状态,当分配到时间片后就可以开始执行。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这才是真正的多线程工作。而直接执行run()方法,会吧run()方法当做一个main线程下的一个普通方法去执行,并不会在某个线程中执行他。

总结:调用start方法可以启动线程并使线程进入就绪状态,而run()方法只是Thread的一个普通方法调用,还是在main主线程里执行,并不会在一个新线程中执行

多次调用 start() 方法会怎么样?

java.lang.IllegalThreadStateException 线程状态非法异常

synchronized关键字

synchronized关键字解决多个线程之间访问资源的同步性,synchronized关键字可以保证它修饰的方法或代码块在任意时刻只能有一个线程执行。

synchronized关键字最主要的三种使用方式

  • 修饰实例方法: 给当前对象加锁,进入同步代码块前要获取当前对象实例的锁
  1. // 此处的synchronized就相当于synchronized(this),锁定的是当前对象
  2. public synchronized void add() {}
  • 修饰静态方法: 给当前类加锁(因为静态方法没有this),会作用于当前类的所有对象实例,因为静态成员不属于任何一个实例对象,是一个类成员。
  1. // 此处的synchronized就相当于synzhronized(T.class),(T的当前类)
  2. public synchronized static void add() {}
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码块之前要获取给定对象的锁

    构造方法可以使用 Synchronized 关键字吗?

    不可以,构造方法本身就属于线程安全的。

synchronized 和 Lock 的区别?

  1. synchronized 是Java 内置关键字;Lock 属于 Java 类;
  2. synchronized 无法判断获取锁的状态;Lock 可以判断是否获取到了锁;
  3. synchronized 会自动释放锁;Lock 必须要手动释放,如果不释放,会造成死锁;
  4. synchronized 如果遇到线程阻塞,其他线程会一直等待;Lock 锁可以使用 tryLock() 方法尝试获取锁,不会一直等待下去;
  5. synchronized 默认是可重入锁、不可中断非公平,不能修改;Lock 属于可重入锁,可以中断锁,可以自定义公平锁和非公平锁;
  6. synchronized 一个锁只能绑定一个条件;Lock 可以同时绑定多个 Condition 对象;
  7. synchronized 适合锁少量的代码同步问题;Lock 适合锁大量的同步代码;

    JMM Java内存模型

    Java 多线程 - 图1
    共享变量存储于主内存,每一个线程都会从主内存读取一个共享变量副本存储于当前线程的本地内存。变量副本之间是不可见的,即线程A改变副本的值,线程B是无法读取到的。

共享变量的可见性需要在变量前加上 Volatile 关键字。这样每当线程副本的共享变量发生改变后,主内存更新后会将其他线程副本失效,重新读取主内存的最新值。

创建线程的方式?

继承 Thread 类,实现 Runnable 接口,实现 Callable 接口。
使用线程池

run 方法和 start 方法有什么区别?

线程创建后,调用 start() 方法线程会进入就绪状态,分配到时间片就可以执行了。
run() 方法是线程的执行内容,重写 run 方法可以定义线程的实现。

实现 Runnable 接口和实现 Callable 接口有什么区别?

Runnable 接口不会返回结果或抛出检查异常,Callable 可以。

线程池

如何创建线程池?

  1. 使用 ThreadPoolExecutor 构造方法;
  2. 使用 Executor 框架的⼯具类 Executors:实际也是调用了 ThreadPoolExecutor 构造方法,阿里巴巴开发手册强烈不允许使用该种方式。
    1. FixedThreadPool : 该方法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    2. SingleThreadExecutor: 方法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先入先出的顺序执⾏队列中的任务。
    3. CachedThreadPool: 该方法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使用可复⽤的线程。若所有线程均在⼯作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复⽤。
      1. // 创建方式
      2. ExecutorService pools = Executors.newFixedThreadPool(3);
      1. // 创建原理:调用 ThreadPoolExecutor 构造方法
      2. public static ExecutorService newFixedThreadPool(int nThreads) {
      3. return new ThreadPoolExecutor(nThreads, nThreads,
      4. 0L, TimeUnit.MILLISECONDS,
      5. new LinkedBlockingQueue<Runnable>());
      6. }

      阿里巴巴开发手册中不允许使用 Executors 创建线程池

      【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

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

    创建线程池参数

    1. public ThreadPoolExecutor(int corePoolSize,
    2. int maximumPoolSize,
    3. long keepAliveTime,
    4. TimeUnit unit,
    5. BlockingQueue<Runnable> workQueue,
    6. ThreadFactory threadFactory,
    7. RejectedExecutionHandler handler);
  3. **corePoolSize**:核心线程数,保留在线程池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut;

  4. **maximumPoolSize**:最大线程数,当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最⼤线程数;
  5. **keepAliveTime**:当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间;
  6. **unit**:keepAliveTime 参数的时间单位;
  7. **workQueue**:超出核心线程数的任务会保存在任务队列, 这个队列将只保存 execute 方法提交的 Runnable任务;
  8. **threadFactory**:执行程序创建新线程时使用的工厂;
  9. **handler**:饱和策略,执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量;

    饱和策略

    饱和策略定义:如果同时运行的线程数量达到最大线程数并且任务队列也放满了任务时,ThreadPoolExecutor 定义了一些测试处理阻塞:

  10. ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。

  11. ThreadPoolExecutor.CallerRunsPolicy :调⽤执⾏⾃⼰的线程运⾏任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话,你可以选择这个策略。
  12. ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
  13. ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

    线程池方法

    执行 execute()方法和 submit()方法的区别是什么呢?

  14. execute() 方法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执行成功与否;

  15. submit() 方法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取返回值, get() 方法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout, TimeUnitunit) 方法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执行完。

    ExecutorService 提交线程任务对象执行的方法:

  16. Future<?> submit(Runnable task):提交一个 Runnable 的任务对象给线程池执行;

  17. Future<?> submit(Callable task):提交一个 Callable 的任务对象给线程池执行,可以通过 get() 得到返回结果。

    ExecutorService 关闭线程池的方法:

  18. shutdown():等待任务执行完毕以后才会关闭线程池;

  19. shutdownNow():立即关闭线程池的代码,无论任务是否执行完毕。

    为什么要用线程池?

  20. 降低资源消耗:减少了创建和销毁线程的次数;

  21. 提高响应速度:不需要频繁的创建线程;
  22. 提高线程的可管理性:线程池可以约束系统中最多的线程数;

    JUC

    假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?

    利用 java.util.concurrent 包下的 CountDownLatch(减数器)或 CyclicBarrier(循环栅栏)可以实现此类问题
    https://www.cnblogs.com/oneBreeze1855/p/9463185.html

    CountDownLatch

    定义一个减法计数器,每有一个线程调用 countDown 方法计数器就会减一,在归零之前,所有线程都会阻塞在 await 方法。

    CyclicBarrier

    定义一个加法计数器,并指定一个 Runnable 任务,当调用 await 方法的线程个数达到指定的值时,就执行指定的任务。

    Semaphore

    定义一个信号量,控制多条线程能同时存在的并发数量,使用 acquire 方法会阻塞抢占一个位置,使用 release 方法会释放一个位置。