2.3 Multi-thread Flashcards

1
Q

说说线程的生命周期和状态?

A

NEW
- 初始状态,线程被构造,还没有调用start()

RUNNABLE
- 运行状态,包含READY & RUNNING 状态

BLOCKED
- 阻塞状态,表示线程阻塞于锁

WAITING
- 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些可定动作(通知和中断)

TIME_WAITING
- 超时等待状态,可以在指定时间内自行返回

TERMINATED
- 终止状态,表示当前线程已经执行完毕

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

产生死锁需要具备的条件有哪些?

A
  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

如何避免线程死锁?

A

上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了:

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资 源需要互斥访问)。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

说说 sleep() 方法和 wait() 方法区别和共同点?

A

两者最主要的区别在于:

sleep 方法没有释放锁,而 wait 方法释放了锁 。

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

Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

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

A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

说一说自己对于 synchronized 关键字的了解?synchronized关键字最主要的三种使用方式有哪些?

A

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

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一 个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有 一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用 这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前 实例对象锁。

修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

什么是双重校验锁实现对象单例?

A
public static Singleton getUniqueInstance() { 
	//先判断对象是否已经实例过,没有实例化过才进入加锁代码
	if (uniqueInstance == null) { //类对象加锁
		synchronized (Singleton.class) { 
			if (uniqueInstance == null) {
				uniqueInstance = new Singleton(); 
			}
		}
	}
	return uniqueInstance;
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1i>3i>2。指令重排在单线程环境下不会出 现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

谈谈 synchronized和ReentrantLock 的区别

A

1 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对 象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不 可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器 下降为0时才能释放锁。

2 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关 键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。 ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

3 ReentrantLock 比 synchronized 增加了一些高级功能 相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:1等待可中断;2可实现公平锁;3可实现选择性通知(锁可以绑定多个条件)

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock 类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制, ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。 Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一 个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的 Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结 合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供 的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注 册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成 很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所 有等待线程。

如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。

4 性能已不是选择标准

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

并发编程的三个重要特性

A
  1. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的 干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
  2. 可⻅性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新 值。volatile 关键字可以保证共享变量的可⻅性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

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

A
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。 但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。 synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗 而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞 volatile关键字能保证数据的可⻅性,但不能保证数据的原子性。synchronized关键字两者都能 保证。
  • volatile关键字主要用于解决变量在多个线程之间的可⻅性,而 synchronized关键字解决的是 多个线程之间访问资源的同步性。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

为什么要用线程池?

A
  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系 统的稳定性,使用线程池可以进行统一的分配,调优和监控。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Runnable接口和Callable接口的区别

A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

execute()方法和submit()方法的区别是什么呢?

A
  1. execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取 返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行 完。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

ThreadPoolExecutor 构造函数重要参数分析

A

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。可指定queue capacity.

ThreadPoolExecutor 其他常⻅参数:

  1. keepAliveTime :当线程池中的线程数量大于corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :executor 创建新线程的时候会用到。 4. handler :饱和策略。关于饱和策略下面单独介绍一下。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

ThreadPoolExecutor 饱和策略有哪些?

A

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy :抛出RejectedExecutionException 来拒绝新任务的处理。默认策略。
  • ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加 队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。 ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

ThreadPoolExecutor执行任务的流程是什么?

A
  1. 提交任务
  2. 检查核心线程池是否已满
    - NO -> 创建线程
    - YES -> 检查等待队列是否已满
    • NO -> 加入队列
    • YES -> 检查线程池是否已满
      • YES -> 按照策略执行