【Java并发编程的艺术】学习笔记 juc

来源:《Java并发编程的艺术》

1.并发编程的挑战

(1)上下文切换
如何减少:无锁并发编程、CAS算法、使用最少线程、协程(单线程里实现多任务的调度,维持多个任务间的切换)
(2)死锁
如何避免:

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

(3)资源限制
如何解决:集群、连接池

2.Java并发机制的底层实现原理

(1)volatile

volatile如何保证可见性?
x86处理器:写操作时lock前缀指令。做了两件事:1)将当前处理器缓存行的数据写回到系统内存。2)在其他CPU里缓存了该内存地址的数据无效。
多处理下:缓存一致性协议,每个处理器嗅探总线上的数据检查是否过期。
目前的处理器:lock#信号不锁总线,锁缓存并写回内存,缓存一致性机制保证修改的原子性,称为“缓存锁定”。IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议维护缓存一致性。

(2)synchronized

  1. 锁的对象?
    静态同步方法:类锁
    普通方法:对象锁
    同步方法块:锁的可能是类可能是对象。锁的是Synchonized括号里配置的对象。
  2. 在JVM里的实现原理?
    代码块同步是使用monitorenter和monitorexit指令实现的,任意一个对象有一个monitor相关联,monitor被持有后,处于锁定状态。
  3. Java对象头?
    Java对象头:MarkWord、对象类型指针、[数值长度]
    MarkWord:
    默认存储对象的HashCode、分代年龄和锁标记位(是否是偏向锁、2bit锁标志);
    偏向锁状态时存储的是线程id、分代年龄、锁标记;
    轻量级锁状态时存储的是指向栈中锁记录的指针;
    重量级锁状态时存储的是指向重量级锁的指针。
  4. 偏向锁
    基本流程:
    访问同步块并且获取锁后,在对象头和栈帧中的锁记录里存储锁偏向的线程ID。
    若对象头的Mark Word里有线程ID,表示已获得锁;若没有,测试是否是偏向锁;
    若是偏向锁,cas将对象头的偏向锁指向当前线程;若不是偏向锁,cas竞争。
    锁撤销
    等到竞争才释放锁;等待全局安全点才撤销锁。暂停拥有偏向锁的线程,若线程活着,该线程继续执行;若不活动,对象头设置为无锁状态。
  5. 轻量级锁
    加锁:1.创建锁记录2.将对象头的MarkWord复制到当前线程的栈桢的锁记录3.cas将对象头中的Mark Word替换为指向锁记录的指针。4.成功就获得锁,失败就自旋获取锁
    解锁:使用原子的CAS操作将Displaced Mark Word替换回到对象,成功表示无竞争,失败膨胀成重量级锁
  6. 锁对比
    偏向锁:加锁解锁不需要额外消耗(优点);存在锁竞争锁撤销(缺点);适用于只有一个线程访问的场景(适用场景)
    轻量级锁:竞争的线程不会阻塞(优点);自旋消耗CPU(缺点);追求响应时间,速度快(适用场景)
    重量级锁:线程竞争不自旋(优点);线程阻塞,响应时间慢(缺点);追求吞吐量,时间长(适用场景)

(3)原子操作

处理器保证原子性:总线锁、缓存锁定+缓存一致性机制
Java实现原子操作:循环CAS、锁机制
CAS实现原子操作的问题:

  1. ABA问题,JDK1.5之后通过AtomicStampedReference解决(检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,)
  2. 循环时间长开销大。可以通过pause指令解决。
  3. 只能保证一个共享变量的原子操作。JDK1.5之后提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3.Java内存模型

(1)基础

