并发编程的艺术

news/2024/10/21 15:48:32/

Volatile

作用

1. 保证共享变量的可见性(volatile修饰的变量进行操作对其他线程是可见的) 2. 插入读写屏障,防止指令重排序。

volatile 的底层实现

以下代码是对volatile修饰的instance变量赋值的汇编语言
java">volatile Singleton instance
instance = new Singleton();

  • 第二行代码是给instance变量进行赋值的操作。
  • 在赋值后,相比于普通变量,多执行了第五行lock······操作。
  • 在lock这一行代码中,有一个’addl $0x0 %esp’的操作,意思是esp寄存器的值+0。

Lock字段

在汇编语言中,Lock字段有两个作用: 1. 使修改的缓存立刻刷新回主存;2. 使其他处理器缓存中相同变量无效。
如何让其他缓存中的变量变得无效?
遵守缓存一致性协议:MESI,MESI使用了总线嗅探机制,当一个处理器修改了volatile变量,会通过数据总线将数据刷新回共享内存,其他处理器通过总线嗅探机制监听数据总线,如果发现自己缓存中对应内存地址被修改,就会使缓存无效,需要重新从共享内存中读取,防止其他缓存存储的是脏数据。

Volatile禁止指令重排序

+ store(存储): 作用于工作内存的变量,把工作内存中的变量值传送到主内存中,以便后续的write操作。 + write(写入): 作用于主存变量,把store操作从工作内存中得到的变量值放入主存变量中。

JMM规定:如果将一个变量从工作内存同步回主存中,就要按顺序执行store和write操作。

volatile空操作的作用:

  • 在lock这一行代码中,有一个’addl $0x0 %esp’的操作,意思是esp寄存器的值+0。这个空操作的作用就是将本处理器的缓存写入了内存,相当于执行了store和write操作;该空操作指令把修改同步到内存时,一位置所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。

Volatile的原子性

如何保证共享变量的修改是原子的?
1. 锁总线

在早期CPU中,当一个处理器通过数据总线将Volatile修饰的变量刷新回内存前会在总线上声言’**Lock#‘**信号,该信号会使其他处理器不能通过总线访问,修改共享内存,这样该处理器就可以独占共享内存。

  1. 锁缓存

锁总线的开销十分大,目前CPU采用粒度更小的锁缓存,锁定对应缓存行来保证原子性,不会发生修改由两个以上处理器缓存的变量。

复合修改
Volatile变量的单次修改是原子的,但是**复合操作是非原子的**。

如果线程A执行<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">count = count + 1;</font>(这是一个复合操作),即使count是volatile的,这个操作也不是原子的。因为<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">count = count + 1;</font>可以分解为读取count的值、将值加1、写回count三个步骤。在这三个步骤中,如果线程B也访问了count,就可能导致数据不一致的问题。

可以使用自旋+CAS来保证符合操作的原子性。

总线风暴

总线嗅探机制是导致总线风暴的主要原因,在多核处理器上,多个核心共用同一根数据总线进行数据交互,如果”缓存一致性流量“突然激增,必然会使得总线的处理能力收到影响,

缓存一致性流量:当主存的数据被多个核心缓存,某一个核心修改了该变量的缓存,通过数据总线将数据刷新回主存,其他核心通过总线嗅探机制使自己缓存无效,这种通过总线来进行通信则称之为–缓存一致性流量。

JDK7中Volatile的使用优化

在JDK7并发包中,有一个LinkedTransferQueue,通过追加字节的方式,来优化队列的入队出队操作,追加64字节,使队首队尾不在同一个缓存行,这样锁缓存的开销就会变小.

Synchronized

Synchronized的用法

静态同步方法
锁是 当前类的class对象
java">public static synchronized void test(){...
}
普通同步方法
锁是 当前实例对象
java">public synchronized void test(){...
}
同步代码块
锁是 括号内对象
java">synchronized(){...
}

Synchronized 锁升级

