java面试突破 - 并发编程面试汇总

目录

1. 多线程有什么用?(为什么用它)

  • 发挥多核CPU的优势!

单核CPU:所谓的多线程是假的!同一时间只会处理一段逻辑,只不过线程切换的快!像多个线程同时运行!

多核CPU:真正的多线程!多段逻辑同时工作!充分利用CPU!

  • 防止阻塞

单CPU时,线程间的上下文切换,反而会降低所谓多线程的效率!但是还是使用多线程,就是为了防止阻塞!(防止某一线程超时!)

  • 便于建模

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

2. 多线程和单线程的区别和联系?

  1. 在单核CPU中,将CPU分成很小的时间片!在每个时刻只能有一个线程执行,是一种微观上轮流占用CPU的机制!

  1. 多线程会存在上下文切换!导致程序变慢;即采用一个拥有两个线程的进程执行所需的时间比一个线程的进程执行两次所需要的时间要多!
  • 结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。

3. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?

  • 线程

与进程相似,一个进程在执行过程中可以产生多个线程
不同点:同类的多个线程共享同一块内存空间和一组系统资源;所以系统在产生线程和线程的切换时,负担比进程小。所以线程被称为轻量级进程

  • 程序

含有指令和数据的文件!被存储在磁盘或者其他数据存储设备当中,可以说是静态的代码

  • 进程

与程序的关系
是程序的一次执行过程,是系统运行程序的基本单位动态的
系统运行一个程序 就是 一个进程从 创建、运行、消亡的过程!(一个进程就是一个执行中的程序
每个进程占有某些系统资源(CPU时间、内存空间、文件、输入输出设备的使用权等);当程序运行时,会被操作系统载入内存空间!

与线程的关系
线程是进程划分为更小的运行单位。
最大的不同:进程基本上是独立的,而线程则不一定,同一个进程的线程间可能会相互影响
另一个不同
进程属于操作系统范畴,同一时间段,可以执行一个以上的程序
线程在同一程序内几乎同时执行一个以上的程序段

4. 线程的创建方式

七种方法:后面带了相应方法的例子

  1. 继承Thread类,作为线程对象存在
  2. 实现runnable接口,作为线程任务存在
  3. 匿名内部类创建线程对象
  4. 创建带返回值的线程
  5. 定时器Timer
  6. 线程池创建线程
  7. 利用java8新特性 stream 实现并发
  • 方法一:继承Thread类,作为线程对象存在(继承Thread对象)
public class CreatThreadDemo1 extends Thread{
	/**
	* 构造方法: 继承父类方法的Thread(String name);方法
	* @param name
	*/
	public CreatThreadDemo1(String name){
		super(name);
	}
	
	@Override
	public void run() {
		//interrupted方法,是来判断该线程是否被中断。
		while (!interrupted()){
			System.out.println(getName()+"线程执行了...");
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		CreatThreadDemo1 d1 = new CreatThreadDemo1("first");
		CreatThreadDemo1 d2 = new CreatThreadDemo1("second");
		
		d1.start();
		d2.start();
		//(终止线程不允许用stop方法,该方法不会施放占用的资源。)
		d1.interrupt(); //中断第一个线程
	}
}
  • 让线程等待的方法
  • Thread.sleep(200); //线程休息2ms
  • Object.wait(); //让线程进入等待,直到调用Object的 notify 或者 notifyAll 时,线程停止休眠
  • 方法二:实现runnable接口,作为线程任务存在
public class CreatThreadDemo2 implements Runnable {
	@Override
	public void run() {
		while (true){
			System.out.println("线程执行了...");
		}
	}
	public static void main(String[] args) {
		//将线程任务传给线程对象
		Thread thread = new Thread(new CreatThreadDemo2());
		//启动线程
		thread.start();
	}
}

Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里

  • 方法三:匿名内部类创建线程对象
public class CreatThreadDemo3 extends Thread{
	public static void main(String[] args) {
		//创建无参线程对象
		new Thread(){
			@Override
			public void run() {
				System.out.println("线程执行了...");
			}
		}.start();
		//创建带线程任务的线程对象
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("线程执行了...");
			}
		}).start();
		//创建带线程任务并且重写run方法的线程对象
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("runnable run 线程执行了...");
			}
		}){
			@Override
			public void run() {
				System.out.println("override run 线程执行了...");
			}
		}.start();
	}
}

