2.多线程

## **一、什么是线程、什么是进程?**

进程可以理解为一个应用程序。比如微信,QQ等,而线程是进程的最小调度单位,一个进程有很多线程,至少有一个线程。

#### **==创建线程有三种方式==**:

(1)继承Thread类,重写Run()方法。这种创建方式优点是编写简单,但是扩展性差。

  (2)    实现Runnable接口,重写Run()方法。

(3)实现callable接口,重写call()方法,创建FutureTask对象。

(2)(3)方式的优点是扩展性好,而且实现callable接口可以通过FutureTask.get()方法获取返回值。





## **二、多线程**

多线程就是指从软件或者硬件上实现多个线程并发执行的技术,具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

那什么是并发?并发就是在一段时间内,有多个指令在单个CPU上交替执行。

并行:在同一时刻,有多个指令在多个CPU上同时执行

【在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go语言设计的哲学】	

​		

#### 什么是==守护线程==,它有什么特点

守护线程,它是一种专门为用户线程提供服务的线程,它的生命周期依赖于用户线程。只有 JVM 中仍然还存在用户线程正在运行的情况下,守护线程才会有存在的意义。否则,一旦 JVM 进程结束,那守护线程也会随之结束。也就是说,守护线程不会阻止 JVM 的退出。但是用户线程会!守护线程和用户线程的创建方式是完全相同的,我们只需要调用用户线程里面的setDaemon 方法并且设置成 true,就表示这个线程是守护线程。因为守护线程拥有自己结束自己生命的特性,所以它适合用在一些后台的通用服务场景里面。比如 JVM 里面的垃圾回收线程,就是典型的使用场景。这个场景的特殊之处在于,当 JVM 进程技术的时候,内存回收线程存在的意义也就不存在了。所以不能因为正在进行垃圾回收导致 JVM 进程无法技术的问题。但是守护线程不能用在线程池或者一些 IO 任务的场景里面,因为一旦 JVM 退出之后,守护线程也会直接退出。就会可能导致任务没有执行完或者资源没有正确释放的问题。

以上就是我对这个问题的理解。

#### ==runnable== 和 ==callable==有什么区别?

相同点:

1、都是接口

2、都可以编写多线程程序

3、都采用Thread.start()启动线程

主要区别:

1、Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

2、Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。

注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。



## 三、==线程的生命周期【线程6种状态分类】==

**操作系统层面有五种状态:新建、就绪、阻塞、运行、终结。**

当一个线程被创建之后,在不同的生命周期有不同的状态。

有新建(New)、可运行(Runnable)、阻塞(Blocking)、等待(Waiting)、计时等待(Timed-Waiting)、终止(Terminated)六种状态。

线程刚被创建(NEW)就是新建状态,调用start()方法,此时线程进入可运行(Runnable)状态。而此刻如果其他线程抢到(锁)CPU的执行权,该线程就会进入阻塞(Blocking)状态,等待下一次获取(锁),拿到锁,拿到之后就会运行。而当线程调用wait()方法后,会释放锁,进入等待状态,直到等待另一个线程去调用notify()notifyAll()唤醒后,重新获得锁才能进入运行状态。倘若唤醒之后尚未得到锁依旧会进入阻塞状态,直到获得锁。

当调用sleep()方法,会进入计时等待状态,当sleep时间结束后会进入执行,执行完后就是线程的终结即死亡状态。



## **四、线程池**

线程池存在的意义是创建线程的成本很高,频繁创建线程和销毁线程会消耗系统的资源。采用线程池,会创建大量的空闲线程,当我们向线程池提交任务时,线程池就会启动一个线程来执行该任务,执行完毕,线程并不会死亡,而是再次返回线程池中【空闲状态】,等待执行下一次任务。



### 线程池的核心参数都有哪些,说说线程池的运行原理?

线程池中总任务量=核心线程数+队列+(池中最大线程数-池中核心线程数)