1.并发编程的两个问题:线程间如何通信(共享内存和消息传递)、线程间如何同步。
Java并发采用共享内存模型, 线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
2.JMM(Java内存模型):线程间共享变量存在主内存;每个线程都有一个私有的本地内存,存共享变量的副本,是抽象概念,涵盖了缓存、寄存器、写缓冲区等。
3.重排序
为了提高性能,重排序:编译器、指令级、内存系统重排序。后两个属于处理器重排序。
JMM处理处理器重排序时会通过内存屏障禁止特定类型的处理器重排序,为程序员提供一致的内存可见性。
4.内存屏障
JMM把内存屏障指令分为4类:LoadLoad,StoreStore,LoadStore,StoreLoad。
StoreLoad是全能型屏障,具有其他3个的效果,但是开销昂贵,需要把写缓冲区中所有数据刷新到内存中。
5.happens-before
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系(不意味着时间前后)。
与程序员相关的happens-before规则:程序顺序规则、监视器锁规则、volatile变量规则、传递性。
其他happens-before:start()规则、join()规则

(2)重排序

1.as-if-serial
不管怎么重排序,单线程程序的执行结果不能改变。
2.为了遵守as-if-serial:编译器和处理器不会对存在数据依赖关系的操作做重排序。
3.控制依赖影响并行度,使用猜测执行来控制。

(3)volatile的内存语义

