年大厂喜欢这样问线程安全,这些知识点我整理好了

2020年,截止目前,我收到了阿里巴巴、腾讯、美团、京东、快手等互联网大厂的面试邀请。求职是一场流程很长的拉锯战,涉及岗位选择、简历投递、简历评估、技术面试、HR面试等环节。

我发现在技术面试中多线程在面试中出现的次数非常非常多,幸好我面试之前也有所准备。今天结合面试经历写一篇面向面经的Java安全" title="线程安全">线程安全有关的最全知识汇总。

本文很干货,很干!请自带茶水

进程和线程

进程线程
量级重量级轻量级
内存私有内存共享内存
同步机制不需要需要
处理安全性杀死进程是安全的杀死线程是不安全的

  1. 进程:拥有整台计算机的资源。私有空间,彼此隔离。

  • 多进程之间不共享内存
  • 进程之间通过消息传递进行协作
  • 一般来说,进程== 程序 ==应用,但一个应用中可能包含多个进程
  • OS支持的IPC机制(pipe/socket)支持进程间通信

不仅是本机的多个进程之间,也可以是不同机器的多个进程之间。

  • JVM通常运行单一进程,但也可以使用ProcessBuilder 创建新的进程。

  1. 线程

    程序内部的控制机制。

  • 进程=虚拟机;线程=虚拟CPU
  • 程序共享、资源共享,共享内存

线程Thread

从Thread类派生子类

publicclassHelloThreadextendsThread{

publicvoidrun(){

System.out.println("Hello from a thread!");

}

publicstaticvoidmain(String args[]){

HelloThread p = new HelloThread();

p.start();

}

//----------启动该线程的两个方式

publicstaticvoidmain(String args[]){

(new HelloThread()).start();

}

}

从Runnable接口构造Thread对象

publicclassHelloRunnableimplementsRunnable{

publicvoidrun(){

System.out.println("Hello from a thread!");

}

publicstaticvoidmain(String args[]){

(new Thread(new HelloRunnable())).start();

}

}

常见创建方法:

new Thread(new Runnable() {

@Override

publicvoidrun(){

}

});

并发很难测试和调试因为竞争条件导致的bug。因为交错interleaving的存在,导致很难复现bug

Thread.sleep():使得线程在一定时间内休眠,进入休眠的线程不会失去对现有monitor或锁的所有权。

Thread.interrupt() 中断

在这里插入图片描述

Thread.yield():使用该方法,线程告知调度器:我可以放弃CPU的占用权,从而可能引起调度器唤醒其他线程(尽量避免在代码中使用)。

publicvoidrun(){

...

for (int i = 0; i < 5; i++) {

if ((i % 5) == 0)

Thread.yield();

}

}

Thread.join():让当前线程保持执行,直到其执行结束。

在这里插入图片描述

线程池

参考链接:www.cnblogs.com/cdf-opensou…

Excutors创建线程池便捷方法如下:

Executors.newFixedThreadPool(100);//创建固定大小的线程池

Executors.newSingleThreadExecutor();//创建只有一个线程的线程池

Executors.newCachedThreadPool();//创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

对于服务端需要长期运行的程序,创建线程池应该使用ThreadPoolExecutor的构造方法

publicThreadPoolExecutor(

int corePoolPoolSize,//线程池长期维持的线程数

int maximumPoolSize, //线程数的上限

long keepAliveTime,//空闲线程存活时间

TimeUnit unit,//时间单位

BlockingQueue<Runnable> workQueue,//任务的排队队列

ThreadFactory threadFactory,//新线程的产生方式

RejectedExecutionHandler handler//拒绝策略

)

java线程池有7大参数,四大特性。

特性一:当池中正在运行的线程数(包括空闲线程)小于corePoolSize时,新建线程执行任务。

特性二:当池中正在运行的线程数大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行。

特性三:当队列里的任务数达到上限,并且池中正在运行的线程数小于maximumPoolSize,对于新加入的任务,新建线程。

特性四:当队列里的任务数达到上限,并且池中正在运行的线程数等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)。

线程安全的策略

  1. 限制数据共享

  2. 共享不可变数据

  3. 共享线程安全的可变数据

  4. 同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行

非同步机制

策略一:限制数据共享

线程之间不共享mutable数据类型

import java.math.BigInteger;

publicclassMain{

publicstaticvoidcomputeFact(finalint n){

BigInteger result = new BigInteger("1");

for(int i = 1;i<=n;i++) {

System.out.printf("Fact %d is workingn",i);

result = result.multiply(new BigInteger(String.valueOf(i)));

}

System.out.printf("Fact %d is %dn",n,result);

}

publicstaticvoidmain(String[] args){

Thread thread1 = new Thread(new Runnable() {

@Override

publicvoidrun(){

computeFact(99);

}

});

thread1.start();

computeFact(100);

}

}