​      我们在项目中也会通过线程池的方式提高程序的性能。 首先线程池可以避免频繁的创建和销毁线程所造成的性能损耗,原理就和数据库连接池 差不多,说白了就是项目启动的时候在线程池中就已经穿件好了指定数量的线程,需要的时 候直接去用,用完后再放回线程池供其他程序进行使用。

​         再者用线程池中的多线程可以处理大批量的数据,比如我要将数据库中的图片加水印或 者在硬盘上批量生成文件,这时候都可以用线程池。 就像要洗100个碗。你可以让一个人去干,这一个就像是一个线程,你也可以让10个人 一块去干,这就是线程池中的多线程,相比而言多线程执行的时间更短,效率更高。

​       在项目中我们是用ThreadPoolExecutor来创建线程池的,它里面有7个核心的参数信息,线程池的==核心线程数,队列、以及线程池的最大值。任务拒绝策略、临时线程存活时间、时间单位,线程工厂==线程池的工作原理是这样的。默认情况 下,创建线程池之后,线程池是没有线程的,需要提交任务之后才会创建线程。如果当前线 程池中的线程数目小与核心线程数,则没来一个任务。就会穿件一个线程去这行这个任务; 如果当前线程池中的数目>=核心线程数,则每来一个任务,会尝试将其添加到队列中,若添 加成功,则该任务会等待空闲线程将其取出去执行;当队列已满,添加失败,就会尝试创建 新的线程去执行这个任务;这时候创建新的线程是根据线程池时的最大线程数为依据的(这个时候添加的就是线程就是临时线程),如 果当前线程池中线程数目的达到最大线程数,在有任务进来就会则会采取任务拒绝策略进行处理;当临时任务结束后,会根据设置的存活时间去剔除临时线程。

​      举列子快递员说的简单点 就是先把核心线程数给占满了,不够用就开始往队列里面放,如果队列也沾满了,就往创建 最大线程池里面放,如果也沾满了就可以根据策略进行拒绝处理。 如果线程池中的线程数量大于corePoolSize[核心线程]时,某线程的空闲时间超过 keepAliveTime【存货时间】,线程将被终止,直至线程池中的线程数目大于corePoolSize【核 心线程】 线程池中的队列,一般常用的有ArrayBlockingQueue【有界阻塞队列】;基于数组的队 列,创建时必须制定大小:还有LinkedBlockingQueue【无界阻塞队列】:基于链表的先进先 出队列,如果创建时没有制定大小,则默认为lnteger.MAX_VALUE;(integer的最大值)

四大任务拒绝策略

​	ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。         	

​    ThreadPoolExecutor.DiscardPolicy: 		   丢弃任务,但是不抛出异常 这是不推荐的做法。
​	ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
​	ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。

==问题?==

​	线程池的参数怎么设置?

​		核心参数:

​			按照任务类型决定:

​			cpu密集型  核心数-1

​			io 密集型任务  核心数*2-1

​       最大线程数:

​				一般都是核心线程数的1.5倍

​	   任务队列

​			ArrayBlockQueue  有界阻塞队列  需要指定大小  (在可以接受无限等待的操作,比如文章的图片审核)

​			LinkBlockQueue   无界阻塞队列  容量默认是Integer 的最大值(关系到用户体验度的操作。越小越好)

​		





### ==线程池工作流程==

核心线程 -->任务队列 -->临时线程(最大线程数-核心线程数)-->拒绝策略



### 在 Java 中 ==Executor== 和 ==Executors== 的区别?

Executors 工具类的可以直接创建不同的线程池。

Executor 是个接口。

ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

**ThreadPoolExecutor** 是Executor接口的实现类,可以创建自定义线程池。

####  



 

### Executors 的==弊端==是什么?

newFixedThreadPool 和 newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM(内存溢出)。

newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。



### 讲下线程池的==线程回收==