创建带线程任务并且重写run方法的线程对象中:为什么只运行了Thread的run方法?
源码:在这里插入图片描述
原因:Thread实现了Runnable接口,而Runnable接口里有一个run方法。
结果:匿名内部类创建线程对象的第三种,会调用Thread类的run方法。而不是Runnable接口的run方法。(输出:override run 线程执行了…)

  • 方法四:创建带返回值的线程
public class CreatThreadDemo4 implements Callable {
	public static void main(String[] args) throws ExecutionException,InterruptedException {
		CreatThreadDemo4 demo4 = new CreatThreadDemo4();
		
		FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最终实现的是runnable接口
	
		Thread thread = new Thread(task);
		thread.start();
		System.out.println("我可以在这里做点别的业务逻辑...因为FutureTask是提前完成任
	务");
		//拿出线程执行的(call方法)返回值
		Integer result = task.get();
		System.out.println("线程中运算的结果为:"+result);
	}
	
	//重写Callable接口的call方法
	@Override
	public Object call() throws Exception {
		int result = 1;
		System.out.println("业务逻辑计算中...");
		Thread.sleep(3000);
		return result;
	}
}

Callable接口介绍:

public interface Callable<V> {
	/**
	 * Computes a result, or throws an exception if unable to do so.
	 * 计算结果,或者抛异常(无法计算时)
	 * @return computed result
	 * @throws Exception if unable to compute a result
	 */
	V call() throws Exception;
}

返回指定泛型的call方法。然后调用FutureTask对象的get方法得到call方法的返回值

  • 方法五:定时器Timer
public class CreatThreadDemo5 {
	public static void main(String[] args) {
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println("定时器线程执行了...");
			}
		},0,1000); //延迟0,周期1s
	}
}
  • 方法六:线程池创建线程
public class CreatThreadDemo6 {
	public static void main(String[] args) {
		//创建一个具有10个线程的线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(10);
		long threadpoolUseTime = System.currentTimeMillis();
		for (int i = 0;i<10;i++){
			threadPool.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName()+"线程执行了...");
				}
			});
		}
		long threadpoolUseTime1 = System.currentTimeMillis();
		System.out.println("多线程用时"+(threadpoolUseTime1-threadpoolUseTime));
		//销毁线程池
		threadPool.shutdown();
		threadpoolUseTime = System.currentTimeMillis();
	}
}
  • 方法七:利用java8新特性 stream 实现并发

5. 线程有哪些基本状态?

操作系统中 有 三态模型 ->五态模型->七态模型;通常说的是五态模型。

  • 三态模型
    在这里插入图片描述
  • 五态模型

在这里插入图片描述

  • 七态模型
    在这里插入图片描述

6. 如何停止一个正在运行的线程?

1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
3、使用interrupt方法中断线程。

7. start()方法和run()方法的区别?

  • 区别:

1、start方法 用来启动相应的线程
2、run方法 只是thread的一个普通方法,在主线程里执行
3、需要并行处理的代码放在run方法中,start方法 启动线程后 自动调用 run方法;
4、run方法必须是public的访问权限,返回类型为void

理解:

run() 是由JVM直接调用的,如果没有启动线程(调用start()方法),就直接调用run(),那么这个run方法其实是运行在当前线程中(如:main中),而不是之前定义的线程中!

  • 总结

只有调用了start()方法,才体现多线程的特性,不同线程里的run()中的代码交替执行!
如果只调用run(),那么代码还是同步代码,必须一个run结束才运行下一个run()。

