多线程&并发篇

目录

1、线程生命周期

2、线程创建方式

3、Callable 与 Future 

4、如何停止一个正在运行的线程 

5、notify() 和 notifyAll() 的区别

6、sleep() 和 wait() 的区别 

7、start() 和 run() 的区别 

8、interrupted 和 isInterruptedd 的区别

9、CyclicBarrier 和 CountDownLatch 的区别 

10、synchronized 和 ReentrantLock 的区别

11、为什么 wait、notify、notifyAll 这些方法不在thread类?

12、为什么 wait 和 notify 方法要在同步块中调用? 

13、Thread类中的yield方法有什么作用? 

14、volatile

15、volatile 可以保证有序性吗? 

16、synchronized

17、Semaphore

18、SynchronizedMap 与 ConcurrentHashMap 的区别 

19、线程之间是如何通信的?

20、锁的优化机制 

21、CAS 的原理 

22、CAS 有什么缺点吗? 

23、什么是AQS? 

24、乐观锁与悲观锁

25、产生死锁的四个必要条件  

26、如何避免死锁?

27、说说ThreadLocal原理

28、有三个线程T1,T2,T3,如何保证顺序执行? 

29、说说线程池 

30、什么是阻塞队列? 

31、线程池核心线程数怎么设置呢?  

32、常用的线程池有哪些?

33、线程池中 submit() 和 execute() 的区别 

34、什么是多线程中的上下文切换? 

35、什么是Daemon线程? 

36、什么是线程安全? 

37、Vector是一个线程安全类吗?

38、线程安全需要保证几个基本特征?  

39、多线程有什么用? 


1、线程生命周期

2、线程创建方式

  • 继承Thread
public class Thread01 extends Thread {
	
	@Override
	public void run() {
		System.out.println("当前线程:"+Thread.currentThread().getId());
	}
 
	public static void main(String[] args) {
		Thread01 thread = new Thread01();
		thread.start();
	}
	
}
  • 实现Runnable接口
public class Runnable01 implements Runnable {
 
	@Override
	public void run() {
		System.out.println("当前线程:"+Thread.currentThread().getId());
	}
	