好的,面试官,这个问题我需要从 3 个方面来回答。首先,线程池里面分为核心线程和非核心线程。核心线程是常驻在线程池里面的工作线程,它有两种方式初始化。向线程池里面添加任务的时候,被动初始化prestartAllCoreThreads主动调用	方法当线程池里面的队列满了的情况下,为了增加线程池的任务处理能力。线程池会增加非核心线程。核心线程和非核心线程的数量,是在构造线程池的时候设置的,也可以动态进行更改。由于非核心线程是为了解决任务过多的时候临时增1加的,所以当任务处理完成后, 工作线程处于空闲状态的时候,就需要回收。

因为所有工作线程都是从阻塞队列中去获取要执行的任务,所以只要在一定时间内,阻塞队列没有任何可以处理的任务,那这个线程就可以结束了。这个功能是通过阻塞队列里面的 poll 方法来完成的。这个方法提供了超时时间和超时时间单位这两个参数当超过指定时间没有获取到任务的时候,poll 方法返回null,从而终止当前线程完成线程回收。默认情况下,线程池只会回收非核心线程,如果希望核心线程也要回收,可以设置 allowCoreThreadTimeOut 这个属性为 true,一般情况下我们不会去回收核心线程。因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处于阻塞状态并没有占用 CPU 资源。以上就是我对这个问题的理解。



## 五、Java官方提供了==五种线程池==

好的。

JDK 中默认提供了 5 中不同线程池的创建方式,下面我分别说一下每一种线程池以及它的特点。

**①newCachedThreadPool**(单例线程池), 是一种可以缓存的线程池,它可以用来处理大量短期的突发流量。

它的特点有三个,最大线程数是 Integer.MaxValue,线程存活时间是 60 秒,阻塞队列用的是 SynchronousQueue,这是一种不存才任何元素的阻塞队列,也就是每提交一个任务给到线程池,都会分配一个工作线程来处理,由于最大线程数没有限制。所以它可以处理大量的任务,另外每个工作线程又可以存活 60s,使得这些工作线程可以缓存起来应对更多任务的处理。

**②newFixedThreaPool**(固定线程池),是一种固定线程数量的线程池。它的特点是核心线程和最大线程数量都是一个固定的值,如果任务比较多工作线程处理不过来,就会加入到阻塞队列里面等待。

**③newSingleThreadExecutor**(缓存线程池),只有一个工作线程的线程池。并且线程数量无法动态更改,因此可以保证所有的任务都按照 FIFO 的方式顺序执行。

**④newScheduledThreadPool**(延迟线程池),具有延迟执行功能的线程池可以用它来实现定时调度 **⑤newWorkStealingPool**,**Java8 里面新加入**的一个线程池它内部会构建一个 ForkJoinPool,利用**工作窃取的算法**并行处理请求,能够合理的使用CPU,进行并发运行任务。可以了解为,工作量一样,A,B同时开发,谁开发的快,谁就多做一些。

这些线程都是通过**工具类 Executors** 来构建的, 线程池的**最终实现类**是**ThreadPoolExecutor**。

以上就是我对这个问题的理解。

## 六、**自定义线程池**

### 创建线程池的==七大参数==:

参数一:核心线程数(不能小于0)

参数二:最大线程数(>=核心线程数)

参数三:临时线程最大存活时间(不能小于0)

参数四:时间单位(参数三的单位)

参数五:任务列队(不能为null)

参数六:创建线程工厂(不能为null,一般用默认线程工厂)

参数七:任务的拒绝策略(不能为null)

 

### **==核心线程数的参数设置==**:     看业务任务类型

​	1.CPU密集型得场景,比如像加解密,压缩、计算等一系列需要大量消耗CPU资源得任务,这些场景大部分都是纯CPU计算;

​	2.CPU密集型通常设置为n+1,这样也可避免多线程环境下CPU资源争抢带来得上下文频繁得开销;



