美文网首页
线程协同

线程协同

作者: 9283856ddec1 | 来源:发表于2020-02-14 19:24 被阅读0次

多线程

什么是多线程?

CPU在线程之间快速切换,制造了线程并行运行的假象,好似它们在一个比实际CPU慢一些的CPU上同时运行。所有的线程都有完全一样的地址空间,共享同样的全局变量,一个线程可以读、写或甚至清除另一个线程的堆栈。

为什么要使用多线程?

使用多线程具有如下原因:

  1. 并行实体拥有共享同一地址空间和所有可用数据的能力,对于某些应用而言,这种能力是必需的,而这是多进程模型所无法表达的。
  2. 由于线程比进程更轻量级,使用它们比进程更容易创建和撤销。在许多系统中创建一个线程较创建一个进程快10~100倍。在有大量线程需要动态和快速修改时,具有这一特性是很有用的。
  3. 在多CPU系统中,多线程是有益的,在这样的系统中,真正的并行有了实现的可能。
    举一例如下图所示:
三个线程的字处理软件.png

第1个线程只是和用户交互。一旦在第1页中的语句被删除掉,交互线程就立即通知格式化线程对整本书重新进行处理。同时交互线程监控键盘和鼠标,并响应诸如滚动第1页之类的简单命令。
第2个线程在得到通知时进行文档的重新格式化。当文档格式化后通知交互线程将内容在屏幕上显示出来。
第3个线程周期性的将RAM中的内容写到磁盘中。许多字处理软件都会每隔若干分钟自动在磁盘上保存整个文档,用于避免程序崩溃、系统崩溃或电源故障而造成用户一整天的工作丢失的情况。

上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换

如何减少上下文切换
  • 无锁并发编程
    多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

  • CAS算法
    Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

  • 使用最少线程
    避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这 样会造成大量线程都处于等待状态。

  • 协程
    在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
这意味代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己实现任何措施来保证多线程的正确调用。

资源竞争控制

CAS操作
什么是CAS?

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS的操作过程

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 要写入的新值 B

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

当多个线程操作同一变量时每个线程会首先会读取到地址值对应的原值作为自己的期望值,然后进行操作,操作完以后在更新的时候他会判断现在地址值对应的值是否与自己的期望值相同,如果相同,就认为自己操作的过程中别人没有进行过操作。则将自己操作后的值更新到地址值对应的位置,如果不同则说明自己操作的过程中一定有其他的线程更新过数据,然后会把当前地址值对应的值返回给用户,用户拿到新值重新上次的操作。不断循环直到更新成功。

优缺点

CAS优点
在并发量不是很高时cas机制会提高效率。

CAS缺点
1)循环时间开销太大;
如果CAS长时间执行不成功,则会给CPU带来交大的执行开销。处理器提供一种pause指令可以缓解这部分问题,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
2)只能保证一个共享变量的原子操作
如果需要对多个共享变量进行同步,就得使用锁,或者将几个共享变量封装起来,使用CAS来进行同步。从Java1.5开始JDK提供AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
3)ABA问题
aba问题:内存值V=100;
threadA 将100,改为50;
threadB 将100,改为50;
threadC 将50,改为100;
场景:小牛取款,由于机器不太好使,多点了几次全款操作。后台threadA和threadB工作,此时threadA操作成功(100->50),threadB阻塞。正好牛妈打款50元给小牛(50->100),threadC执行成功,之后threadB运行了,又改为(100->50)。

解决aba问题:对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。

volatile关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个 变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是 可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特 性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要 从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问 的可见性。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

volatile是如何来保证可见性的呢?
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操 作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一 致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当 处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状 态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

用volatile变量修饰的共享变量进行写操作时,会在汇编阶段增加Lock指令,该指令在多核处理器下引发两件事件:
1)Lock前缀指令引起当前处理器缓存回写到系统内存;
2)一个处理器的缓存回写到内存会导致其他处理器里缓存了该内存地址的数据无效;

  • Java变量的读写通过几种原子操作完成工作内存和主内存的交互:
    1)lock:作用于主内存,把变量标识为线程独占状态。
    2)unlock:作用于主内存,解除独占状态。
    3)read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
    4)load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
    5)use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
    6)assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
    7)store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
    8)write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

  • volatile如何保持内存可见性?
    volatile的特殊规则就是:read、load、use动作必须连续出现,assign、store、write动作必须连续出现。
    如此保证:
    1> 每次读取前必须先从主内存刷新最新的值;
    2> 每次写入后必须立即同步回主内存当中;

