多线程面试题

线程、线程池相关接口和类

线程,线程池,线程安全 

线程->线程池->jdk自带线程池->并发(线程安全)
Runable接口->Thread
->public abstract void run();
Callable接口
->V call()
Future接口+Runable接口->RunnableFuture接口->FutureTask
 									 ->public FutureTask(Callable<V> callable)
 									 ->public FutureTask(Runnable runnable, V result)
 		 +Delayed接口->ScheduledFuture接口+RunnableFuture接口->RunnableScheduledFuture
Executor接口->ExecutorService接口->AbstractExecutorService抽象类->ThreadPoolExecutor(->ScheduledThreadPoolExecutor)、ForkJoinPool
			->ScheduledExecutorService
 			 -><T> Future<T> submit(Callable<T> task);
 			 -><T> Future<T> submit(Runnable task, T result);
Executors工具类

ForkJoinPool和ThreadPoolExecutor都是继承自AbstractExecutorService抽象类,所以它和ThreadPoolExecutor的使用几乎没有多少区别,除了任务变成了ForkJoinTask以外。
ForkJoinPool和ThreadPoolExecutor最主要的区别就是ForkJoinPool中每一个线程都有属于自己的队列,当某个线程队列任务全部执行完了时,会通过"窃取工作"从别的线程队列中取出一个任务进行执行。
具体的策略就是每一个线程维护一个自己的队列,先进后出(FILO)将任务塞到队列的头部,执行任务时从队列头部取出任务执行。其他线程从队列尾部窃取任务执行。减少阻塞消耗,特别适用于计算型任务。 ​

ForkJoinPool的使用很简单: 
public static void main(String[] args) throws InterruptedException, ExecutionException  {
    ForkJoinPool forkJoinPool = new ForkJoinPool(10); // 创建线程池
    FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {

        @Override
        public String call() throws Exception {
            System.out.println("hello ForkJoinPool");
            return "hello future";
        }

    });

    forkJoinPool.submit(futureTask); // 执行任务
    System.out.println(futureTask.get());
}


一.线程知识点
1.有返回值的线程:
        RunnableFuture<String> futrue = new FutureTask<>(new Callable<String>() {
        	@Override
        	public String call() {
        		System.out.println("这是call方法");
				return "tianming";
        		
        	}
        });
        Thread t1 = new Thread(futrue);
        t1.start();
        System.out.println(futrue.get()); // tianming
        
        RunnableFuture<String> futrue2 = new FutureTask<>(new Callable<String>() {
        	@Override
        	public String call() {
        		System.out.println("这是call方法");
				return "tianming";
        		
        	}
        });
        ExecutorService fixedThreadPoo = Executors.newFixedThreadPool(3);
        fixedThreadPoo.execute(futrue2);
        System.out.println(futrue2.get()); // tianming
        fixedThreadPoo.isShutdown();
    一个Callable接口能有多少知识点?https://blog.csdn.net/sinat_32849897/article/details/123725587


二.线程池知识点
1.线程池的概念:https://www.sohu.com/a/427850776_355142
2.jdk线程池原理:https://zhuanlan.zhihu.com/p/483120946
2.参数:核心线程、最大线程数、超时时间、阻塞队列(拒绝策略)
Java线程池七个参数详解:核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、线程工厂、拒绝策略 https://blog.csdn.net/Anenan/article/details/115603481 


1.jdk自带哪些线程池、及使用场景:https://www.lmlphp.com/user/57821/article/item/803260/
    使用场景:
    1.newFixedThreadPool 指定核心和最大线程相同的线程池,LinkedBlockingQueue,适用于普通场景
    2.newCachedThreadPool 核心池大小为0,线程池最大线程数目为最大整型,这意味着所有的任务一提交就会加入到阻塞队列中,适用于适合处理执行时间比较小的任务
    3.newSingleThreadExecutor 核心池大小为1,线程池最大线程数目也为1,LinkedBlockingQueue,使用于需要按序执行任务的场景
    4.newScheduledThreadPool 指定corePoolSize,线程池最大线程数目为最大整型,DelayedWorkQueue,可以执行延时任务,也可以执行带有返回值的任务