​    1.I/O 密集型得场景再开发中比较常见,比如像M有SQL数据库读写、文件得读写、网络通信等任务,这类任务不会  特别消耗CPU资源,但是IO操作比较耗时,会占用比较多得时间;

​	2.I/O密集型通常设置为2n+1,其中**n为CPU核数**

​     总结:我们现在都是javaweb开发嘛,大多数的操作都是与数据库进行交互的,所以基本上都是io密集型,存储的瓶颈不在于自己的服务器,除非是自己提供的服务,就那些提供的计算的服务以外,大部分都是io密集的,可能都是根据最后的压测来评估这个数次,cpu在算法相关的用的多些,的确会把cpu打满



### **四大==拒绝策略:==**

ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法。

ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务

ThreadPoolExecutor.CallerRunsPolicy:  调用任务的run()方法绕过线程池直接执行【主线程执行】

### **==执行任务顺序==**:

核心线程 -->任务队列 -->临时线程(最大线程数-核心线程数)--> 拒绝策略

线程安全问题:多线程环境下,多个线程操作共享数据。



### **==解决线程安全问题的方式:加锁==**

线程问题:多线程操作共享数据会发生线程安全问题。例如有三个窗口一起买100张电影票,在窗口二在买第3张电影票时,还没来得及做减票操作,就被窗口三抢买第三张票的执行权,这样就会出现买重复票的问题。

#### **(1)synchronized锁**

Synchronized锁是个关键字,用c++语言实现,是内置锁,非公平锁。作用在变量或方法上,表示只有一个线程能够获取锁执行代码,其他线程阻塞。Synchonized锁会自动释放锁。底层是有Jvm执行两个命令。某一个线程进来执行时,会执行**`monitorenter`**命令,判断当前锁对象的status是否=0?如果=0,就会执行该逻辑,并且将status+1=1;当第二个线程进来时,如果status=1,他就会阻塞。等到第一个线程执行完释放后,会执行**`monitorexit`**,ststus-1=0即status=0,第二个线程才会拿到锁执行。

#### **(2)lock锁**

lock锁是一个接口,用java语言实现,公平锁。ReentrantLock是它的实现类。ReentrantLock的无参构造是非公平锁,带参构造是公平锁。ReentrantLock锁需要手动加锁和释放锁。

#### **(3)CAS算法**

CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。

在java中锁分为乐观锁和悲观锁。

**悲观锁**是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。

而**乐观锁**采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— `内存位置(V)`、`预期原值(A)`和`新值(B)`。如果内存地址里面的值和A的值是一样的【内存值=原值】,那么就将内存里面的值更新成新值B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

**CAS的问题**

①.CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了原子类AtomicStampedReference来解决问题。

②.CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。



## **线程相关问题总结**

### **Wait()方法和Sleep()方法**

两者都可以是线程暂停执行

sleep方法,不会释放资源(本质是占用线程),如果占具锁资源,则其他线程不可进;wait方法会释放锁资源,即其他线程可进来。

wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

### **Notify()方法和NotifyAll()方法**

如果线程调用了wait()方法后,线程处于等待状态。需要其他线程调用notify()方法或者notifyAll()方法唤醒再去抢占资源执行任务。

notify()方法只会唤醒一个等待中的线程,具体是哪个线程有JVM决定,而notifyAll()方法会唤醒所有等待中的线程。

### **==Start()==方法和==Run()==方法的**区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。



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

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。



### ==start()==方法为==什么能开启多线程==?

真正实现开启多线程的是start() 方法中的 start0() 方法。

调用start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE);具体什么时候执行,取决于 CPU ,由 CPU 统一调度;我们又知道 Java 是跨平台的,可以在不同系统上运行,每个系统的 CPU 调度算法不一样,所以就需要做不同的处理,这件事情就只能交给 JVM 来实现了,start0() 方法自然就表标记成了 native。