	public static void main(String[] args) {
		Runnable01 runnable = new Runnable01();
		new Thread(runnable).start();
	}
	
}
  • 实现Callable接口( JDK1.5>=
public class Callable01 implements Callable<Integer> {
 
	@Override
	public Integer call() throws Exception {
		System.out.println("当前线程:"+Thread.currentThread().getId());
		int i = 10/2;
		return i;
	}
	
	public static void main(String[] args) throws Exception {
		Callable01 callable = new Callable01();
		FutureTask<Integer> futureTask = new FutureTask<>(callable);
		Thread thread = new Thread(futureTask);
		thread.start();
		System.out.println("当前线程执行结果:"+futureTask.get());
	}
	
}
  • 线程池方式创建
ExecutorService executor = Executors.newFixedThreadPool(10);

总结: 

  • 采用实现Runnable、Callable接口的方式创建线程的优缺点 

优点:线程类只是实现了Runnable或者Callable接口,还可以继承其他类。这种方式下,多个线程
可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法 。 

  • 采用继承Thread类的方式创建线程的优缺点 

优点:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用
this即可获取当前线程
缺点:因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。 

3、Callable 与 Future 

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable。

Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future 用于获取结果。 

4、如何停止一个正在运行的线程 

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止,但是不推荐这个方法,因为stopsuspendresume一样都是过期作 废的方法。
  • 使用interrupt方法中断线程。
class MyThread extends Thread {
 volatile boolean stop = false;
 public void run() {
 while (!stop) {
 System.out.println(getName() + " is running");
 try {
 sleep(1000);
 } catch (InterruptedException e) {
 System.out.println("week up from blcok...");
 stop = true; // 在异常处理代码中修改共享变量的状态
 }
 }
 System.out.println(getName() + " is exiting...");
 }
}
class InterruptThreadDemo3 {
 public static void main(String[] args) throws InterruptedException {
 MyThread m1 = new MyThread();
 System.out.println("Starting thread...");
 m1.start();
 Thread.sleep(3000);
 System.out.println("Interrupt thread...: " + m1.getName());
 m1.stop = true; // 设置共享变量为true
 m1.interrupt(); // 阻塞时退出阻塞状态
 Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
 System.out.println("Stopping application...");
 }
}

5、notify() 和 notifyAll() 的区别

notify 可能会导致死锁,而 notifyAll 则不会。

任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码。

使用 notifyall 可以唤醒 所有处于wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。

wait() 应配合 while 循环使用,不应使用 if,务必在 wait() 调用前后都检查条件,如果不满足,必须调 用 notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。

notify() 是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致 死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续 notify() 下一个线程,并且自身需要重新回到 WaitSet 中。

6、sleep() 和 wait() 的区别 

对于 sleep() 方法,我们首先要知道该方法是属于 Thread 类中的。而 wait() 方法,则是属于Object 类中的。

sleep() 方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用 sleep() 方法的过程中,线程不会释放对象锁。

当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。 

7、start() run() 的区别 

start() 方法被用来启动新创建的线程,而且 start() 内部调用了 run() 方法,这和直接调用run()方法的效果不一样。当你调用 run() 方法的时候,只会是在原来的线程中调用,没有新的线程启动,start() 方法才会启动新线程。 

8、interrupted isInterruptedd 的区别

interrupted() 和 isInterrupted() 的主要区别是前者会将中断状态清除而后者不会。

Java多线程的中断机制是用内部标识来实现的,调用 Thread.interrupt() 来中断一个线程就会设置中断标识为 true。当中断线程调用静态方法 Thread.interrupted() 来检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted() 用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出 InterruptedException 异常的方法都会将中断状态清零。

9、CyclicBarrier 和 CountDownLatch 的区别 

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

  • CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值 -1 而已,该线程继续运行。
  • CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务。
  • CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch 就不可再用了。

10、synchronized ReentrantLock 的区别

相似点: 

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如
果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等
待,而进行线程阻塞和唤醒的代价是比较高的。

区别: 

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互
斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和
unlock()方法配合try/finally语句块来完成。

Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,
ReentrantLock类提供了一些高级功能,主要有以下3项:

  1. 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
  2. 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
  3. 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。

11、为什么 wait、notify、notifyAll 这些方法不在thread

明显的原因是 Java 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线 程需要等待某些锁那么调用对象中的 wait() 方法就有意义了。如果 wait() 方法定义在 Thread 类中,线 程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所 以把他们定义在Object类中因为锁属于对象。 

12、为什么 wait 和 notify 方法要在同步块中调用? 

  • 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
  • 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
  • 还有一个原因是为了避免wait和notify之间产生竞态条件。 

wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经
获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。

在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某
个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。

调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用
notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设
置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。 

13、Thread类中的yield方法有什么作用? 

yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法,而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield() 的线程有可能在进入到暂停状态后马上又被执行。 

14、volatile

一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义: 

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
  2. 禁止进行指令重排序。 

volatile 不是原子性操作,使用volatile 一般用于 状态标记量单例模式的双检锁

15、volatile 可以保证有序性吗? 

什么叫保证部分有序性

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句 2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5 的顺序是不作任何保证的。

16、synchronized

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的
Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一
个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核
态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

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

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

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

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

17、Semaphore

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。 

18、SynchronizedMap 与 ConcurrentHashMap 的区别 

SynchronizedMap() 和 Hashtable 一样,实现上在调用 map 所有方法时,都对整个 map 进行同步。而ConcurrentHashMap的实现却更加精细,它对 map 中的所有桶加了锁。所以,只要有一个线程访问 map,其他线程就无法进入 map,而如果一个线程在访问 ConcurrentHashMap 某个桶时,其他线程,仍然可以对 map 执行某些操作。

所以,ConcurrentHashMap 在性能以及安全性方面,明显比 Collections.synchronizedMap() 更加
有优势。同时,同步操作精确控制到桶,这样,即使在遍历 map 时,如果其他线程试图对 map 进行数据修改,也不会抛出 ConcurrentModificationException。 

19、线程之间是如何通信的?

共享内存:

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。 如:线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

  1. 线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
  2. 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。

消息传递: 

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行
通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。 

20、锁的优化机制 

从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。锁的状态从低到高依次为:无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。 

自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起
线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,
可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开
启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。

自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上
的自旋时间和锁的持有者状态来决定。

锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要
加锁,就会进行锁消除。

锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操
作序列之外。

偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后
这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线
程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他
线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开
启偏向锁。

轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用
CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,
当前线程就尝试自旋来获得锁。

21、CAS 的原理 

CAS 叫做 CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数: 

  1. 变量内存地址,V表示
  2. 旧的预期值,A表示
  3. 准备设置的新值,B表示 

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。 

22、CAS 有什么缺点吗? 

ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,
更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。 

循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 

只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多
个可以通过AtomicReference来处理或者使用锁synchronized实现。 

23、什么是AQS 

AQS 全称为 AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说 java.util.concurrent 的基础是 CAS 的话,那么 AQS 就是整个 Java 并发包的核心了,
ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式
连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队
列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方法,以实现自己的并发功能。 

24、乐观锁与悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候
都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做
操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是
在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:

  1. 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
  2. java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

25、产生死锁的四个必要条件  

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

26、如何避免死锁?

指定获取锁的顺序:
比如某个线程只有获得A锁和B锁才能对某资源进行操作,规定只有获得A锁的线程才有资格获取B锁。 

27、说说ThreadLocal原理

ThreadLocal是Java所提供的线程本地存储机制,可以理解为线程本地变量,用来解决Java多线程并发问题的一种途径。他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 

ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,
Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对
的能力。弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key
为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 

但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出
现这个问题的。 

使用场景:

  • 使用日期工具类,当用到SimpleDateFormat作为成员变量是,可以使用ThreadLocal保证线程安全。
  • 全局用户信息。
  • 保证一个线程获取到的数据库连接Connection是同一个。
  • Spring框架中也大量使用ThreadLocal保证线程安全。

28、有三个线程T1,T2,T3,如何保证顺序执行? 

public class JoinTest {
    public static void main(String[] args) {
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1");
            }
        });
        final Thread t2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     // 引用t1线程,等待t1线程执行完
                     t1.join();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("t2");
             }
         });
         Thread t3 = new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     // 引用t2线程,等待t2线程执行完
                     t2.join();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("t3");
             }
         });
         // 这里三个线程的启动顺序可以任意,大家可以试下!
         t3.start();
         t2.start();
         t1.start();
     }
}

