常见问题
线程与进程的关系、区别与缺点
图解进程和线程的关系
下图是 Java 内存区域

从上图可以看出,一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈、本地方法栈线程是进程划分后的更小运行单位,线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程刚好相反
为什么程序计数器是私有的
程序计数器的主要作用
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
需要注意的是,如果执行的 native 方法,那么程序计数器记录的 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条的地址
所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置
虚拟机栈和本地方法栈为什么是私有的
虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直到执行完成的过程,就对应着一个栈帧在 Java 虚拟机中入栈、出栈的过程
本地方法栈:和虚拟机栈类似,区别是:虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用的 Native 方法服务
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
堆和方法区
堆和方法区是所有线程共享的资源
堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存)
方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
(JDK1.7之后 JVM 将运行时常量池从方法区移了出来,在 Java 堆中开辟了一块区域存放运行时常量池)
线程的生命周期和状态
使用多线程可能会导致内存泄漏、上下文切换频繁、死锁

什么是上下文切换?
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态
任务从保存到再加载的过程就是一次上下文切换
什么是线程死锁?如何避免死锁?
多个线程同时被阻塞,他们的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止
产生死锁的四个条件
互斥条件:该资源任意一个时刻只由一个线程占有
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:线程已获得的资源在未使用完之前不能被其他资源强行剥夺,只有自己使用完毕后才释放资源
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
避免线程死锁(一一对应)
互斥条件无法破坏,因为这就是我们用锁的目的
一次性申请所有的资源
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占用的资源
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放
synchronized关键字是怎么使用的
最主要的三种使用方式
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
Synchronized void method () { //业务代码 }
修饰静态方法:也就是给当前类给锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员
synchronized staic void method() {
//业务代码
}
修饰代码块:指定加锁对象,对给定对象/类加锁。
synchronized(this|object)
表示进入同步代码块前要获得给定对象的锁synchronized(类.class)
表示进入同步代码块前要获得给定类的锁Synchronized (this) { //业务代码 }
Java 内存模型(JMM)
当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了变量的值,而另一个线程还在继续使用它的寄存器中的变量值的拷贝,造成数据的不一致 !

要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是共享而且不稳定的,每次使用它都要到主存中进行读取所以 volatile 除了防止 JVM 的指令重排之外,还可以保证变量的可见性

synchronized和volatile关键字的区别
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
volatile 可以保证数据的可见性但是不能保证数据的原子性,synchronized 两者都能保证
volatile 主要用于解决变量在多个线程之间的可见性,而 synchronized 解决的是多个线程之间访问资源的同步性
ThreadLocal 原理
主要解决的是让每个线程绑定自己的值
每个 ThreadLocal 变量相当于一个格子
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
public class ThreadLocal<T> {
...
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
...
}
Thread 类中有一个 threadLocals,默认为 null
只有当当前线程调用 ThreadLocal 类的 get 或 set 方法时才创建它(从源码可以看到),最终的变量是放在了当前线程的 threadLocals 中,并不是存在 ThreadLocal 中
ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值 ThreadLocals 的键就是 ThreadLocal 对象,值就是要存放的 Object 对象
ThreadLocal的内存泄漏问题
ThreaLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强制的情况下,在垃圾回收的时候,key 会被清掉,而 value 不会被清掉
这样一来,ThreadLocalMap 里面就会出现 key 为 null 的 Entry,这个 value 永远无法被 GC 回收,就会发生内存泄漏。但是 ThreadLocal 已经考虑了这种情况,在调用 set、get、remove 方法时,会清理掉 key 为 null 的数据
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;
}
}
...
}
线程池
为什么使用线程池
降低资源消耗。通过重复利用已创建的线程降低线程的创建和销毁造成的消耗
提高响应速度。当任务到达时,不需要创建线程就可以立即执行
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
Runnable 接口和 Callable 接口的区别
Runnable 接口不会返回结果或者抛出检查异常,但是 callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runable 接口
执行 execute()和 submit()方法
execute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get 方法获取返回值,get 方法会一直阻塞当前线程直到任务完成,而使用带参 get 方法(get(long timeout, TimeUnit unit))则只会阻塞一段时间,这时候有可能任务未完成
如何创建线程池
通过 Executors 创建,可以创建三种类型的 ThreadPoolExecutor
FixedThreadPool:一个固定线程数量的线程池。允许请求的队列长度为 Integer.MAX_VALUE
SingleThreadExecutor:一个只有一个线程的线程池。允许请求的队列长度为 Integer.MAX_VALUE
CachedThreadPool:一个可以根据实际情况调整线程数量的线程池。允许创建的线程数量为 Integer.MAX_VALUE
通过 ThreadPoolExecutor 构造方法实现

我们只看最长的构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:最小可以同时运行的线程数量
maximumPoolSize:当队列中存放的任务达到队列容量时,当前可以同时运行的线程数变为最大线程数
wordQueue:当新任务来的时候先判断当前运行的线程是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
keepAliveTime:当线程池的数量大于核心线程数时,如果这时没有新的任务提交,那么核心线程外的线程会在等待 keepAliveTime 之后销毁
unit:时间单位
threadFactory:创建新线程的工厂
handler:饱和策略
ThreadPoolExector.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理ThreadPoolExector.CallerRunsPolicy
:调用执行自己的线程运行任务,会降低新任务的提交速度,影响整体性能ThreadPoolExector.DiscardPolicy
:不处理新任务,直接丢弃掉ThreadPoolExector.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求
线程池原理

AQS及其原理
AQS 是一个构建同步器和锁的框架,使用 AQS 能简单且高效的构造出大量的同步器
核心思想:
如果被请求的资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制是用 CLH 队列的变种实现的,将暂时获取不到锁的线程加入到队列中
CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS 中的队列是 CLH 变体的虚拟双向队列(FIFO),AQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配
