Java:多线程

1. 为什么要使用多线程?

1)从计算机底层来说

  • 线程可比作轻量级进程,是程序执行的最小单位,线程间切换和调度的成本远远小于进程;
  • 另外,多核CPU可支持多个线程同时执行,降低了线程上下文切换的开销。
    单核计算机时代: 主要是为了提高CPU和IO设备等的综合利用率(如果只有一个线程,CPU执行时,IO空闲,IO执行时,CPU空闲,利用率只有50%,多线程可以一个CPU计算式,另一个进行IO操作)
    多核计算机时代: 主要是为了提高计算机CPU利用率(如果只用一个CPU核心,其他核心空闲,多线程可以充分利用多个CPU核心)

2)从互联网发展趋势来说

  • 当前系统动不动就会需要百万级甚至千万级的并发量;
  • 多线程并发编程是支持高并发系统的基础,利用好多线程可提高系统整体并发能力及性能。

2. 使用多线程可能带来什么问题?(怎么解决这些问题?)

  • 内存泄漏
  • 上下文切换
  • 死锁

3. 什么是上下文切换?

  • 一个任务在CPU时间片用完时,会先保存自己的任务状态再切换到其他任务,这样下次加载时就从保存的位置开始执行。任务从保存到再加载的过程就叫做上下文切换。

详解:多线程编程时,一般线程数会大于CPU核心数。而一个CPU核心一次只能执行一个线程。为使所有线程都得到有效的执行,CPU采取的策略是每个线程分配一个时间片并轮转的方式。一个线程执行完自己的时间片就会重新进入就绪状态,CPU让给其他线程使用,等待下次时间片。这个过程就是一次上下文切换。

4. Java中有哪些锁(锁的分类)?

1)由ReentrantLock和Synchronized创建的一系列锁

  • 公平锁与非公平锁(Synchronized只能实现非公平锁)
  • 互斥锁与非互斥锁
  • 可重入锁与非可重入锁
  • 编译优化角度 -- 锁消除和锁粗化
  • 不同的位置使用Synchronized -- 类锁和对象锁

2)从锁的设计理念来分

  • 悲观锁(Java中Synchronized)
  • 乐观锁(版本号机制、CAS算法)

CAS是一种更新的原子操作,比较当前值与传入值是否一样,一样则更新,否则失败。

5. 什么是线程死锁?

  • 多个线程同时被阻塞 ,它们中的一个或多个在等待某个资源被释放。由于线程进入无限期阻塞等待,程序无法正常终止,即发生死锁。
  • 例如:线程A持有资源1,线程B持有资源2,线程A和线程B都企图得到对方的资源,因此陷入互相等待状态,形成死锁。
  • 这个例子满足形成死锁的四个必要条件:
    a. 互斥条件
    b. 请求保持条件
    c. 不可剥夺条件
    d. 循环等待条件

6. 如何避免线程死锁?

  • 为了避免死锁,只需破坏四个必要条件中的一个即可。
    a. 破坏互斥条件:这个条件无法破坏,因为锁的存在就是为了使线程互斥的(临界资源只能被互斥的访问)。
    b. 破坏请求保持条件:一次性申请所有的资源。
    c. 破坏不可剥夺条件:申请资源没有申请到时,释放自己持有的资源。
    d. 破坏循环等待条件:靠按需申请资源来预防 。按某一顺序来申请资源,释放资源则反序释放。

7. 谈谈对Synchronized关键字的了解?

  • Synchronized解决的是多线程访问资源的同步性。Synchronized可以保证同一时刻只有一个线程可以访问其修饰的方法或代码块。
  • Java 6 之前(synchronized属于重量级锁):
    a. 监视器锁(Monitor)依赖底层操作系统 Mutex Lock来实现;
    b. Java的多线程会映射到底层操作系统的多线程之上;
    c. 挂起或唤醒一个线程需要底层操作系统帮忙;
    d. 操作系统实现线程切换需要从用户态转换到内核态,这个状态转换需要很长的时间。
  • Java 6(JDK1.6) 之后:
    a. Java官方从JVM层面对synchronized进行了较大优化,锁效率已经优化的很好了;
    b. JDK1.6对锁引入了大量优化,如自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等技术来减少锁操作的开销。

8. 说说怎么使用Synchronized关键字?

  • 修饰实例方法。作用于当前 对象实例加锁,进入同步代码块前,需要先获得当前 对象实例的锁。
  • 修饰静态方法。给当前 类加锁,会作用于类的所有对象实例,进入同步代码块前,要获得当前class的锁。
    a. 静态方法不属于任何一个实例对象 ,是类成员(static表示这是当前类的一个静态资源,不管new了多少个对象实例,只有一份)。
    b. 如果线程A调用一个实例对象 的非静态synchronized方法,线程B可以调用这个实例对象所属类 的静态synchronized方法。
    c. 因为访问静态synchronized方法占用的是当前类的锁访问非静态synchronized方法占用的是当前实例对象锁
  • 修饰同步代码块。指定对象加锁,对给定对象/类加锁。
    a. synchronized(this|object) 表示进入指定同步代码块前要获得给定对象 的锁。
    b. synchronized(类.class)表示进入指定同步代码块前要获得当前class类 的锁。

