线程知识点总结(2)

 

多用户并发访问是网站的基本需求,大型网站的并发用户数会达到数万,单台服务器的并发用户也会达到数百。CGI编程时代,每个用户请求都会创建一个独立的系统进程去处理。由于线程比进程更轻量,更少占有系统资源,切换代价更小,所以目前主要的Web应用服务器都采用多线程的方式响应并发用户请求,因此网站开发天然就是多线程编程。

从资源利用角度看,使用多线程的原因主要有两个:IO阻塞与多CPU。当前线程进行IO处理的时候,会被阻塞释放CPU以等待IO操作完成,由于IO操作(不管是磁盘IO还是网络IO)通常都需要较长的时间,这时CPU可以调度其他的线程进行处理。前面我们提到,理想的系统Load是既没有进程(线程)等待也没有CPU空闲,利用多线程IO阻塞与执行交替进行,可最大限度地利用CPU资源。使用多线程的另一个原因是服务器有多个CPU,在这个连手机都有四核CPU的时代,除了最低配置的虚拟机,一般数据中心的服务器至少16核CPU,要想最大限度地使用这些CPU,必须启动多线程。

网站的应用程序一般都被Web服务器容器管理,用户请求的多线程也通常被Web服务器容器管理,但不管是Web容器管理的线程,还是应用程序自己创建的线程,一台服务器上启动多少线程合适呢?假设服务器上执行的都是相同类型任务,针对该类任务启动的线程有个简化的估算公式可供参考:

启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]*CPU内核数

最佳启动线程数和CPU内核数量成正比,和IO等待时间成正比。如果任务都是CPU计算型任务,那么线程数最多不超过CPU内核数,因为启动再多线程,CPU也来不及调度;相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。

多线程编程一个需要注意的问题是线程安全问题,即多线程并发对某个资源进行修改,导致数据混乱。这也是缺乏经验的网站工程师最容易犯错的地方,而线程安全Bug又难以测试和重现,网站故障中,许多所谓偶然发生的“灵异事件”都和多线程并发问题有关。对网站而言,不管有没有进行多线程编程,工程师写的每一行代码都会被多线程执行,因为用户请求时并发提交的,也就是说,所有的资源——对象、内存、文件、数据库,乃至另一个线程都可能被多线程并发访问。

编程上,解决线程安全的主要手段有如下几点:

将对象设计为无状态对象:所谓无状态对象是指对象本身不存储状态信息(对象无成员变量,或者成员变量也是无状态对象),这样多线程并发访问的时候就不会出现状态不一致,Java Web开发中常用的Servlet对象就设计为无状态对象,可以被应用服务器多相处并发嗲用处理用户请求。而Web开发中常用的贫血模型对象都是些无状态对象。不过从面向对象设计的角度看,无状态对象时一种不良设计。

使用局部对象:即在方法内部创建对象,这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。

并发访问资源时使用锁:即多线程访问资源的时候,通过锁的方式使多线程并发操作转化为顺序操作,从而避免资源被并发修改。随着操作系统和编程语言的进步,出现各种轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小,但是锁导致线程同步顺序执行,可能会对系统性能产生严重影响。

 

 

 

1.什么是线程?

答:线程是指程序中的一个执行流。

2.什么是多线程?

答:多线程是指程序中包含多个执行流。

3.进程和线程的区别?

答:进程是指正在执行的程序,线程是程序中的一个执行流。一个进程可以包含多个线程。

4.为什么目前主要的Web应用服务器都采用多线程的方式响应并发用户请求,而不采用多进程?线程相比于进程有何优点?

答:线程比进程更轻量,占用的系统资源更少,切换的代价更小。

5.什么情况下会用到多线程?

答:响应用户并发请求。

6.我们为什么要使用多线程?

答:为了充分利用CPU资源,提高效率。

充分利用系统资源中的“充分”体现在什么地方?

答:(1)当前线程进行IO处理(读写磁盘资源)时,会被阻塞释放CPU执行权以等到IO操作完成,通常IO操作都需要较长时间,若使用多线程编程,这时CPU就可以调度其他的线程进行处理。

(2)现在服务器大多都是多核CPU,使用多线程可以最大限度地使用这些CPU。

7.一台服务器上启动多少线程合适呢?

答:启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]*CPU内核数

最佳启动线程数和CPU内核数量成正比,和IO等待时间成正比。

8.多线程有几种实现方案,分别是哪几种?

答:三种。

继承Thread类,重写run()方法;

实现Runnable接口,重写run()方法;

实现Callable接口,重写call()方法。