synchronized使用偏向锁在非多线程竞争下减小锁的开销,轻量锁使用线程栈上的数据结构避免了操作系统级别的锁,重锁则涉及到操作系统级的互斥锁.
偏向锁
偏向锁是乐观锁,实际上并没有真正意义上的加锁。
  • 加锁
    • 在线程进入同步代码块前,首先会查看锁对象头MarkWord信息,是否为当前线程ID
      • 是,则表示已经加锁成功,执行同步代码块。
      • 不是
        • 查看对象头信息中偏向锁标志位是否为1
          • 为1,通过CAS尝试置换MarkWord字段,将偏向锁指向当前线程
            • 置换成功,则加锁成功
            • 置换失败,则表示发生了竞争,偏向锁释放,锁升级为轻量锁
          • 不为1,将标志位改为1,通过CAS获取锁
  • 释放锁

持有偏向锁的线程不会主动释放锁,偏向锁是等待竞争才会释放,当发生了竞争,首先会暂停持有偏向锁的线程,等待全局安全点,检查持有偏向锁的线程是否存活?

- 如果存活:升级为轻量锁
- 不存活: 将偏向锁标志位设置为0,无锁状态。
  • 如何判断线程是否存活?

线程start的时候会将自己写入一个thread_list中,是一个链表;当线程不存活的时候,会将自己从thread_list中清理掉;因此只需要判断thread_list中是否含有线程即可。

轻量锁
+ **获取锁:**

复制锁对象的markword字段到当前线程的栈帧中成为displaced Markword,然后使用CAS将锁对象的markword字段替换为指向线程栈帧锁记录的指针,替换成功则枷锁成功。

替换失败,表示其他线程竞争锁;尝试使用自旋获取锁,当自旋达到阈值获取锁失败,锁升级为重锁。

替换成功,不升级。

重锁
重锁是依赖对象内部的monitor锁来实现,monitor锁依赖操作系统的MutexLock实现,所以重锁也称为互斥锁。

当锁升级为重锁,锁对象的markword字段存储的是: 指向mutex的指针。

同步代码块中 代码同步是通过monitorenter和monitorexit进出monitor实现;

而同步方法是通过方法修饰符的Acc_Synchronized字段来完成。

为什么重锁开销大?

操作系统会将争夺锁的线程阻塞,被阻塞的线程会消耗CPU,但是阻塞或者唤醒一个线程都需要操作系统从用户态到内核态相互转换,转换是十分耗费时间的。

CAS

进程

线程

线程是比进程更轻量级的调度执行单位,线程的引入可以将一个进程的资源分配和执行调度分开,各个线程共享进程资源,独立调度,线程是Java中处理器资源调度最基本单位。

内核线程的实现

内核线程就是直接由操作系统内核支持的线程,由内核完成线程的切换调度,并负责将线程的任务映射到各个处理器上。

程序一般不会直接使用内核线程,而是使用内核线程提供的轻量级进程。这种轻量级进程和内核线程之间是1:1的关系,称为一对一线程模型。

KLT为内核线程

Java线程的实现

主流商用Java虚拟机的线程模型采用的都是基于操作系统的原生线程模型来实现,即采用1:1的线程模型。

Java与协程

内核线程的局限
+ 今天Web应用业务量的增长,应对业务复杂化而不断进行服务的细分,减少单个服务的复杂度,增加复用性的同时,也不可避免增加了服务的数量,缩短了留给每一个服务响应的时间,也要求每个服务的提供者能够同时处理数量更庞大的请求,这样才不会出现请求由于某个服务被阻塞而出现等待。 + 1比1的内核线程模型,这种映射到操作系统上的线程缺陷是切换,调度成本高昂,系统容纳线程数量有限,但现在每个请求本身的执行时间变得很短,数量变得很多的前提下,用户线程切换的开销甚至可能会接近计算本身的开销,造成严重浪费。
协程的复苏
处理器要去执行线程 A 的程序代码时,并不是仅有代码程序就能跑得起来,程序是数据与代码的

组合体,代码执行时还必须要有上下文数据的支撑。而这里说的“上下文”,以程序员的角度来看,是

方法调用过程中的各种局部的变量与资源;以线程的角度来看,是方法的调用栈中存储的各类信息