2.jdk自带workQueue:工作队列,存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口。
        JDK默认的工作队列有五种:
        ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
        LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无界),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。当构造LinkedBlockingQueue对象时传入参数,变为有界队列,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
        SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
        PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
        DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。

2.Java线程池状态和状态切换
介绍线程池的五种状态RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED,并简述五种状态之间的切换。

在类ThreadPoolExecutor中定义了一个成员变量ctl,是个Integer的原子变量,用来记录线程池状态和线程池线程个数,另外定义了五个static final变量表示线程池的各个状态,部分JDK源码如下:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; running运行
private static final int SHUTDOWN = 0 << COUNT_BITS; shutdown关闭
private static final int STOP = 1 << COUNT_BITS; stop停止
private static final int TIDYING = 2 << COUNT_BITS; tidying整理
private static final int TERMINATED = 3 << COUNT_BITS; terminated结束

由此可见,线程池的状态有如下五种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
RUNNING 线程池的初始化状态是RUNNING,能够接收新任务,以及对已添加的任务进行处理。
状态切换:线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
SHUTDOWN 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
STOP 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
状态切换: 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
TIDYING 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。
当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
TIDYING 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
状态切换:线程池在STOP状态下,且执行的任务为空时,就会由STOP -> TIDYING。
TERMINATED 线程池彻底终止,就变成TERMINATED状态。
状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。


3.Java 关闭线程池
shutdown
shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。
调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。

isShutdown
第二个方法叫作 isShutdown(),它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。
这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务

isTerminated
这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了。
比如我们上面提到的情况,如果此时已经调用了 shutdown 方法,但是还有任务没有执行完,那么此时调用 isShutdown 方法返回的是 true,而 isTerminated 方法则会返回 false。
直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。

awaitTermination
主要用来判断线程池状态的。
    在shutdown()调用之后使用,阻塞当前线程,在这之后可以继续提交任务,设置等待超时时间,等待所有任务都执行完成,检查线程池是否终止,如果终止返回 true,否则返回 false,并解除阻塞
    如果在超时之前所有任务执行完毕,表示线程池已经终止,返回true,否则返回false
    如果在shutdown()之前使用,线程池未终止,awaitTermination()锁在等待终止状态,造成死锁
比如我们给 awaitTermination() 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:
    等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true
    等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false
    等待期间线程被中断,方法会抛出 InterruptedException 异常
调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。

shutdownNow
shutdownNow(),它和 shutdown() 的区别就是多了一个 Now,表示立刻关闭的意思,不推荐使用这一种方式关闭线程池。
在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。
shutdown 没有返回值,而 shutdownNow 会返回关闭前任务队列中未执行的任务集合(List)


4.实例
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		testScheduledThreadPool();
	}
	
	public static void testFixedThreadPool() {
		// 获取计算机有几个核
		int processors = Runtime.getRuntime().availableProcessors();
		System.out.println(processors);
		// 第一种线程池:固定个数的线程池,可以为每个CPU核绑定一定数量的线程数
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(processors*2); // 大多cpu的线程数为物理核心的2倍
		for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                	// 执行逻辑
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
		// 局部变量Executors线程池一定要手动关闭,不然会造成内存泄露;公用线程池不用关闭
        fixedThreadPool.shutdown();
	}
    

问题:
1.如何解决并发

2.面试被问到核心线程数和最大线程数分别设置为多少?并且这么设置的根据是什么?
你这个问题前几天才有个问过过。cpu核心数如果是x,任务执行时间是m,任务等待时间是n,最大核心数一般就是x*(m+n)/m
虽然线程池大小的设置受到很多因素影响,但是这里给出一个参考公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
所以并不是单纯的只是配一个CUP核心数就ok了。但一般都是整数倍

3:java线程池需要shutdown吗_公用线程池要不要shutdown?https://fangshixiang.blog.csdn.net/article/details/82021919
局部变量Executors线程池一定要手动关闭,不然会造成内存泄露;公用线程池不用关闭

