Java多线程面试题

1) 什么是线程、进程?

进程是程序执行时的一个实例,是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位。

2) 线程和进程有什么区别?

(1) 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2) 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3) 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
(4) 系统开销:进程上下文切换比进程大

3) 如何在Java中实现线程?

两种方式:继承Thread类或者实现Runnable接口

public class Thread implements Runnable { ...}
public interface Runnable { ...}

4) 用Runnable还是Thread?

1、Runnable更容易实现资源共享
2、java的单继承机制
在程序开发中只要是多线程肯定永远以实现Runnable接口为主。

6) Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程

7) Java中Runnable和Callable有什么不同?

创建线程的两种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这两种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
参考:https://www.cnblogs.com/dolphin0520/p/3949310.html

8) Java中CyclicBarrier 和 CountDownLatch有什么不同?

CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。
1、CountDownlatch描述的是1个线程或N个线程等待其他线程的关系。
     CyclicBarrier描述的是多个线程内部相互等待的关系。
2、CountDownlatch是一次性的,CyclicBarrier可重用

9) Java内存模型是什么?

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

Java内存模型

10) Java中的volatile 变量是什么?

volatile是一个特殊的修饰符,只有成员变量才能使用它。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。
1、保持内存可见性
每次读取前必须先从主内存刷新最新的值。
每次写入后必须立即同步回主内存当中。
2、防止指令重排
volatile关键字通过“内存屏障”来防止指令被重排序。
参考:https://www.cnblogs.com/monkeysayhi/p/7654460.html

11) 什么是线程安全?Vector是一个线程安全类吗?

如果一段代码在多线程和单线程环境下运行结果是一样的,并且变量的值也和预期是一样的,就是线程安全的。(强调的是正确性)
Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。

12) Java中什么是竞态条件? 举个例子说明。

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。 临界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。

常见竞态条件:

  • 先检查后执行(Check-Then-Act)
  • 和读取-修改-写入

13) Java中如何停止一个线程?

当run() 或者 call() 方法执行完的时候线程会自动结束。如果要手动结束一个线程:
1、你可以用volatile 的布尔变量来控制退出run()方法的循环
2、如果是运行状态的线程,使用interrupt() 方法将中断状态设置为true,用isInterrupted() 方法判断中断是否为true状态退出循环
3、对于被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞的线程,使用interrupt() 方法将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

14) 一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行,同时释放该线程持有的对象锁。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

15) 如何在两个线程间共享数据?

1,如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。

2,如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款

16) Java中notify 和 notifyAll有什么区别?

  • notify:只会唤醒等待该锁的其中一个线程。
  • notifyAll:唤醒等待该锁的所有线程。

对象内部锁
其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。

锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。

等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。
如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。

锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。

17) 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

因为JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁。

wait(),notify(),notifyAll()必须在sychronized同步代码块中使用,且要用当前线程持有的锁来调用。
(1)为什么wait()必须在同步(Synchronized)方法/代码块中调用?
答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。
(2)为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?
答:notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)

首先,要明白,每个对象都可以被认为是一个"监视器monitor",这个监视器由三部分组成(一个独占锁,一个入口队列,一个等待队列)。注意一个对象只能有一个独占锁,但是任意线程都可以拥有这个独占锁。

对于对象的非同步方法而言,任意时刻可以有任意个线程调用该方法。

对于对象的同步方法而言,只有拥有这个对象的独占锁才能调用这个同步方法。如果这个独占锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,此线程进入入口队列(锁池)。

若一个拥有该独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并加入对象的等待队列(等待池);(为什么使用wait()?希望某个变量被设置之后再执行,notify()通知变量已经被设置。)

某个线程调用notify(),notifyAll()方法是将等待队列(等待池)的线程转移到入口队列(锁池),然后让他们竞争锁,所以这个调用线程本身必须拥有锁。

18) 什么是ThreadLocal变量?

ThreadLocal实例为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
使用及原理:
1、定义

static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();

2、设置值

// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);

// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);

3、运行时结构


运行时结构

当调用ThreadLocal的set()/get()方法的时候,首先获取当前的线程Thread t = Thread.currentThread();

