在计算机发明之前,人们处理大量的计算是通过人工处理的,耗费人力,成本很大而且错误较多。为了处理大量的数学计算问题,人们发明了计算机。
最初的计算机只能接受一些特定的指令,用户输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。显然这样效率低下,在很多时候,计算机都处在等待状态。
既然传统计算机那么慢,那么能不能把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机,计算机通过不断得读取指令进行相应的操作。
就这样,批处理操作系统诞生了。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
虽然批处理操作系统的诞生提高了任务处理的便捷性(省略了用户输入的时间),但是仍然存在一个很大的问题:
假如有两个任务A和B,需要读取大量的数据输入(I/O操作),而其实CPU只能处在等待状态,等任务A读取完数据再能继续进行,这样就白白浪费了CPU资源。于是人们就想,能否在任务A读取数据的过程中,让任务B去执行,当任务A读取完数据之后,暂停任务B,让任务A继续执行?
这时候又出现了几个问题:内存中始终都只有一个程序在运行,而想要解决上述问题,必然要在内存中装入多个程序,如何处理呢?多个程序使用的数据如何辨别?当一个程序暂停后,随后怎么恢复到它之前执行的状态呢?
这时候,人们就发明了进程,用一个进程对应一个程序,每个进程都对应一定的内存地址和内存空间,并且只能自己使用自己的内存空间,多个进程之间的内存互不共享,且进程之间彼此不打扰。
进程同时也保存了程序每时每刻的运行状态,为进程切换提供了如可能。
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的2状态进行恢复,接着继续执行。
并发是能够让操作系统从宏观上看起来同一时间段执行多个任务。 换句话说,进程让操作体统的并发成为了可能,至此出现多任务操作系统。
虽然并发从宏观上看是有多个任务在执行,但是实际上对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源,操作系统一般通过CPU时间片轮转来实现并发。
总的来说,并发就是在一段时间内多个进程轮流使用同一个 CPU,多个进程形成并发。

在同一时刻多个进程使用各自的 CPU,多个进程形成并行。并行需要多个 CPU 支持。

出现了进程之后,操作系统的性能(CPU利用率)得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们不满足,逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一个事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
举一个例子:对于监控系统这个进程来说,不仅要与服务器端进行通信获取图像数据并将图像信息显示在画面上,还要处理与用户的交互操作。如果在一个时刻该系统正在与服务器通信获取图像数据,而用户在监控系统上点击了一个按钮,那么系统只能等获取完图像后才能与用户进行交互操作。如果获取图像需要10s,用户就得等待10s。显然这样的系统,无法满足人们的需求。
为了让子任务可以分开执行,即上个例子说的,在与服务器通信获取图形数据的同时相应用户,为了处理这种情况,人们发明了线程,一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。在用户点击按钮的时候,可以暂停获取图像数据的线程,让出CPU资源,让UI线程获取CPU资源,响应用户的操作,响应完后再切换回来,获取图像数据的线程重新获取CPU资源。让用户感觉系统在同时做很多事,满足用户对实时性的要求。线程的出现是为了解决实时性的问题
总的来说,线程是进程的细分,通常,在实时性操作系统中,进程会被划分为多个可以独立运行的子任务,这些子任务被称为线程,多个线程配合完成一个进程的任务。
注意
一个进程包含多个线程,但是这些线程共享进程占有的内存地址空间和资源。进程是操作系统进行资源分配的基本单位(进程之间互不干扰),而线程是操作系统进行CPU调度的基本单位(线程间互相切换)。

假设 P 进程抢占 CPU 后开始执行,此时如果 P 进行正在进行获取网络资源的操作时,用户进行UI 操作,此时 P 进程不会响应 UI 操作。可以把 P 进程可以分为 Ta、Tb 两个线程。Ta 用于获取网络资源,Tb 用于响应 UI 操作。此时如果 Ta 正在执行获取网络资源时、用户进行 UI 操作,为了做到实时性,Ta 线程暂时挂起,Tb 抢占 CPU 资源,执行 UI 操作,UI 操作执行完成后让出CPU,Ta 抢占 CPU 资源继续执行请求网络资源。
总结
- 线程再一次提高了CPU的利用率
- 线程是包含在进程中,是对进程任务的细分,线程共享进程资源(内存资源等)
- 线程细分后称为 CPU 调度的基本单位。进程称为操作系统资源分配的基本单位。
- 根本区别:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的基本单位
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分。
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
在 Java 中实现线程的方式有 2 种,一种是继承 Thread,一种是实现 Runnable 接口。
如果一个进程没有任何线程,我们成为单线程应用程序;如果一个进程有多个线程存在,我们成为多线程应用程序。进程执行时一定会有一个主线程(main 线程)存在,主线程有能力创建其他线程。多个线程抢占 CPU,导致程序的运行轨迹不确定。多线程的运行结果也不确定。
线程开启我们需要用到了类,API中该类中定义了有关线程的一些方法,具体如下:
构造方法
- :分配一个新的线程对象。
- :分配一个指定名字的新的线程对象。
- :分配一个带有指定目标新的线程对象。
- :分配一个带有指定目标新的线程对象并指定名字。
常用方法
- :获取当前线程名称。
- :导致此线程开始执行; Java虚拟机调用此线程的run方法。
- :此线程要执行的任务在此处定义代码。
- :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
- :返回对当前正在执行的线程对象的引用。
继承 Thread 实现多线程,必须重写 run 方法,启动的时候调用的也是调用线程对象的start()方法来启动该线程,如果直接调用run()方法的话,相当于普通类的执行,此时相当于只有主线程在执行。