内核线程的调度成本来自于用户态与内核态之间的状态转换,开销主要来自于响应中断,保护,恢复,执行现场的成本。将这些工作从操作系统交到程序员手里,那么就可以主动控制开销。

**协程:**协程拥有自己的上下文和栈,完全由用户控制,所以是用户态的轻量级线程;协程调度切换的时候将寄存器上下文和栈保存到其他地方,再切回来的时候恢复之前保存的寄存器上下文和栈,因此协程能够保留上一次调用时的状态。

协程的优点:

  • 轻量级:由用户态的程序库自己管理,不需要操作系统的介入
  • **非阻塞:**协程允许编写非阻塞代码,可以提高IO密集型应用的性能
  • **减少上下文切换开销:**协程由程序控制,因此可以减少上下文切换的开销,这对于高并发很重要

线程状态

1. NEW 初始状态,线程刚被创建但没有执行; 2. Runnable 运行状态,线程运行,在OS中是就绪和运行的统称; 3. Blocked 阻塞状态,线程阻塞于锁; 4. Waiting 等待状态,表示等待其他线程通知或者唤醒; 5. Time_Waiting 超时等待,指定时间自己返回; 6. Terminated 终止状态,线程已执行完毕,被销毁。

线程之间通信

**线程通信的本质就是"同步"和"数据共享".由于线程是共享进程的内存空间的,因此他们可以直接访问彼此的数据,数据共享有个前提,就是线程之间的协调与同步,确保多个线程能够安全且高效地共享资源和数据,而不会产生数据不一致问题.**
通过volatile和synchronized关键字
+ volatile可以保证多个线程缓存的同一个共享变量被修改时的可见性,但是过多的使用volatile修饰成员变量会使得程序的执行效率降低. + synchronized确保在同一时刻,只有一个线程处于同步方法或者同步代码块中;对于同步块的使用,通过monitorenter和monitorexit指令进出同步代码块;而对于同步方法是通过**方法修饰符的ACC_SYNCHRONIZED字段**来完成,无论通过哪种方法,本质都是通过对象监视器(monitor,任意一个对象都拥有自己地monitor)进行获取,而这个过程是排他的,同一时刻只有一个线程获取monitor;只有先获取对象监视器才能进入同步方法,如果有线程没有获取成功,就会阻塞在同步方法的入口中.

通过volatile来实现线程之间数据的共享,搭配synchronized关键字确保多线程的数据安全,以此来实现线程之间的通信.

等待通知模式
**等待通知模式是指,线程A调用了对象O的wait()方法进入等待状态,而另一线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作.这两个线程通过对象O完成交互.**
wait()释放锁,该线程进入Waiting状态,等待其他线程通知或者被中断才会返回
notify()通知一个在对象上Waiting的线程
notifyAll()通知所有Waiting在该对象上的线程
  • 线程的等待/通知通过同步块/同步方法的加锁来调用Object.wait(),Object.notify()等方法实现
  • 等待的线程会被放在锁对象的等待队列中
  • 当有线程执行notify()唤醒,就会将等待队列中的线程唤醒,进入同步队列变为Blocked状态等待CPU的调度

:::success

  1. 使用wait(),notify()…时,需要先获取锁
  2. 调用wait()方法后,线程状态有Running->Waiting状态并将当前线程放入锁对象的等待队列中
  3. notify或者notifyAll方法调用后等待线程依旧不会从wait方法返回,还需要调用notify或者notifyAll方法的线程释放锁
  4. 被通知的线程,从对象的等待队列移动到同步队列,状态由Waiting->Blocked
  5. 从wait方法返回的前提就是获得锁.

:::

等待通知的经典范式:

java">//等待方
synchronized(lockObject){while(判断条件){不符合lockObject.wait();}符合...
}
//通知方
synchronized(lockObject){改变条件;lockObject.notifyAll();
}
管道
**管道的输入输出流和普通IO或者网络IO不同之处在于,它主要是用于线程之间的数据传输,而传输的媒介为内存.**
  • 具体实现:
    • PipedOutputStream,PipedInputStream,PipedReader,PipedWriter,前两种面向字节,后两种面向字符
