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

480 阅读9分钟

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

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

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

进程和线程

进程 线程
量级 重量级 轻量级
内存 私有内存 共享内存
同步机制 不需要 需要
处理安全性 杀死进程是安全的 杀死线程是不安全的
  1. 进程:拥有整台计算机的资源。私有空间,彼此隔离。
  • 多进程之间不共享内存
  • 进程之间通过消息传递进行协作
  • 一般来说,进程== 程序 ==应用,但一个应用中可能包含多个进程
  • OS支持的IPC机制(pipe/socket)支持进程间通信

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

  • JVM通常运行单一进程,但也可以使用ProcessBuilder 创建新的进程。
  1. 线程 程序内部的控制机制。
  • 进程=虚拟机;线程=虚拟CPU
  • 程序共享、资源共享,共享内存

线程Thread

从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():让当前线程保持执行,直到其执行结束。

在这里插入图片描述

线程池

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

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,对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)。

线程安全的策略

  1. 限制数据共享

  2. 共享不可变数据

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

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

非同步机制

策略一:限制数据共享

线程之间不共享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 对象。

在这里插入图片描述

策略二: Immutability

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

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

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

同步机制

Lock锁机制

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

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

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

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

syncronized

代码块

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

public void synchroMethod(int m){
        synchronized (m){
            
        }
    }

方法声明时

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

public synchronized void synchroMethod(int m){
        
}

synchronized后面括号里是对象

线程获得的是对象锁,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 方法。

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()操作

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

notify()与notifyAll()

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

public synchronized notifyJoy() {
	joy = true;
	notifyAll();
}

notify()与notifyAll()的区别

总结

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

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

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

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