//ThreadLocalMap的set方法
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 拿到当前线程的 ThreadLocalMap
    if (map != null) // 判断 ThreadLocalMap 是否存在
        map.set(this, value); // 调用 ThreadLocalMap 的 set 方法
    else
        createMap(t, value); // 创建 ThreadLocalMap
}

之后通过当前线程去获取ThreadLocalMap,ThreadLocalMap map = getMap(t);,getMap(t)方法源码:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

通过传入的线程类实例t,获取线程t中的ThreadLocalMap对象,我们继续追踪Thread类,发现Thread的成员变量是包含一个 ThreadLocal.ThreadLocalMap实例。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

获取到了ThreadLocalMap实例map后,判断其是否为null,如果不为空,直接调用map.set(this, value)方法,将调用的ThreadLocal对象和传入数据value存储到map中。如果为null,则会调用createMap(t, value)方法,直接初始化ThreadLocalMap,赋值给线程t中的threadLocals。

   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

通过对ThreadLocal的set方法的追踪我们大致了解了ThreadLocal数据存储的过程,每次存储数据都会获取方法被调用的线程,之后将数据和threadlocal对象存储到对应线程的ThreadLocalMap中。这样的存储方式就实现了数据线程之间的隔离。通过不同线程取出的ThreadLocalMap是不同,自然获取到存储的数据也是不同的。

ThreadLocalMap是ThreadLocal的静态内部类。每个线程持有自己的 ThreadLocalMap,ThreadLocalMap 初始容量为16(即图中的16个槽位),ThreadLocalMap数据结构采用 数组 + 开放地址法(冲突未使用链表结构解决),Entry 继承 WeakReference。

ThreadLocalMap使用ThreadLocal的弱引用

可能造成内存泄露原因:如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

19) 什么是FutureTask?

在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

20) Java中interrupted 和 isInterruptedd方法的区别?

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

public class Thread{     
  public void interrupt(){...}     
  public boolean isInterrupted(){...}     
  public static boolean interrupted(){...} 
}

interrupt方法能中断目标线程。调用interrupt并不意味着立即停止目标线程正在运行的工作,而只是传递了请求中断的消息。

isInterrupted方法能返回目标线程的中断状态。

静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。

23) Java中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。更多内容详见答案

同步容器是线程安全的。同步容器将所有对容器状态的访问都串行化,以实现他们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。并发容器是针对多个线程并发访问设计的,改进了同步容器的性能。通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。

24) Java中堆和栈有什么不同?

为什么把这个问题归类在多线程和并发面试题里?因为栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。更多内容详见答案

25) 什么是线程池? 为什么要使用它?

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。更多内容详见这篇文章

线程池,从字面含义来看,是指一组同构工作线程的资源池。线程池与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时额分摊在线程创建和销毁过程中产生的巨大开销。另外一个好处是,当请求到达时,工作线程已经存在,因此不会犹豫等待创建线程而延迟任务的执行,从而提高了响应性。通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。

可以通过调用Executors中的静态工厂方法之一来创建线程池:

(1)、newCachedThreadPool:创建一个可缓存的线程池;(如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程)

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

(2)、newFixedThreadPool:创建一个固定长度的线程池;(可控制线程最大并发数,超出的线程会在队列中等待)

ExecutorService executorService = Executors.newFixedThreadPool(3);

(3)、newScheduleThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

(4)、newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。(如果当前线程意外终止,会创建一个新线程继续执行任务,这和我们直接创建线程不同,也和newFixedThreadPool(1)不同。)

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

26) 如何写代码来解决生产者消费者问题?

https://www.jianshu.com/p/e39270996bf9

27) 如何避免死锁?

image

Java多线程中的死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?
  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测

28) Java中活锁和死锁有什么区别?

1.死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2.活锁:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去.(很绅士,互相谦让)
3.饥饿:这是个独木桥(单进程),桥上只能走一个人,B来到时A在桥上,B等待;
而此时比B年龄小的C来了,B让C现行(A走完后系统把进程分给了C),
C上桥后,D又来了,B又让D现行(C走完后系统把进程分个了D)
以此类推B一直是等待状态