8. Runnable接口和Callable接口的区别

  • 返回值不同

1、Runnable接口中的run()方法返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2、Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

利用这一点,可以解决这些问题:

某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?

Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。

9. 什么是线程安全?

多线程访问同一段代码,不会产生不确定的结果!

  • 数据不共享(private成员),一定线程安全!
  • 同步控制:synchronized……

如何确保线程安全?

对非安全的代码进行加锁控制
使用线程安全的类
多线程并发情况下,线程共享的变量改为方法级的局部变量

10. 在多线程中,什么是上下文切换(context-switching)?

单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。(一段代码在分配到的时间片内没有完成,从运行->就绪;直到下一次调用,此时时间片分给另一段程序,发生上下文切换)

上下文切换时:要保存当前线程运行的位置到内存;并从内存中加载需要恢复的线程的信息!(耗时)

11. 什么是竞态条件?你怎样发现和解决竞争?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件

解决:

临界区中使用适当的同步就可以避免竞态条件。
一种是用synchronized,一种是用Lock显式锁实现。

12. 用户线程和守护线程有什么区别?

守护(Daemon)线程:运行在后台,为JVM中所有非守护(前台)线程的运行提供便利服务**(佣人)**:只要当前 JVM 实例中 尚存任何一个非守护线程没有结束,守护线程就全部工作;只有 最后一个非守护线程结束时,守护线程随着JVM一同结束工作!

用户(User)线程:运行在前台,执行具体的任务。如:main、连接网络的子线程等!

例如:main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程

  • 区别:用户线程结束,JVM退出(不管有没有守护线程在运行)。守护线程不影响JVM的退出!(因为没有被守护者,Daemon也就没有运行的必要了)

  • 注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
  2. 守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
  4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

13. 如何创建守护线程?以及在什么场合来使用它?

任何线程都可以被设置为守护线程和用户线程;

Thread.setDaemon(boolean);
//true则把该线程设置为守护线程,
//反之则为用户线程。

Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

守护线程相当于后台管理者 比如 : 进行内存回收,垃圾清理等工作

14. 线程安全的级别

  1. 不可变
    不可变得对象一定是线程安全的,并且永远不需要额外的同步!

Java类库中大多数基本数值类如IntegerStringBigInteger都是不可变的。

  1. 无条件的线程安全
    由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要额外的同步

RandomConcurrentHashMapConcurrent集合atomic

  1. 有条件的线程安全
    对于单独的操作可以是线程安全的,但是某些操作序列可能需要额外同步。

有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器

  1. 非线程安全(线程兼容)
    不是线程安全的,但是可以通过正确使用同步而在并发环境下安全使用!

ArrayListHashMap

  1. 线程对立
    不管是否采用同步措施,都不能在多线程环境中并发的使用!

如System.setOut()、System.runFinalizersOnExit()

15. 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

  • 线程调度器

是一个操作系统服务,负责为Runnable 状态的线程分配CPU时间!一旦创建一个线程并启动它,他的执行便依赖于线程调度器的实现!

  • 时间分片

指将可用的CPU时间分配给可用的Runnable线程的过程!(可以基于线程优先级、线程等待时间)

线程调度并不受JVM控制,所以由应用程序来控制他是更好的选择!

16. volatile关键字的作用

一旦一个共享变量(类的成员变量、静态成员变量) 被 volatile 修饰后,有两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序

volatile 与 synchronized 的区别!

锁、(when)什么地方使用、修改可见性+原子性、是否优化

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
    synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别
    synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,并不能保证原子性;
    synchronized则可以保证变量的修改可
    见性和原子性
  • volatile不会造成线程的阻塞;
    synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化;
    synchronized标记的变量可以被编译器优化

从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic 包下的类,比如 AtomicInteger

17. volatile 变量和 atomic 变量有什么不同?

