Java并发相关知识点梳理和研究

1. 知识点思维导图

(图比较大,可以右键在新窗口打开)

2. 经典的wait()/notify()/notifyAll()实现生产者/消费者编程范式深入分析 & synchronized

注:本节代码和部分分析参考了你真的懂wait、notify和notifyAll吗

看下面一段典型的wait()/notify()/notifyAll()代码,对于值得注意的细节,用注释标出。

import java.util.ArrayList;
import java.util.List;

public class Something {
    private Buffer mBuf = new Buffer(); // 共享的池子

    public void produce() {
        synchronized (this) { // 注1、注2
            while (mBuf.isFull()) { // 注3
                try {
                    wait(); // 注4
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();  // 注5、注6
        }
    }

    public void consume() {
        synchronized (this) { // 见注1、注2
            while (mBuf.isEmpty()) { // 注3
                try {
                    wait(); // 注4
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll(); // 注5、注6
        }
    }

    private class Buffer {
        private static final int MAX_CAPACITY = 1;
        private List innerList = new ArrayList<>(MAX_CAPACITY);

        void add() {
            if (isFull()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.add(new Object());
            }
            System.out.println(Thread.currentThread().toString() + " add");

        }

        void remove() {
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.remove(MAX_CAPACITY - 1);
            }
            System.out.println(Thread.currentThread().toString() + " remove");
        }

        boolean isEmpty() {
            return innerList.isEmpty();
        }

        boolean isFull() {
            return innerList.size() == MAX_CAPACITY;
        }
    }

    public static void main(String[] args) {
        Something sth = new Something();
        Runnable runProduce = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.produce();
                }
            }
        };
        Runnable runConsume = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.consume();
                }
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runConsume).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(runProduce).start();
        }
    }
}
  • 注1:wait()/notify()/notifyAll()必须在synchronized块中使用
  • 注2:使用synchronized(this)的原因是,这段代码的main(),是通过实例化Something的对象,并使用它的方法来进行生产/消费的,因此是一个指向this的对象锁。不同的场景,需要注意同步的对象的选择。
  • 注3:必须使用while循环来包裹wait()。设想一种场景:存在多个生产者或多个消费者消费者,以多个生成者为例,在缓冲区满的情况下,如果生产者通过notify()唤醒的线程仍是生产者,如果不使用while,那么获取锁的线程无法重新进入睡眠,锁也不能释放,造成死锁。
  • 注4:wait()会释放锁
  • 注5:notfiy()、notifyAll()会通知其他在wait的线程来获取锁,但是获取锁的真正时机是锁的原先持有者退出synchronized块的时候。
  • 注6:使用notifyAll()而不是notfiy()的原因是,仍考虑注3的场景,假如生产者唤醒的也是生产者,后者发现缓冲区满重新进入阻塞,此时没有办法再唤醒在等待的消费者线程了,也会造成死锁。
  • 注7:notfy()在JVM规范里是随机唤醒一个,但是在Hotspot里的实现是先进先出的 13.1 大佬问我: notify()是随机唤醒线程么?

扩展知识点1:synchronized块的两个队列

synchronized入口是将线程放入同步队列,wait()是将线程放入阻塞队列。notify()/notifyAll()实际上是把线程从阻塞队列放入同步队列。wait/notify/notifyAll方法需不需要被包含在synchronized块中,为什么?

扩展知识点2:synchronized重入原理

synchronized是可重入的,原理是它内部包含了一个计数器,进入时+1,退出时-1。 Java多线程:synchronized的可重入性

扩展知识点3:作用范围

synchronized支持三种用法:修饰静态方法、修饰实例方法、修饰代码块,前两种分别锁类对象、锁对象实例,最后一种根据传入的值来决定锁什么。
synchronized是基于java的对象头实现的,从字节码可以看出包括了一对进入&退出的监视器。
深入理解Java并发之synchronized实现原理

扩展知识点4:分布式环境synchronized的意义

单看应用所运行的的单个宿主机,仍然可能有多线程的处理模式,在这个前提下使用并发相关技术是必须的。

扩展知识点5:哪些方法释放资源,释放锁