### 线程池中 ==submit()== 和 ==execute()==方法有什么区别?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有。

异常处理:submit()方便Exception处理。





### 线程并发安全体现在三方面:==可见性、有序性、原子性。==

**可见性**:一个线程对共享变量修改,另一个线程能看到修改的结构。

**有序性**:一个线程的时候,代码按照编写顺序执行

**原子性**:一个线程内多行代码以一个整体运行,执行的过程中不能有其他线程插队。要么都成功,要么都失败。

Volatile能否保证线程安全?保证禁止指令重排【内存屏障实现】

不能。Volatile能保证共享变量的可见性与有序性,但不能保证原子性。

**原子性:**

多线程情况下,一个线程读取数据进行操作,还没来得写回,就被另一个线程读取操作写回,此时第一个线程再去操作就会发生不一致的问题。

假设 m = 10,a线程对m进行+5,b线程对m进行-5;正常情况下结果是10。但在多线程情况下会发生指令交错,导致结果会不正确。

如果a线程在+5的过程中,b线程抢走了cpu的执行权,执行-5操作,此时m=5,但是此刻a线程并不知道m=5,所以依旧会读取m=10的数据,然后进行+5操作,那么m=15会覆盖掉m=5的结果。【如何解决原子性:加锁(synchronized、lock、CAS算法)】

**可见性:**

volatile关键字保证了可见性。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

那么如何保证可见性的呢?是通过lock + MESI缓存一致性协议。

对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改。

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据。 

### 如何保证==线程的执行顺序==

a.)使用Thread原生方法**`join方法`**

**join方法**是使所属的线程对象x正常执行run()方法中的任务,而当前线程进行无限的阻塞,等到线程x执行完成后再继续执行当前线程后面的代码。

b.)**使用线程间通信的等待/通知机制**--**wait()方法**。+ **notify()方法**唤醒

wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。

c.)**使用线程的CountDownLatch线程计数器**

CountDownLatch 的作用是:当一个线程需要另外一个或多个线程完成后,再开始执行。比如主线程要等待一个子线程完成环境相关配置的加载工作,主线程才继续执行,就可以利用 CountDownLatch 来实现。

~~~java
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器

await();//阻塞当前线程,将当前线程加入阻塞队列。

countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
~~~

### 如何保证A,B,C三个线程执行顺序

保证线程**同时执行**可以用于并发测试。可以使用倒计时锁`CountDownLatch`实现让三个线程同时执行

分别用两个方法实现了分别用`volatile`和倒计时锁`CountDownLatch`实现**顺序执行**

实现三个线程**交错打印**,可以使用ReentrantLock以及3个Condition来实现



### ==程序计数器==是线程共享的还是私有的

1. 是线程私有的 、不会存在内存溢出
2. 它是唯一一个在java虚拟机规范中没有OOM的区域
3. 作用:是用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令



### 你了解==ThreadLocal==的原理吗?怎么==保证线程安全==?

threadlocal是一个线程本地存储类,提供了给线程存储变量的功能,但是threadlocal本身不能存储,它是给每一个线程创建了一个threadlocalmap,每个线程都可以访问当前线程map的value值,ThreadLocal操作值的时候是取得当前线程的ThreadLocalMap对象,然后把值设置到了这个对象中,这样对于不同的线程得到的就是不同的ThreadLocalMap,那么向其中保存值或者修改值都只是会影响到当前线程,这样就保证了线程安全。

其内部维护了一个ThreadLocalMap,该Map用于存储每一个线程的变量副本。并且key为线程对象,value为对应线程的变量副本。

###  ==Threadlocal== 和==synchronize的区别==

synchronized加锁后性能降低,synchronized的原理是以时间换空间,提供一份变量,让多个线程排队访问

synchronized给我的感觉是以时间换空间,好比说它给每一个线程操作数据时提供了一个屋子,线程在操作数据的时候,其他线程处于阻塞状态在外面排队。