java">public class Main {public static void main(String[] args) throws IOException {PipedReader pr = new PipedReader();PipedWriter pw = new PipedWriter();//将输入输出流进行连接pr.connect(pw);Thread printThread = new Thread(new Print(pr),"PrintThread");printThread.start();int receice = 0;try{while((receice = System.in.read())!=-1){pw.write(receice);}}catch (IOException e){}}static class Print implements Runnable {PipedReader in;public Print(PipedReader pipedReader){this.in = pipedReader;}public void run(){int receive = 0;try{while((receive=in.read())!=-1){System.out.print((char)receive);}} catch (IOException e) {}}}
}

输出结果:

Thread.join()方法
当一个线程A在执行过程中调用B.join()方法,就会立即进入B线程中执行,A进入等待队列,等待B线程调用自身的B.notifyAll()唤醒才会返回A中执行.
ThreadLocal
ThreadLocalMap

线程中断

**线程中断是线程之间协作的主要方法之一,在程序中经常会有一些不达目的不退出的线程,当达到了目的我们可以通知这个线程可以结束了;当然,线程在不同状态下遇到中断会产生不同的响应,有的会抛出异常,有的没有变化,有的会结束线程.**
API
每一个线程都有一个状态位,标识当前线程对象是否被中断状态.
java">//isInterrupted是一个实例方法,
主要用于判断当前线程对象中断标志位是否被标记.
如果被标记返回true,表示当前线程已经被中断.
public boolean isInterrupted()
//interrupt是一个实例方法,
该方法用于设置当前线程对象的中断标识位。
public void interrupt()
//interrupted是一个静态的方法,
用于返回当前线程是否被中断并复位。
public static boolean interrupted()

**锁可以防止多个线程同时访问共享资源,在Lock出来之前,Java一直使用Synchronized关键字实现锁的功能**

Lock和Synchronized

synchronized和Lock的差异
1. 所处层面

:::info

  • synchronized是JVM层面的锁
  • Lock是一个类,属于应用层面的锁

:::

  1. 锁的效率

:::color1

  • 在少量竞争情况下,synchronized和lock效率差不多
  • 但是在竞争十分激烈的情况下,synchronized需要内核态到用户态的转换,所以效率极低

:::

  1. 加锁的方式

:::info

  • synchronized是隐式获取锁和释放锁
  • Lock需要手动获取锁和释放锁

相比于synchronized更加灵活;但是也容易造成死锁的现象发生

:::

  1. 等待与非阻塞

:::color1

  • synchronized同步块中A获取了锁,B线程会一直等待,不会释放CPU,直到A释放锁
  • Lock可以使用tryLock()尝试获取锁,是非阻塞式地获取锁,方法会立即返回,true表示获取成功,false表示获取失败

:::

  1. 公平与非公平

:::info

  • synchronized是非公平锁,肯能会导致线程饥饿
  • Lock可以设置为公平锁,通过线程等待时间来实现公平锁,会使用更多的CPU资源

:::

  1. 响应中断

:::color1

  • synchronized在等待锁的时候不可以被中断,synchronized获取到锁不响应中断
  • Lock可以响应中断,Lock的 lockInterruptibly 方法获取到锁的线程被中断的时候抛出异常并且释放锁

:::

  1. 实现调度的方式

:::info

  • synchronized使用Object的notify,wait,notifyAll实现调度
  • Lock通过condition监视器实现线程的调度

:::

  1. 实现原理

:::color1

  • synchronized底层是通过操作系统指令实现同步
  • Lock通过AQS和CAS操作实现线程同步

:::

Lock接口提供的synchronized不具备的特性
+ 非阻塞式获取锁,tryLock() + 超时获取锁: Lock(long),指定时间内没有获取锁则返回 + 能被中断获取锁: 获取到锁的线程能够响应中断,当被中断时,抛出中断异常,并且释放同步锁

AQS

AQS队列同步器,是用来构建锁或同步组件的基础框架,通过内置FIFO队列和int成员变量完成同步资源获取线程的排队和同步状态工作;

同步器是面向锁的实现者,简化锁的实现方式,屏蔽了线程排队,同步状态管理,等待唤醒等底层操作.锁是面向使用者,锁和同步器很好隔离了使用者和实现者锁关注的领域.