注意点:错把volatile变量当做原子变量。
volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作,即:1) 基本类型的自增(如count++)等操作不是原子的。2) 对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的。

synchronized关键字

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程 在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。锁的具体形式表现如下:

形式 作用对象 作用范围
代码块(同步代码块) 括号里配置的对象 大括号{}括起来的代码
普通方法(同步方法) 当前实例对象 整个方法
静态方法 当前类的Class对象 整个静态方法

说明:1)synchronized -类:防止多个线程同时访问这个类中的synchronized static方法,对类的所有对象实例起作用。
2)synchronized-对象:某实例的范围,防止多个线程同时访问这个实例中的synchronized 方法。

  • 代码块
// 使用类
public void method() {
    synchronized(ClassName.class) {
         // todo
     }
}

public void method(SomeObject obj) {
    // obj 锁定的对象
    synchronized(obj) {
        // todo
    }
}
  • 普通方法
public synchronized void method() {
   // todo
}
  • 静态方法
public synchronized static void method() {
   // todo
}

Monitor
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用 时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获 取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED 状态。


对象、对象监视器、同步队列和执行现场之间的关系.png

说明:任意线程对Object(Object由synchronized保护)的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新 尝试对监视器的获取。

Java对象头

锁到底存在哪里,锁里面存储什么信息?synchronized用的锁是存在Java对象头里的,对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Mark Word:默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下表所示:

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据,如下表所示:


Mark Word的状态变化.png

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

无锁

无锁表示没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

HotSpot[1]的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同 一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并 获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS(compare and swap)操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
(1)偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正 在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着, 如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈 会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他 线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。


偏向锁的获取和撤销流程.png

(2)关闭偏向锁
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如 有必要可以使用JVM参数来关闭延迟:-XX:
BiasedLockingStartupDelay=0。如果你确定应用程 序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:- UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并 将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失 败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。


争夺锁导致的膨胀流程图.png
重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

三种锁的优缺点比较如下所示:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,与执行非同步方法相比仅存在纳秒级的差距 如果线程存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗cpu 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗cpu 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

Lock接口

Lock接口(以及相关实现类)用来实现锁功能,提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
Lock接口提供的synchronized不具备的主要特性:

特性 描述
尝试非阻塞的获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其它线程获取到,则成功获取并持有锁
能被中断的获取锁 与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

使用如下所示:

Lock lock = new ReentrantLock(); 
lock.lock(); 
try { 
} finally { 
    lock.unlock(); 
}

注意:
1> 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常, 异常抛出的同时,也会导致锁无故释放。
2> 在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。

ReentrantLock

重入锁ReentrantLock,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实 现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再 次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到 该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁 被释放时,计数自减,当计数等于0时表示锁已经成功释放。

ReentrantWriteLock

ReentrantWriteLock读写锁,在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升。

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知 机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并 进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。。改用读写锁实现上述功 能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写 操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用 等待通知机制的实现方式而言,变得简单明了。

示例代码:

public class Cache { 
    static Map<String, Object> map = new HashMap<String, Object>(); 
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 
    static Lock r = rwl.readLock(); 
    static Lock w = rwl.writeLock(); 
    // 获取一个key对应的value 
    public static final Object get(String key) { 
        r.lock(); 
        try { 
            return map.get(key); 
        } finally { 
            r.unlock(); 
        } 
    }

    // 设置key对应的value,并返回旧的value 
    public static final Object put(String key, Object value) { 
        w.lock(); 
        try { 
            return map.put(key, value); 
        } finally { 
            w.unlock(); 
        } 
    }

    // 清空所有的内容 
    public static final void clear() { 
        w.lock(); 
        try { 
            map.clear(); 
        } finally { 
            w.unlock(); 
        } 
    } 
}