所谓资源,指的是系统资源。

wait(): 线程进入阻塞状态,释放资源,释放锁,Object类final方法(notify/notifyAll一样,不可改写)。
sleep(): 线程进入阻塞态,释放资源,(如果在synchronized中)不释放锁,进入阻塞状态,唤醒随机线程,Thread类静态native方法。
yield(): 线程进入就绪态,释放资源,(如果在synchronized中)不释放锁,进入可执行状态,选择优先级高的线程执行,Thread类静态native方法。
如果线程产生的异常没有被捕获,会释放锁。
sleep和yield的比较

可以进一步地将阻塞划分为同步阻塞——进入synchronized时没获取到锁、等待阻塞——wait()、其他阻塞——sleep()/join(),可以参考线程的状态及sleep、wait等方法的区别

再进一步地,Java线程状态转移可以用下图表示(图源《Java 并发编程艺术》4.1.4 节)

WAITING状态的线程是不会消耗CPU资源的。

3. 线程数调优

理论篇

本节参考了《Java并发编程实战》8.2节,也可以结合面试问我,创建多少个线程合适?我该怎么说帮助理解,其中的计算题比较有价值。

前置知识

I/O密集型任务:I/O任务执行时CPU空闲。
CPU密集型任务:进行计算
有的任务是二者兼备的。为了便于分析,不考虑。

定性分析

场景:单核单线程/单核多线程/多核多线程。单核多线程+CPU密集型不能提升执行效率,多核+CPU密集型任务可以;单核多线程+I/O密集型可以提升执行效率。
因此,I/O耗时越多,线程也倾向于变多来充分利用IO等待时间。

定量分析

对于CPU密集型,线程数量=CPU 核数(逻辑)即可。特别的,为了防止线程在程序运行异常时不空转,额外多设一个线程线程数量 = CPU 核数(逻辑)+ 1
对于I/O密集型,最佳线程数 = CPU核数 * (1/CPU利用率) = CPU核数 * (1 + I/O耗时/CPU耗时)
为什么CPU利用率=1/(1+ I/O耗时/CPU耗时)?简单推导一下:

1/(1+ I/O耗时/CPU耗时) = 1/((CPU耗时+I/O耗时)/ CPU耗时) = CPU耗时/总耗时 = CPU利用率

如何获取参数——CPU利用率?

因为利用率不是一成不变的,需要通过全面的系统监控工具(如SkyWalking、CAT、zipkin),并长期进行调整观测。
可以先取2N即2倍核数,此时即假设I/O耗时/CPU耗时=1:1,再进行调优。

阿姆达尔定律

CPU并发处理时性能提升上限。
S=1/(1-a+a/n)
其中,a为并行计算部分所占比例,n为并行处理结点个数。
简单粗暴理解【阿姆达尔定律】

Java线程池篇

基本属性

/**
 * 使用给定的初始参数和默认线程工厂创建一个新的ThreadPoolExecutor ,并拒绝执行处理程序。 使用Executors工厂方法之一可能更方便,而不是这种通用构造函数。
参数
 *  corePoolSize - 即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut
 *  maximumPoolSize - 池中允许的最大线程数
 *  keepAliveTime - 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最大时间。
 *  unit - keepAliveTime参数的时间单位
 *  workQueue - 在执行任务之前用于保存任务的队列。 该队列将仅保存execute方法提交的Runnable任务。
 * threadFactory - 执行程序创建新线程时使用的工厂
 */
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

常见线程池

由java.util.concurrent.Executors创建的线程池比较常用,而不是使用ThreadPoolExecutor的构造方法。
注意:有的不带参数的方法,默认最大大小是Integer.MAX_VALUE,可能导致资源耗尽,尽量不要用。

名称 特性 corePoolSize maximumPoolSize keepAliveTime + unit workQueue threadFactory
newFixedThreadPool 线程池大小为固定值 nThreads nThreads 0 ms LinkedBlockingQueue DefaultThreadFactory
newSingleThreadExecutor 线程池大小固定为1 1 1 0 ms LinkedBlockingQueue DefaultThreadFactory
newCachedThreadPool 0 Integer.MAX_VALUE 60L s SynchronousQueue DefaultThreadFactory
newScheduledThreadPool 延迟执行任务或按周期重复执行任务 0 Integer.MAX_VALUE 0 ns DelayedWorkQueue DefaultThreadFactory