避免全局变量。例如下面是不安全的,存在多个线程,同时访问getInstance()方法,创建出两个PinballSimulator 对象。

在这里插入图片描述

策略二: Immutability

使用不可变数据类型和不可变引用,避免多线程之间的race condition

策略三:线程安全的数据类型

如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。

同步机制

Lock锁机制

可重入锁:基于线程的分配。

读写锁:对一个资源的访问分成了2个锁,比如文件,分为读锁和写锁。例如ReadWriteLock()

可中断锁:可以中断的锁机制。例如Lock是可中断锁。

公平锁: 以请求锁的顺序来获取锁。有多个线程在等待一个锁,当锁被释放时,等待时间最久的线程会获取该锁,公平锁。

syncronized

代码块

对某一代码块使用,sychronized后面的括号里面是变量,一次只有一个线程进入该代码块。

publicvoidsynchroMethod(int m){

synchronized (m){

}

}

方法声明时

方法声明时使用,表示一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队。

publicsynchronizedvoidsynchroMethod(int m){

}

synchronized后面括号里是对象

线程获得的是对象锁,synchronized后面括号里是一个对象。

publicvoidsynchroMethod(int m){

synchronized(this){

}

}

注意,构造方法没有必要使用synchronized方法,因为构造方法的对象在从构造器返回之前一直被限制在单线程中。

synchronized method 与 synchronized(this) block的区别:

  • 后者需要显式的给出lock,且不一定非要是this

  • 后者可停工更细粒度的并发控制

同步机制给性能带来极大影响。除非必要,否则不要用。Java中很多mutable的类型都不是threadsafe就是这个原因。

尽可能减小lock的范围。直接使用synchronized同步Method,说明没有先思考清楚到底lock谁,然后再synchronized(…)。

publicstaticsynchronizedbooleanfindReplace(EditBuffer buf, ...)

将获得静态锁,在class层面上锁。同一时间内只有一个线程能够执行该方法,即使其他线程在不同的内存区取用数据,是安全的。这对性能带来极大损耗。

  • Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。

  • 所有关于threadsafe的设计决策也都要在ADT中记录下来。

如果A线程在synchronized (list) { ... }

ArraysList方法的add(),其中size是全局变量,没有synchronization,存在线程不安全的风险

publicvoidadd(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 方法。

Locking原则

  • 任何共享的mutable变量/对象必须被lock所保护

  • 如果一个不变量涉及到多个mutable变量的时候,它们必须被同一个lock所保护

  • monitor pattern中,ADT所有方法都被同一个synchronized(this)所保护

原子操作

不可被中断的一个或一系列操作。减少内存一致性的错误风险。

  • 轻量级的同步机制

  • 原子变量的改变对于其他线程是可见的

private volatile int counter;

优点:比synchronized更加有效

缺点:需要更多地关注内存一致性

在这里插入图片描述

Liveness: deadlock, starvation and livelock

死锁

多个线程竞争lock,相互等待对方释放lock

在这里插入图片描述
在这里插入图片描述

死锁的解决方案

  1. 设置锁获取的顺序

在这里插入图片描述

缺点:

  • 它不是模块化的-代码必须知道系统或至少子系统中的所有锁。

  • 在获取第一个锁之前,代码可能很难知道它将需要哪些锁。它可能需要做一些计算才能弄清楚。

在这里插入图片描述

2. coarser locking

使用单个锁监管多个对象实例,甚至是程序的一个子系统。

例如,下列代码使用Castle的对象锁进行同步化

在这里插入图片描述

缺点:

  • 用单个锁监听很多可变数据,不能实时获取这些数据

  • 在最坏的情况下,用一个锁保护程序中所有的东西,程序变成单线程

Starvation

因为其他线程lock时间太长,一个线程长时间无法获取其所需的资源访问权(lock),导致无法往下进行。

Livelock

一个线程经常会响应另一个线程的动作。

线程不会被阻断,他们可能忙于响应其他线程而不能恢复工作。

在这里插入图片描述

线程间协作的方法

wait()

该操作使object所处的当前线程进入阻塞/等待状态,直到其他线程调用该对象的notify()操作

publicsynchronizedvoidguardedJoy(){

// 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!");

}

notify()与notifyAll()

随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态

publicsynchronizednotifyJoy(){

joy = true;

notifyAll();

}

notify()与notifyAll()的区别

总结

咱们玩归玩,闹归闹,别拿面试开玩笑。

线程安全在面试中出现的次数非常非常多,一旦问到了,大家一定要回答全面,不要丢三落四,回答到点上。大家面试前要把基础打牢,多写并发线程的程序代码,多线程在笔试题中也很常见。

如果有收获?希望老铁们来个三连,点赞、收藏、转发

创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客

以上是 年大厂喜欢这样问线程安全,这些知识点我整理好了 的全部内容, 来源链接: utcz.com/a/28575.html

回到顶部