总结

  • synchronized加到静态方法或synchronized(class)都是给当前类加锁。
  • synchronized加到实例方法上是给对象实例上锁。
  • 尽量不要使用synchronized(String a),因为在JVM中,字符串常量池具有缓存功能。

9. Synchronized关键字底层原理?

  • synchronized修饰同步代码块的情况
    a. 底层使用monitorenter和monitorexit指令 ,monitorenter指向同步代码块开始的地方,monitorexit指向同步代码块结束的地方。monitorenter指令执行时,线程试图获取锁,也就是获得对象监视器monitor的持有权。
    b. monitorenter尝试获取同步代码块的锁 => 如果对象未锁定/当前线程已经持有这个锁 => 锁计数器+1。否则阻塞等待,直到持有锁的线程释放锁。
    c. monitorexit是线程退出同步代码块时:锁计数器-1 => 锁计数器减为0,锁释放。
  • synchronized修饰方法的情况
    synchronized修饰方法时并没有使用monitorenter和monitorexit指令,而是使用的是ACC_SYNCHRONIZED 标识。JVM在调用时通过这个标识来区分这个方法是不是同步方法,从而执行相应的同步调用。

两者本质上都是对象监视器monitor的获取。

10. volatile关键字

首先了解一下Java内存模型JMM

  • JDK1.2之前
    JMM总是从主存(共享内存)中读取变量,是不需要特别注意的。
    2

  • 当前
    a. 线程可以把变量保存在本地内存中,而不是在主存中进行读写。
    b. 这样就可能出现一个线程在主存中修改了一个值,而另一个线程还在使用它本地内存中这个变量值的拷贝,造成数据不一致 (存在的问题)。
    2

要解决这个问题就需要用volatile来修饰变量,这就指示JVM,这个变量是共享且不稳定的,每次使用它,都要去主存(共享内存)中读取。
volatile关键字除了防止JVM指令重排,一个重要的作用就是保证变量的可见性。

11. 说说synchronized关键字和volatile关键字的区别

synchronized关键字和volatile关键字两者是互补的关系,不是对立的关系!

  • volatile是线程同步的轻量级实现,性能比synchronized好。但是volatile只能用于变量,而synchronized可以修饰方法以及代码块。(性能和作用范围不同)
  • volatile只能保证数据的可见性,不能保证数据的原子性,而synchronized都可以保证。(能保证的数据性质不同)
  • volatile解决的是变量在多线程之间的可见性,synchronized解决的是多线程访问资源的同步性。(解决的问题不同)

12. 使用线程池的好处?

  • 降低资源消耗:线程池可以重复使用已有的线程,减少了线程创建和销毁造成的资源消耗;
  • 提高响应速度:任务到达时,不用等线程创建,可以直接执行;
  • 提高线程的可管理性:线程是稀缺资源,如果无限的创建线程,不仅会增加资源消耗,还会降低系统稳定性,线程池可以对线程进行统一的分配、调优和监控。

13. 如何创建线程池?

Executor方法 (《阿里巴巴Java开发手册不允许使用》)

  • newFixedThreadPool(int threads)
  • newCachedThreadPool()
  • newSingleThreadExecutor()
  • newScheduledThreadPool(int curPoolSize)

不允许使用的原因是易造成内存泄漏(OOM)
造成OOM的原因(以newFixedThreadPool方法为例):

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

newFixedThreadPool与newSingleThreadExecutor导致OOM的原因都为以上原因;
newCachedThreadPool与newScheduledThreadPool创建的线程数都可能是Integer.MAX_VALUE,而创建这么多线程,必然有可能导致OOM。
参考链接:https://blog.csdn.net/yan88888888888888888/article/details/83927609

ThreadPoolExecutor

  • 可以直接使用ThreadPoolExecutor,自己为它的队列设置容量。这样任务个数超过指定容量就会抛出异常。

14. ThreadPoolExecutor类

  • ThreadPoolExecutor类提供了四个构造方法;
  • 以下展示包含参数最多的一个构造方法:
 /**
  * ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。 
  */
  public ThreadPoolExecutor(int corePoolSize, 
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit, 
                            BlockingQueue workQueue, 
                            ThreadFactory threadFactory, 
                            RejectedExecutionHandler handler) { 
       if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) 
           throw new IllegalArgumentException(); 
       if (workQueue == null || threadFactory == null || handler == null) 
           throw new NullPointerException(); 
       this.corePoolSize = corePoolSize; 
       this.maximumPoolSize = maximumPoolSize; 
       this.workQueue = workQueue; 
       this.keepAliveTime = unit.toNanos(keepAliveTime); 
       this.threadFactory = threadFactory; 
       this.handler = handler; 
   } 

15. ThreadPoolExecutor类构造函数参数分析

最重要的三个参数

  • corePoolSize
  • maximumPoolSize
  • workQueue

其他重要参数

  • keepAliveTime:当线程池中的线程数大于corePoolSize时,核心线程池外的线程不会立即销毁,而是等待keepAliveTime时间后再被回收销毁。
  • unit
  • theadFactory:executor创建新线程时会用到。
  • handler:饱和策略。

16. ThreadPoolExecutor饱和策略

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

参考:JavaGuide

步履不停
原文地址:https://www.cnblogs.com/yuanyunjing/p/15165439.html