9.使用多线程就一定会提高CPU的利用率(提高效率)吗?

答:不一定。多线程如果使用得当会提高CPU的利用率,但如果使用不当的话,不仅不能提高CPU的利用率,反而会降低。因为多线程的操作流程要比单线程的多得多,比如线程的创建和销毁,线程之间的调度,CPU执行权的切换等等,而单线程是没有这些问题的,所以使用多线程不一定就会提高CPU的利用率。

10.什么是线程安全问题?

答:线程安全问题是指多个线程修改、访问同一资源时,产生的结果不确定的情况叫做线程安全问题。

如何解决线程安全问题?

答:1.将对象设计为无状态对象。简单来说,一个类中只有方法,没有属性,这样的类叫无状态类,这个类的实例对象叫无状态对象。

2. 使用局部对象。即在方法内部创建对象,这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。

3. 加同步。加同步有两种方法:使用sychronized关键字和使用Lock接口。

11.多线程实现同步的方式有哪些?

答:synchronized关键字;使用Lock接口。

12.同步代码块、同步方法和同步静态方法的锁对象分别是什么?

答:同步代码块的锁是任意对象

  同步方法的锁是this对象

  同步静态方法的锁是类的字节码对象 

13. 面试题:当一个线程进入一个对象的一个synchronized()方法后,其它线程是否可进入此对象的其他方法?

 

答:这取决于方法本身。如果该方法是synchronized()方法(锁对象是this对象),则不可以进入;如果该方法是非sychronized()方法,则可以进入;如果该方法是静态方法或者静态的synchronized()方法(锁对象是类的字节码对象),则可以进入。

14.同步的弊端有哪些?

答:效率低。每一次访问都需要判断有没有锁。

  如果出现了同步嵌套,容易出现死锁问题。

15.什么是死锁?

答:死锁指的是两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。

16.用代码写一个死锁。

package 死锁;

 

class Test implements Runnable{

//定义一个标志

private boolean flag;

//构造函数

Test(boolean flag){

this.flag = flag;

}

//重写run()方法

public void run() {

if(flag){

synchronized (MyLock.locka) {

System.out.println("if locka");

synchronized (MyLock.lockb) {

System.out.println("if lockb");

}

}

}else{

synchronized (MyLock.lockb) {

System.out.println("else lockb");

synchronized (MyLock.locka) {

System.out.println("else locka");

}

}

}

 

 

}

 

}

//定义两个锁

class MyLock{

static Object locka = new Object();

static Object lockb = new Object();

}

//主函数

public class DeadLock {

public static void main(String[] args) {

Thread t1 = new Thread(new Test(true));

Thread t2 = new Thread(new Test(false));

t1.start();

t2.start();

}

}

17.如何避免死锁问题的产生?

答:1.加锁顺序要一致。假如出现同步嵌套,都是先等待锁A,再等待锁B,而不是交叉等待。即一个嵌套先等待锁A,再等待锁B,另一个嵌套先等待锁B,再等待锁A。

  2.要避免锁未释放的情况。sychronized关键字实现的同步会自动释放锁,而Lock接口需要在finally块中调用unlock()方法释放锁。

18.启动一个线程是run()还是start()?它们的区别?

答:用start()方法来启动一个线程。

区别:

run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用

start():启动线程,并由JVM自动调用run()方法

19.sleep()和wait()方法的区别?

答:sleep():属于Thread类;必须指定时间,计时时间一到,线程会自动被唤醒;不释放锁;可以在任何地方使用;

wait():属于Object类;可以不指定时间,也可以指定时间需要被外界唤醒;释放锁;只能在同步代码块中使用。

20.为什么wait(),notify(),notifyAll()等方法都定义在Object类中?

答:因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。而Object代表任意的对象,所以,定义在这里面。

21.线程有几种状态?

答:新建、就绪、运行、阻塞、死亡。其中,阻塞状态又分三种情况:无限期等待:wait()方法,需要被外界唤醒

  限期等待:sleep()方法,能够自动唤醒。

  同步等待:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中,等待获取同步锁。

22.线程的生命周期图

答:新建 -- 就绪 -- 运行 -- 死亡

   新建 --  就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡

注意:

就绪:线程具有CPU的执行资格,但没有CPU的执行权(没有抢到CPU执行权)。(就像一名参赛选手,有比赛的资格,但还没有轮到他上场)

运行:有CPU执行资格,也有CPU执行权。

阻塞状态又分三种情况:

无限期等待:wait()方法,需要被外界唤醒

   限期等待:sleep()方法,能够自动唤醒

   同步等待:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中,等待获取同步锁。