1.volatile具有可见性,单个volatile的读/写原子性
2.内存语义:
volatile的写-读与锁的释放-获取有相同的内存效果
volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
3.volatile内存语义的实现
JMM针对编译器制定的volatile 重排序 规则表:
(1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
(2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
(3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现重排序,保守策略下插入 内存屏障
(1)在每个volatile写操作的前面插入一个StoreStore屏障。
(2)在每个volatile写操作的后面插入一个StoreLoad屏障。
(3)在每个volatile读操作的后面插入一个LoadLoad屏障。
(4)在每个volatile读操作的后面插入一个LoadStore屏障。

(4)锁的内存语义

1.内存语义:
(1)线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
(2)线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
(3)线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
2.锁内存语义的实现
Lock的公平锁:在获取锁时首先读volatile变量state,在释放锁的最后写volatile变量state,
Lock的非公平锁:CAS,同时具有有volatile读和volatile写的内存语义。
3.concurrent包的实现
首先,声明共享变量为volatile。
然后,使用CAS的原子条件更新来实现线程之间的同步。
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

(5)final域的内存语义

1.final域 遵守的两个 重排序规则:
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
2.写final域的重排序规则
禁止把final域的写重排序到构造函数之外
1)JMM禁止编译器把final域的写重排序到构造函数之外。
2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
可以保证:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
3.读final域的重排序规则
禁止处理器重排序初次读对象引用与初次读该对象包含的final域。编译器会在读final域操作的前面插入一个LoadLoad屏障。
可以保证:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
4.对于引用类型的final域
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
5.在X86处理器中,final域的读/写不会插入任何内存屏障。

(5)happens-before

1.JMM遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。
2.as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
3.as-if-serial语义和happens-before的目的:为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

(6)双重检查

volatile的作用:禁止重排序(初始化和引用两个操作)

4.Java并发编程基础

(1)线程简介

在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

  1. 为什么要使用多线程?
    多核处理器提高程序执行效率、更快的响应时间
  2. 线程6种状态?
    初始、运行、阻塞、等待、超时等待、终止
  3. 守护线程(Daemon)
    Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,不能依靠finally块中的内容来确保执行关闭或清理资源。

(2)启动和终止线程

  1. 启动线程:线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
  2. 中断线程:线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
  3. suspend()、resume()和stop()不适用:suspend()容易引发死锁问题,stop方法不会保证线程正常释放。

(3)线程间通信

  1. volatile和synchronized关键字
    volatile可以让其他线程感知变化。synchronized关键字,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
  2. wait/notify机制
    依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。
  3. 管道的输入/输出流
    管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
  4. Thread.join()
    如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
  5. ThreadLocal的使用
    ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

5.Java中的锁

(1)Lock接口

Synchronized不具备的主要特性:尝试非阻塞获取锁、获取到锁的线程可以响应中断、超时获取锁

(2)队列同步器(AQS)

AQS:是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

  1. 设计基于模板方法。提供getState()、setState()、compareAndSetState()访问或修改同步状态。可重写的方法有try Acquire()、tryRelease()、tryAcquireShared()、tryReleaseShared()、isHeldExclusively()。
  2. 实现
    (1)同步队列:
    同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
    在这里插入图片描述
    设置尾结点的方法:
    当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
    设置首节点的方法:
    同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
    (2)独占式同步状态获取与释放
    通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感。
    aquire()方法的流程:
    a.调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态.
    b.如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部.
    c.调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态.
    d.如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
    在这里插入图片描述
    在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,为什么?
    1.头结点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。2.维护同步队列的FIFO原则。由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
    总结:
    在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
    (3)共享式同步状态获取与释放
    共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。
    释放:
    对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)
    方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
    (4)独占式超时获取同步状态

(3)重入锁

  1. 重入实现
    ReentrantLock的nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
  2. 公平锁与非公平锁实现区别
    ReentrantLock的tryAcquire方法法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断 ,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
    公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

(4)读写锁

  1. 概念
    之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
  2. 适用于读多于写的场景。
  3. 方法:readLock()方法和writeLock()
  4. 实现:读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。写锁是一个支持重进入的排它锁,如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。
  5. 锁降级
    锁降级是指当前拥有的写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
    为什么不支持锁升级?目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

(5)LockSupport

park():阻塞 unpark():唤醒

(6)Condition

  1. Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁,Condition依赖Lock对象。一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
  2. 实现
    等待队列:是一个单向的FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
    等待await():如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于以同步队列的首节点(获取了锁的节点)的线程构造新的节点加入到Condition的等待队列中。
    通知signal():调用该方法的前置条件是当前线程必须获取了锁,获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。

6.Java并发容器和框架

(1)ConcurrentHashMap

  1. 为什么使用ConcurrentHashMap?
    第一,在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。1.7 头插法会产生死循环、数据丢失、数据覆盖的问题,1.8 改为尾插法,解决死循环,但是还会有数据覆盖的问题。第二,HashTable使用synchronized来保证线程安全效率低。
  2. ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。
  3. 通过再散列定位Segment,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。
  4. get()操作如何不加锁?
    get方法里将要使用的共享变量都定义成volatile类型,定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。
  5. put()操作
    由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。
  6. 如何扩容?
    在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

(2)ConcurrentLinkedQueue

  1. 非阻塞的实现方式来实现线程安全队列ConcurrentLinkedQueue。ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。使用CAS算法实现。
  2. 入队:整个入队过程主要做两件事情:第一是定位出尾节点;第二是使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
  3. 出队:出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。流程:首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

(3)阻塞队列

  1. 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
    1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
    2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
  2. 实现:使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现。当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现,调用setBlocker先保存一下将要阻塞的线程,然后调用unsafe.park阻塞当前线程。

(4)Fork/join

  1. Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
  2. 工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
  3. 为什么要使用工作窃取?大任务划分子任务,子任务分到不用队列,A线程做完可以帮助B线程干活。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列 ,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
  4. 工作窃取优缺点:
    工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。
    工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。
  5. Fork/Join 框架的设计
    步骤一,分割任务;步骤二,执行任务合并结果

7.Java中原子操作类

原子更新基本类型:
AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。
原子更新数组:
AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素.
原子更新引用类型:
AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。
原子更新字段类:
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的引用类型。

8.Java中的并发工具类

  1. CountDownLatch
    CountDownLatch允许一个或多个线程等待其他线程完成操作。
  2. CyclicBarrier
    CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
    应用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
  3. CyclicBarrier和CountDownLatch的区别
    CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。
  4. Semaphore
    Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
    应用场景:流量控制,比如数据库连接
  5. Exchanger
    Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
    应用场景:遗传算法、校对工作

9. 线程池

处理流程:
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程射到硬件处理器上

10.实战

生产者消费者模型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值