二者相似,但是功能不同!

  • Volatile变量:可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
    例如:用volatile修饰count变量那么 count++ 操作就不是原子性的。
  • atomic:AtomicInteger类提供的atomic方法可以让这种操作具有原子性
    如:getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

18. volatile 是什么?可以保证有序性吗?

参考16题的两层语义!volatile 不是原子性操作

  • 什么叫保证部分有序性?

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行

x = 2; 		 //语句1
y = 0;		 //语句2
flag = true; //语句3
x = 4; 		 //语句4
y = -1; 	 //语句5

1、2 在 3 之前;4、5 在 3 之后;
但是 1、2 与 4、5 的顺序由于不是volatile的,所以不确定!

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

  • 使用 Volatile 一般用于 状态标记量单例模式的双检锁

19. 什么是Java内存模型?

Java Memory Model,JMM
定义了一种多线程访问Java内存的规范!

  1. JMM将内存分为了主内存工作内存!类的状态,也就是类之间共享的的变量,是存储在主内存中的,每次Java线程 用到这些变量时,会读一次主内存中的变量,并让这些内存在自己的工作内存拷贝一份,运行时操作的都是自己工作内存中的,线程结束后,将值更新到主内存中!
  2. 定义了几个原子操作,用于操作主内存和工作内存中的变量!
  3. 定义了volatile变量使用规则!(屏障)
  4. happens-before,即先行发生原则,定义了操作A 必然先行发生于 操作B 的一些规则!
    比如:在同一个线程中,控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于 后面对于同一个锁进行 lock 的动作 等等
    只要符合这些规则,则不需要额外的同步措施,如果某段代码不符合所有的 happens-before 规则,则这段代码一定是线程非安全的!

20. sleep方法和wait方法有什么区别?

  1. sleep()属于 Thread类;wait()属于Object类
  2. sleep 导致程序暂停(指定时间),把CPU让给其他线程,但是监控状态依旧保持,当达到指定的时间到了又会恢复运行状态;此过程不会释放锁
    wait方法,线程会放弃对象锁,进入等待此对象的等待锁定池,只有调用notify() 后才进入对象锁定池等待重新获得锁

21. 线程的sleep()方法和yield()方法有什么区别?

  1. sleep让CPU时不考虑优先级,直接让出!
    yield 只会给相同优先级或更高优先级的线程以运行的机会!
  2. 线程执行sleep()后进入阻塞(blocked)状态,而执行yield()后进入就绪(ready)状态
  3. sleep()声明抛出InterruptedException,而yield()没有声明异常
  4. sleep 比 yield 具有更好的可移植性!(与CPU调度有关)

22. Thread.sleep(0)的作用是什么?

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

23. 线程类的构造方法、静态块是被哪个线程调用的?

请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

举例:假设Thread2中new了Thread1,main函数中new 了Thread2,那么:

1、Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
2、Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

24. 在线程中你怎么处理不可控制异常?

Java中有两种异常

  1. 非运行时异常(Checked Exception):这种异常必须在方法声明的throws语句指定,或者在方法体内捕获。例如:IOException和ClassNotFoundException。
  2. 运行时异常(Unchecked Exception):这种异常不必在方法声明中指定,也不需要在方法体中捕获。例如,NumberFormatException。

因为run()不支持 throws语句,所以当 线程对象的 run 方法 抛出 非运行异常时,我们必须捕获并且处理他。
当线程抛出一个未捕获的异常时,JVM将有以下处理方法:

首先,查找线程对象的 未捕获异常处理器。
如果找不到,继续找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器。
如果还没找到,JVM 将继续查找
默认的未捕获异常处理器

如果没有一个处理器存在。JVM将执行默认行为将堆栈异常记录打印到控制台,并退出程序!

  • 未捕获异常处理器的使用:类实现UncaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法