线程

  1. 程序、进程、线程的区别是什么? 举个现实的例子说明。

    1. 程序(Program):是一个指令的集合。程序不能独立执行,只有被加载到内存中,系统为它分配资源后才能执行

    2. 进程(Process):一个执行中的程序称为进程。
      进程是系统分配资源的独立单位,每个进程占有特定的地址空间。
      程序是进程的静态文本描述,进程是程序在系统内顺序执行的动态活动。

    3. 线程(Thread):是进程的“单一的连续控制流程“。
      线程是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单位。
      线程不能独立存在,必须依附于某个进程。一个进程可以包括多个并行的线程,一个线程肯定属于一个进程。
      Java虚拟机允许应用程序并发地执行多个线程。

    4. 举例:如一个车间是一个程序,一个正在进行生产任务的车间是一个进程,车间内每个从事不同工作的工人是一个线程。

  2. 线程的几种状态
    1. 新建状态(New):新创建了一个线程对象。

    2. 可运行状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

    3. 运行状态(Running):就绪状态的线程获取了CPU,开始执行线程的run()方法。 

    4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况又分为三种:

      1. 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“对象等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用对象的notify()或notifyAll()方法才能被唤醒,wait是object类的方法

      2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁等待池”中。

      3. 其他阻塞:运行的线程执行sleep(),JVM会把该线程置为阻塞状态。当sleep()状态超时时,线程重新转入可运行状态。sleep是Thread类的方法

    5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  3. Java中通过哪些方式创建多线程类?java创建多线程的四种方式_白白甜甜冰的博客-CSDN博客_java创建多线程
    1. 继承Thread类创建线程,Thread类有实现Runnable接口
    2. 实现Runnable接口创建线程
    3. 实现Callable接口创建多线程(jdk5.0后新增的方法)
    4. 匿名内部类
  4. 用Runnable还是Thread?
    这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使 用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好 了。
  5. Thread 类中的start() 和 run() 方法有什么区别?
    这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
  6. 当调用一个线程对象的start方法后,线程马上进入运行状态吗?
    不是,只是进入可运行状态,等待分配CPU时间片。一旦得到CPU时间片,即进入运行状态。
  7. A线程的优先级是10,B线程的优先级是1,那么当进行调度时一定会调用A吗?A线程的优先级是10,B线程的优先级是1,那么当进行调度时一定会调用A吗?(线程的调度)
    不一定,Java中在对线程调度时,也采用了优先级调度策略,具体策略为:“优先级高的线程应该有更大的获取CPU资源执行的概率,优先级低的线程并不是总不能执行”。
  8. Sleep()和yield()有什么区别
    1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;
      yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    2. 线程执行sleep()方法后转入阻塞(blocked)状态,执行yield()的线程有可能在进入到暂停状态后马上又被执行
    3. sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    4. sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。
  9. 说说:sleep、yield、join、wait方法的区别。
    1. sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入同步阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。 作用于线程
      1. Thread.sleep()方法用来暂停线程的执行,将CPU放给线程调度器。
      2. Java有两种sleep方法,一个只有一个毫秒参数,另一个有毫秒和纳秒两个参数。
      3. 与wait方法不同,sleep方法不会释放锁
      4. 如果其他的线程中断了一个休眠的线程,sleep方法会抛出Interrupted Exception。
      5. 休眠的线程在唤醒之后不保证能获取到CPU,它会先进入可运行状态,与其他线程竞争CPU。
      6. Thread.sleep()方法是一个静态方法,它暂停的是当前执行的线程。
    2. join(): 当前线程等待,调用此方法的线程执行结束再继续执行。如:在main方法中调用t.join(),那main方法在此时进入阻塞状态,一直等t线程执行完,main方法再恢复到就绪状态,准备继续执行。
      join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。作用于线程

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

    4. wait():

      1. wait方法是针对一个被同步代码块加锁的对象,也就是说wait只能在同步(synchronize)环境中被调用

      2. wait方法在进入wait状态的时候会释放对象的锁

      3. 进入wait状态的线程能够被notify和notifyAll线程唤醒

  10. wait、notify、notifyAll是在Thread类中定义的方法吗?作用分别是什么?
    wait(),notify(),notifyAll()不属于Thread类,而是属于Object类,也就是说每个对象都有wait(),notify(),notifyAll()的功能。
    因为同步锁的是对象,而wait(),notify(),notifyAll()都是跟锁有关的方法。
    三个方法的作用分别是:
    1. wait:当线程执行了对一个特定对象的wait()调用时,那个线程被放到特定对象的等待池中。此外,调用wait()的线程自动释放对象的锁标志。

    2. notify:对象调用notify()方法时,将从对象的等待池中移走一个任意的线程,并放到对象的锁等待池中,直到获得对象的锁标志。

    3. notifyAll: 对象调用notifyAll()方法时,将从对象的等待池中移走所有的线程,并放到对象的锁等待池中,直到获得对象的锁标志。

  11. 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
    这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。
  12. 为什么不推荐使用stop和destroy方法来结束线程的运行?
    1. destroy():该方法最初用于破坏该线程,但不作任何资源释放。它所保持的任何监视器都会保持锁定状态。不过,该方法决不会被实现。即使要实现,它也极有可能以 suspend() 方式被死锁。如果目标线程被破坏时保持一个保护关键系统资源的锁,则任何线程在任何时候都无法再次访问该资源。如果另一个线程曾试图锁定该资源,则会出现死锁。
    2. stop():此方法可以强行中止一个正在运行或挂起的线程。但stop方法不安全,就像强行切断计算机电源,而不是按正常程序关机。可能会产生不可预料的结果。
  13. Java中如何停止一个线程?
    Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。

  14. 写个代码说明,终止线程的典型方式。
    1.  当run()方法执行完后,线程就自动终止了。

    2. 但有些时候run()方法不会结束(如服务器端监听程序),或者其它需要用循环来处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

  15. 一个线程运行时发生异常会怎样?
    简单的说,如果异常没有被捕获该线程将会停止执行
    Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法 进行处理。
  16. 下面的代码,实际上有几个线程在运行:

    public static void main(String[] argc) throws Exception {
    
                Runnable r = new Thread6();
    
                Thread t = new Thread(r, "Name test");
    
                t.start();
    
    }
    
    两个:线程t和main()方法(主线程)。

