07.并发
07.并发
7.1 综述
- 你可能很熟悉多任务,这是操作系统的一种能力,看起来可以同时刻运行多个程序,例如:在下载邮件同时可以打印文件,如今人们往往有多cpu的计算机,但并发进程不受限于CPU的数目,多线程程序在更低一层扩展了多任务的概念:单个程序在同时完成多个任务,如果一个程序可以同时执行多个任务,则称这个程序是多线程的
- 多进程和多线程的区别:多进程每个进程是独立的,有各自的变量,而多线程的数据是共享的,共享变量使得数据的通信更加方便,但会有风险,实际应用中,多线程很有用
- 并发的原因:
- 更好地利用单个CPU:能够更好地利用计算机中的资源
- 更好地利用多个CPU或CPU内核:需要为应用程序使用多个线程,以便能够利用所有CPU或CPU核心
- 在响应能力方面更好的用户体验:提出的请求可以由后台线程执行,GUI线程在此期间可以自由地响应其他用户请求
- 在公平性方面更好的用户体验:在用户之间更公平地共享计算机资源,客户端的请求由各自的线程执行,没有任务可以独占时间
7.2 线程
- 什么是线程:可以设计一个简单程序,完成银行账户的资金转账,可以使用多个线程,如果使用单独的线程完成任务,在单独的线程中,我们可以定义银行类存储账目信息,然后通过一个函数式接口,传入任务,比如说使用Runnable,从这个接口的对象完成操作,构造一个Thread对象
var t = new Thread(r);然后t.start()启动线程,这个方法会立即返回,新的线程会并发执行,如果你写了两个r并让他们在上下两行分别执行,他们的输出是交错的,说明两个线程在并发执行
直接调用Thread或Runnable的run方法是不会产生一个线程的,它只会在本线程执行这些操作,应当调用start方法
线程状态:线程一般处于六种不同的状态,调用
getState方法能够获得线程的状态- New(新建)
- Runnable(可运行)
- Blocked(阻塞):
- Waiting(等待)
- Timed Waiting(计时等待)
- Termainated(终止)
新建线程:新建线程是使用new Thread(r)创建之后还没有开始运行,当处于这个状态时,可以进行线程的基础准备工作
可运行线程:一旦调用了start(),线程就变成了可运行线程,但一个可运行的程序可能正在运行,也可能没有运行
- java没有把运行中作为一个状态,一旦开始运行,它也不一定始终保持运行状态,具体线程调度细节依赖于操作系统提供的服务,抢占式调度系统会给每个可运行线程一个时间片执行任务,并且在线程数目多于处理器时再分配,并考虑优先级
阻塞和等待线程:当线程处于阻塞或等待时,它暂时不工作,而且消耗最少的资源,要由线程调度器重新激活,具体细节取决于它是如何不工作的
- 可能是试图获得一个内部的对象锁,而这个锁被其他线程占有,这个线程会被堵塞
- 线程在等待其他线程的运行条件,这个时候就进入等待状态
- 线程调用了有超时参数的方法,会使线程进入计时等待的状态,直到超时期满或接受到适当的通知
终止线程:线程会由于两个情况终止
- run方法正常退出
- 因为没有捕获的异常终止了run方法,意外终止
- 可以调用线程的stop方法杀死线程,它会抛出一个ThreadDeath异常,但现在已被废弃
7.3 线程属性
中断线程状态:当程序出现异常或执行完毕返回时,线程将终止,除了已经弃用的stop方法没有方法能够使它强制终止,不过interrupt方法用来请求终止一个线程,当调用intertupt方法时,会设置线程的中断状态,这是每个线程都有的boolean标志,每个线程都应该不时检查这个变量,以判断是否中断
- 要想检查是否设置了中断状态,可以调用Thread.currentThread()方法获得当前线程,然后调用.isInterrupt方法,这个方法需要线程没被阻塞,如果阻塞会抛出InterruptedException异常,如果在使用了sleep或wait调用阻塞的线程上使用了isInterrupt方法,这个线程将被中断
- 没有任何要求如何处理被中断的方法,中断的状态只是要引起注意,中断的线程可以决定如何响应中断
- 如果在迭代中重复执行sleep或其他中断方法,就不应该使用isInterrupt方法,sleep方法会清除中断状态,并抛出InterruptException异常,因此如果循环使用了这些方法,应该捕获抛出的异常,而不是检查状态
- 捕获InterruptException异常,应该在捕获后使用interrupt方法设置中断状态,或者将其抛给调用线程的执行者
守护线程:可以调用
t.setDaemon(true)将一个线程转换成守护线程,守护线程的唯一用途是为其它线程提供服务,比如计时器线程就是个例子,如果只剩下守护线程,虚拟机会退出,这一方法必须在线程启动前调用线程名:在默认情况下,线程有容易记得名字,例如:
Thread-2,可以使用setName方法设置线程名,在转储时,可能有用未捕获异常的处理器:线程的run方法不能抛出任何检查型异常,但是,未捕获的非检查型异常会使线程终止,对于可以传播的异常并没有任何的catch子句,这些异常在线程死亡前,这些异常会被传递到用于处理未捕获异常的处理器,这个处理器必须属于一个实现了
Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法uncaughtException,可以用setUncaughtExceptionHandler方法为任何线程安装处理器,如果没有设置默认处理器,默认处理器为null,如果没有为单个线程安装处理器,那么处理器应该是该线程的ThreadGroup对象(可以一起管理的线程的集合)- ThreadGroup类实现了多线程的异常处理器接口,它的uncaughtException方法会执行操作:
- 如果该线程有父线程组,使用父线程组的uncaughtException方法
- 否则,如果Thread.getDefaultExceptionHandler方法不为空,则调用它
- 否则,如果Throwable是一个ThreadDeath实例什么都不做
- 否则,将线程名和Throwable的栈轨迹输出到System.err流
- ThreadGroup类实现了多线程的异常处理器接口,它的uncaughtException方法会执行操作:
线程的优先级:在JAVA程序设计语言中,每个线程都有一个优先级,默认情况下,一个线程会继承构造它的那个线程的优先级,用setPriority方法可以提高或降低任何一个线程的优先级(在Thread中有对应的优先级int常量(从1到10))
7.4 同步
- 竞态条件:在大多数情况下,两个或两个以上的线程要同时对同一个数据进行存取,这种情况可能会相互覆盖,可能会导致对象被破坏,这种情况就称为竞态条件
- 同步存取:为了避免多线程破坏数据,必须学习如何同步存取,比如银行系统,会出现几个线程同时更新银行账户余额,一段时间后不知不觉就会出现错误
- 竞态条件详解:假如在银行中更新账户学习时出现这个问题,两个线程在同时执行
accounts[to] += amount,这不是原子操作,这个指令可能做以下处理,1️⃣先将accounts[to]加载到寄存器,2️⃣增加amount,3️⃣返回accounts[to],假如第一个线程执行了1和2操作,然后运行权被抢占,而第二个线程开始执行并执行完成,第一个线程恢复后,进行第3步,会覆盖第2个线程的更新 - java的测试程序能够检查到上方的破坏,当然也有可能出现假警报,使用
javap -c -v查看类的虚拟机字节码,可以看到每一条语句的执行都是由1或者多条指令组成的,在这些指令任何一条上,都有可能中断,在一个有多核的现代处理器上,出问题的风险相当高
- 锁对象:java提供了synchronized关键字来达到防止其他并发访问代码块,而java5之后又提供了ReetrantLock类处理这种情况,先说ReetrantLock类
- ReetrantLock类是通过以下形式保护代码块
myLock.lock(); //a ReetrantLock Object
try{
...
}finally{
myLock.unLock(); //确保即使出现异常也能够解锁
}
//使用锁时,不能使用try-with-resource
//因为解锁语句不是close,而且一般希望多个线程共享一个变量这个结构能确保即使有大量线程进入临界区,一旦一个线程锁定了锁对象,其他线程无法通过lock语句,直到第一个线程释放了这个锁对象,你可以把锁对象设置为类变量,在需要的函数里使用(但需要所有线程的所有操作都是对同一个类的实例使用,否则锁是不同的,不会堵塞)
重入锁:ReentrantLock是独占锁且可重入的,通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出
- 锁对象存在一个计数来跟踪对lock的嵌套调用,每使用一次lock应该对应一个unlock释放,这样可以使被保护的代码块调用使用相同锁的方法
- 注意不要在临界区域出现异常而跳出,此时,锁将被释放,对象可能被破坏
- 公平锁:可以使用
new ReentrantLock(boolean)构造一个公平锁,在锁释放后倾向于等待时间最长的线程,但速度很慢,而且对于已经被线程调度器忽略的线程不会得到公平的处理
条件对象:对于需要满足某些条件才能执行,可以先等待其他线程完成后再执行的线程而言,可以使用条件对象解锁,并等待其他线程执行,条件对象可以通过ReentrantLock对象的newCondition()方法生成,有
await()方法,可以使线程进入这个条件的等待集,它将暂停并放弃锁,直到其他线程完成前置操作并调用条件对象的signalAll()方法,会重新激活等待集中的所有线程,当这些线程被移除等待集时,他们将成为可运行线程,调度器会把他们激活,signalAll方法只是通知这些线程可以再次检查条件,因此一般使用while(condition){ condition.await(); },当一个线程使用await暂停时,没有办法自己激活,需要其他线程重新调用,如果没有,它永远不会运行,产生死锁现象,如果所有可能解锁的线程都使用await方法,那么程序会永远挂起,条件对象还有随机激活一个线程的signal方法,如果这个被激活的线程条件仍未满足,那仍然不能运行,并且可能出现死锁锁对象和条件对象小结:
- 锁可以用来保护代码片段,一次只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码的线程
- 锁可以有多个相关联的条件对象
- 每个条件对象管理已经进入保护代码但是不能运行的线程
- synchronized:Lock和Condition接口允许程序员充分控制锁定
