2020 年 5 月 19 日 Java 并发编程专题
这篇博客仅用来记录自己的成长历程,如果能对大家有所帮助,我将不胜欢喜,以下内容的主要来源是《Java核心技术卷一》,本篇文章对我自己来说主要的目标就是回顾一下多线程相关的知识,整理一下自己的知识框架。之所以是从最后一篇开始整理,是希望自己能把书翻得旧一点。
1、概述
关于多线程的知识主要是从线程的定义,创建线程,中断线程,线程的状态,线程的属性,线程同步,阻塞队列,线程安全的集合为入口进行说明的。
2、线程的定义
在了解多线程的定义之前,先说明一下什么是多任务,多任务就是操作系统在同一时刻运行多个程序的能力,操作系统将CPU的时间片分配给每一个进程,给人以并行处理的感觉。
在多任务的定义下,多线程程序在较低的层级上扩展了多任务的概念:即一个程序同时执行多个任务,通常每个任务简称为一个线程。同时可以运行一个以上的线程程序称为多线程程序。
这里我们说明了多任务和多线程的定义。两者的本质区别就在于每个进程拥有自己的一整套变量,进程之间不会共享数据,而线程之间则会共享数据。与进程相比较线程更“轻量级”。
3、线程的创建
目前线程的创建主要有两种:实现Runnable接口和继承Thread类,注意不要把通过Callable和FutureTask创建线程的方式纳入进来。
启动线程时不要调用Thread类或者Runnable对象的Run方法,执行run方法只会执行同一个线程中的任务,而不会启动一个新的线程。应该调用Thread.start方法去启动线程,这个方法将创建一个执行run方法的新线程。
最常用的方法:
start()
run()
4、中断线程
当run方法执行方法体中的最后一条语句的时后,并经由return语句返回时,或者出现了在方法中没有捕获的异常时,线程终止,注意没有可以强制线程终止的方法,interrupt可以用来请求终止线程,调用该方法的时候,线程的中断状态会被置位,每一个线程都应该时不时的检查这个标志。
如果线程被阻塞就检查不到这个状态,在此时如果调用interrupt方法,阻塞调用将会被异常中断
如果调用了interrupt使得中断状态被置位时,调用了sleep方法,它不会休眠,相反会抛出一个InterruptedException。
最常用的方法:
interrupt()【向线程发送中断请求,中断状态被置位true】
interrupted()【测试当前线程是否被中断,会讲当前的线程中断状态重制位false】
isInterrupted()【判断当前线程状态是否被中断】
currentThread()【获取当前线程】
5、线程状态
线程的一共有6种状态:New【新建】,Runnable【可运行】,Blocked【被阻塞】,Timed Waiting【计时等待】,Terminated【被终止】
new:指的是new Thread(r)时,该线程还没有开始运行
Runnable:指的是调用start方法之后,线程处于Runnable状态,在任何给定的时刻 ,一个可运行的线程可能正在运行,也可能没有运行。
被阻塞和等待线程:它们暂时不活动,不运行任何代码且消耗最少的资源,只要有线程调度器重新激活它。
当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则线程进入阻塞状态
当线程等待另一个线程通知通知调度器一个条件时,它自己进入等待状态
线程的终止:
run方法执行完成后,正常退出自然而然的死亡
因为一个没有捕获的异常终止了run方法而意外死亡
6、线程属性
线程的属性要和线程的状态区分开来,状态是会变的,属性是伴随线程的。属性主要包括:线程的优先级,守护线程,线程组,以及处理未捕获异常的处理器。
线程的优先级:线程的优先级可以通过setPriority()设置线程的优先级,每当线程调度器有机会选择新的线程时,会优先选择具有较高优先级的线程,但是线程的优先级是高度依赖于系统的。
守护线程:通过t.setDeamon(true)将线程转化为守护线程,守护线程的唯一用途就是为其他线程提供服务,当只剩下守护线程时,虚拟机就退出了
未捕获异常处理器:线程的run方法不能抛出任何受检查异常,都必须在线程内部进行异常的捕获处理,但是如果有一个未捕获的、非受检查的异常出现时就会导致线程的终止,在线程死亡之前,异常会被传递到一个用于未捕获异常的处理器。
注意:如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象,最好不要在自己的程序中使用线程组。
7、同步
两个或者两个以上的线程需要同时共享对同一数据的存取,根据各线程访问的次序,可能会产生错误的数据,这样的情况就称为竞争条件。
简单的说线程同步需要考虑的问题就是,多个线程并发的查询和修改数据时,需要保证数据的一致性。所谓的竞争条件就是,一个线程不能准确的读取到另一个线程修改后的数据。
锁对象:
解决数据的一致性问题:必须要使用到锁对象,目前有两种机制能接受代码块受并发访问的干扰:
1、synchronized关键字
2、ReentrantLock. [加锁和解锁。mylock.lock() , my lock.unlock(). ]
通过这两种方式保证操作的原子性。
注意:ReentrantLock锁是可重入的,当一个线程持有该锁的时候,可以重复的获取已经持有的锁,锁保持一个持有计数,来跟踪lock方法的嵌套使用,线程在没调用一次lock都需要调用一次unlock来释放锁,当持有计数变为0的时候,当前线程就会释放该锁
条件锁对象:【主要是Lock和Conditio相关的操作】
通常一个线程进入临界区,却发现在某一条件满足之后才它才能执行,这样就需要一个条件对象来管理那些已经获得锁但是却不能做有用工作的线程,【条件锁对象----等待唤醒机制】
一个锁对象可以有一个或者多个相关的条件对象,可以用 new Condition方法获得一个条件对象。当一个线程调用await方法的时候,它没有办法重新激活自身,只能寄希望于其他线程待用signalAll方法,注意在调用signallAll方法的时候并不会立即激活一个等待线程,它是解除等待线程的阻塞,重新开始锁的竞争。
总结:
锁用来保护代码片段。任何时刻只有一个线程能执行被保护的代码片段
锁可以管理试图进入被保护代码段的线程
锁可以用一个或者多个条件对象
每个条件对象管理那些已经进入被保护的代码段还不能运行的线程。
synchronized:
从java1.0开始每一个对象内部都一个内部锁。如果有一个方法用synchronized声明,那么对象锁将保护整个方法,也就说要调用该方法,线程必须获得对象的内部锁。
synchronized能声明方法,也能修饰方法体。
【最好不要使用Lock/Condition 和synchronized关键字 ,可以多使用JUC中类】
同步阻塞:
首先区分一下同步和异步,阻塞和非阻塞的概念:
首先同步和异步指的是CPU对时间片的利用,主要看请求发起方对消息结果的获取是主动发起的,还是被动通知的。
其次阻塞和非阻塞通常指的是I/O操作,简单的说就是我们调用了一个函数,在等待这个函数返回结果之前,当前的线程是处于挂起状态还是运行状态,如果是挂起状态,就等待返回结果,这就是同步阻塞,如果仍然是运行状态,就意味着当前线程可以继续处理其他任务,但是需要时不时的看一下结果,这就是同步非阻塞
同步:发起一次请求后,线程主动的获取结果,或者隔一段时间获取一次
异步:发起一次请求之后,服务器将处理完成后的将结果返回给线程
阻塞:发起一次请求之后,在收到应答之前,线程被挂起的
非阻塞:发起一次请求之后,在收到应答之前,线程继续运行
综上所述:这里所说的同步阻塞,指的是请求方发起的,一直在等待应答结果,线程在等待结果返回之前是被挂起的。
volatile 域
volatile关键字为实例域的同步提供了一种免锁机制。如果声明一个域为volatile那么编译器和虚拟机就知道该域可能被另一个线程并发的更新。
注意:Volatile变量不能提供原子性,假设对共享变量除了赋值之外不完成其他操作,那么可以将这些共享变量声明为volatile
8、死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
9、线程局部变量
有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例
10、锁测试和锁超时
线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞,如果调用带有超时参数的trylock,那么线程如果在等待期间被中断,将抛出InterruptException异常
11、读写锁
使用读写锁的必要步骤:
1、构造一个ReentrantReadWriteLock
2、抽取读写锁
3、对需要的修改方法加读写锁
12、阻塞队列
对于许多线程问题,可以通过使用一个或者多个队列以优雅且安全的方式将其形式化,生产者向对列插入元素,消费者线程则取出他们,使用对列可以安全的从一个线程向另一个线程传递数据。
当试图向对列添加元素而对列已满,或者是想从对列取出元素,而队列已空的时候,阻塞队列会导致线程阻塞。
阻塞队列的方法分为以下三类:这取决于的当队列满或为空的时候,他们的响应方式
1、如果把队列作为线程管理工具来使用,应该用take和put方法,会出现阻塞
2、如果试图向满的队列或者空队列移除元素的时候,add和remove、element会抛出异常
3、在一个多线程的程序中,队列会在任何时候为满或为空应该调用offer、poll、peek方法,这些方法会给一个错误提示而不会抛出异常
常用的队列有:
1、LinkedBlockingQueue 容量没有上线,是一个双端队列,遵循先进先出
2、ArrayBlockQueue 在构造的时候需要制定容量
3、PriorityBlockingQueue是一个带优先级的队列
13、线程安全的集合
上面的阻塞队列就是线程安全的集合,常见的高效的映射,集和队列有:ConcurentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue,这些集合允许并发的访问数据结构不同的部分,使竞争最小化。
注意:使用ConcurrenthashMap在并发的进行单词统计的时候,如果简单的调用put和take方法并不会实现原子更新操作,因为有可能另一个线程也在同时更新计数,这时候需要不断地调用map.replace(word,oldvalue,newvalue)以实现原子更新操作。
concurrenthashMap中不允许有NULL值
14、Callable和Future
Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
Future保存异步计算的结果,可以启动一个计算,将Future交个某个线程,然后忘记它,future对象的所有者在计算好结果之后就会获得它。
callable和Future是配合着使用的,Future和Thread是配合起来使用的关系依赖如下:
callable=> Fulture=>Thread
结果是通过Future的get()方法获得的。get方法会阻塞线程。
15、执行器(Executor)
构建一个线程是有一定的代价的,因为涉及到与操作系统交互,如果程序中创建了大量生命很短的线程,应该尽量使用线程池。一个线程包含了许多准备运行的空线程。节省线程创建和销毁的开销。另一好处就是减少并发线程的数目;
线程池都是通过执行器创建出来的,简单的说就是执行器(Executor)中包含了许多的静态方法用来构建线程池
常见的线程池有:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newSheduledThreadPool、newSingleThreadScheduledExecutor。
newCachedThreadPool:方法构建一个线程池、对于每个任务,如果有空线程可以使用,立即让它执行,如果没有可用的空线程,就立即创建一个。
newFixedThreadPool:方法构建一个具有固定任务大小的线程池,如多线程池中的线程被使用完,新的线程会被放入到队列中。
newSingleThreadExecutor:是一个已经退化的大小为1的线程池,只能是一个接着一个执行
newSheduledThreadPool:该方法返回的是一个SchedulerExcecutorService接口,该接口具有预定执行或者重复执行任务的特性,可以预定Runnable或者Callable在初始的延迟之后只运行一次,也可以预定Runable对象周期的运行
总结:
1、调用Executor类中的静态方法newCachedThreadPool和newFixedThreadPool
2、调用submit方法提交Runnable或Callable对象
3、如果想要取消任务或者如果提交Callable对象,那么就要保存好返回的future对象
4、当不在使用线程池的时候调用shutdown
16、Fork-Join框架(分叉合并)
fork-join框架是针对一些应用,对每个处理器内核分配使用一个线程来完成计算密集型任务。fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方式称为工作密取、每个线程都有一个双端队列来完成任务,一个工作线程将子任务压入其双端队列的对头,一个工作线程空闲时,它会从另一个双端队列的对尾"密取"一个任务。
案例:
17、同步器
JUC包中,包含了几个能帮助人们管理相互合作的线程集的类,常用的同步器有:CyclicBarrier、Phaser、CountDownLatch、Exchanger、Semaphore、SynchronousQueue。
首先简单的说一下什么是信号量:
概念上将一个信号量管理许多许可证,为了通过信号量,线程通过调用acquire请求许可,其实没有实际的许可对象,信号量仅仅维护一个计数。
CyclicBarrier:CyclicBarrier实现了一个集结点,类似于运动员起跑,允许线程集等待至其中预定的数目的线程达到一个公共栅栏,然后选择一个进行 处理栅栏的动作。
注意:障栅是循环的,因为可以等待所有线程被释放后被重用
Phaser:类似于循序障栅,不过有一个可变的计数。
CountDownLatch:允许线程等待直到计数器为,倒计时门栓是一次性的,一旦计数变为0就不能再使用了。
Exchanger:允许两个线程在要交换的对象准备好时,交换对象。当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器。
Semaphore:允许线程等待直到被允许继续执行线程
SynchronousQueue:同步队列允许一个线程把一个对象交给另一个线程,同步队列是一种生产者与消费者线程配对机制。当一个线程调用SynchronousQueue的Put方法的时候,它会阻塞直到另一个线程调用take为止。
版权声明: 本文为 InfoQ 作者【瑞克与莫迪】的原创文章。
原文链接:【http://xie.infoq.cn/article/e8486c4136c87bd3e4e8a5fee】。文章转载请联系作者。
评论