1.刚才在写wait()的时候提到了sleep(),就说一下二者的区别。
wait() | sleep() |
来自Object类 | 来自Thread类 |
wait():会释放同步锁,让其他线程进入synchronized代码块执行。 | sleep():如果当前线程持有同步锁,那么sleep是不会让线程释放同步锁的。 |
wait()只能在同步控制方法或者同步控制块里使用,否则会出现IllegaMonitorStateException异常 | sleep()可以在任何地方使用 |
恢复方式不同:两者会暂停当前线程,wait()需要在其他线程调用同一对象的nofify()notifyAll() | sleep()在时间到了会重新恢复 |
2.既然提到了sleep(),就说一下sleep()和yield()方法有什么区别?
sleep() | yield() |
执行sleep()方法后进入超时等待(TIMED_WAITING)状态 | 而yield()方法进入就绪(REDAY)状态 |
sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会; | yield()方法只会给相同优先级或更高优先级的线程以运行的机会。 |
join()方法用法:
join() |
用于等待当前线程终止。如果一个线程A执行了threadB.join()语句,那么就代表着当前线程A等待threadB线程终止之后才从threadB.join()返回继续执行自己的代码。 |
3.run()和start()方法的区别?
run() | start() |
普通方法调用,在主线程中执行,不会新建一个线程来执行。 | 新启动一个线程,这时此线程处于就绪状态(可运行)并没有运行,一旦得到cpu时间片,就开始执行run()方法。 |
4.上边多次提到了运行状态,那就来说一下线程的状态流转
状态 | 解释 |
NEW | 新建但是尚未启动的线程处于此状态,没有调用start()方法 |
RUNNABLE | 包含就绪(READY)和运行中(RUNNING)两种状态。线程调用start()方法就会进入到就绪(READY)状态,等待获取cpu时间片,如果成功获取到cpu时间片,则会进入运行中(RUNNING)状态 |
BLOCKED | 线程在进入同步方法、同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。 |
WAITING | 无限期等待另一个线程执行特定操作的线程处于此状态,需要被唤醒,否则会一直等待下去例如Object.wait()需要等待另一个线程执行Object.notify()或者Object.notifyAll;对于Thread.join(),需要等待指定的线程终止。 |
TIMED_WAITING | 在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟WAITING类似,区别在于该状态有超时时间参数,在超时时间到了之后会自动唤醒,避免了无限期的等待。 |
TERMINATED | 执行完毕已经退出的线程处于此状态 |
总结:线程在给定的时间内只能处于一种状态,这些状态是虚拟机状态,不反映任何操作系统线程状态。
5.提到了线程,就说一下什么是并发和并行。
并发:两个或多个事件在同一时间间隔发生。(就是你这在吃饭你突然来电话了,你就接电话,然后接着去吃饭)
并行:两个或者多个事件在同一时刻发生。(一边打电话一边吃发)
6.既然提到了线程就说一下线程和进程的区别?
一个进程就是一个程序
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位,一个程序至少一个进程,一个进程至少一个线程,同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。同一进程内的线程共享本进程的资源如内存、IO、cpu等,但是进程之间的资源是独立的。所以一个进程崩溃后,在保护模式下不会对其他进程造成影响,但是一个线程崩溃整个进程都死掉。但是进程切换时消耗的资源大,效率低,所以在涉及频繁的切换时,使用线程要好于进程。
java默认开启两个线程:main(主线程) GC(垃圾回收)
7.创建线程的几种方式
①. 继承Thread类创建线程类
-
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
-
创建Thread子类的实例,即创建了线程对象。
-
调用线程对象的start()方法来启动该线程。
②. 通过Runnable接口创建线程类
-
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
-
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
-
调用线程对象的start()方法来启动该线程。
③. 通过Callable和Future创建线程
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
8.既然提到了Callable,就说一下Runnable和Callable的区别?
Runable:接口中的run()方法的返回值是void,它只是单纯的去执行run()方法中的代码而已.无法判断任务是否被线程池执行成功与否。
Callable:接口中call()方法是有返回值的,是一个泛型,和Future,FutureTask配合可以用来获取异步执行的结果。线程池会 返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成 功,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线 程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞 当前线程一段时间后立即返回,这时候有可能任务没有执行完。
9.synchonized各种加锁场景的作用范围(synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。)
作用于静态方法,锁住对象实例(this),每一个对象实例有一个锁 |
作用于Lock.class,锁住的是Lock的Class对象,也是全局只有一个。 |
作用于静态方法,锁住的是类Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。 |
作用于this,锁住的是对象实例,每一个对象实例有一个锁 |
作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个 |
10.提到了Lock就说一下synchronized 与Lock的区别。
synchronized | Lock |
是java中的关键字,是内置的语言实现 | 是一个接口 |
不需要手动获取锁和释放锁,再发生异常时,会自动释放锁,不会导致死锁现象发生 | 发生异常时,如果没有主动通过unLock()去释放锁,很可能会造成死锁现象,因此需要在finally块中释放锁 |
使用synchronized时,等待的线程会一直等待下去直到获取到锁。 | 使用更加灵活,可以有响应中断,有超时时间等 |
随着synchronized 这些年的优化性能没有明显差距,尽量使用synchronized ,如果synchronized 无法满足需求,才会选择Lock |
11. synchronized和volatile有什么区别?
synchronized | volatile |
可以修饰方法以及代码块(可以修饰变量) | volatile关键字只能用于变量 |
可能会发生阻塞 | 多线程访问不会发生阻塞 |
两者都能保证 | 能保证数据的可用性,但不能保证数据的原子性 |
解决多个线程之间访问资源的同步性 | 主要用于解决变量在多个线程之间的可见性 |
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized 关键字要好synchronized 关键字在 JavaSE1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引 入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
12.提到了死锁就说一下什么是死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
13.提到了死锁,就说一下如何检测死锁
死锁的四个必要条件 |
互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。 |
请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。 |
不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。 |
环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1) 占有(i=0, 1, …, n-1),Pn 等待的资源被 P0占 有,如下图所示。 |
14.怎么预防死锁?
预防死锁的方式就是打破四个必要条件中的任意一个即可。 |
打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件 |
打破请求和保持条件:1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。 |
打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。 |
打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。 |
15.创建线程池有几种方式?
new FixedThreadPool(int nThreads) | 创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未知预期的错误而结束时,线程池会补充一个新的线程。 |
new CachedThreadPool() | 创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不受限制 |
new SingleThreadExecutor() | 这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能够确保依照任务在队列中的顺序来串行执行。 |
new ScheduledThreadPool(int corePoolSize) | 创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer |
16.线程池的核心属性
threadFactory(线程工厂) | 用于创建工作线程的工厂 |
corePoolSize(核心线程数) | 当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。 |
workQueue(队列) | 用于保留任务并移交给工作线程的阻塞队列 |
maximumPoolSize(最大线程数) | 线程池允许开启的最大线程数 |
handler(拒绝策略) | 往线程池添加任务时,将在下面两种情况触发拒绝策略:1(线程池运行状态不是RUNNING)2(线程池已达到最大线程数,并且阻塞队列已满) |
keepAliveTime(保持存活时间) | 如果线程池数超过corePoolSize,ze多余的线程空闲时间超过keepAliveTime时会被终止。 |
17.线程池都有哪种状态?
线程池有五种状态Running、ShutDown、Stop、Tidying、Terminated。
RUNNING | (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。 (02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0! |
SHUTDOWN | (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。 |
STOP | (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。 |
TIDYING | (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 |
TERMINATED | (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。 (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。 |
18.线程池的执行流程(提交任务执行的execute()方法)
19.execute()方法和submit()方法的区别
-
接收的参数不一样
-
submit有返回值,而execute没有
-
submit方便Exception处理
-
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
-
submit()方法用于提交需要返回值的任务。线程池会 返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成 功 ,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线 程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法会阻塞 当前线程一段时间后立即返回,这时候有可能任务没有执行完。
20.线程池有哪些拒绝策略?
AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。
DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。
CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。
21.java中如何保证多线程的线程安全。
线程安全在三个方面体现:
-
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
-
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
-
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
22.介绍一下Atomic原子类。
自JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞
总的来说就是提供非阻塞的线程安全编程
23.JUC 包中的原子类是哪 4 类?
基本类型: 使用原子的方式更新基本类型
AtomicInteger(整形原子类) | 可以用原子方式更新的 int 值。 |
AtomicLong(长整型原子类) | 可以用原子方式更新的 long 值 |
AtomicBoolean(布尔型原子类) | 可以用原子方式更新的 boolean 值。 |
AtomicIntegerArray(整形数组原子类) | 可以用原子方式更新其元素的 int 数组 |
AtomicLongArray(长整型数组数组原子类) | 可以用原子方式更新其元素的 long 数组。 |
AtomicReferenceArray(引用类型数组原子类) | 可以用原子方式更新其元素的对象引用数组。 |
AtomicStampedRerence | AtomicStampedReference 维护带有整数“标志”的对象引用(原子更新引用类型里的字段原子类 ),可以用原子方式对其进行更新。 |
AtomicMarkableReference | AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。 |
AtomicIntegerFieldUpdate | 基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。(原子更新整形字段的更新器) |
AtomicLongFieldUpdater | 基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。(原子更新长整形字段的更新器) |
AtomicStampedReference | AtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。(
原子更新带有版本号的引用类型。该类将整
数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出
现的 ABA 问题。
) |
24.AtomicInteger 类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设
置为输
入值(update)
public final void lazySet(int newValue)//最终设置为 newValue,使用 lazySet 设置之后可能导致其他线
程
在之后的一小段时间内还是可以读到旧的值。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用 AtomicInteger 之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
25.简单介绍一下 AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile和native方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒死锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用 volatile 修饰保证线程可见性
状态信息通过 procted 类型的 getState,setState,compareAndSetState 进行操作//返回同步
状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS 操作)将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
28.AQS对资源的两种共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock,又可分为公平锁和非公平锁。
isHeldExclusively()//该线程是否正在独占资源。只有用到 condition 才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正
数表示成
功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回 true,失败则返回 false。