Java多线程相关知识点汇总

1.ThreadLocal
2.如何保证高并发场景下的线程安全?
3.JUC(java.util.concurrent)包
4.volatile
5.信号量同步
6.线程池
7.线程同步类
8.并发集合类
9.锁机制

1.ThreadLocal

  • ThreadLocal如何实现多线程数据隔离?
  • ThreadLocal内存泄漏问题?
  • ThreadLocal脏数据问题?

ThreadLocal主要功能:

  • 进行对象跨层传输,使用ThreadLocal,打破层次间的约束。
  • 线程间数据隔离。
  • 进行事务操作,存储线程事务信息。

注意ThreadLocal并不是解决多线程下共享资源的技术

首先需要了解下四类引用:

  • 强引用
    对象有强引用指向,并且GC Roots可达,不会回收。
  • 软引用
    引用强度弱与“强引用”,用在非必须的场景,在OOM即将发生的时候,垃圾回收器会将这些软引用指向的对象放入回收范围。
  • 弱引用
    引用强度弱于前两者,弱引用指向的对象只存在弱引用这一条路径,那么在下次YGC时会被回收。YGC时间不确定,则弱引用何时被回收也不确定。
  • 虚引用
    极其虚弱的引用对象,完成定义就无法通过该引用获取对象了。一般场景很难使用到。

ThreadLocal 与Thread类图关系,自己拍的图片,大家先看一下,等有时间我自己画一下。


Java多线程相关知识点汇总_第1张图片
ThreadLocal类图

ThreadLocal有一个静态内部类ThreadLocalMap和Entry。
Thread中持有一个ThreadLocalMap属性,在ThreadLocal->createMap()赋值的。ThreadLocal与ThreadLocalMap联系的方法:get() set() remove()
Entry继承自WeaReference,Entry只有一个value成员,它的key是ThreadLocal对象。

下面是栈与内存的的角度分析Thread ThreadLocal ThreadLocalMap三者之间的关系。


Java多线程相关知识点汇总_第2张图片
ThreadLocal弱引用关系
  • 1个Thread有且仅有1个ThreadLocalMap对象
  • 1个Entry对象的Key弱引用指向1个ThreadLocal对象
  • 1个ThreadLocalMap独享存储多个Entry对象
  • 1个ThreadLocal对象可以被多个线程共享
  • ThreadLocal对象不持有Value,Value由线程的Entry对象持有。
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

所有Entry对象都会被ThreadLocalMap类实例化的threadLocals持有,线程执行完毕,线程对象内的实例属性均会被垃圾回收。Entry中的Key是ThreadLocal弱引用,即使线程正在运行过程中,只有ThreadLocal对象引用被置为null,Entry的Key就会自动在下次YGC时被垃圾回收。而ThreadLocal使用set() get()时会自动将key==null的value置为null,使value也能被垃圾回收。

ThreadLocal内存泄漏问题:

Java多线程相关知识点汇总_第3张图片
ThreadLocal内存泄漏图解

Entry中的Key-ThreadLocal对象失去引用后,触发弱引用机制来回收Entry中的Value是不可能的,在上面的图中,ThreadLocal被回收了,但是如果没有显式的回调remove(),这时候Entry中的Value是不会被立即回收的,可能会造成内存泄漏。

线程池操作可能产生的ThreadLocal脏数据

2.如何保证高并发场景下的线程安全?

2.1 数据单线程可见
2.2 只读对象
final修饰的变量
2.3 线程安全类
StringBuffer,以及采用synchronized修饰的其他类。
2.4 同步锁工作机制

3.JUC(java.util.concurrent)包

3.1 线程同步类

object.wait()/Object.notify()
CountDownLatch
Semaphore
CyclicBarrier

3.2 并发集合类

ConcurrentHashMap
ConcurrentSkipListMap
CopyOnWriteArrayList
BlockingQueue

3.3 线程管理类

ThreadLocal
Executors静态工厂
ThreadPoolExecutor
ScheduledExecutorService

3.4 锁相关类

ReentrantLock

4.volatile

volatile解决的是多线程共享变量的可见性问题,但是不具备synchronized的互斥性,所以对volatile变量的操作并非都具有原子性。

//Author:[email protected]