workQueue分析:

  • SynchronousQueue 不“直接”持有元素,而是持有线程,让生产者线程和消费者线程配对,无法配对的就会阻塞

    • 支持先进先出和先进后出(栈)两种模式
  • LinkedBlockingQueue 缓冲队列

    • 不设置容量则为Integer.MAX_VALUE
    • put和get各有一个非公平ReentrantLock和对应的Condition
  • DelayedWorkQueue

    • ScheduledThreadPoolExecutor的静态内部类,Worker会不断地从这里取任务执行
    • 堆,如果未到时间,poll()返回null,这样Worker就不会执行了
    • 新插入的元素是堆头时会通过Condition avaliable唤醒等待的线程。这个avaliable保证线程不会无限循环,而是阻塞在锁这里。
  • ArrayBlockingQueue 缓冲数组队列

    • 只有一个ReentrantLock和两个Condition,因此不能put和get并行
    • 可设置公平和非公平
    • 必须指定大小

其他:
ArrayBlockingQueue

线程工厂的作用

用来创建线程,统一在创建线程时设置一些参数,如是否守护线程。线程一些特性等,如优先级。
可参考004-多线程-JUC线程池-ThreadFactory线程工厂

多线程的异常处理

子线程的异常,主线程不会处理。默认只会将子线程的堆栈打印到console上,这一步也不是主线程做的。
如果想要处理,可以通过以下方式:

  • 设置线程的异常处理器即Thread.setDefaultUncaughtExceptionHandler()。线程池下可以用线程工厂批量设置。
  • extends线程池,重写afterExecuteafterExecute(Runnable r, Throwable t)这个方法。默认是空的
  • 使用future,在get()时处理。但是get()只会抛出InterruptedException、ExecutionException这两种异常。

4. 并发容器相关

并发容器可以说是一个面试时的高频问题了,网络上也有很多介绍,这里就不重复解读,将相关的知识整理一下,边看源码边读文章效果会很好。
先提一句,Vector是线程安全的,为啥现在不推荐用呢?看源码可以知道,它将大部分方法都加了synchronized,牺牲了性能换取线程安全,是不可取的。如果真的有需要线程安全的容器,可以用Collections.synchronizedList()来手动给list加synchronized。
再补充一句,其实Vector和Collections.synchronizedList()使用复合操作或迭代器Iterator时也不是线程安全的,具体解释会在下一篇博客Java容器中介绍。

ConcurrentHashMap

先重点介绍Map的两个实现类HashMap和ConcurrentHashMap

 public final boolean equals(Object o) {
     if (o == this)
         return true;
     if (o instanceof Map.Entry) {
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         if (Objects.equals(key, e.getKey()) &&
             Objects.equals(value, e.getValue()))
             return true;
     }
     return false;
 }

ConcurrentLinkedQueue

ConcurrentLinkedQueue使用CAS无锁操作,保证入队出队的线程安全,但不保证遍历时的线程安全。遍历要想线程安全需要单独加锁。
由于算法的特性,这个容器的尾结点是有延迟的,tail不一定是尾节点,但p.next == null的节点一定是尾结点。
入队出队操作很抽象,需要画图帮助理解源码,对应的源码分析可参考并发容器-ConcurrentLinkedQueue详解

5. AQS解读

抽象队列同步器AbstractQueuedSynchronizer(AQS)是JUC中很多并发工具类的基础,用来抽象各种并发控制行为,如ReentranLock、Semaphore。顺便一提,线程池的工作线程模型Worker也实现了AQS。
之前试着直接读源码,效果不太好,还是建议结合质量较高的文章来读,这里推荐一篇:Java并发之AQS详解,并且作者还在不断更新。
这里简单记录一下总结的点。