活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。

29) 怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

30) 你如何在Java中获取线程堆栈?

对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows你可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。

31) JVM中哪个参数是用来控制线程的栈堆栈小的

这个问题很简单, -Xss参数用来控制线程的堆栈大小。

32) Java中synchronized 和 ReentrantLock 有什么不同?

(1)、synchronized通过object的wait()/notify()调度 lock通过condition调度
(2)、synchronized可加方法上或synchronized代码块使用 jvm c++实现,lock只能嵌套在代码中java实现
(3)、不存在或并发低的情况下synchronized性能优于lock , 但是并发升高时synchronized性能下降很快
(4)、synchronized非公平锁,lock可以指定公平性
(5)、synchronized代码块发生异常时能自动释放锁,lock需加在finally中,若lock出现死锁也能通过有限时等待释放死锁

Lock lock = new ReenTrantLock(); 
... lock.lock(); 
try{     
  //更新对象状态     
  //捕获异常,并在必要时恢复不变性的条件 
}finally{
     lock.unlock(); 
}

我觉得这篇博文区别总结挺详细: synchronized和锁(ReentrantLock) 区别

33) 有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而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.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(); 
    } 
}

34) Thread类中的yield方法有什么作用?

yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

35) Java中ConcurrentHashMap的并发度是什么?

ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。欲了解更多并发度和内部大小调整请阅读我的文章How ConcurrentHashMap works in Java

并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。ConcurrentHashMap默认的并发度为16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。运行时通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment。segmentShift与segmentMask都是在构造过程中根据concurrency level被相应的计算出来。

如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。(文档的说法是根据你并发的线程数量决定,太多会导性能降低)。更多参考ConcurrentHashMap总结

36) Java中Semaphore是什么?

Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,信号量维护了一个许可集合,许可的初始数量可以通过构造函数来指定。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。更多详细信息请点击这里

37)如果你提交任务时,线程池队列已满。会时发会生什么?

这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常。

当有界队列被填满后,拒绝策略开始发挥作用。ThreadPoolExecutor的拒绝策略可以通过setRejectExecutionHandler来修改。(如果某个任务被提交到一个已经被关闭的Executor时,也会用到饱和策略。)JDK提供了几种不同的RejectExecutionHandler实现,每种实现都包含有不同的拒绝策略:AbortPolicy、DiscardPolicy和DiscardOldestPolicy、CallerRunsPolicy。

中止策略(AbortPolicy)是默认的拒绝策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。当新任务的提交无法保存到队列中等待执行的时候,
抛弃策略(DiscardPolicy)会悄悄抛弃该任务。
抛弃最旧的策略(DiscardOldestPolicy)则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。
“调用者运行”策略(CallerRunsPolicy)实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

38) Java线程池中submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,用于提交不需要返回值的任务,它定义在Executor接口中; 而submit()方法用于提交需要返回值的任务,该可以返回持有计算结果的Future对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

    ExecutorService executorService =  Executors.newCachedThreadPool();         executorService.execute(new Runnable() {
             @Override
             public void run() {
                 // TODO Auto-generated method stub
             }
         });
ExecutorService executorService =  Executors.newCachedThreadPool();
         Callable<String> callable = new Callable<String>() {
             public String call() throws Exception {
                 System.out.println("This is  ThreadPoolExetor#submit(Callable<T> task) method.");                 return "result";
             }
         };
         Future<?> future = executorService.submit(callable);
         try {
             Object s = future.get();
         } catch (InterruptedException e) {
             //处理中中断异常
         } catch (ExecutionException e) {
             //处理无法执行任务异常
         } finally { 
            executorService.shutdown();
         } 

39) 什么是阻塞式方法?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。更多详细信息请点击这里

44) Java中的ReadWriteLock是什么?

一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。

public interface ReadWriteLock{
     Lock readLock();
     Lock writeLock();
 }

在读写锁的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。