29、说说线程池 

@Configuration
public class ThreadConfig {
 
    @Bean
    public ThreadPoolExecutor executor(ThreadPoolProperties properties) {
        return new ThreadPoolExecutor(properties.getCoreSize(),
                                      properties.getMaxSize(),
                                      properties.getKeepAliveTime(),
                                      TimeUnit.SECONDS,
                                      new LinkedBlockingQueue<>(),
                                      Executors.defaultThreadFactory(),
                                      new ThreadPoolExecutor.AbortPolicy());
    }
 
}

ThreadPoolExecutor 参数:

  • int corePoolSize:核心线程数量,线程池创建好以后就存在
  • int maximumPoolSize:最大线程数量,控制资源
  • long keepAliveTime:存活时间,如果当前线程数量大于corePoolSize,线程空闲时间大于keepAliveTime,释放空闲的线程(maximumPoolSize-corePoolSize)
  • TimeUnit unit:时间单位
  • BlockingQueue<Runnable> workQueue:阻塞队列,如果任务很多,就会将目前多的任务放在队列中,只要线程空闲,就会去队列中取出新的任务继续执行 

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列,不指定就是Integer最大值。
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque
:一个由链表结构组成的双向阻塞队列。

  • ThreadFactory threadFactory:线程创建工厂
  • RejectedExecutionHandler handler:如果队列满了,按照我们指定的拒绝策略拒绝执行任务 

new AbortPolicy():丢弃抛异常,默认策略。
new CallerRunsPolicy():直接调用run方法。
new DiscardOldestPolicy():丢弃队列队头任务,并执行当前任务。
new DiscardPolicy():丢弃当前任务,也不抛出异常。   

工作流程:

  1. 线程池创建,准备好corePoolSize数量的核心线程,准备接受任务。
  2. corePoolSize满了,就将再进来的任务放入阻塞队列中。
  3. workQueue满了,就直接开启新线程执行,最大只能开到maximumPoolSize指定的数量。
  4. maximumPoolSize满了,使用handler拒绝任务。
  5. 任务都执行完成,空闲很多线程,在指定的keepAliveTime后,释放maximumPoolSize-corePoolSize这些线程。

示例: 一个 corePoolSize=7,maximumPoolSize=20,workQueue=50的线程池,100个并发进来怎么处理?

7个任务立即执行,50个任务进入队列,再开13个线程进行执行,剩下30个任务使用拒绝策略。 

总结:

  • 降低资源的消耗:通过重复利用已创建好的线程降低线程的创建和销毁带来的损耗。
  • 提高响应速度:因为线程池中的线程没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行。
  • 提高线程的可管理性:线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配。

30、什么是阻塞队列? 

阻塞队列(BlockingQueue):是一个支持两个附加操作的队列。 
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元
素的线程会等待队列可用。

JDK7提供了7个阻塞队列。分别是:

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:一个不存储元素的阻塞队列。
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿
元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

Java 5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实
现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized这些关键字。而
在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,
安全方面也有保障。

BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工
具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已
满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,
正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元
素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入
队列,然后解析线程不断从队列取数据解析。

31、线程池核心线程数怎么设置呢?  

CPU密集型: 

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

IO密集型:

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占
用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 : 核心线程数=CPU核心数量*2。 

32、常用的线程池有哪些?

  • newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
  • newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 

33、线程池中 submit() execute() 的区别 

两个方法都可以向线程池提交任务,execute() 方法的返回类型是 void,它定义在 Executor 接口中,而 submit() 方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。   

34、什么是多线程中的上下文切换? 

在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后
继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们
需要记住每本书当前读到的页码。

在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称
作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是
多任务操作系统和多线程环境的基本特征。 

35、什么是Daemon线程? 

所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。

注意:后台线程在不执行 finally 子句的情况下就会终止其run()方法。

比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。  

36、什么是线程安全? 

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每
次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失
误。很显然你可以将集合类分成两组,线程安全和非线程安全的。 

37、Vector是一个线程安全类吗?

Vector 是用同步方法来实现线程安全的, 而和它相似的 ArrayList 不是线程安全的。 

38、线程安全需要保证几个基本特征?  

  • 原子性:简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
  • 可见性:是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
  • 有序性:是保证线程内串行语义,避免指令重排等。 

39、多线程有什么用? 

  • 发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至
16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费
了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过
线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线
程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用
CPU的目的。

  • 防止阻塞 

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

  • 便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,
建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务
D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值