面试题:(扩展)

1.就绪和等待状态有什么区别?

答:阻塞状态有CPU的执行资格,但是没有CPU的执行权,这一状态程序员无法控制。

等待状态没有CPU的执行资格,这一状态程序员可以控制。

2.无限期等待状态(sleep)和限期等待状态(wait)有什么区别?这一个问题,通常这样问:sleep()和wait()方法的区别?

答:sleep():属于Thread类;必须指定时间, 计时时间一到,线程会自动被唤醒;不释放锁;可以在任何地方使用;

wait():属于Object类;可以不指定时间,也可以指定时间,需要被外界唤醒;释放锁;只能在同步代码块中使用。

23.终止线程的方法有哪些?

答:Thread类提供了两个方法:stop(),suspend(),都可以用来终止线程。二者的区别是:

当调用Thread.stop()来终止线程时,会释放锁,可能会导致程序执行的不确定性。

当调用Thread.suspend()来终止线程时,不会释放锁,可称之为挂起线程,容易发生死锁。

通常情况下,结束线程最好的方法是让线程自行结束进入Dead状态,即提供某种方式让线程能够自动结束run()方法的执行,例如设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run()方法从而终止线程。

System.exit()也可以终止线程。其实是终止虚拟机JVM。

24.什么是守护线程?

答:java提供了两种线程:守护线程和用户线程。

守护线程在后台服务于用户线程,是用户线程的“保姆”。

GC属于守护线程。

25.Join()方法的作用?

答:将两个线程合并,用于实现同步功能。

26.什么是线程同步?

答:当多个线程访问同一资源时,为了防止线程安全问题的发生,需要线程同步。实现线程同步的方法一般有两种:加sychronized关键字、使用Lock接口。

Sychronized关键字是通过同步代码块和同步方法来实现同步,Lock接口是通过调用lock()和unlock()方法来实现同步。

27.synchronized关键字和Lock接口有什么区别?(阿里巴巴面试)

答:1.sychronized是关键字,Lock是接口。

2.sychronized关键字修饰的同步代码块或同步方法,锁的获取和释放,并不直观;而使用Lock接口,可以指定获取锁和释放锁的位置,比较直观。

3.synchronized关键字是托管给JVM执行的,在发生异常时,会自动释放锁,因此不会出现死锁;而Lock通过代码来操作,在发生异常时,不会自动释放锁,可能出现死锁,所以我们一般将Lock释放锁的操作放在finally块里。

4.代码写法上有区别。sychronized关键字实现同步,是通过同步代码块或同步方法实现的,在代码块上或方法上加sychronized关键字进行修饰。而Lock接口实现同步,是通过调用其lock()和unlock()方法实现的,并且lock()和unlock()方法要配合try/finally语句块来完成。

5.相比于synchronized关键字,Lock接口功能更高级。比如:等待可中断,可实现公平锁,以及锁可以绑定多个条件。

28.你了解多线程中的volatile关键字吗?简单介绍一下。

答:volatile关键字是Java虚拟机提供的最轻量级的同步机制。通过直接修饰共享变量,可以保证共享变量在内存中的可见性。

  什么是内存可见性?

  “可见性”指的是当一条线程修改了共享变量的值时,其他线程可以立即得知这个修改。

   什么是内存不可见?

  在多线程中,程序运行时,共享变量会存放在主内存中,每个线程在创建的时候都会创建一个属于自己的工作内存,它会将主内存中的共享变量保存一份在自己的工作内存中,这样一来就很容易造成当一个线程对共享变量进行修改时,另一个线程不知道的情况,即变量在内存中彼此不可见。 造成这个问题的原因是因为:普通变量的值在线程间传递均需要通过主内存来完成,即当线程A对共享变量进行修改后,需要同步到主内存中,而线程B在获取共享变量时,获取到的是修改前的值,而不是修改后的值。

而当共享变量被volatile关键字修饰后,就能够保证共享变量的内存可见性了。

volatile关键字如何保证共享变量的内存可见呢?

volatile保证共享变量内存可见性的原理是在线程每次访问共享变量时都要进行一次刷新,线程访问共享变量访问的是它本身工作内存中拷贝的主内存的那一份,刷新保证了线程每次访问之前,工作内存都要重新从主内存中拷贝一份共享变量,这就保证每次在工作内存中访问到的共享变量都是主内存中的最新版本。

注意:线程对共享变量进行修改,要分两步分,第一步是线程对工作内存中的共享变量副本进行修改,第二步是将工作内存中修改后的共享变量同步到主内存中,只有这两步全部完成,才实现了线程对共享变量的修改操作。