package concurrency;
import java.lang.Thread.UncaughtExceptionHandler;
public class Main2 {
	public static void main(String[] args) {
	Task task = new Task();
	Thread thread = new Thread(task);
	thread.setUncaughtExceptionHandler(new ExceptionHandler());
	thread.start();
	}
}
class Task implements Runnable{
	@Override
	public void run() {
		int numero = Integer.parseInt("TTT");
	}
}
class ExceptionHandler implements UncaughtExceptionHandler{
	@Override
	public void uncaughtException(Thread t, Throwable e) {
		System.out.printf("An exception has been captured\n");
		System.out.printf("Thread: %s\n", t.getId());
		System.out.printf("Exception: %s: %s\n",
		e.getClass().getName(),e.getMessage());
		System.out.printf("Stack Trace: \n");
		e.printStackTrace(System.out);
		System.out.printf("Thread status: %s\n",t.getState());
	}
}

  • 默认未捕获异常处理器的定义:

Thread类还有另一个方法可以处理未捕获到的异常,即静态方法setDefaultUncaughtExceptionHandler()。这个方法在应用程序中为所有的线程对象创建了一个异常处理器。

25. 同步方法和同步块,哪个是更好的选择?

原则:同步的范围越小越好!

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。

但是存在锁粗化现象!

例如:
StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。(同一个操作一直加锁解锁,明显没必要,浪费性能!)

26. 什么是CAS?

CAS,全称为Compare and Swap,即比较-替换。

假设有三个操作数:内存值V旧的预期值A要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

27. CAS 有什么缺陷,如何解决?

在这里插入图片描述

  • ABA问题

现象:并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

解决:可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。(给值加一个修改版本号)

  • 循环开销时间长

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的开销!

解决:CAS思想体现,有一个自旋次数,就是为了避开这个耗时问题……

  • 只能保证一个变量的原子操作

问题:CAS只能对一个变量执行操作的原子性,如果对多个变量,CAS目前无法直接保证操作的原子性!

解决:(两种)
使用互斥锁来保证原子性;
将多个变量封装成对象,通过AtomicReference来保证原子性!

28. 什么是AQS?

AQS : AbstractQueuedSynchronizer(抽象队列同步器)
CAS 是 java.util.concurrent 的基础AQS 是java并发包的 核心。ReentrantLock、CountDownLatch、Semaphore 等等都用到了AQS;

  • 本质

双向队列的形式连接所有的Entry,比方说 ReentrantLock,所有等待的线程都被放在一个Entry中并联成双向队列。

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

29. 线程池的作用

(可联系到如何用、好处、启动策略)
合理使用线程池带来的三个好处。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等待线程的创建就立即执行。
  • 高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配调优和监控

30. ThreadLocal 是什么?