threadlocal的原理是以空间换时间,它会为每一个线程都提供一个变量副本,好比给每一个线程都准备了一个屋子处理数据,为每个线程都提供了一份变量副本。同时访问且互不干扰。



### ThreadLocal如何实现==线程隔离==的,实现原理是什么?

ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离



### ThreadLocal存在==内存泄露==问题吗?

存在!	根源是:**由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用**。

想要避免内存泄露就要**手动remove()掉**!









### 线程的调度模式是什么?

​	两分时调度和抢占式式调度。

​	分时调度:轮流获取CPU使用权。

​	抢占式调度:优先级高的线程占用CPU。



### Java 中你怎样==唤醒一个阻塞的线程==?

首先 ,wait()notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;

其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。

### sleep()wait() 有什么区别?

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

### 为什么 wait(), notify()notifyAll()必须在同步方法或者同步块中被调用?

这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁,也就是synchronized对象锁。





### Java ==线程数过多==会造成什么==异常==?

1、线程的生命周期开销非常高

2、消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

3、降低稳定性JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。



### 怎么==理解线程安全==?

简单来说,在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线程如何去交替执行。在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能按照预期的结果来反馈,那么这个类就是线程安全的。实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性。原子性呢,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断的,因为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前后执行结果不一致的问题。这个和数据库里面的原子性是一样的,简单来说就是一段程序只能由一个线程完整的执行完成,而不能存在多个线程干扰。CPU 的上下文切换, 是导致原子性问题的核心, 而 JVM 里面提供了Synchronized 关键字来解决原子性问题。可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的。导致可见性问题的原因有很多,比如 CPU 的高速缓存、CPU 的指令重排序、编译器的指令重排序。有序性,指的是程序编写的指令顺序和最终 CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决。在我看来,导致有序性、原子性、可见性问题的本质,是计算机工程师为了最大化提升 CPU 利用率导致的。比如为了提升 CPU 利用率,设计了三级缓存、设计了 StoreBuffer、设计了缓存行这种预读机制、在操作系统里面,设计了线程模型、在编译器里面,设计了编译器的深度优化机制。以上就是我对这个问题的理解。



### ==生产者和消费者模式==

概念:多个线程进行生产,同时多个线程进行消费,两种角色通过内存缓冲区进行通信。

即所谓生产者消费者问题实际上主要是包含了两类线程:一类是生产者线程用于生产数据;一类是消费者线程用于消费数据。为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

优点:极大的解决了代码之间的耦合程度
注释:解耦的意思假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。





## 七、**JUC包**

锁(locks)部分:提供适合各类场合的锁工具;
原子类(atomic)部分:原子变量类相关,是构建非阻塞算法的基础;
并发框架(executor)部分:提供线程池相关类型;
并发集合(collections) 部分:提供一系列并发容器相关类型;
同步工具(tools)部分:提供相对独立,且场景丰富的各类同步工具,如信号量、闭锁、栅栏等功能;

![](C:\Users\chaijt\Desktop\面试稿\img\image-20221101202533515.png)



### 什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

java.util.concurrent.atomic(JUC包下) 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray



### 谈谈你对==AQS==的理解![img](file:///C:\Users\chaijt\AppData\Local\Temp\ksohtml30724\wps5.png)

AQS 是多线程同步器, 它是 J.U.C 包中多个组件的底层实现, 如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.

从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。

排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。

共享锁也称为读锁, 就是在同一时刻允许多个线程同时获得锁资源,比如

CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。



### 说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

 

### 有使用过什么并发工具类吗?

CountdownLatch和Semaphore。

 

### CountdownLatch有什么作用?

控制线程等待时间,间接控制线程的执行顺序

CountDownLatch(倒计时器)是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

 

### Semaphore有什么作用?

控制当前方法的线程数量

Semaphore(信号量/通行令牌)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

你可能感兴趣的:(java)