同步队列
同步器内部有一个FIFO同步队列(双向队列)来完成同步状态的管理;当多个线程竞争锁,有线程获取锁失败就会被构造成同步队列的Node节点加入同步队列,同时会阻塞该线程,当锁空闲的时候,就会唤醒同步队列的首节点Head,使其再次尝试获取同步状态.
java">同步器{队列首节点指针;队列尾节点指针;
}
同步队列节点{等待状态 waitStatus,前驱节点 prev,后继节点 next,等待队列的后继节点 nextWaiter,获取同步状态的线程 thread
}

获取同步状态失败的线程会被添加值同步队列的尾部:

当一个线程成功获取同步状态,其他线程无法获取同步状态,就会被构造成节点添加值同步队列的尾部,而加入队列尾部的线程有多个,需要保证线程安全,所以使用CAS设置成尾节点.

获取同步状态成功的线程添加为队列首节点:

因为获取同步状态的线程只有一个,所以设置首节点的时候不需要使用CAS,只需要将首节点设置为原首节点的后继节点,并断开原首节点的next指针即可替换成功.

独占式同步状态的获取与释放
在获取同步状态时,同步器维护了一个同步队列,同步状态获取成功的线程为队列的head,失败的线程通过CAS自旋加入到队列的尾部,线程进入Waiting状态;head释放同步状态后,使用LockSupport方法唤醒后继节点,或者非首节点被中断后,检查自己前驱节点是否为head,如果是,尝试获取同步状态,不是线程进入等待状态;

节点自旋获取同步状态的行为刚好可以满足队列的FIFO原则,也防止了过早通知发生同步状态争夺.

共享式同步状态的获取与释放
共享与独占最大的区别就是,共享 可以有多个线程同时获取到同步状态,通过资源数量控制同时获取同步状态的线程数量.

**释放:**共享式同步状态的head释放后,会唤醒后续处于等待状态的节点而不是head的后继节点,与独占式不同的是,释放同步状态(或资源数)必须确保线程安全,一般通过CAS循环保证,因为释放同步状态的操作会同时来自多个线程.

独占式曹氏获取同步状态
独占曹氏获取同步状态,区别在于未获取到同步状态的线程进入等待状态,当被唤醒的时候,需要先判断是否超时,如果超时直接返回,没超市再尝试获取同步状态.

ReentrantLock重入锁

ReentrantLock重入锁, 表示该锁能够支持一个线程对资源的重复加锁,该锁还能支持获取锁公平和非公平的选择,公平锁的效率没有非公平锁的效率高,公平锁能够减少线程线程饥饿的发生.
实现重进入
+ 线程再次获取锁,锁需要去识别请求锁的线程是否为当前占据锁的线程,如果是则再次获取成功. + 锁的最终释放: 线程重复n次获取锁,随后在第n次释放锁,其他线程能够获取到锁; 锁对于获取进行计数自增,锁释放计数自减,当计数器=0表示锁已经释放.
公平锁与非公平锁
公平锁需要满足请求的绝对时间顺序,也就是FIFO;

非公平锁只需要使用CAS设置同步状态成功则表示当前线程成功获取了锁; 公平锁在获取同步状态的时候判断条件多了一个hasQueuedPredecessors()方法,同步队列中当前节点是否有前驱节点的判断,如果返回true表示需要等待前驱节点线程获取锁并释放锁后才能继续获取锁.

非公平锁,关键在tryAcquire()方法,该方法有线尝试直接获取锁,不必进入同步队列.当获取锁失败才会进入同步队列中等待被唤醒,从而可以插队获取锁;在非公平锁中,刚释放锁的线程再次尝试获取锁的概率会更大,因为这样可以减少线程的切换,提高锁的效率,系统的吞吐量,但是插队会带来线程饥饿的问题.

读写锁

ReentrantLock和Mutex都是排他锁,同一时刻只允许一个线程获取同步状态.而读写锁可以允许多个读线程访问,写线程独占.

