Java多线程面试题(面试官常问)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Java多线程⾯试题(⾯试官常问)

进程和线程

进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是从⼀个进程从创建、运⾏到消亡的过程。在Java中,当我们启动main函数时其实就是启动了⼀个JVM的进程,⽽mian函数所在的线程就是这个进程中的⼀个线程,称为主线程。

线程是⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程都有⾃⼰的程序计数器、虚拟机和本地⽅法栈,所以系统在产⽣⼀个线程,或在各个线程之间切换⼯作是,负担要⽐进程⼩很多,所以线程也称轻量级进程。

并发和并⾏

并发:同⼀时间段内,多个任务都在执⾏(单位时间内不⼀定同时执⾏)

并⾏:单位时间内,多个任务同时执⾏。

上下⽂切换

多线程编程中⼀般线程的个数都⼤于CPU核⼼的个数,⽽⼀个CPU核⼼在任意时刻内只能被⼀个线程使⽤,为了让这些线程

都能得到有效执⾏,CPU采取的策略时为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程属于⼀次上下⽂切换。

换句话说,当前任务在执⾏完CPU时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。

sleep()和wait()

最主要的区别是sleep()⽅法没有释放锁,⽽wait()⽅法释放了锁。

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

wait()通常⽤于线程间交互/通信,sleep()通常⽤于暂停执⾏。

wait()⽅法被调⽤后,线程不会⾃动苏醒(除⾮超时),需要别的线程调⽤同⼀个对象上的notify()或notifyAll()⽅法。

⽽sleep()⽅法执⾏完后,线程会⾃动苏醒。

start()和run()

为什么调⽤start()⽅法时会执⾏run()⽅法,为什么不能直接调⽤run()⽅法?

当我们new⼀个Thread时,线程进⼊了新建状态,调⽤start()⽅法,会启动⼀个线程并使线程进⼊就绪状态,等分到时间⽚后就可以开始运⾏了。

start()会执⾏线程的相应准备⼯作,然后⾃动执⾏run()⽅法的内容,这是真正的多线程⼯作。

⽽直接执⾏run()⽅法会把run⽅法当作⼀个main线程下的普通⽅法去执⾏,并不是在某个线程中执⾏它,所以这不是多线程⼯作。

synchronized关键字

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

synchronized主要的三种使⽤⽅式:

1.修饰实例⽅法

作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁。

2.修饰静态⽅法

给当前类加锁,会作⽤于类的所有对象实例,因为静态成员是类成员,不属于任何⼀个实例对象,所以线程A调⽤⼀个实例对象的⾮静态synchronized⽅法,⽽线程B调⽤该实例对象所属类的静态synchronized⽅法时是允许的,不会冲突互斥。因为访问静态synchronized⽅法占⽤的是当前类的锁,⽽访问⾮静态synchronized⽅法占⽤的是当前实例对象锁。

3.修饰代码块

指定加锁对象,进⼊同步代码库前要获得给定对象的锁。

synchronized和ReentrantLock:

1.两者都是可重⼊锁

即⾃⼰可以再次获取⾃⼰的内部锁。⽐如⼀个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁时还是可以获取的。

2.前者依赖JVM⽽后者依赖API

synchronized是依赖于JVM实现的,ReentrantLock是依赖于JDK层⾯实现的。

3.ReentrantLock⽐synchronized功能多

ReentrantLock增加了⼀些⾼级功能,主要说有三点:①等待可中断②可实现公平锁③可实现选择性通知。

volatile关键字

当前Java内存模型下,线程可以把变量保存到本地内存(如寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还在继续使⽤它在寄存器中变量值的拷贝,造成数据的不⼀致。volatile关键字就是解决这个问题,指⽰JVM这个变量不稳定,每次使⽤它都要到主存中进⾏读取。除此之外还有⼀个重要的作⽤是防重排。

并发执⾏的三个重要特性:

1.原⼦性

要么所有的操作都得到执⾏并且不会收到任何因素⼲扰⽽中断,要么所有的操作都不执⾏。可使⽤synchronized来保证代码原⼦性。

2.可见性

当对⼀个共享变量进⾏了修改后,那么另外的线程都是⽴即可以看到修改后的最新值。volatile可以保证可见性。

3.有序性

代码在执⾏过程中的先后顺序,Java在编译器以及运⾏期间的优化,代码的执⾏顺序未必就是编写代码时候的顺序,即指令重排。volatile可以禁⽌指令重排优化。

ThreadLocal

通常情况下,我们创建的变量时可以被任何⼀个线程访问并修改的。如果要实现每⼀个线程都有⾃⼰的专属本地变量该如何解决?这就需要ThreadLocal类了。

ThreadLocal类主要解决的就是让每个线程绑定⾃⼰的值,可以将ThreadLocal类⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。

当创建⼀个ThreadLocal变量后,访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal名称的由来。可以使⽤get()和set()⽅法来获取默认值或将其值更改为当前线程所存副本的值,从⽽避免了线程安全问题。

实际上,ThreadLocal类有⼀个静态内部类ThreadLocalMap,可以把ThreadLocalMap看作是ThreadLocal类定制的HashMap,最终的变量是放在了当前线程的ThreadLocalMap中,⽽不是ThreadLocal类上,可以看作ThreadLocal类是ThreadLocalMap的封装,传递了值。

如果再同⼀个线程中声明了两个ThradLocal对象的话,会使⽤Thread内部仅有的那个ThreadLocalMap存放数据的,TheadLocalMap的key就是ThreadLocal对象,value就是ThreadLocal对象调⽤set⽅法设置的值。

ThreadLocalMap使⽤的key为ThreadLocal的弱引⽤,⽽value是强引⽤。所以ThreadLocal没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽value不会被

清理掉。这样⼀来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value永远⽆法被GC回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调⽤set()、get()、remove()⽅法的时会清理掉key为null 的记录。使⽤完ThreadLoca l⽅法后最好⼿动调⽤remove()⽅法。

线程池

池化技术⼤家应该很熟悉,线程池、数据库连接池、Http连接池等等都是对这思想的应⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。

使⽤线程池,可以降低资源消耗、提⾼响应速度、提⾼线程的可管理性。

Runnable和Callable

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

⼯具类Excutors可以实现Runnable对对象和Callable对象之间的相互转换。

@FunctionalInterface

public interface Runnable{

//没有返回值也⽆法抛出异常

public abstract void run();

}

@FunctionalInterface

public interface Callable<V>{

//@return 计算得出结果

//@throws 如果⽆法计算结果,则抛出异常

}

execute()和submit()

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

2. submit()⽅法⽤于提交需要返回值的任务。线程池会返回⼀个Future类型对象,通过这个对象可以判断任务是否执⾏成功。创建线程池

相关文档
最新文档