Skip to content

线程并发

线程安全

线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,比如i++,初始化值为0,那么两个线程来同时执行这行代码,如果代码是线程安全的,那么最终的结果应该就是一个线程的结果为1, 一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。所以线程安全,主要指的是一段代码在多个线程同时执行的情况下,能否得到正确的结果。

守护线程

线程分为用户线程和守护线程,M的后台线程,比如垃圾回收线程就是守护线程, 守护线程会在其他普通线程都停止运行之后自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。

并发 & 并行 & 串行

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行
  • 并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行

创建线程的方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test implements Runnable {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(new Test());
    }

    @Override
    public void run() {
        System.out.println("hello world");
    }
}

为什么不建议使用Executors创建线程池

  • FixedThreadPool
  • SingleThreadExecutor

在这里可以看一下newFixedThreadPool的源码

java
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

发现创建的队列为LinkedBlockingQueue,是一个无界阻塞队列,如果使用该线程池执行任务如果任务过多就会不断的添加到队列中,任务越多占用的内存就越多,最终可能耗尽内存,导致oom. 并且使用Executors来创建线程池也不能自定义线程的名字, 不利于排查问题

线程池有哪几种状态

  • RUNNING 线程池正常运行,可以接受新任务
  • SHUTDOWN 线程池关闭,不能接受新任务,会把任务队列当中的任务执行完毕后关闭
  • STOP 线程池关闭,不能接受新任务,不会执行剩余任务
  • TIDING 线程池中没有线程在运行,会自动切换成该状态,并且会调用terminate()方法
  • TERMINATED terminated()方法执行后,进入该状态

线程池底层实现原理 (队列 + 线程)

此时线程池的线程数用n表示

  • n < corePoolSize 时,即使线程池中线程都处于空闲状态,也会创建新的线程用来处理被添加的任务
  • n = corePoolSize 时,但缓冲队列workQueue没满,那么任务会添加到缓冲队列中
  • n >= corePoolSize 时,缓冲队列满,并且n < maximumPoolSize时,创建新的线程来处理任务
  • n > corePoolSize 时,缓冲队列满,并且n = maximumPoolSize时,那么通过handler所指定的策略来处理任务
  • n > corePoolSize 时,若某线程的超时时间 > keepAliveTime时,线程将被终止,这样线程池可以动态调整池中的线程数

Synchronized & ReentrantLock

SynchronizedReentrantLock
java当中的一个关键字JDK1.5之后的API
自动加锁和释放锁手动
JVM层面的锁API层面
非公平锁公平锁 & 非公平锁
锁的是对象,锁信息存储在对象头里int类型的state标识来标识锁的状态
底层有锁升级的过程底层没有锁升级的过程

ThreadLocal

  • ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  • ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap, Map的key为ThreadLocal对象,Map的value为需要缓存的值
  • 造成内存泄露,在每一次使用之后调用remove方法请求数据
java
    private ThreadLocal<String> local = new ThreadLocal<>();
    public void a(){
        local.set("1");
        b();
    }

    public void b(){
        String s = local.get();
        local.remove();
    }

ThreadLocal 底层原理

ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值

ReentrantLock的公平锁和非公平锁

  • 公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
  • 非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的

ReentrantLock的 lock() & tryLock()

  • tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
  • lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

Synchronized 锁升级过程

  • 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入
  • 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  • 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  • 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记, 如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

造成死锁的原因

  • 一个资源每次只能被一个线程使用
  • 一个线程在阻塞等待某个资源时,不释放已占有资源
  • 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
  • 若干线程形成头尾相接的循环等待资源关系

需要注意:加锁顺序、加锁时限、死锁检查(提前预防)

CountDownLatch & Semaphore

AQS (AbstractQueuedSynchronizer)

  • AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
  • 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。
    • 线程队列,就是用来给线程排队的
    • state(红绿灯),用来控制线程排队或者放行的
  • 在做可重入锁场景下,state就用来表示加锁的次数。
    • 无锁 0
    • 加锁 state+1
    • 释放锁 state-1

By Modify.