2020年,截止目前,我收到了阿里巴巴、腾讯、美团、京东、快手等互联网大厂的面试邀请。求职是一场流程很长的拉锯战,涉及岗位选择、简历投递、简历评估、技术面试、HR面试等环节。
我发现在技术面试中多线程在面试中出现的次数非常非常多,幸好我面试之前也有所准备。今天结合面试经历写一篇面向面经的Java线程安全有关的最全知识汇总。
本文很干货,很干!请自带茶水
进程 | 线程 | |
---|---|---|
量级 | 重量级 | 轻量级 |
内存 | 私有内存 | 共享内存 |
同步机制 | 不需要 | 需要 |
处理安全性 | 杀死进程是安全的 | 杀死线程是不安全的 |
不仅是本机的多个进程之间,也可以是不同机器的多个进程之间。
从Thread类派生子类
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
HelloThread p = new HelloThread();
p.start();
}
//----------启动该线程的两个方式
public static void main(String args[]) {
(new HelloThread()).start();
}
}
从Runnable接口构造Thread对象
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
常见创建方法:
new Thread(new Runnable() {
@Override
public void run() {
}
});
并发很难测试和调试因为竞争条件导致的bug。因为交错interleaving的存在,导致很难复现bug
Thread.sleep():使得线程在一定时间内休眠,进入休眠的线程不会失去对现有monitor或锁的所有权。
Thread.interrupt() 中断
Thread.yield():使用该方法,线程告知调度器:我可以放弃CPU的占用权,从而可能引起调度器唤醒其他线程(尽量避免在代码中使用)。
public void run() {
...
for (int i = 0; i < 5; i++) {
if ((i % 5) == 0)
Thread.yield();
}
}
Thread.join():让当前线程保持执行,直到其执行结束。
参考链接:https://www.cnblogs.com/cdf-opensource-007/p/8769777.html
Excutors创建线程池便捷方法如下:
Executors.newFixedThreadPool(100);//创建固定大小的线程池
Executors.newSingleThreadExecutor();//创建只有一个线程的线程池
Executors.newCachedThreadPool();//创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
对于服务端需要长期运行的程序,创建线程池应该使用ThreadPoolExecutor
的构造方法
public ThreadPoolExecutor(
int corePoolPoolSize,//线程池长期维持的线程数
int maximumPoolSize, //线程数的上限
long keepAliveTime,//空闲线程存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务的排队队列
ThreadFactory threadFactory,//新线程的产生方式
RejectedExecutionHandler handler//拒绝策略
)
java线程池有7大参数,四大特性。
特性一:当池中正在运行的线程数(包括空闲线程)小于corePoolSize时,新建线程执行任务。
特性二:当池中正在运行的线程数大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行。
特性三:当队列里的任务数达到上限,并且池中正在运行的线程数小于maximumPoolSize,对于新加入的任务,新建线程。
特性四:当队列里的任务数达到上限,并且池中正在运行的线程数等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)。
限制数据共享
共享不可变数据
共享线程安全的可变数据
同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行
线程之间不共享mutable数据类型
import java.math.BigInteger;
public class Main {
public static void computeFact(final int n){
BigInteger result = new BigInteger("1");
for(int i = 1;i<=n;i++) {
System.out.printf("Fact %d is working\n",i);
result = result.multiply(new BigInteger(String.valueOf(i)));
}
System.out.printf("Fact %d is %d\n",n,result);
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
computeFact(99);
}
});
thread1.start();
computeFact(100);
}
}
避免全局变量。例如下面是不安全的,存在多个线程,同时访问getInstance()方法,创建出两个PinballSimulator 对象。
使用不可变数据类型和不可变引用,避免多线程之间的race condition
如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。
可重入锁:基于线程的分配。
读写锁:对一个资源的访问分成了2个锁,比如文件,分为读锁和写锁。例如ReadWriteLock()
可中断锁:可以中断的锁机制。例如Lock是可中断锁。
公平锁: 以请求锁的顺序来获取锁。有多个线程在等待一个锁,当锁被释放时,等待时间最久的线程会获取该锁,公平锁。
对某一代码块使用,sychronized后面的括号里面是变量,一次只有一个线程进入该代码块。
public void synchroMethod(int m){
synchronized (m){
}
}
方法声明时使用,表示一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队。
public synchronized void synchroMethod(int m){
}
线程获得的是对象锁,synchronized后面括号里是一个对象。
public void synchroMethod(int m){
synchronized(this){
}
}
注意,构造方法没有必要使用synchronized方法,因为构造方法的对象在从构造器返回之前一直被限制在单线程中。
synchronized method 与 synchronized(this) block的区别:
后者需要显式的给出lock,且不一定非要是this
后者可停工更细粒度的并发控制
同步机制给性能带来极大影响。除非必要,否则不要用。Java中很多mutable的类型都不是threadsafe就是这个原因。
尽可能减小lock的范围。直接使用synchronized同步Method,说明没有先思考清楚到底lock谁,然后再synchronized(…)。
public static synchronized boolean findReplace(EditBuffer buf, ...)
将获得静态锁,在class层面上锁。同一时间内只有一个线程能够执行该方法,即使其他线程在不同的内存区取用数据,是安全的。这对性能带来极大损耗。
Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。
所有关于threadsafe的设计决策也都要在ADT中记录下来。
如果A线程在synchronized (list) { … }
ArraysList方法的add(),其中size是全局变量,没有synchronization,存在线程不安全的风险
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
Collections.synchronizedList方法是线程安全的。
List<String> sharedList = Collections.synchronizedList(new ArrayList<String>());
在iterate部分需要使用synchronized(sharedList) { … } 来block
synchronized static是某个类的范围,它可以对类的所有对象实例起作用。
synchronized 是某实例的范围,只对一个实例起作用,synchronized isSync(){}防止多个线程同时访问这个实例中的synchronized 方法。
任何共享的mutable变量/对象必须被lock所保护
如果一个不变量涉及到多个mutable变量的时候,它们必须被同一个lock所保护
monitor pattern中,ADT所有方法都被同一个synchronized(this)所保护
不可被中断的一个或一系列操作。减少内存一致性的错误风险。
轻量级的同步机制
原子变量的改变对于其他线程是可见的
private volatile int counter;
优点:比synchronized更加有效
缺点:需要更多地关注内存一致性
多个线程竞争lock,相互等待对方释放lock
它不是模块化的-代码必须知道系统或至少子系统中的所有锁。
在获取第一个锁之前,代码可能很难知道它将需要哪些锁。它可能需要做一些计算才能弄清楚。
2. coarser locking
使用单个锁监管多个对象实例,甚至是程序的一个子系统。
用单个锁监听很多可变数据,不能实时获取这些数据
在最坏的情况下,用一个锁保护程序中所有的东西,程序变成单线程
因为其他线程lock时间太长,一个线程长时间无法获取其所需的资源访问权(lock),导致无法往下进行。
一个线程经常会响应另一个线程的动作。
该操作使object所处的当前线程进入阻塞/等待状态,直到其他线程调用该对象的notify()操作
public synchronized void guardedJoy() {
// This guard only loops once for each special event,
// which may not be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
notify()与notifyAll()的区别
咱们玩归玩,闹归闹,别拿面试开玩笑。
线程安全在面试中出现的次数非常非常多,一旦问到了,大家一定要回答全面,不要丢三落四,回答到点上。大家面试前要把基础打牢,多写并发线程的程序代码,多线程在笔试题中也很常见。