结构特点

  • volatile int state标记位,标识当前的同步状态。具体的用法和使用AQS的工具类有关。同时,在做CAS的时候,state的状态变更是通过计算该变量在对象的偏移量来设置的。
  • CLH队列。CLH锁(Craig,Landin andHagersten)是一种在SMP(Symmetric Multi-Processor对称多处理器)架构下基于单链表的高性能的自旋锁,队列中每个节点代表一个自旋的线程,每个线程只需在代表前一个线程的节点上的布尔值locked自旋即可,如图

    图源和CLH的详解见算法:CLH锁的原理及实现
  • exclusiveOwnerThread独占模式的拥有者,记录现在是哪个线程占用这个AQS。

操作特点

  • 对state使用>0和<0的判断,初看代码很难看懂,这么写的原因是负值表示结点处于有效等待状态,而正值表示结点已被取消
  • 大量的CAS:无论是获取锁、入队、获取锁失败后的自旋,全部是依赖CAS实现的。
  • 没有使用synchronized:不难理解,如果使用了同步块,那么其实现ReentranLock就没有和synchronized比较的价值了。不过这一点很少有文章专门提到。
  • LockSupport类的unpark()/park()方法的使用:回忆上文提到的线程状态,如果线程获取不到AQS控制的资源,需要将线程置于waiting,对应可选的方法是wait()/join()/park()。在AQS这个场景下,显然一没有synchronized,二没有显式的在同一个代码块中用join处理多线程(借助队列来处理线程,线程相互之间不感知),那么只有park()才能达到目的。

处理流程

获取资源acquire(int)

  1. 尝试获取资源(改写state),成功则返回
  2. CAS(失败则自旋)加入等待队列队尾
  3. 在队列中自旋,尝试获取一次资源(前提:队头+ tryAcquire()成功),每次失败都会更改线程状态为waiting。自旋时会看看前驱有没有失效的节点(即不再请求资源的),如果有就插队到最前面并把前面无效节点清理掉便于gc
  4. waiting状态中不响应中断,获取资源后才会补一个自我中断selfInterrupt (调用Thread.currentThread().interrupt())

释放资源release(int)

  1. 尝试释放,成功则处理后续动作,失败直接返回false
  2. 唤醒(unpark)等待队列的下一个线程。如果当前节点没找到后继,则从队尾tail从后往前找。

共享模式获取资源acquireShared(int)

除了抽象方法tryAcquireShared()以外,基本和acquire(int)一致。
在等待队列中获取资源后,会调用独有的setHeadAndPropagate()方法,将这个节点设为头结点的同时,检查后续节点是否可以获取资源。

共享模式释放资源releaseShared()

和release(int)区别在于,唤醒后继时,不要求当前线程节点状态为0。举例:当前线程A原先拥有5个资源,释放1个,后继的等待线程B刚好需要1个,那么此时A、B就可以并行了。

未实现的方法

为了便于使用AQS的类更加个性化,AQS有一下方法直接抛UnsupportedOperationException。

  • isHeldExclusively()
  • tryAcquire()
  • tryRelease()
  • tryAcquireShared()
  • tryReleaseShared()
    不写成abstract方法的原因是,避免强迫不需要对应方法的类实现这些方法。比如要写一个独占的锁,那么就不需要实现共享模式的方法。

AQS小结

读完源码总结一下,AQS是一个维护资源和请求资源的线程之间的关系的队列。对于资源(有序或无序的)获取和释放已经提取成了线程的出入队方法,这个队列同时维护上线程的自旋状态和管理线程间的睡眠唤醒。

应用

本节可以看作为《JAVA并发变成实战》14.6的引申。

ReentrantLock

用内部类Sync实现AQS,Sync实现ReentrantLock的行为。Sync又有FairSync和UnfairSync两种实现。FairSync,lock对应aquire(1);UnfairSync,lock先CAS试着获取一次,不行再aquire(1)。
实际上,ReentrantLock的公平/非公平锁只在首次lock时有区别,入队后唤醒仍是按顺序的。可以参考reentrantLock公平锁和非公平锁源码解析
Sync只实现了独占模式。

注意:CyclicBarrier直接用了ReentrantLock,没有直接用AQS。

Semaphore