各类锁介绍

Java主流锁.png
乐观锁 VS 悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。[2]

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

悲观锁和乐观锁的适用场景:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
自旋锁 VS 适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。我们就可以让后面那个请求锁的线程通过自旋的方式不放弃CPU的执行时间,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。其流程如下所示:

自旋与非自旋锁的流程.png

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自适应锁,自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

见synchronized介绍!

公平锁 VS 非公平锁

公平锁,是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁,是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

ReentrantLock内部实现公平锁与非公平锁。

可重入锁 VS 非可重入锁

可重入锁,又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。NonReentrantLock是非可重入锁。

独享锁 VS 共享锁

独享锁,也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁,是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

ReentrantReadWriteLock里面读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因此读锁和写锁是分离的。

线程间通信

等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个 过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模 式隔离了“做什么”(what)和“怎么做”(How),在功能层面上实现了解耦,体系结构上具备了良 好的伸缩性,但是在Java语言中如何实现类似的功能呢?

简单的办法是让消费者线程不断地循环检查变量是否符合预期,如下面代码所示,在 while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。

while (value != desire) { 
    Thread.sleep(1000); 
}
doSomething();

上面这段伪代码在条件不满足时就睡眠一段时间,这样做的目的是防止过快的“无效”尝 试,这种方式看似能够解实现所需的功能,但是却存在如下问题。
1)难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡得过久,就不能及时 发现条件已经变化,也就是及时性难以保证。
2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现 条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

Java通过内置的等待/通知机制能够很好地解决上面的矛盾并实现所需的功能。等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object上,方法和描述如表所示。

方法名称 描述
notify() 通知一个在对象上等待的线程,使其从wait()方法返回
notifyAll() 通知所有等待在该对象上的线程
wait() 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或中断才会返回,需要注意调用wait()方法后,会释放对象的锁
wait(long) 超时等待一段时间,如果没有通知就超时返回
wait(long, int) 对于超时时间更细粒度的控制,可以达到纳秒

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
示例代码如下:

public class WaitNotify { 
static boolean flag = true; 
    static Object lock = new Object(); 
    public static void main(String[] args) throws Exception { 
        Thread waitThread = new Thread(new Wait(), "WaitThread"); 
        waitThread.start(); 
        TimeUnit.SECONDS.sleep(1); 
        Thread notifyThread = new Thread(new Notify(), "NotifyThread"); 
        notifyThread.start(); 
    }

    static class Wait implements Runnable { 
        @Overide
        public void run() { 
            // 加锁,拥有lock的Monitor 
            synchronized (lock) { 
                // 当条件不满足时,继续wait,同时释放了lock的锁 
                while (flag) { 
                    try {
                        System.out.println(Thread.currentThread() + " flag is true. 
                         wait @ " + new SimpleDateFormat("HH:mm:ss").
                         format(new Date())); 
                        lock.wait(); 
                    } catch (InterruptedException e) { } 
                }
                // 条件满足时,完成工作
               System.out.println(Thread.currentThread() + " flag is false. 
                          running @ " + new SimpleDateFormat("HH:mm:ss").
                          format(new Date())); 
            } 
        } 
    }

    static class Notify implements Runnable { 
        public void run() { 
            // 加锁,拥有lock的Monitor 
            synchronized (lock) { 
                // 获取lock的锁,然后进行通知,通知时不会释放lock的锁, 
                // 直到当前线程释放lock后,WaitThread才能从wait方法中返回 
                System.out.println(Thread.currentThread() + " hold lock. notify 
                                         @ " + new SimpleDateFormat("HH:mm:ss").
                                         format(new Date())); 
                lock.notifyAll(); 
                flag = false; 
                SleepUtils.second(5); 
            }
            // 再次加锁 
           synchronized (lock) { 
               System.out.println(Thread.currentThread() + " hold lock again. 
                  sleep @ " + new SimpleDateFormat("HH:mm:ss").
                  format(new Date())); 
               SleepUtils.second(5); 
            } 
        } 
    } 
}