从结果我们可以看出,每一次抢占CPU资源的线程是不同的,多个线程轮流使用 CPU,谁先抢占到谁使用 CPU 并执行线程。所以执行结果不确定。
编码简单
线程类已经继承了Thread类了就无法再继承其他类了,功能不能通过其他类继承拓展,功能没有那么强大。
采用也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。

- 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现Runable或Callable线程任务对象。
- 其实Thread类本身也是实现了Runnable接口的。
- 唯一的遗憾是不能直接得到线程执行的结果!
实现多线程还有另一种方式,那就是实现接口,前面的两种方式都没办法拿到线程执行返回的结果,因为run()方法都是void修饰的。但是这种方式是可以拿到线程执行返回的结果。
步骤
- 定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。
- 重写线程任务类的call方法,这个方法可以直接返回执行的结果。
- 创建一个Callable的线程任务对象。
- 把Callable的线程任务对象包装成一个未来任务对象。
- 把未来任务对象包装成线程对象。
- 调用线程的start()方法启动线程
- 线程任务类只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现Runable或Callable线程任务对象。
- 能直接得到线程执行的结果!
- 唯一的遗憾就是编码比较复杂,写的代码会比较多。
需求:模拟售票窗口买票的过程,共有五张票


- 继承Thread类后,不能再继承其他类,而实现了Runnable接口后还可以继承其他类。
- 实现Runnable接口更方便共享资源,同一份资源,多个线程并发访问,如果多个线程需要访问共享资源,优先考虑Runnable方式,如果线程不访问共享资源,可以考虑继承Thread。
- Thread类本身也是实现类Runnable接口的。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免Java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池可以放入实现Runable或Callable类线程。
多线程访问共享资源的同时,存在一个十分严重的问题,那就是会导致共享资源数据错乱。
假设我们拿一种执行情况来分析


- 线程通过抢占CPU的方式工作,在执行过程中,随时可能CPU时间片的时间到了,然后被挂起,在程序的任何地方都有可能被切换出去
- 由于随时被挂起或者切换出CPU,导致访问共享资源会出现数据错乱,解决方法为加锁
我们可以设置线程的优先级调用,优先级越高 ,被 CPU 调动的可能性越大,但不一定是优先级越高就一定先执行。,有可能设置了最高的优先级但是确实最后调用。
强制执行(join方法)会导致其他线程阻塞,当线程执行完以后,其他线程阻塞原因消除,进入就绪状态
线程调用(sleep方法)方法,传入一个毫秒值,会导致当前线程进入阻塞状态,阻塞时间到了以后线程进入就绪状态,sleep方法会抛出一个编译时异常

- 线程休眠导致当前线程进入阻塞状态,休眠时间结束后,线程进入就绪状态,抢占CPU,抢到后继续运行
- 线程休眠过程中可以被中断,所以存在一个编译时异常:,外界程序中断该线程时,休眠时间提前结束,进入就绪状态,等待CPU调度执行。
当前线程礼让后,线程进入就绪状态。
stop表示强制停止一个线程,停止一个线程的风险较大,不建议使用,通过发送中断信号中断线程,线程就会在在那个时间点结束
中止正在运行的线程,该线程不会立即结束,而是继续执行,在适当的时机选择结合异常处理机制结束,异常处理机制可以保证线程继续执行,通过异常处理机制让一个线程正常结束。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中这个枚举中给出了六种线程状态:

用new关键字建立一个线程后,该对象就处于新生状态,处于新生状态的多线程有自己的内存空间,通过调用start()方法进行就绪状态。
处于就绪状态的线程具备了运行的条件,但是还没有分配到CPU,处于线程就绪队列,等待系统为其分配CPU,当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为"CPU调度",等待状态还有一个名字也叫作就绪状态。
在运行状态的线程执行自己的run方法中的代码,直到因为等待某资源而阻塞或者完成任务而死亡,如果在给定的时间内没有执行结束,就会被系统换下来回到等待执行的状态。
处于运行状态的线程在某种情况下,比如说执行了sleep(睡眠)方法,或者是等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态
在阻塞状态的的线程不会马上进入就绪队列,只有当引起阻塞状态的原因消除时,如睡眠时间已到或者等待的I/O设备空闲下来,线程便进入了就绪状态,重新进入到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。冻结状态、静止状态都是阻塞状态
死亡状态是线程生命周期中最后的一个状态。
引起线程死亡的原因有三种:
- 正常运行的线程执行完了他的全部工作。
- 线程被强制性地终止(stop方法)。
- 线程抛出了未捕获的异常。
在多线程环境下,什么时候会出现数据错乱的问题?
多个线程并发访问共享资源,并对共享资源进行破坏性操作(增删改)的时候,一定会出现数据错乱的问题
如何解决
在多线程环境下,如果对共享资源进行破坏性操作的时候,需要同步操作。
如果希望一系列操作(在代码中可以认为是很多句语句),要么都执行,要么都不执行,我们把这种操作叫做原子性操作,原则性操作可以认为是业务上不可分割的单元。
Java实现原子性操作的过程叫做同步操作,常见的有两种方式实现同步:
- 同步代码块
- 同步方法
- Lock锁
把原子性操作放到一个代码块中,就是同步代码块,使用关键字
改造之前的火车卖票的代码
原则上,锁对象建议使用共享资源,但是遵循以下两个点:
- 在实例方法中建议使用作为锁对象,此时正好是共享资源。
- 在静态方法中建议使用字节码作为锁对象。
中的 obj 称为同步监视器,同步代码块中的同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器,且同步监视器不能是基本数据类型,同时也不推荐使用包装类型(会有自动拆箱和装箱)
- 如果需要实现原子性操作,必须对共享资源加锁。
- 如果线程运行时,发现不是加锁的那个线程,那么此时会导致该线程阻塞,进入阻塞状态。
- 如果是需要对共享资源进行破坏性操作的时候,推荐使用实现接口会比较方便。
当原子性操作代码很长且需要重复调用的时候,可以考虑将同步代码块中的代码抽取出来变成同步方法。
同步方法中无需指定同步监视器,因为同步方法的监视器就是this,也就是对象本身。
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。
Lock锁也称同步锁,加锁与释放锁方法化了,他是显示的,需要我们手动加,方法如下:
- :加同步锁。
- :释放同步锁。
多个线程由于处在同一个进程,所以互相通信是比较容易的。
线程通信的核心方法:
- : 让当前线程进入到等待状态 此方法必须锁对象调用。
- : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用。
- : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用。
线程通信的经典模型:生产者与消费者问题。
- 生产者负责生成商品,消费者负责消费商品。
- 生产不能过剩,消费不能没有。
模拟一个案例:小明和小红有一个共同账户(共享资源),他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。做整存整取:10000元。
- 线程通信是一种等待唤醒机制。
- 线程安全必须早同一个共享资源才需要通信,而且必须保证线程安全。
- 线程安全,性能差。
- 线程不安全性能好,假如开发中不会存在多线程的安全问题,建议使用线程不安全的设计类。
在介绍多线程并发修改变量不可见现象的原因之前,我们先看看另一种Java内存模型(和Java并发编程有关的模型):JMM。
JMM(Java Memory Model):Java内存模型是Java虚拟机规范中定义的一种内存模型,Java内存模型是标准化的,他屏蔽了底层不同计算机的硬件的不同
Java内存模型描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。
JMM有以下规定:
- 所有的共享变量都存储于主内存(这里的变量是指实例变量和类变量,不包含局部变量,因为局部变量的线程是私有的,不存在竞争的问题)
- 每一个线程都有自己独立的工作内存,线程的工作内存保留了被线程使用的变量的工作副本
- 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存的变量。
本地内存和主内存之间的关系:


- 子线程1从主内存中读取到数据并复制到其对应的工作内存。
- 修改flag的值为true,但是这个时候flag的值还并没有写会主内存。
- 此时main方法读取到了flag的值为false。
- 当子线程1将flag的值写回去之后,由于main函数中的调用的是系统底层的代码,速度快,快到没有时间再去读取主内存中的值,所以此时读取到的值一直是。
- 此时我们能想到的办法是,如果main线程从主内存中读取到了flag最新的值,那么if语句就可以执行了。
- 每个线程都有自己的工作内存,线程都是从主内存中拷贝到共享变量的副本值
- 每个线程都是在自己的工作内存中操作共享变量的。
6.2.4.1、加锁
第一个线程进入代码块前后,执行过程如下:
- 线程获得锁
- 清空工作内存
- 从主内存中拷贝共享变量的最新值变成副本
- 执行代码
- 将修改后的值重新放回主内存中
- 线程释放锁
6.2.4.2、对共享变量使用volatile关键字修饰
我们还可以对共享变量用关键字修饰,关键字的作用是在多线程并发下修改共享变量实现可见性。,一旦一线程修改了修饰的变量,另一个线程可以立即读取到最新值。