和ReentrantLock类似,Semaphore也有一个内部类Sync,但相反的是这个Sync只实现了共享模式的acquire()/release()。
Semaphore在acquire()/release()时会计算资源余量并设置,其中unfair模式下的acquire会无条件自旋CAS,fair模式下只有在AQS里不存在排队中的后继的情况下才会CAS,否则自旋。

CountDownLatch

同样有一个内部类Sync,但是不再区分fair/unfair,并且是共享模式的。
await()调用的是acquireSharedInterruptibly(),自然也存在自旋的可能,只是编程时一般不这么用。countDown()时释放一个资源继续在releaseShared()里自旋直到全部释放。

FutureTask

新版的FutureTask已经重写,不再使用AQS,这里就不再提了。

ReentrantReadWriteLock

可重入读写锁,涉及到锁升级,这里没有研究的很透彻,有兴趣可以自行了解。
注意到读锁和写锁是共用同一个Sync的。

6 JMM到底是个啥?

The Java memory model specifies how the Java virtual machine works with the computer's memory (RAM)。
—— Java Memory Model
虽然被冠以”模型“,JMM实际上是定义JVM如何与计算机内存协同工作的规范,也可以理解为__指令__与其操作的__数据__的行为。这样,自然而然地引入了指令重排序、变量更改的可见性的探讨。
JMM定义了一个偏序关系,称之为happens-before。不满足happens-before的两个操作可以由JVM进行重排序。

6.1 什么是偏序关系

假设 R 是集合 A 上的关系,如果R是自反的、反对称的和传递的,则称 R 是 A 上的一个偏序。偏序关系
那么,自反的、反对称的和传递的,又是什么?下面粘贴了百度百科相关词条:

  • 自反关系:设 R是 A上的一个二元关系,若对于 A中的每一个元素 a, (a,a)都属于 R,则称 R为自反关系。
  • 反对称关系:集合 A 上的二元关系 R 是反对称的,当且仅当对于X里的任意元素a, b,若a R-关系于 b 且 b R-关系于 a,则a=b。
  • 传递关系:令R是A上的二元关系,对于A中任意的 ,若 ,且 ,则 ,则称R具有传递性(或称R是传递关系)。

上面的反对称关系稍微不好理解,转换成逆否命题就好理解了:若a!=b,那么R中不能同存在aRb和bRa。

6.2 偏序关系和JMM

将R作为两个操作间的关系,集合A是所有操作的集合,那么就可以理解JMM为什么实际上是一套偏序关系了。

6.3 happens-before规则

这部分的说明很多文章都是有差异,比如锁原则,JLS(Java Language Specification,Java语言规范)特指的是监视器锁,只不过显式锁和内置锁有相同的内存语义而已。这里直接摘录原文并配上说明。原文见Chapter 17. Threads and Locks

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

If an action x synchronizes-with a following action y, then we also have hb(x, y).

If hb(x, y) and hb(y, z), then hb(x, z).

The wait methods of class Object (§17.2.1) have lock and unlock actions associated with them; their happens-before relationships are defined by these associated actions.

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

For example, the write of a default value to every field of an object constructed by a thread need not happen before the beginning of that thread, as long as no read ever observes that fact.

More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

The happens-before relation defines when data races take place.

A set of synchronization edges, S, is sufficient if it is the minimal set such that the transitive closure of S with the program order determines all of the happens-before edges in the execution. This set is unique.

It follows from the above definitions that:

An unlock on a monitor happens-before every subsequent lock on that monitor.

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

A call to start() on a thread happens-before any actions in the started thread.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

The default initialization of any object happens-before any other actions (other than default-writes) of a program.

试着翻译一下各项规则:
先定义hb(x, y)表示操作x和操作y的happens-before关系。

  1. 同一个线程的操作x, y,代码中顺序为x, y,那么hb(x, y)
  2. 对象构造方法要早于终结方法完成
  3. 如果x synchronizes-with y那么hb(x,y)
  4. 传递性,hb(x, y) 且hb(y,z)则hb(x,z)
  5. 同一个监视器锁解锁需要hb所有加锁(注:该规则扩展到显式锁)
  6. volatile的读hb所有写(该规则扩展到原子操作)
  7. 线程start() hb所有它的启动后的任何动作
  8. 线程中所有操作hb 对它的join()
  9. 对象默认构造器hb对它的读写