读写锁是一种性能优化措施,在一些特定的情况下能实现更高的并发性。在实际情况中,对于在多处理器系统上被频繁读取的数据结构,读写锁能够提高性能。而在其他情况下,读写锁的性能比独占锁的性能要略差一些,这是因为它们的复杂性更高。

45) 多线程中的忙循环是什么?

忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。你可以查看这篇文章获得更多信息。

46)volatile 变量和 atomic 变量有什么不同?

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

47) 如果同步块内的线程抛出异常会发生什么?

这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。

48) 单例模式的双检锁是什么?

这个问题在Java面试中经常被问到,但是面试官对回答此问题的满意度仅为50%。一半的人写不出双检锁还有一半的人说不出它的隐患和Java1.5是如何对它修正的。它其实是一个用来创建线程安全的单例的老方法,当单例实例第一次被创建时它试图用单个锁进行性能优化,但是由于太过于复杂在JDK1.4中它是失败的,我个人也不喜欢它。无论如何,即便你也不喜欢它但是还是要了解一下,因为它经常被问到。你可以查看how double checked locking on Singleton works这篇文章获得更多信息。

在早期的JVM中,同步(甚至是无竞争的同步)都存在着巨大的性能开销。因此,人们想出来了许多“聪明的”技巧来降低同步的影响,有些技巧很好,有些技巧是不好的,甚至是糟糕的,DCL就属于“糟糕”的一类。

public class DoubleCheckedLocking {     private static Resource resource;     public static Resource getInstance() {         if (resource == null) {             synchronized (DoubleCheckedLocking.class) {                 if (resource == null) {                     resource = new Resource();                 }             }         }         return resource;     } }

DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。

在JVM的后续版本中(Java 5.0以及更高的版本)中,如果resource声明为volatile类型,那么就能启用DCL,并且这种凡是对性能的影响很小,因为volatile变量读取操作的性能通常只是略高于非volatile变量读取操作的性能。

public class Singleton {     private volatile static Singleton uniqueInstance;     private Singleton() {     }     public static Singleton getInstance() {         //检查实例,如果不存在则进入同步代码块         //注意:只有第一次,才彻底执行if中所有代码         if (uniqueInstance == null) {             synchronized (Singleton.class) {                 //进入区块后,再检查一次。如果仍然是null,才创建实例                 if (uniqueInstance == null) {                     uniqueInstance = new Singleton();                 }             }         }         return uniqueInstance;     } } 

然而,DCL的这种使用方法已经被广泛地废弃了——促使该模式出现的动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在了,因为它不是一种高效地优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。

“延迟初始化占位类模式”中使用了一个专门的类来初始化Resource。JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。当任何一个线程第一次调用getResource时,都会使ResourceHolder被加载和被初始化,此时静态初始化器将执行Resource的初始化操作。

public class ResourceFactory {     private static class ResourceHolder {         public static Resource resource = new Resource();     }     public static Resource getResource() {         return ResourceHolder.resource;     } }

49) 如何在Java中创建线程安全的Singleton?

这是上面那个问题的后续,如果你不喜欢双检锁而面试官问了创建Singleton类的替代方法,你可以利用JVM的类加载和静态变量初始化特征来创建Singleton实例,或者是利用枚举类型来创建Singleton,我很喜欢用这种方法。你可以查看如何优雅地手写单例模式获得更多信息。

50) 写出3条你遵循的多线程最佳实践

这种问题我最喜欢了,我相信你在写并发代码来提升性能的时候也会遵循某些最佳实践。以下三条最佳实践我觉得大多数Java程序员都应该遵循:

  • 给你的线程起个有意义的名字。

    这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。

  • 避免锁定和缩小同步的范围

    锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

  • 多用同步类少用wait 和 notify

    首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。

  • 多用并发集合少用同步集合

    这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。我的文章Java并发集合有更详细的说明。

51) 如何强制启动一个线程?

这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。

52) Java中的fork join框架是什么?

fork join框架是JDK7中出现的一款高效的工具,Java开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。你可以查看这篇文章获得更多信息。

53) Java多线程中调用wait() 和 sleep()方法有什么不同?

Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。你可以查看这篇文章获得更多信息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容