- 只能修饰实例变量和类变量,而可以修饰方法以及代码块
- 保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),而是一种排他互斥的机制,可以保证线程安全。
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
执行结果:不保证一定是10000
以上问题主要是发生在count++操作上,count++操作包含3个步骤:
- 从主内存中读取数据到工作内存
- 对工作内存中的数据进行++操作
- 将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。

1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态
2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100
3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中
4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作
5)线程B将101写入到主内存
6)线程A将101写入到主内存
虽然计算了2次,但是只对A进行了1次修改。
小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。
缺点:性能差。
7.4.2.1、概述
Java从JDK5开始提供了包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。我们可以使用原子类来保证原子性操作,从而保证线程安全。
7.4.2.2、常用API
我们以Integer的原子类进行讲解。
CAS的全成是: (比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
- 在内存地址V当中,存储着值为10的变量。

- 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

- 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

- 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,说明值已经被更改过了,提交失败。

- 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

- 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的,说明并没有人修改过值。

- 线程1进行SWAP,把地址V的值替换为B,也就是12。

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
Synchronized是从悲观的角度出发,是一个典型的悲观锁。
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!
CAS是从乐观的角度出发,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!很多数据库都会使用到乐观锁机制。
Map集合中的经典集合:HashMap它是线程不安全的,性能好。如果在要求线程安全的业务情况下就不能用这个集合做Map集合,否则业务会崩溃。
为了保证线程安全,可以使用Hashtable。Hashtable是线程安全的Map集合,但是性能较差!(已经被淘汰了,虽然安全,但是性能差)
为什么说HashTable的性能差呢?我们看看源码可以得知,HashTable的每一个方法都用修饰了,实在是过于悲观。
ConcurrentHashMap不止线程安全,而且效率高,性能好,最新最好用的线程安全的Map集。
我们执行后可以发现出来的错误是有以下三种:
- 没有达到预期的效果

- 抛出异常

- 结果错误



我们查看HashTable的源码我们可以发现他的每一个方法都用修饰了,实在是过于悲观。
在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap上锁机制:CAS + 局部(synchronized)锁定(分段式锁)

- HashMap是线程不安全的。
- Hashtable线程安全基于synchronized,综合性能差,被淘汰了。
- ConcurrentHashMap:线程安全的,分段式锁,综合性能最好,线程安全开发中推荐使用
CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。
需求
线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。
CountDownLatch构造器以及方法
- CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。
- CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch的wait()方法的线程阻塞状态解除,继续执行。
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。这里的屏障相当于需要达到的要求。
CyclicBarrier构造方法:
CyclicBarrier重要方法:
制作员工线程
制作开会线程
制作测试类
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
Semaphore(发信号)的主要作用是控制线程的并发数量。他的机制和一样都是上锁,但是,但某个时间段内,只能有一个线程允许执行。Semaphore可以设置同时允许几个线程执行。它的作用是控制访问特定资源的线程数目。
Semaphore构造方法:
Semaphore重要方法:
我们测试一下只允许一个线程的案例。
制作一个Service类
制作线程类
测试类

我们测试一下只允许两个线程的案例。
修改Service类

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
Exchanger构造方法:
Exchanger重要方法:
- 制作线程A,并能够接收一个Exchanger对象:
- 制作main()方法

- 制作线程A
- 制作线程B
- 制作测试类

exchange方法我们可以设置不一直等待,可以设置一个超时时间。
- 制作线程A
- 制作测试类

我们可以做可以做数据校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致。
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
于是Java提供了一种思想:线程池,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java中线程池的顶级接口是,但是严格意义上讲并不是一个线程池,而是一个执行线程的工具。真正的线程池接。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法有:
- :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
- :获取线程池中的某一个线程对象,并执行
接口:用来记录线程任务执行完毕后产生的结果。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。
- 提交Runnable接口子类对象。
- 关闭线程池(一般不做)。
- : 获取线程池中的某一个线程对象,并执行.
Future : 表示计算的结果。
- : 获取计算完成的结果。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
举个例子:客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)
- 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失
死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免,且死锁一般存在资源的嵌套请求!
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/15077.html