synchronizes-with又是啥?查阅了一下,表示”这个关系表示一个行为在发生时,它首先把要操作的那些对象同主存同步完毕之后才继续执行“。参考JMM(Java内存模型)中的核心概念
JLS上对happens-before的解释翻译过来还是不太好理解,《Java并发编程实战》的解释和Happens-beofre 先行发生原则(JVM 规范)一样,可以参考下。

最后可以发现,JMM只是一套规则,并没有提到具体的实现,程序员知道Java有这一重保证即可。

7. 短篇话题整理总结

7.1 ThreadLocal的用法总结

应用场景:在多线程下替代类的静态变量(static),在多线程环境进行单个 的数据隔离。

为什么推荐使用static修饰ThreadLocal?

这时才能保证"一个线程,一个ThreadLocal",否则便成了“一个线程,(多个对象实例时)多个ThreadLocal”。
可能会有内存泄漏:ThreadLocalMap的Entry是弱引用,但指向的对象value不是,如果Entry被回收,value还在。解法是手动remove掉。
(本节参考了《Java并发编程实战》)

引申话题:gc时ThreadLocal的弱引用被回收,是否会造成程序bug?

答案是可以避免。
先回顾下各种引用 Java:强引用,软引用,弱引用和虚引用
然后再结合场景分析JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?

7.2 CountDownLatch和CyclicBarrier区别

https://blog.csdn.net/tolcf/article/details/50925145
CountDownLatch的子任务调用countDown后会继续执行直至该线程结束。
CyclicBarrier的子任务await时会暂停执行;可重复使用,即await的数目达到设置的值时,唤醒所有await的线程进行下一轮。

7.3 ReentrantLock用了CAS但为什么不是乐观锁?

https://blog.csdn.net/qq_35688140/article/details/101223701
我的看法:因为仍有可能造成阻塞,而乐观锁更新失败则会直接返回(CAS允许自旋)。
换一个角度,悲观锁是预先做最坏的设想——一定会有其他任务并发,那么就先占好坑再更新;乐观锁则是认为不一定有并发,更新时判断再是否有问题。这样看来ReentrantLock从使用方式上来说是悲观锁。

7.4 双重检查加锁

public classDoubleCheckedLocking{ //1
      private static Instance instance; //2
      public staticI nstance getInstance(){ //3
            if(instance==null){ //4:第一次检查
                  synchronized(DoubleCheckedLocking.class){ //5:加锁
                        if(instance==null) //6:第二次检查
                              instance=newInstance(); //7:问题的根源出在这里
                  } //8
            }//9
            return instance;
      }
}

问题

一个线程看到另一个线程初始化该类的部分构造的对象,即以上代码注释第4处这里读到非null但未完全初始化

原因

注释第7处,创建对象实例的三步指令1.分配内存空间2.初始化3.引用指向分配的地址,2和3可能重排序

解决

方案1,给instance加violatile
方案2,使用占位类,在类初始化时初始化对象,如下

public class InstanceFactory {
      private static class InstanceHolder{
            public static Instance instance= newInstance();
      }
      public static Instance getInstance() {
            return InstanceHolder.instance;  //这里将导致InstanceHolder类被初始化
      }
}

7.5 FutureTask

FutureTask是Future的实现类,可以使用Future来接收线程池的submit()方法,也可以直接用FutureTask封装任务,作为submit()的参数。具体的用法可以参考Java并发编程:Callable、Future和FutureTask
新版的FutureTask不再使用AQS。
FutureTask设置了当前工作线程,对于其任务维护了一个内部状态转换状态机,通过CAS做状态判断和转换。
当其他线程来get()时,如果任务未完成则放入等待队列,自旋直到取到结果(for循环+LockSupport.park()),否则直接取结果。
具体实现原理可以参考《线程池系列一》-FutureTask原理讲解与源码剖析

7.6 JDK1.6锁优化之轻量级锁和偏向锁