线程池Java中的线程池
Executors工具类提供的线程池只需要关注前四种就ok

线程安全

  1. Java内存模型是什么?
    1. 主内存与工作内存
    2. 内存间交互操作
    3. 对于volatile型变量的特殊规则
    4. 对于long和double型变量的特殊规则
    5. 原子性、可见性与有序性
    6. 先行发生原则
  2. Java中的volatile 变量是什么?
  3. volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。
  4. Java中的volatile关键字是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
    自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性、顺序性和一致性。
  5. volatile 变量和 atomic 变量有什么不同?
    这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性 的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

    简单来说, volatile 只解决可见性,不保证同步。
    atomic 则是保证了可见性 和同步性。内部采用CAS算法实现。
  6. 说一下 atomic 的原理?
  7. 什么是线程安全?
    就是能保证共享数据的并发性。
  8. 什么是不可变对象,它对写并发应用有什么帮助?
    另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个java面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么String是不可变的。

  9. Lock和synchronized的选择
    总结来说,Lock和synchronized有以下几点不同:

    1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

    2. lock需要主动释放锁:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

    3. 等待可中断:Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

    4. 读写锁:Lock可以提高多个线程进行读操作的效率。java读写锁 ReentrantReadWriteLock_java reentrantreadwritelock_清风拂来水波不兴的博客-CSDN博客

    5. 公平锁:按照申请锁的时间顺序来依次获得锁

    6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  10. 多线程同步的方式
    1. 同步有两种方式,第一种是使用关键字synchronized,第二种是使用接口lock

    2. 对于线程交互,synchnronized有object对象的wait(),notify()和notifyall()等方法;lock也有相对应的Condition.await(),Condition.signal()和Condition.signalall()等方法

    3. synchronized会死锁,lock也会死锁,但是lock可以使用lock.trylock()尝试获得锁,如果超出设定时间就放弃,而不是一直请求

    4. synchronized会自动解锁,lock必须手动解锁lock.unlock()

  11. Java中的同步集合与并发集合有什么区别?
    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在 多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
  12. 如何避免死锁?
  13. Java中活锁和死锁有什么区别?
    这是上题的扩展,活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个 人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者 进程的状态可以改变但是却不能继续执行。
  14. 你如何在Java中获取线程堆栈?
    对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。
    在 Windows你可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。
    你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。
  15. 你将如何使用thread dump?你将如何分析Thread dump?
    在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。 
  16. JVM中哪个参数是用来控制线程的堆栈大小的
    这个问题很简单, -Xss参数用来控制线程的堆栈大小。
  17. Java中ConcurrentHashMap的并发度是什么?
    ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。
  18. Java中的ReadWriteLock是什么?
    一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程 持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读 锁。
  19. 多线程中的忙循环是什么?
    忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
  20. 如果同步块内的线程抛出异常会发生什么?
    这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。
  21. 单例模式的双检锁是什么?
    这个问题在Java面试中经常被问到,但是面试官对回答此问题的满意度仅为50%。一半的人写不出双检锁还有一半的人说不出它的隐患和 Java1.5是如何对它修正的。它其实是一个用来创建线程安全的单例的老方法,当单例实例第一次被创建时它试图用单个锁进行性能优化,但是由于太过于复 杂在JDK1.4中它是失败的,我个人也不喜欢它。无论如何,即便你也不喜欢它但是还是要了解一下,因为它经常被问到。
  22.  如何在Java中创建线程安全的Singleton?
    这是上面那个问题的后续,如果你不喜欢双检锁而面试官问了创建Singleton类的替代方法,你可以利用JVM的类加载和静态变量初始化特征来创建Singleton实例,或者是利用枚举类型来创建Singleton,我很喜欢用这种方法。
  23. 写出3条你遵循的多线程最佳实践
    这种问题我最喜欢了,我相信你在写并发代码来提升性能的时候也会遵循某些最佳实践。以下三条最佳实践我觉得大多数Java程序员都应该遵循:
    1. 给你的线程起个有意义的名字。
      这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
    2. 避免锁定和缩小同步的范围
      锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
    3. 多用同步类少用wait 和 notify
      首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断 优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
    4. 多用并发集合少用同步集合
      这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。
  24. 如何强制启动一个线程?
    这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。
  25. 在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
    |lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。
  26. 用Java编程一个会导致死锁的程序,你将怎么解决?
  27. 这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。
  28.  什么是原子操作,Java中的原子操作是什么?
    JDK6 以后,新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger。
    而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 incrementAndGet 是线程安全的,同一个时间,只有一个线程可以调用这个方法
  29. 什么是竞争条件?你怎样发现和解决竞争?
    这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验,or writing code which is free of data race or any other race condition。关于这方面最好的书是《Concurrency practices in Java》。
  30. Java中你怎样唤醒一个阻塞的线程
    这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了IO阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。
  31. 你在多线程环境中遇到的共同的问题是什么?你是怎么解决它的?
    多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。
  32. 补充的其它几个问题:
    1. Java中用到的线程调度算法是什么?
      抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
      操作系统中可能会出现某条线程常常获取到VPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
    2. 在线程中你怎么处理不可捕捉异常?
      在java中要捕捉多线程产生的异常,需要自定义异常处理器,并设定到对应的线程工厂中
    3. 为什么使用Executor框架比使用应用创建和管理线程好?
    4. 在Java中Executor和Executors的区别?
      1. Executor 接口对象能执行我们的线程任务;
      2. Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
      3. ExecutorService 接口继承了Executor接口并进行了扩展,提供了更多的方法,我们能够获得任务执行的状态并且可以获取任务的返回值。
    5. 如何在Windows和Linux上查找哪个线程使用的CPU时间最长?
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值