通常创建线程有几种方式?
创建线程的常用四种方式:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口(JDK1.5>=)
- 线程池方式创建
通过继承 Thread 类或者实现 Runnable 接口、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法返回值,可以声明抛出异常而已。因此将实现 Runnable 接口和实现 Callable 接口归为一种方式。
采用实现 Runnable、Callable 接口的方式创建线程的优缺点
- 优点:
线程类只是实现了 Runnable 或者 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象。所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 - 缺点:
编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法
- 优点:
采用继承 Thread 类的方式创建线程的优缺点:
- 优点:
编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获取当前线程 - 缺点:
因为线程类已经继承了 Thread 类,Java 语言是单继承的,所以就不能再继承其他父类了。
- 优点:
说说线程的生命周期
先来看一张图:
这六个状态就对应线程的生命周期。下图为线程对应状态以及状态出发条件:
说说 synchronized 的使用和原理
线程同步中的锁对象
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类 Class 对象
- 对于同步方法块,锁是 Synchronized 括号里配置的对象(对象为普通对象则锁定的是该对象,对象为 Class 对象则锁定的是 Class 对象)
JVM 基于进入和退出 Monitor 对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的。
monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法的结束处和异常处,JVM 保证每个 monitorenter 必须有对应的 monitorexit 与之配对。synchronized 和 ReentrantLock 区别
- synchronized 是关键字,ReentrantLock 是 JUC 下面的一个类。
- JDK 1.5 之前同步锁只有 synchronized。
- 都是可重入的同步锁。
- synchronized 只有非公平锁,ReentrantLock 默认为非公平锁,但是可以手动设置为公平锁。
- ReentrantLock 需要手动释放锁
try{--objectLock.lock();}---finally--{objectLock.unlock();}
,synchronized 隐形释放(方法或者代码块执行完、异常)。 - ReentrantLock 可中断,synchronized 不可中断,一个线程引用锁的时候,别的线程只能阻塞等待。
- ReentrantLock 和 synchronized 持有的对象监视器不同。
- ReentrantLock 能够将 wait/notify/notifyAll 对象化。synchronized 中,锁对象的 wait 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件。
- synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
- ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。
什么是线程安全?
按照《Java 并发编程实战》(Java Concurrency in Practice) 的定义就是:线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。
通俗易懂的说法:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替执行,我们在主程序中不需要做任何同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。线程安全需要保证几个基本特征
- 原子性:
简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。 - 可见性:
是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。 - 有序性:
是保证线程内串行语义,避免指令重排等。
- 原子性:
说一下线程之间是如何通信的?
线程之间的通信有两种方式:共享内存和消息传递。
说说你对 volatile 的理解
说一下 volatile 和 synchronized 的区别?
- volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
- volatile 仅能实现变量的修改可见性,不能保证原子性。而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化。synchronized 标记的变量可以被编译器优化。
Thread 调用 run 方法和调 start 方法的区别?
- 调用 run 方法不会再启一个线程,跟着主线程继续执行,和调普通类的方法一样;
- 调用 start 方法表示启动一个线程。
面试扩散
下面代码将输出什么内容?不清楚的建议自己去试试。
说一下 Java 创建线程池有哪些方式?
- 通过 java.util.concurrent.Executors 来创建以下常见线程池:
- 也可以通过 java.util.concurrent.ThreadPoolExecutor 来创建自定义线程池,其中核心的几个参数:
- 通过 java.util.concurrent.Executors 来创建以下常见线程池:
线程池原理:
说说 ThreadLocal 底层原理是什么,怎么避免内存泄漏?
推荐阅读:ThreadLocal 面试六连问,中高级必问
说说你对 JUC 下并发工具类
Semaphore
是一种新的同步类,它是一个计数信号。从概念上讲,信号量维护了一个许可集合。
- 如有必要,在许可可用前会阻塞每一个 acquire() 方法,然后再获取该许可。
- 每个 release() 方法,添加一个许可,从而可能释放一个正在阻塞的获取者。
- 但是,不使用实际的许可对象,Semaphore 只对可用许可的数量进行计数,并采取相应的行动。
信号量常常用于多线程的代码中,比如数据库连接池。
CountDownLatch
字面意思是减小计数(CountDown)的门闩(Latch)。它要做的事情是,等待指定数量的计数被减少,意味着门闩被打开,然后进行执行。
CountDownLatch 默认的构造方法是 CountDownLatch(int count),其参数表示需要减少的计数,主线程调用 await() 方法告诉 CountDownLatch 阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch 的 CountDown() 方法,减小计数(不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。CyclicBarrier
字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞,直到 parties 个线程到达,结束阻塞。
CyclicBarrier 和 CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。
CountdownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作。可以向 CountdownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await() 方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次 —— 计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountdownLatch 的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountdownLatch 对象的 #await() 方法,其他的任务执行完自己的任务后调用同一个 CountdownLatch 对象上的 countDown() 方法,这个调用 #await() 方法的任务将一直阻塞等待,直到这个 CountdownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。