在没有读写锁的时候,Java使用等待通知机制+synchronized完成读写操作.Java并发包中读写锁的实现是通过ReentrantReadWriteLock实现.该包提供特性:

  • 公平性选择
  • 重入
  • 锁降级

ReadWriteLock常用方法:

  • getReadLockCount() 返回当前读锁被获取的次数(包括重入次数)
  • getReadHoldCount() 返回当前线程获取读锁的次数(保存在ThreadLocal)
  • getWriteHoldCount()返回写锁的获取次数
  • isWriteLocked()判断写锁是否被获取
写锁的获取与释放
**获取:**

写锁是一个排他锁,如果一个线程成功获取写锁,写锁状态+1;获取写锁的时候需要判断读锁是否存在,如果存在不能获取,进入Waiting状态,因为写锁需要对所有读锁可见,等待所有读锁释放,才可以获取写锁;当一个线程获取写锁的时候,写锁的状态不是0,并且获取写锁的线程不是自己,则当前线程进入Waiting状态.

释放:

写锁的释放与ReentrantLock类似,每次释放,写状态-1,直到写状态为0,释放成功,前面的写锁对于后面是可见的.

读锁的获取与释放
读写锁通过一个int32变量维护多个线程的读,和一个写线程的状态;

前16位记录写状态,后16位记录读状态;如果想获取写状态=int&0x0000FFFF,读状态=int>>>16(无符号右移),当写状态+1=int+1,当读状态+1=S+1<<16;

上图表示,写状态=3,一个线程获取了写锁并且重进入了2次;读状态=2表示这个线程同时获取读锁2次.

锁降级
当前线程释放写锁之前,需要先获取读锁,最后处理完数据再释放读锁,为了当获取写锁的线程在修改完数据后,需要用到修改后的数据,防止被其他线程修改;
java">public static void run(){writeLock.lock();try{atomicInteger.compareAndSet(atomicInteger.get(), atomicInteger.get()+1);readLock.lock();}finally {writeLock.unlock();}try{System.out.println(atomicInteger.get());}finally {readLock.unlock();}
}

LockSupport

Condition

在Java中,每一个Object都有一组监视器方法wait,notify,notifyAll,配合synchronized可以实现等待通知模式;Condition也提供类似监视器方法,依赖Lock对象相互配合

与Object监视器不同的是:

  • 依赖Lock对象,获取Lock锁,通过Lock.newCondition()获取监视器
  • 等待队列有多个
  • 调用方法不同,await(),signal()
  • 既可以设置不支持等待线程响应中断,也可以xiangying其他线程中断返回

使用方法:

一般都会将Condition作为成员变量;await() 当前线程进入等待状态;直到被唤醒,或者被其他线程中断;当线程从await()返回,表    示该线程已经获取到了锁;awaitUninterruptibly()不响应中断的等待;直到被唤醒;awaitNanos(long nanos)等待超时;awaitUntil(Date dealine)等待至指定时间;signal()唤醒一个在等待队列的线程;signalAll()唤醒所有在等待队列的线程;
实现分析
Condition是AbstractQueuedSynchronized的内部类,等待队列的节点同样是内部类;

等待队列:

每一个Condition都包含一个等待队列,同样是FIFO队列,也就是说有多个等待队列,而且Condition是Object同步器的内部类,每一个Condition实例都可以访问同步器方法;

等待队列的加入:

当线程成功获取到锁,才可以调用await()方法,该线程将会释放锁,并且构造成节点加入等待队列的尾部,加入尾部的时候不需要CAS保证,因为调用await()的前置条件是获取到锁,线程是安全的.释放锁后唤醒同步队列的后继节点;

通知唤醒:

signal()方法会唤醒等待队列中等待时间最长的节点,调用该方法的前置条件也是先获取到了锁,然后找到等待队列的首节点,将其加入到同步队列,然后使用LockSupport方法唤醒,进而调用同步器的acquireQueued()加入到获取同步状态的竞争.

成功获取到锁后从之前的await()方法返回;

signalAll()相当于对等待队列的所有节点均执行一次signal(),节点全部移动到同步队列当中;