所以,这就出现了一种情况:主内存中的共享变量volatile int i=1,假如两个线程同时对共享变量进行修改,线程A,i =2,修改了工作内存中的共享变量,还没有同步到主内存中,这时,线程B抢到了CPU的执行权,也开始对共享变量进行修改,修改之前先刷新,获取到的最新的i=1,修改为i=3,并同步到主内存中,主内存i=3,这时,CPU执行权被线程A抢走了,主内存i=2,最后发现i=2。有人就有疑问了?我觉着i应该为3呀,为什么是2?这不就是共享变量并没有内存可见嘛!你看,我线程A先修改的i = 2,我修改完之后,线程B刷新获取到的值还是i=1,并不是i=2,这不是没有实现内存可见吗?我的回答是这样:我前面说过了,对共享变量的修改分两步,第一步是修改工作内存中的共享变量副本,第二步是将修改结果同步到主内存中去。上面的情况,当线程B抢到CPU执行权,对共享变量进行修改时,线程A还没有完成对共享变量的修改,所以这是主内存中i=1就是最新版本,线程B刷新获取到i=1并没有违背内存可见性。

29.你了解多线程中的synchronized关键字吗?简单介绍一下。

答:当多个线程操作同一资源的时候,容易产生线程安全问题,解决线程安全问题的方法是同步,synchronized关键字是同步的一种方式。它可以加在代码块上构成同步代码块,也可以加在方法上构成同步方法,用synchronized关键字解决同步问题需要锁对象,同步代码块的锁对象是任意对象,同步方法的锁对象是this对象,同步静态方法的锁对象是类的字节码对象。

30.volatile相较于synchronized的区别是什么?

答:1.volatile可以直接修饰变量,而synchronize不可以直接修饰变量

   2.volatile只能使用在变量级别(通过直接修饰变量实现),而synchronized既可以变量级别,也可以使用在方法和类级别(通过同步代码块和同步方法实现)。

  3.volatile能变量的内存可见性、有序性,不能保证变量的原子性;而sychronized既可以保证变量的内存可见性、有序性,也可以保证变量的原子性。

31.为什么volatile关键字不能保证变量的原子性?原因何在?

答:原因是volatile关键字修饰的变量,如果当前值与该变量以前的值相关,则volatile关键字不起作用。例如,有一个共享变量volatile int i = 1;在多个线程中执行了一下操作:i++或i = i+1,这时,volatile不能保证i值的内存可见性。

32.被volatile关键字修饰的变量具备哪些特点?

答:可见性和有序性。

33.对于volatile型变量的特殊规则

答:当一个变量定义为volatile之后,它将具备两种特性:

第一是保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得到的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。

volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用sychronized或java.util.concurrent中的原子类)来保证原子性。

£运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

£变量不需要与其他的状态变量共同参与不变约束。

第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存中描述的所谓的“线程内表现为串行的语义”。

上面的描述仍然不太容易理解,我们举一个具体的例子来说明:

public class Single{

    //构造函数私有化

private Single(){}

   

    //定义静态成员变量

private static Single s = null;

    

    //对外提供公共的访问方法

public static Single getInstance(){

if(s == null){

synchronized(Single.class){

if(s == null){

s = new Single();

}

}

}

return s;

}

}

就如上面所示,这个代码看起来很完美,理由如下:

•如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能。

•如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象。

•当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象。

通过上面的分析,DCL看起来确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?

在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

1. 分配内存空间;

2. 初始化对象;

3. 将内存空间的地址赋值给对应的引用;

但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

1. 分配内存空间;

2. 将内存空间的地址赋值给对应的引用;

3. 初始化对象;

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象。假如有两个线程A、B,线程A先执行,但是发生重排序了,所以线程A返回的singleton对象是一个没有被初始化的对象,仅仅只是一个地址而已。当线程B访问的时候,singleton != null,但是线程B拿到的仅仅只是一个地址而已,是一个没有被初始化的对象。

通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:

Singleton = new Singleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

1. 不允许初始化阶段步骤2、3发生重排序。

2. 允许初始化阶段步骤2、3发生重排序,但是不允许其他线程“看到”这个重排序。

这里我们讲第一种解决方案,很简单:将变量singleton声明为volatile即可:

public class Single{

    //构造函数私有化

private Single(){}

   

    //定义静态成员变量

private volatile static Single s = null;

    

    //对外提供公共的访问方法

public static Single getInstance(){

if(s == null){

synchronized(Single.class){

if(s == null){

s = new Single();

}

}

}

 

return s;

}

}

当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。