输出结果:
Thread[WaitThread,5,main] flag is true. wait @ 22:23:03 Thread[NotifyThread,5,main] hold lock. notify @ 22:23:04 Thread[NotifyThread,5,main] hold lock again. sleep @ 22:23:09 Thread[WaitThread,5,main] flag is false. running @ 22:23:14

注意细节:
1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。

WaitNotify.java运行过程.png

运行过程说明如下:
WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁 并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,
NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到 SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后, WaitThread再次获取到锁并从wait()方法返回继续执行。

Condition接口

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式。通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比项与结果如表所示:

对比项 Object Monitor Methods Condition
前置条件 获取对象的锁 调用Lock.lock()获取锁,调用Lock.newCondition()获取Condition对象
调用方式 直接调用,如obj.wait() 直接调用,如condition.await()
等待队列个数 一个 多个
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待 支持 支持
当前线程释放锁并进入等待状态到将来某个时刻 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

示例如下所示:

Lock lock = new ReentrantLock(); 
Condition condition = lock.newCondition(); 
public void conditionWait() throws InterruptedException { 
    lock.lock(); 
    try { 
        condition.await(); 
    } finally { 
        lock.unlock(); 
    } 
}

public void conditionSignal() throws InterruptedException { 
    lock.lock(); 
    try { 
        condition.signal(); 
    } finally { 
        lock.unlock(); 
    } 
}

如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会 释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程 才从await()方法返回,并且在返回前已经获取了锁。

LockSupport工具

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应 工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread) 方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停 车,而unpark方法则是指车辆启动离开,这些方法以及描述如表所示。

方法名称 描述
park() 阻塞当前线程,如果调用unpark方法或者当前线程被中断,才能从park方法返回
parkNanos(long nanos) 阻塞当前线程,最长不超过nanos纳秒,返回条件在park的基础上增加了超时返回
parkUntil(long deadline) 阻塞当前线程,直到deadline时间
unpark(Thred thread) 唤醒处于阻塞状态的线程thread

Java中的并发工具类

等待多线程完成 CountDownLatch

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。[3]

CountDownLatch的用法

CountDownLatch典型用法:1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法:2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足

CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

示例代码如下:

/**
 * 主线程等待子线程执行完成再执行
 */
public class CountdownLatchTest {
    public static int LATCH_SIZE = 5;
    public static void main(String[] args) {
        try{
            CountDownLatch latch = new CountDownLatch(LATCH_SIZE);
            // 新建5个任务
            for(int i=0; i<LATCH_SIZE; i++){
                new WorkerThread(latch).start();
             }
            
            System.out.println("主线程等待.");
            // 主线程等待多个线程任务的完成
            latch.await();
            System.out.println("主线程继续执行");
        } catch(InterruptedException e){
            e.printStackTrace();
        }
    }

    static class WorkerThread extends Thread{
        CountDownLatch mLatch;
        public WorkerThread(CountDownLatch latch){
            mLatch = latch;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep((long) (Math.random() * 10000));
                System.out.println(Thread.currentThread().getName() + "执行操作");
                // 将CountDownLatch的数值减1
                mLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:
主线程等待.
Thread-1 执行操作
Thread-3 执行操作
Thread-4 执行操作
Thread-2 执行操作
Thread-5 执行操作
主线程继续执行

同步屏蔽 CyclicBarrier

让所有线程都等待完成后才会继续下一步行动。

CyclicBarrier简介

CyclicBarrier的默认构造函数是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier已经到达了屏障,然后当前线程被阻塞。

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
  • parties 是参与线程的个数
  • Runnable 最后一个到达线程要做的任务
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, 
                                     BrokenBarrierException, TimeoutException
  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程
    await() 时被中断或者超时

实例代码如下:

public class CyclicBarrierDemo {

    static class TaskThread extends Thread {
        
        CyclicBarrier barrier;
        
        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");
                
                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        int threadNum = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
            
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 完成最后任务");
            }
        });
        
        for(int i = 0; i < threadNum; i++) {
            new TaskThread(barrier).start();
        }
    }   
}

打印结果:
Thread-1 到达栅栏 A
Thread-3 到达栅栏 A
Thread-0 到达栅栏 A
Thread-4 到达栅栏 A
Thread-2 到达栅栏 A
Thread-2 完成最后任务
Thread-2 冲破栅栏 A
Thread-1 冲破栅栏 A
Thread-3 冲破栅栏 A
Thread-4 冲破栅栏 A
Thread-0 冲破栅栏 A