实战(线程安全的消费生产)
```java public class 多个等待队列 {
private static final Lock lock = new ReentrantLock();
//生产者等待队列
private static final Condition condition1 = lock.newCondition();
//消费者等待队列
private static final Condition condition2 = lock.newCondition();
private static volatile AtomicInteger num =new AtomicInteger(10);public static void main(String[] args) {while(true){new Thread(() -> {runProducter();}).start();new Thread(() -> {runUser();}).start();}}//生产者
public static void runProducter(){lock.lock();try{while(num.get()>5){condition1.await();}System.out.println(Thread.currentThread().getName()+" 生产+1 ="+(num.incrementAndGet()));condition2.signalAll();}catch (Exception e){}finally {lock.unlock();}
}
//生产者
public static void runUser(){lock.lock();try{while(num.get()<1){condition2.await();}System.out.println(Thread.currentThread().getName()+" 消费+1 ="+(num.decrementAndGet()));condition1.signalAll();}catch (Exception e){}finally {lock.unlock();}
}

}


<h2 id="cQj4K">并发容器</h2>
<h3 id="v3Wqd">ConcurrentHashMap</h3>
<h4 id="a4Sjp">JDK1.7</h4>
1.7中ConcurrentHashMap是通过分段锁来保证容器的线程安全;首先将数据分为一段一段(这个段存储在segment数组中),为每一段分配一把锁,当一个线程占用锁访问段中的数据时,其他线程只能访问其他段的数据![](https://cdn.nlark.com/yuque/0/2024/png/42842095/1725433158554-51a0dbc2-46b0-4eb0-a511-b4856e7f91ad.png)Segment数组继承了ReentrantLock,所以Segment是一种可重入锁,默认大小为16,也就是同时支持16个线程并发写;<h4 id="kRxK3">JDK1.8</h4>
1.8取消了分段锁,采用Node+CAS+Synchronized.锁的粒度由一段变成了当前链表/红黑树,也就是说不发生Hash冲突也就不会产生并发问题,不会影响其他Node读写.<h4 id="MuAL1">不保证复合操作的原子性</h4>
复合操作是指由put,get组成的操作序列;例如,先判断一个键是否存在,根据结果进行插入和更新,这种操作过程可能会被其他线程打断,导致结果不符合预期.ConcurrnetHashMap提供了一些原子性的复合操作,如 putIfAbsent,compute,computeIfAbsent,computeIfPresent,merge等.这些方法都可以接收一个函数作为参数,根据给定的key和value来计算一个新的value,并且将其更新到map中.```java
// 线程 A
map.putIfAbsent(key, value);
// 线程 B
map.putIfAbsent(key, anotherValue);
或者:
// 线程 A
map.computeIfAbsent(key, k -> value);
// 线程 B
map.computeIfAbsent(key, k -> anotherValue);

很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 ConcurrentHashMap 的初衷。在使用 ConcurrentHashMap 的时候,尽量使用这些原子性的复合操作方法来保证原子性。

ConcurrentLinkedQueue

阻塞队列

+ 支持阻塞的插入方法: 添加队列的时候,如果队列已满,会阻塞添加元素的线程直到队列有空位. + 支持阻塞的移除方法: 当队列为空的时候,会阻塞获取元素的线程,等待队列变为非空.

Forkjoin

1. 分治 fork 2. 工作队列 3. 工作窃取 4. 结果合并join

并发工具

Semaphore信号量

Semaphore是用于控制同时访问特定资源变量的线程数量;通过内部维护一个原子计数器和提供线程安全的访问控制方法来保证资源的并发安全.

Semaphore提供两个主要的方法来控制资源的访问:

1. acquire() 用于请求资源,如果资源不可用会阻塞线程
2. release() 用于释放资源,使得其他被阻塞的线程可以继续执行

Exchange线程交换数据

线程池