33.原子性、可见性与有序性

答:Java内存模型是围绕着在并发过程中如果处理原子性、可见性和有序性这3个特征来建立的,我们逐个来看一下哪些操作实现了这3个特性。

原子性(Atomicity)。由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子性协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。

如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐士地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性(Visibility)。可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了volatile之外,Java还有两个关键字能实现可见性,即sychronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值。

有序性(Ordering)。Java内存模型的有序性在前面讲解volatile时也详细地讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java语言提供了volatile和sychronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而sychronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

总结:

原子性:原子即不可分割的意思,要么操作全部成功,要么全部失败。举个例子,给int i=5赋值,只有两种情况,要么赋值成功,要么赋值不成功。所以说基本数据类型的访问读写是具备原子性的。

可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

有序性:有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

什么是指令重排序现象?

答:举个例子。创建对象的过程大体可分为三步:

1.分配内存空间

2.初始化对象

3.将内存空间的地址赋值给对应的引用

但由于指令重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

1.分配内存空间

2.将内存空间的地址赋值给对应的引用

3.初始化对象

 

线程池(掌握的不好)

34.什么是线程池?

答:线程池可以理解为是一个可以容纳多个线程的容器,里面的线程可以处于等待状态,避免了频繁创建和销毁线程造成的系统资源浪费。

35.为什么要使用线程池?

答:在java开发中,如果每一个请求的到来都需要创建一个新线程,那么对于系统资源的浪费是非常严重的,我们需要想一个办法来尽可能减少创建和销毁线程的次数。于是线程池应运而生了。线程池就是一个可以容纳多个线程的容器,里面的线程可以处于等待状态,用的时候直接从线程池中拿,不用的时候再放回到线程池中去,避免了频繁创建和销毁线程造成的系统资源浪费。

补充:

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。

使用线程池的好处?

答:1.频繁创建和销毁线程会降低系统执行效率。线程池可以缓存线程,当线程池中的线程执行完一个任务时,并不会直接销毁,而是会处于等待状态,当有新任务到来的时候,等待的线程可以付勇。

2.避免因为线程并发数量过多而导致的系统资源阻塞。线程能共享系统资源,如果同时执行的线程数量过多,就有可能导致系统资源不足而产生阻塞的情况,运用线程池能有效的控制线程最大并发数,避免以上问题。

3.对线程进行一些简单的管理。比如延时执行、定时循环执行的策略等。

36.线程池的创建一般是通过ThreadPoolExecutor类来进行的,ThreadPoolExecutor类的构造函数有多个,对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置,主要的参数有哪些呢?

答:核心线程池大小(corePoolSize)、最大线程池大小(maximumPoolSize)、workQueue(任务队列)、保持存活时间(keepAliveTime)、unit(keepAliveTime的单位,秒、分钟啊)等。

corePoolSize:核心线程池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把新到达的任务放到任务队列中进行缓存。

线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。 核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干。如果设置allowCoreThreadTimeOut=true,闲置状态的核心线程也会被销毁。

maximumPoolSize:最大线程池大小。最大线程数=核心线程数+非核心线程数。这个参数表示线程池中最多能创建多少个线程。

workQueue:任务队列。当所有核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。说白了,我线程池中的核心线程数量有限,可是任务源源不断的往线程池中加,线程不够呀,所以把多余的任务放在任务队列中进行等待。当任务队列也满了的时候,线程池就会创建非核心线程了,当核心线程和非核心线程总数超过maximumPoolSize线程池允许创建的最大线程数时,就会抛异常了,我这个线程池承受不住啦!

keepAliveTime:线程保持存活时间。一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉。如果设置allowCoreThreadTimeOut=true,则会作用于核心线程。

当一个任务被添加进线程池时,经过哪些步骤?

:1.线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务。

2.线程数量达到了corePoolSize,则将任务添加到任务队列进行缓存。

3.当任务队列已满,新建线程(非核心线程)执行任务。

4.当任务队列已满,并且总线程数又达到了maximumPoolSize,就会抛出异常。

另一种说法:

线程池按以下行为执行任务

  1. 当线程数小于核心线程数时,创建线程。

  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

  3. 当线程数大于等于核心线程数,且任务队列已满

       若线程数小于最大线程数,创建线程

       若线程数等于最大线程数,抛出异常,拒绝任务

37.线程池的种类有哪些?

答:Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的,它们分别是:固定大小线程池(FixedThreadPool)、可缓存线程池(CacheThreadPool)、单线程池(SingleThreadPool )、定时器线程池(newScheduledThreadPool)。

 

你可能感兴趣的:(线程)