实际上二者是有联系的,都是基于mark word实现。这个转换关系可以用《深入理解Java虚拟机》第十三章的插图表现

但是这个图没有体现轻量级锁释放后,仍可恢复为可偏向的。

7.7 问题排查三板斧

  1. top查看内存占用率,-H可以看线程(不会完整展示),-p [pid]看指定进程的线程
    注意:linux线程和进程id都是在pid这一列展示的。
  2. pstack跟踪进程栈,strace查看进程的系统操作。多次执行pstack来观察进程是不是总是处于某种上下文中。
  3. jps直接获取java进程id,jstat看java进程情况。jstate可用不同的参数来查看不同纬度的信息:类加载情况、gc统计、堆内存统计、新生代/老年代内存统计等,具体可以参考【JVM】jstat命令详解---JVM的统计监测工具
  4. jstack打印java线程堆栈,和pstack展示方式很像,是java纬度的
  5. jmap打印java内存情况,-dump可以生成dump文件
  6. 分析dump文件,如MAT

7.8 Thread.join()原理

根据源码可以发现,join()实质是判断对应线程是否还在存活,如果还在存活会一直空转(指定了超时时间则只空转这么久)。
再加上注释:线程终结时会notifyAll(),唤醒在join()中调用wait()的等待中的线程。
同时,这里是需要synchronized的。

public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {  // isAlive是native方法
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

只看源码,是有可能在调用join()前线程就完结的。

7.9 Future和FutureTask

  • Future是一个(将来的)执行结果的抽象封装,可以通过接口判断是否执行完成、取得执行结果。持有Future的引用,在编码过程中会清晰一些,不需要关心具体执行的对象(如线程池)的状态,也不需要使用具体执行对象的API来获取结果。
  • FutureTask是RunnableFuture的实现,RunnableFuture继承了Future和Runnable,封装了执行内容(一个Callable)、执行状态、执行结果,需要手动启动。
  • CompletableFuture是JDK1.8新增的,用以支持函数式编程下stream并发处理流式数据的场景,内部默认使用ForkJoinPool。可以阅读实践:使用了CompletableFuture之后,程序性能提升了三倍的示例代码及CompletableFuture基本用法

关于ForkJoinPool,又可以单独写文章了,请自行了解。

8. LeetCode多线程习题

原题目和详解参考Concurrency - 力扣

1114.按序打印

按照指定次序完成一系列动作,可以看做是buffer为1的1对1生产者消费者模型。

1115.交替打印FooBar

交替执行(不完全是生产者-消费者模型)某些动作。
可用的解法:

  • synchronized
  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • Lock

1116.打印零与奇偶数:0102...

和1114类似

1188. 设计有限阻塞队列

注意: 使用synchronize解法时,wait()应置于while中循环判断.
如果只用if,唤醒后不再次判断dequeue可能NPE
本题可以加深理解为什么要用while

1195. 交替打印字符串

根据AC的解法推断, 每个线程只调用对应方法一次,因此需要在方法内部循环
不推荐只用synchronized,四个线程按顺序打印, 如果使用单一的锁很容易饥饿导致超时

推荐解法:
AtomicInteger无锁解法
CylicBarrier高效解法
Semaphore加锁

1279. 红绿灯路口

题目难懂,暗含条件:车来时红绿灯不是绿的,则强制变绿通过。红绿灯本身的时间没有严格控制

延伸阅读

什么是分布式锁
一文了解分布式锁

9. 未展开的话题

并发研究之CPU缓存一致性协议(MESI)
线程池原理(四):ScheduledThreadPoolExecutor
一半是天使一半是魔鬼的Unsafe类详解 —— unsafe类都有什么?用偏移量直接访问、线程操作、内存管理和内存屏障、CAS

10. 其他参考

Java并发高频面试题

11. TODO 待学习

https://www.cnblogs.com/skywang12345/p/3498483.html#p1
https://www.cnblogs.com/skywang12345/p/3498497.html


作者:五岳
出处:http://www.cnblogs.com/wuyuegb2312
对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 
posted @ 2020-06-12 02:14  五岳  阅读(494)  评论(0编辑  收藏  举报
回到顶部