线程池的工作流程
![](https://img-blog.csdnimg.cn/img_convert/0210534be7c6e0f5d39696f78f7b3ee7.png)
  1. 如果当前运行线程<CorePoolSize,则创建新的线程去执行任务(需要获取全局锁)
  2. 如果当前运行线程>=CorePoolSize,则将任务加入到任务队列BlockingQueue
  3. 如果任务队列已满,则创建新的线程去执行任务(获取全局锁)
  4. 如果当前运行线程>MaximumPoolSize,任务将会被拒绝,并调用rejectedExecution()

采取上述步骤的总体设计思路,是为了在执行execute()方法的时候,尽可能避免获取全局锁,当ThreadPoolExecutor完成预热之后(线程数量>=CorePoolSize),几乎所有的execute()方法调用都是执行步骤2,步骤2不需要获取全局锁.

线程池的核心参数
1. 最大线程数量 2. 核心线程数量 3. 线程存活时间 4. 线程存活时间单位 5. 线程工厂 6. 任务队列 7. 拒绝策略
线程池的核心线程数的设置

http://www.ppmy.cn/news/1540829.html

相关文章

vue后台管理系统从0到1(5)

文章目录 vue后台管理系统从0到1&#xff08;5&#xff09;完善侧边栏修改bug渲染header导航栏 vue后台管理系统从0到1&#xff08;5&#xff09; 接上一期&#xff0c;我们需要完善我们的侧边狼 完善侧边栏 我们在 element 组件中可以看见&#xff0c;这一个侧边栏是符合我们…

element plus e-table表格中使用多选,当翻页时已选中的数据丢失

摘要&#xff1a; 点击第一页选中两个&#xff0c;再选择第二页&#xff0c;选中&#xff0c;回到第一页&#xff0c;之前选中的要保留&#xff01; element ui table 解决办法&#xff1a; :row-key“getRowKeys” &#xff08;写在el-table中&#xff09; methods中声明 ge…

99. UE5 GAS RPG 被动技能实现

在这一篇&#xff0c;我们在之前打下的基础下&#xff0c;实现一下被动技能。 被动技能需要我们在技能栏上面选择升级解锁技能后&#xff0c;将其设置到技能栏&#xff0c;我们先增加被动技能使用的标签。 FGameplayTag Abilities_Passive_HaloOfProtection; //被动技能-守护光…

【微信小程序_17_生命周期】

摘要:本文介绍了小程序的生命周期,包括生命周期的定义、分类、生命周期函数等内容。生命周期分为应用生命周期和页面生命周期,生命周期函数由小程序框架提供,会按次序自动执行,开发人员可利用这些函数在特定时间点执行操作,如在页面加载时初始化数据。 微信小程序_17_生命…

【进阶OpenCV】 (21) --卷积神经网络实现人脸检测

文章目录 卷积神经网络实现人脸检测一、加载CNN人脸检测模型二、图像预处理三、绘制人脸矩形框 总结 卷积神经网络实现人脸检测 opencv可以直接通过readnet来读取神经网络。dlib也可以的。 任务&#xff1a;使用dlib库中的卷积神经网络&#xff08;CNN&#xff09;人脸检测模…

OpenGL、OpenCL 和 OpenAL 定义及用途

OpenGL 全称&#xff1a;Open Graphics Library&#xff0c;即开放图形库。是一种跨编程语言、跨平台的编程接口规格&#xff0c;用于二维和三维图形的绘制。它是一个功能强大、调用方便的底层图形库&#xff0c;提供了丰富的绘图函数&#xff0c;包括基本图形绘制、变换、光照…

【Lean 4 学习】用Lean 4证明自然数的平方差公式

引言 最近开始学习Lean 4来做数学证明&#xff0c;虽然挺有挑战&#xff0c;但是对于我这个30多岁的大叔来说有种刚学编程时候探索的乐趣hhh自然数平方差公式这个问题&#xff0c;是我刚学了平方和公式&#xff0c;想变变给自己练手用的&#xff0c;结果卡了我好久&#xff0c…

入侵及防护:7个迹象说明你的手机可能被入侵!

在现代社会中&#xff0c;手机已成为我们生活中不可或缺的一部分。然而&#xff0c;随着智能手机的普及&#xff0c;手机安全问题也日益严重。手机被入侵的风险不仅影响个人隐私&#xff0c;还可能导致财产损失。本文将为你介绍7个迹象&#xff0c;帮助你判断手机是否可能被入侵…