public class VolatileAtomic {

  private static volatile long count = 0L;
  private static final int NUMBER = 10000;

  public static void main(String[] args) {
    Thread subtraceThread = new SubtraceThread();
    subtraceThread.start();
    for(int i=0;i 
  

这个结果不为0就说明volatile变量是不具备synchronized的互斥性的,这很简单,因为count++与count--在底层是分为三步操作的,并不是原子操作。所以如果想得到结果为0,只需要在count++与count--上加锁即可。
volatile非常适合“一写多读”的场景,“一写多读”的经典场景是CopyOnWriteArrayList

JVM的happens-before原则:

  • 程序次序原则:一个线程内,代码按照编写时的顺序执行。
  • 锁住规则:unlock操作优先于lock操作
  • volatile变量规则:如果一个变量使用volatile关键字来修饰,一个线程对它进行读操作,另一个线程对它进行写操作,那么写操作肯定要优先于读操作。
    volatile具有保证顺序性的语义。

volatile与synchronized的区别:

1.使用上区别

  • 1.1 volatile关键字只能修饰实例变量和类变量,不能修饰方法以及方法参数、局部变量、常量等。
  • 1.2 synchronized关键字不能修饰变量,只能修饰方法和代码块。
  • 1.3 volatile修饰的变量可以为null,但是synchronized修饰的monitor不能为null

2.对原子性的保证

  • 2.1 volatile无法保证原子性
  • 2.2 synchronized 执行中无法被打断,可以保证代码的原子性。

3.对可见性的保证

  • 3.1 两者均可以保证共享资源在多线程间的可见性,实现机制不同。
  • 3.2 synchronized使用monitor enter与monitor exit通过排他方式使得同步代码串行化,monitor exit之后所有的共享资源都会被刷新到主内存中。
  • 3.3 volatile使用机器指令“:lock;”方式迫使其他线程工作内存中的数据失效,不得不到主内存中再次加载。

4.对有序性的保证

  • 4.1 volatile关键字禁止JVM编译器以及处理器对其进行重排序,可以保证有序性。
  • 4.2 synchronized修饰的代码块或者函数中也可能发生指令重排

5.其他区别

  • 5.1 volatile不会使线程陷入阻塞
  • 5.2 synchronized关键字会使线程进行阻塞状态。

5.信号量同步

信号量实现多线程交替打印ABC的问题:

//[email protected]

import java.util.concurrent.Semaphore;

public class ABC_Semaphore {

  public static Semaphore A = new Semaphore(1);
  public static Semaphore B = new Semaphore(0);
  public static Semaphore C = new Semaphore(0);

  static class ThreadA extends Thread {

    @Override
    public void run() {
      for(int i=0;i<10;i++) {
        try {
          A.acquire();
          System.out.print('A');
          B.release();
        } catch(Exception e) {

        }
      }
    }
  }

  static class ThreadB extends Thread {

    @Override
    public void run() {
      for(int i=0;i<10;i++) {
        try {
          B.acquire();
          System.out.print('B');
          C.release();
        } catch(Exception e) {

        }
      }
    }
  }

  static class ThreadC extends Thread {

    @Override
    public void run() {
      for(int i=0;i<10;i++) {
        try {
          C.acquire();
          System.out.print('C');
          A.release();
        } catch(Exception e) {

        }
      }
    }
  }

  public static void main(String[] args) {
    new ThreadA().start();
    new ThreadB().start();
    new ThreadC().start();
  }
}
Java多线程相关知识点汇总_第4张图片
image.png

6.线程池

线程池的作用:

  • 1.利用线程池管理并复用线程、控制最大并发数等。
  • 2.实现任务线程队列缓存策略和拒绝机制。
  • 3.实现某些与时间相关的功能,如定时执行、周期执行等。
  • 4.隔离线程环境。例如交易服务和搜索服务在同一台服务器上,分别开启两个线程池,其中交易线程的消耗肯定大一点。这种将不同的服务隔离开来,便于处理较慢和较快的服务。
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • 1.corePoolSize表示常驻核心线程数。如果等于0,则任务完成之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
  • 2.maximumPoolSize表示线程池能够容纳同时执行的最大线程数。如果执行的线程数大于maximumPoolSize,需要借助第五个参数来将其缓存在队列中。如果maximumPoolSize与corePoolSize相等,就是固定大小的线程池。
  • 3.keepAliveTime表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime,线程会被销毁,直到剩下corePoolSize个线程,避免浪费内存资源。当线程池的线程数大于corePoolSize时keepAliveTime才会起作用。当ThreadPoolExecutor的allowCoreThreadTimeOut变成设为true,核心线程超时也会被回收。
  • 4.unit表示时间单位。
  • 5.workQueue表示缓存队列。当请求的线程数大于maximumPoolSize,线程进入BlockingQueue阻塞队列。
  • 6.threadFactory表示线程工厂。它用来生产一组相同任务的线程。
  • 7.handler表示执行拒绝策略的对象。当超过第5个参数workQueue的任务缓存区上限的时候,可以通过该策略处理请求,这是简单的限流保护。

线程池相关的类图:


Java多线程相关知识点汇总_第5张图片
线程池相关类图

7.线程同步类

Java多线程相关知识点汇总_第6张图片
Lock继承关系.jpg

AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。AQS抽象类包含如下几个方法:

  • AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。共享模式时只用 Sync Queue, 独占模式有时只用 Sync Queue, 但若涉及 Condition, 则还有 Condition Queue。在子类的 tryAcquire, tryAcquireShared 中实现公平与非公平的区分。
  • 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源volatile state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
    整个 AQS 分为以下几部分:
  • Node 节点, 用于存放获取线程的节点, 存在于 Sync Queue, Condition Queue, 这些节点主要的区分在于 waitStatus 的值(下面会详细叙述)
  • Condition Queue, 这个队列是用于独占模式中, 只有用到 Condition.awaitXX 时才会将 node加到 tail 上(PS: 在使用 Condition的前提是已经获取 Lock)
  • Sync Queue, 独占 共享的模式中均会使用到的存放 Node 的 CLH queue(主要特点是, 队列中总有一个 dummy 节点, 后继节点获取锁的条件由前继节点决定, 前继节点在释放 lock 时会唤醒sleep中的后继节点)
  • ConditionObject, 用于独占的模式, 主要是线程释放lock, 加入 Condition Queue, 并进行相应的 signal 操作。
  • 独占的获取lock (acquire, release), 例如 ReentrantLock。
  • 共享的获取lock (acquireShared, releaseShared), 例如 ReeantrantReadWriteLock, Semaphore, CountDownLatch

参考这篇文章的应用 Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

经典的面试题:多线程交替打印ABC的多种实现方法

7.1 CountDownLatch

CountDownLatch初始时定义了资源总量 state=count,执行countDown()不断-1,直到为0,当state=0时时才能获取锁。释放后state就一直为0。CountDownLatch是一次性的,用完之后如果再想使用就重新创建一个。

7.2 Semaphore

见《5.信号量同步》
Semaphore定义了资源总量state=permits,当state>0可以获取锁,将state减1,当state=0时只能等待其他线程释放锁,当释放锁时state加1,其他等待的线程又能获得这个锁。当permits定义为1时,就是互斥锁,当permits>1时就是共享锁。

7.3 CyclicBarrier

CyclicBarrier是基于ReentrantLock实现的,它比CountDownLatch优越的地方是可以循环使用。

8.并发集合类

这在Java底层数据结构中总结这一块内容。

8.1 ConcurrentHashMap
8.2 ConcurrentSkipListMap
8.3 CopyOnWriteArrayList
8.4 BlockingQueue

9.锁机制

9.1 ReentrantLock

要想支持重入性,就要解决两个问题:1.在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。我们知道,同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。

公平锁 VS 非公平锁

  • 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
  • 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
9.2 ReentrantLockReadWriteLock

ReadWriteLock: 允许读操作并发执行;不允许“读/写”, “写/写”并发执行。当数据结构需要频繁的读时,ReadWriteLock相比ReentrantLock与synchronized的性能更好。

9.3 StampedLock

读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,

因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,

使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!

参考:
并发Lock之AQS(AbstractQueuedSynchronizer)详解
Java8对读写锁的改进:StampedLock

你可能感兴趣的:(Java多线程相关知识点汇总)