Thread-4 到达栅栏 B
Thread-0 到达栅栏 B
Thread-3 到达栅栏 B
Thread-2 到达栅栏 B
Thread-1 到达栅栏 B
Thread-1 完成最后任务
Thread-1 冲破栅栏 B
Thread-0 冲破栅栏 B
Thread-4 冲破栅栏 B
Thread-2 冲破栅栏 B
Thread-3 冲破栅栏 B
从打印结果可以看出,所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

CyclicBarrier使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

控制并发线程数 Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以 保证合理的使用公共资源。
比如××马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须 在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶 入××马路,但是如果前一百辆中有5辆车已经离开了××马路,那么后面就允许有5辆车驶入马 路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完 成,看见红灯就表示线程被阻塞,不能执行。

应用场景

Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假 如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程 并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这 时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连 接。

示例代码如下:

public class StudySemaphore {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        
        //信号量,只允许 3个线程同时访问
        Semaphore semaphore = new Semaphore(3);

        for (int i=0;i<10;i++){
            final long num = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //获取许可
                        semaphore.acquire();
                        //执行
                        System.out.println("Accessing: " + num);
                        Thread.sleep(new Random().nextInt(5000)); // 模拟随机执行时长
                        //释放
                        semaphore.release();
                        System.out.println("Release..." + num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        executorService.shutdown();
    }
}

说明:虽然有10个线程在执行,但是只允许3个并发执行。

线程间交换数据 Exchanger

Exchanger用于进行线程间的数据交 换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也 执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产 出来的数据传递给对方。

应用场景

用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行 录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。
示例代码:

public class ExchangerTest { 
    private static final Exchanger<String>exgr = new Exchanger<String>(); 
    private static ExecutorServicethreadPool = Executors.newFixedThreadPool(2); 
    public static void main(String[] args) { 
        threadPool.execute(new Runnable() { 
        @Override 
        public void run() { 
            try { 
                String A = "银行流水A"; 
                // A录入银行流水数据 
                exgr.exchange(A);
            } catch (InterruptedException e) { } 
        } 
    }); 
    threadPool.execute(new Runnable() { 
        @Override 
        public void run() { 
            try { 
                String B = "银行流水B"; 
                // B录入银行流水数据 
                String A = exgr.exchange("B"); 
                System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
                                               + A + ",B录入是:" + B); 
            } catch (InterruptedException e) { } 
        } 
    }); 
    threadPool.shutdown(); 
    } 
}

说明:如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发 生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)设置最大等待时长。

Java并发容器

CopyOnWriteArrayList

Copy-On-Write是一种用于程序设计中的优化策略,其基本思路是:多个线程共享一个列表,当某个线程想要修改这个列表的元素时,会把列表中的元素Copy一份,然后进行修改,修改完成之后再将新的元素设置给这个列表,这是一种延时懒惰策略。
CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。在对CopyOnWrite容器进行并发的读不需要加锁,因为当前容器不会添加、移除任何元素。这种实现方式会在添加、移除元素时占用的内存空间翻一倍,因此这是以空间换时间。

ConcurrentHashMap

(1)线程不安全的HashMap
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表 形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获 取Entry。

(2)效率低下的HashTable
HashTable是HashMap的线程安全实现,容器内部使用synchronized来保护线程安全,但在线程竞争激烈的情况下,HashTable的效率非常低下。因为,当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法可能进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

(3)ConcurrentHashMap的锁分段技术可有效提升并发访问率
ConcurrentHashMap使用锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其它线程访问。有些方法需要跨段,如size()和containsValue(),它们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

BlockingQueue

阻塞队列是一个支持阻塞插入和阻塞移除操作的队列。
1)支持阻塞的插入方法:当队列满时,队列阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者
是从队列里取元素的线程。阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。

BlockingQueue重要的方法如下表所示:

函数名 作用
add(e) 把元素e加到BlockingQueue里,如果可以容纳,则返回true,否则抛出异常
offer(e) 把元素e加到BlockingQueue里,如果可以容纳,则返回true,否则返回false
offer(e,time,unit) 把元素e加到BlockingQueue里,如果可以容纳,则返回true,否则在等待指定的时间之后继续尝试添加,如果失败则返回false
put(e) 把元素e加到BlockingQueue里,如果不能容纳,则调用此方法的线程被阻塞直到有空间再继续添加
take(e) 取走BlockingQueue排在队首的对象,若BlockingQueue为空,则进入等待状态直到有新的对象被加入为止
poll(time,unit) 取出并移除队列中的队首元素,如果设定的阻塞时间内还没有获得数据,那么返回null
element() 获取队首元素,如果队列为空,那么抛出NoSuchElementException异常
peek() 获取队首元素,如果队列为空,那么返回null
remove() 获取并移除队首元素,如果队列为空,那么抛出NoSuchElementException异常

几种常见的队列结构:
ArrayBlockingQueue
数组实现的、线程安全的、有界的阻塞队列。内部通过“互斥锁”保护竞争资源,按照FIFO(先进先出)原则对元素进行排序,元素都是从队尾插入到队列,从头部开始返回。
默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照 阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平 的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问 队列。为了保证公平性,通常会降低吞吐量。

LinkedBlockingQueue
单向链表实现的有界阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。该队列按照FIFO(先进先出)排序元素,新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。

ConcurrentLinkedQueue
基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序。

LinkedBlockingDeque
双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。该阻塞队列支持线程安全,可以指定队列的容量(防止过度膨胀),如果不指定默认容量大小等于Integer.MAX_VALUE。

PriorityBlockingQueue
支持优先级的无界阻塞队列,默认情况下元素采取自然顺序 升序排列。可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化 PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证 同优先级元素的顺序。

参考资料:

[1] 现代操作系统,Andrew S. Tanenbaum
[2] java中的各种锁详细介绍,(https://www.cnblogs.com/jyroy/p/11365935.html)
[3] CountDownLatch的理解和使用(https://www.cnblogs.com/Lee_xy_z/p/10470181.html)
[4] CAS机制总结 (https://www.cnblogs.com/sunweiye/p/10972769.html)
[5] 深入理解Java虚拟机,胡志明
[6] Android开发进阶--从小工到专家,何红辉
[7] Java并发编程的艺术,方腾飞,魏鹏,程晓明

相关文章

  • java虚拟机读书笔记之线程调度

    java线程调度 线程调度主要有两种方式,协同式线程调度和抢占式线程调度。1、协同式: 线程的执行时间由线程本身...

  • 线程协同

    多线程 什么是多线程? CPU在线程之间快速切换,制造了线程并行运行的假象,好似它们在一个比实际CPU慢一些的CP...

  • Lua 协同程序(coroutine)

    什么是协同(coroutine)? Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局...

  • Java线程入门第三篇

    Java内存模型(jmm) Why:保证多线程正确协同工作 看图说明:文字解释:线程a和线程b通信过程,首先线程a...

  • Unity3D 中的协同程序Coroutine

    今天咱就说说协同程序coroutine。 什么是协同程序 先说说啥是协程:它的表现形式非常像线程,对线程有过接触的...

  • 线程通信

    线程通信的方式 要想实现线程之间的协同, 如: 线程先后执行顺序, 获取某个线程的执行结果等, 涉及线程之间的相互...

  • [Java]线程和锁

    0x00 线程调度 线程调度指的是系统为线程分配CPU使用权。分为两种: 协同式线程调度线程想用CPU多久就用多久...

  • Java线程通信-线程协调API

    线程通信 想要实现多个线程之间的协同,如:线程执行顺序,获取某个线程执行结果等,则需要使用线程之间互相通信。 文件...

  • 线程通信的方法

    线程通信的方法 程序在使用多线程执行任务时,经常需要线程之间协同工作。此时,我们需要了解线程通信的手段。 线程通信...

  • java并发之线程通信

      谈到并发我们就会想到多线程,要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。都涉...

网友评论

      本文标题:线程协同

      本文链接:https://www.haomeiwen.com/subject/mqocfhtx.html