线程本地变量,指 ThreadLocal 中填充的变量属于 当前线程,该变量对其他线程是隔离的。ThreadLocal 为变量在每个线程中都创建副本,每个线程可以访问自己内部的副本变量。(常问

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
  • 使用场景
  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息。
  4. 数据库连接,Session会话管理。

有什么用?

以空间换时间,每个Thread里面维护一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,排除线程安全问题。

ThreadLocal 原理

内存结构图:

在这里插入图片描述
由结构图可以看出:

  • Thread 对象中持有一个 ThreadLocal.ThreadLocalMap的 成员变量。
  • ThreadLocalMap 内部维护了 Entry数组,每个Entry 代表一个完整的对象, key 是ThreadLocal 本身,value 是 ThreadLocal 的泛型值。

部分源码:

public class Thread implements Runnable {
	//ThreadLocal.ThreadLocalMap是Thread的属性
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal中的关键方法set()和get():

public void set(T value) {
	Thread t = Thread.currentThread(); //获取当前线程t
	ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
	if (map != null)
		map.set(this, value); //K,V设置到ThreadLocalMap中
	else
		createMap(t, value); //创建一个新的ThreadLocalMap
}
public T get() {
	Thread t = Thread.currentThread();//获取当前线程t
	ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
	if (map != null) {
		//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}

ThreadLocalMap的Entry数组:

static class ThreadLocalMap {
	static class Entry extends WeakReference<ThreadLocal<?>> {
		/** The value associated with this ThreadLocal. */
		Object value;
		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
	}
}
  • 实现原理解答
  1. Thread类持有一个类型为ThreadLocal.ThreadLocalMap 的 实例变量 ThreadLocals,即每个线程都有属于自己的ThreadLocalMap
  2. ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 本身,value 是 ThreadLocal的泛型值。
  3. 每个线程在往ThreadLoacl 里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal 为引用,在自己的map里找对应的可以,从而实现线程隔离

ThreadLocal 内存泄露问题

  • ThreadLocal 引用示意图:

在这里插入图片描述
ThreadLocalMap 中使用的 key 是 ThreadLocal 的弱引用,容易在JVM内存空间不足时,被回收!
如果被回收,则,Entry 中的 key 不存在,而 value 还在,造成内存泄露的问题!

解决办法:

使用完ThreadLocal 后,及时的调用 remove()方法,释放内存空间!

  • 应用场景

数据库连接池
会话管理中使用!

31. wait()、notify()、notifyAll()

notify()和notifyAll()有什么区别?

  • 共同点:都是Object对象,用于通知正在等待该对象的线程的方法。
  • 不同点:notify 唤醒一个等待该对象的线程;
    notifyAll 唤醒所有等待该对象的线程。(使其重新进入锁的争夺队列)
  • 造成问题:notify 可能会造成死锁
    正确场景: WaitSet中等待的是相同的条件,唤醒任一个都能正确处理下来的事项,如果唤醒的线程没法处理,务必确保继续notify下一个,并自身重新进入WaitSet。

为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

区别在于:wait()方法立即释放对象监视
器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

32. 什么是阻塞(Blocking)和非阻塞(Non-Blocking)?

通常用来形容多线程间相互影响

比如一个线程占用了临界区资源,那么其他所有需要这个而资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。

33. 什么是自旋

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。

既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

34. 自旋锁的优缺点?

自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁。

虽然自旋锁效率比互斥锁高,但它会存在下面两个问题:

1、自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。
2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

  • 总结

要慎重使用自旋锁,适合 锁使用者 保持锁时间比较短 并且 锁竞争不激烈的情况。正是由于自旋锁使用者 一般保持锁时间非常短,所以采取自旋而不是睡眠(阻塞)。

35. 常用的线程池模式以及不同线程池的使用场景?

  • 四种Java自带线程池:
  1. newFixedThreadPool 创建一个指定工作线程数量的线程池。
    每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始最大值,则将提交的任务存入到池队列中。
  1. newCachedThreadPool 创建一个可缓存的线程池。
    特点:
  • 工作线程的创建数量几乎没有限制(小于Integer.Max_VALUE),这样可灵活的往线程池添加线程!
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认1分钟),则该工作线程自动终止。终止后,如果有新任务,则重新创建一个工作线程。
  1. newSingleThreadExecutor 创建一个单线程化Executor,即只创建唯一的工作线程来执行任务,如果这个线程异常结束,会有另一个取代他,保证顺序执行
  • 特点:
    可保证顺序执行,并在任意给定的时间不会有多个线程是活动的。
  1. newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer

36. 在Java中Executor、ExecutorService、Executors的区别?

ExecutorExecutorService 这两个接口主要的区别是(四个):

  • ExecutorService 接口继承Executor 接口,是 Executor 的子接口
  • Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,
    ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。
  • Executor 中的 execute() 方法 不返回任何结果,
    ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
  • 除了允许客户端提交一个任务,ExecutorService
    还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。

Executors 类提供工厂方法用来创建不同类型的线程池。

newSingleThreadExecutor() 创建一个只有一个线程的线程池,
newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,
newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

37. 如何控制某个方法允许并发访问线程的大小?(信号量)

Semaphore 有两个重要方法:

semaphore.acquire() : 请求一个信号量,将信号量减1;(如果变为负,再次请求就会阻塞)

semaphore.release()释放一个信号量,此时信号量个数+1

38. 如何创建线程池?

虽然可以通过Executor 区创建,但是由于他的弊端,一般强制不使用它!

Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

方式一:通过Executor框架的工具类Executors来实现

在这里插入图片描述

方式二:通过构造方法(ThreadPoolExecutor )实现
在这里插入图片描述

39. 不同情况怎么使用线程池?

高并发、任务执行时间短

线程池的线程数设置为CPU+1,减少线程上下文切换

并发不高、任务执行时间长

时间集中在IO(IO密集型):因为IO不占用CPU,所以不要让所有CPU闲下来,可以加大线程池中的线程数目,让Cpu处理更多业务

时间集中在计算上(计算密集型):和上一种一样,线程数设置少一点,减少线程上下文切换;

高并发、业务执行时间长

这种更看重整体架构的设计,看能不能做缓存,能不能增加服务器,能不能使用中间件做任务拆分和解耦、最后根据上一种考虑线程池!

40. Java中interrupted 和isInterruptedd方法的区别?

  • 主要区别:前者会将中断状态清除而后者不会。
    Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。

当中断线程调用静态方法Thread.interrupted()检查中断状态时,中断状态会被清零

非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。

  • 注意:一个线程的中断状态可能被其他线程调用中断来改变

41. 说一说自己对于 synchronized 关键字的了解

解决:多个线程之间访问资源的同步性
作用:可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行!

早期效率低的原因:在Java早期版本中,synchronized 属于重量级锁,因为监视器锁(monitor)是依赖底层的操作系统的 MUtex Lock来实现的,Java的线程是映射到操作系统的原生线程上的。如果挂起、唤醒一个线程,都需要操作系统帮忙,此时线程间的上下文切换(用户态->内核态)耗时;

JDK1.6 优化:自旋锁、适应性自旋锁、锁消除、锁粗化、锁升级(无锁->偏向锁->轻量级锁->重量级锁)……(从JVM层面优化)

42. 怎么使用 synchronized 关键字(三种使用方式)?

  1. 修饰实例方法:对当前对象加锁,进入同步代码块前要获得当前对象的锁!
  2. 修饰静态方法:相当于给加锁,(区分类和对象!)

例:如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象。

原因:因为访问静态 synchronized 方法占用的锁是当前的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

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

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

43. Java中如何获取到线程dump文件?

线程dump:线程堆栈;死循环、死锁、阻塞、页面打开慢等额问题

  • 获取线程堆栈有两步:
  1. 获取到线程的pid(jps命令/ps -ef | grep java命令)
  2. 打印线程堆栈(jstack pid命令/kill -3 pid)

此外,Thread类提供了getStackTrace()方法来获取线程堆栈!是一个实例方法,与具体实例线程绑定!

44. 如何在两个线程间共享数据?

通过线程之间共享对象就可,然后通过wait/notify/notifyAllawait/signal/signalAll进行唤起和等待

阻塞队列(BlockingQueue) 就是为了线程之间共享数据设计的!

第一种:将共享数据封装到一个对象,把这个共享数据所在的对象传递给不同的Runnable(线程)。
第二种:将这些Runnable对象作为一个类的内部类,共享的数据作为外部类成员变量,对共享数据的操作分配给外部类来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作!

实例:

class ShareData {
	private int x = 0;
	public synchronized void addx(){
		x++;
		System.out.println("x++ : "+x);
	}
	public synchronized void subx(){
		x--;
		System.out.println("x-- : "+x);
	}
}
public class ThreadsVisitData {
	public static ShareData share = new ShareData();
	public static void main(String[] args) {
		//final ShareData share = new ShareData();
		new Thread(new Runnable() {
			public void run() {
				for(int i = 0;i<100;i++){
					share.addx();
				}
			}
		}).start();
		new Thread(new Runnable() {
			public void run() {
				for(int i = 0;i<100;i++){
					share.subx();
				}
			}
		}).start();
	}
}

45. Java中的死锁

多个线程抢夺有限个资源时,会出现死锁现象!

  • 四个必要条件

1.互斥条件:一段时间内,某一资源只能被一个线程占用,其他线程请求需要等待!
2. 请求和保持:一个线程去请求其他资源时,占用原有资源,不释放
3. 不可剥夺:线程已经占用的资源,除非自己使用完主动释放,不能被抢占!
4. 环路等待:a等B,B等C,C又等A

  • 如何避免与检测

预防死锁

  • 破坏互斥条件:资源同时访问而非互斥使用!(行不通,很多时候互斥是固有特性,不能改,打印机?)
  • 破坏请求与保持:只有能全部满足才去拿资源!(资源会闲置,利用率低下)
  • 破坏不可剥夺:获得一些,但另外的无法获得,则释放已占用的(只适用于内存、处理器资源)(拿到了又放弃,耗时!)
  • 破坏环路等待:给资源编号,按顺序依次进行!

银行家算法解决(避免)死锁问题

核心思想:分配资源之前,判断系统是否是安全的;若是,才分配。每分配一次资源就测试一次是否安全,不是资源全部就位后才测试。

具体不细说!

  • 还可以设置加锁顺序,要以相同的顺序获取锁!

46. 什么是可重入锁(ReentrantLock)?

Java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,允许把锁的实现作为Java类。具体的实现在特定的类中。(抽象类的作用

ReentrantLock 类 实现了 Lock,与synchronized 相同的并发性和内存语义,添加了(锁投票、定时锁、可中断锁等候)等特性;

  • 实际作用:
    ReentrantLock 有一个与锁相关的获取计数器,如果拥有某个锁的线程再次得到锁,则计数器+1,释放时也需要释放两次才能真正释放!

和synchronized 类似,只有退出最外层synchronized 块才释放锁

47. synchronized 关键字的底层原理?

synchronized 关键字底层原理属于 JVM 层面

通过查看字节码,我们可以知道:

  • 修饰同步代码块
    synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • 修饰方法

synchronized 修饰的方法 并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

48. synchronized和ReentrantLock的区别?

  • 相同点:都是可重入锁

本质区别:synchronized关键字ReentrantLock

提供了更加灵活的特性

  • 扩展性体现在:
  1. ReentrantLock 可以对获取锁的等待时间进行设置,可避免死锁!(等待可中断!)
  2. ReentrantLock 可以获取各种锁的信息。
  3. ReentrantLock 可以灵活的实现多路通知!
  • 获取、释放锁不同: synchronized 自动加锁、释放;而 ReentrantLock 需要lock、unlock(手动)。
  • ReentrantLock 可以设置为非公平锁、公平锁;而synchronized 默认为非公平锁!
  • 锁唤醒情况:ReentrantLock 可以根据条件 分组唤醒多个线程!

ReentrantLock实现流程!

  • 非公平锁
  • 上锁
    锁可用:直接CAS持有
    不可用:判断是否可重入
    ~ 可重入:锁状态+请求数
    ~ 不可重入:CAS循环进入等待队列的队尾!
  • 处理中断:
    LockSupport类支持,通过JNI 代用本地操作系统 来完成挂起的任务。
    被唤起后,先检查是否中断

49. ConcurrentHashMap的并发度是什么?

并发度就是 segment 数组(1.8以后是key.value数组)的大小(默认为16)
意味着:最多可同时16条线程操作!

50. ReadWriteLock是什么?

是一个读写锁接口!读锁共享、写锁独占!

51. FutureTask是什么?

表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这
个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

52. CyclicBarrier(回环栅栏)和CountDownLatch(线程计数器)的区别?

都位于java.util.concurrent包下!

在这里插入图片描述

有任何问题,请联系我,一定及时修改

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值