这一章要了解的:
-
什么是线程(创建线程的两种方式,推荐使用Runnable接口的方式)
-
中断线程(注意interrupted和isInterrupted的区别)
-
线程的状态(6种状态)
-
线程属性(4个)
-
同步***(锁对象 条件对象 synchronized关键字 同步阻塞)
-
阻塞队列(重点理解队列)
-
线程安全的集合
-
Callable与Future
-
执行器
-
同步器
-
线程与Swing
*一个程序可以执行多个任务,通常,每一个任务称为一个线程,它是线程控制的简称*(想象一下QQ,可以接收消息的同时发送消息)
*可以同时运行一个以上线程的程序称为多线程程序*
**多线程与多进程的区别:本质区别在于每个进程拥有自己的一整套变量,而线程则是共享数据;线程更“轻量级”,创建,撤销一个线程比启动新进程的开销要小得多
static void sleep(long millis):休眠给定的毫秒数:不会创建新的线程,而是用于暂停当前线程的活动
一 什么是线程
1 使用线程给其他任务提供机会
**如果需要执行一个比较耗时的任务,应当并发地运行任务**
在一个单独的线程中执行一个任务的简单过程:
-
将任务代码一道实现了Runnable接口的类(方法一)的run方法中
public interface Runnable{
void run();
}
可以用lambda表达式创建一个接口的实例:
Runnable r = ()->{要执行的任务代码};
2.由Runnable创建一个Thread对象
Thread t = new Thread(r);
3.启动线程
t.start();
也可以通过构建一个Thread类的子类定义一个线程(方法二,但是这种方法不再推荐,可以使用线程池来解决问题)
class MyThread extends Thread{
public void run();
}
ps:直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程;应该调用Thread.start方法,这个方法将创建一个执行run方法的新线程
二 中断线程
1 没有强制线程终止的方法。interrupt方法可以用来请求终止线程
2 当对一个线程调用interrupt方法时,线程的中断状态将被置位。要想弄清楚是否被置位,应该:
while(!Thread.currentThread().isInterrupted()&&其他限制){
要做的工作
}
ps:如果线程被阻塞,就无法检测中断状态,这是产生InterrruptedException异常的地方
3 每个线程都应该不时地检查中断状态这个标志,以判断线程是否被中断
4 interrupted和isInterrupted的比较:interrupted是一个静态方法,它检测当前线程是否被中断。而且调用interrupted方法会清除该线程的中断状态
isInterrupted是一个实例方法,可用来检验是否有线程被中断,调用isInterrupted方法不会改变中断状态
常用方法:java.lang.Thread
-
void interrupt()向线程发送中断请求,线程的中断状态被置为true,如果目前该线程被一个sleep调动阻塞,那么InterruptedException异常被抛出
-
static boolean interrupted()测试当前线程是否被中断,它将当前线程的中断状态重置为false
-
boolean isInterrupted()测试线程是否被终止,这一调用不改变线程的中断状态
三 线程状态
线程的6种状态:
-
new(新创建)
-
Runnable(可运行)
-
Blocked(被阻塞)
-
Waiting(等待)
-
Timed waiting(及时等待)
-
Terminated(被终止)
1 新创建线程
使用new操作符 ,new Thread(r)
当一个线程处于新创建状态时,程序还没有运行线程中的代码,在线程还没有运行之前还要做一些基础工作
2 可运行线程
一旦调用start方法,线程处于runnable状态
对于一个可运行的线程可能正在运行也可能没有运行
3被阻塞线程和等待线程
-
当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态
只有当所有其他线程释放该锁,并且线程调度器允许本县城持有它的时候,该线程变成非阻塞状态
当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态
4 被终止的线程
1 线程被终止的原因:
-
因为run方法正常退出而自然死亡(自然死亡)
-
因为一个没有捕获的异常终止了run方法而意外死亡(非自然死亡)
常用方法:java.lang.Thread
-
void join()等待终止指定的线程
-
void join(long millis)等待指定的线程死亡或者经过指定的毫秒数
-
Thread.State getState()得到这一线程的状态
四 线程属性(线程优先级 守护线程 处理未捕获异常的处理器 线程组)
1 线程的优先级:默认情况下,一个线程继承它的父线程的优先级,可以使用setPriority方法提高或降低任何一个线程的优先级
常用方法:java.lang.Thread
-
void setPriority(int new Priority)设置线程的优先级(1-10)
-
static void yield():导致当前线程处于让步状态
2 守护线程:t.setDaemon(true)
用途:为其他线程提供服务
守护线程应该永远不去访问固有资源
3 未捕获异常处理器:线程的run方法不能抛出任何受查异常
该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法
void unCaughtException(Thread t,Throwable e)
安装线程处理器的方法
-
用setUncaughtExceptionHandler方法为任何线程安装一个处理器
-
用Thread类的静态方法setDefaultUnCaughtExceptionHandler为所有线程安装一个默认的处理器
如果不安装默认的处理器,则默认的处理器为空。但是如果不为独立的线程安装处理器,此时的处理器就是线程的ThreadGroup对象
线程组:线程组是一个可以统一管理线程的集合。默认情况下,创建的所有线程属于相同的线程组,建议不要在自己的程序中使用
ThreadGroup类实现Thread.UncaughtExceptionHandler接口,它的uncaughtException方法操作流程
-
如果该线程有父线程组,那么父线程组的uncaughtException方法被调用
-
否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器(有默认处理器),则调用该处理器
-
否则,输出栈轨迹到标准错误输出流上
好例子https://blog.youkuaiyun.com/hongxingxiaonan/article/details/50527169
五 同步
1 没有实现同步的例子
2 竞争条件:需要保证线程在失去控制之前方法运行完成,就能保证并发访问的干扰
3 用锁对象来保证并发访问的干扰的两种方法:
-
使用synchronized关键字
-
使用ReentrantLock保护代码块
myLock.lock();//调用ReentrantLock实例化 对象
try{
}finally
{
myLock.unlock();//必须将解锁操作放在finally子句之内,否则线程将永远阻塞
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当有其他线程调用lock时,它们将处于阻塞状态,直到第一个线程释放锁对象
锁时可以重入的,因为线程可以重复地获取已经持有的锁
锁保持一个持有计数来跟踪对lock方法的嵌套调用,被一个锁保护的代码可以调用另一个使用相同的锁的方法
补充:什么是临界区?同一时刻只能有一个任务访问的代码区
WARNNING:要留心临界区中的代码,不要因为异常的抛出而跳出临界区
4 条件对象:用来管理那些已经获得了一个锁但是却不能做有用工作的线程(用来管理线程的)
使用锁来保护检查与转账动作来确保没有其他线程在本检查余额与转账活动之间修改金额:
public void transfer(int from,int to,int amount){
bankLock.lock();
try{
while(accounts[from]<amount){
//wait 保证bankLock的排他性访问
}
}
finally{
bankLock.unlock();
}
}
一个锁可以有一个或多个条件对象
使用newCondition方法获得一个条件对象
class Bank{
private Condition sufficientFunds;
public Bank(){
...sufficientFunds = bankLock.newCondition();
}
}
等待获得锁的线程和调用 await 方法的线程存在本质的不同:
-
1.4.1)一旦一个线程调用 await 方法, 它进入该条件的等待集, 当锁可用时,该线程不能马上解除阻塞;
-
1.4.2)相反,它处于阻塞状态, 直到另一个线程调用同一个条件上的signalAll 方法为止, 这一调用重新激活 因为这一条件而等待的所有线程;
-
1.4.3)一旦锁成为可用的, 它们中的某个将从 await 方法调用返回, 获得该锁并从阻塞的地方继续执行;
signalAll方法的作用:它仅仅是通知正在等待的线程,此时有可能已经满足条件,值得再次去检测该条件
死锁现象的发生:当一个线程调用await时,它没有办法重新激活自身,只能寄希望于其他线程,如果没有其他线程重新激活等待的线程,它就永远不再运行了,该程序就被挂起了
何时调用signalAll方法:在对象的状态有利于等待线程的方向改变时调用
例
public void transfer(int from,int to,int amount){
bankLock.lock();
try{
while(accounts[from]<amount)
sufficient.await();
...转账业务
sufficientFunds.singleAll();//不会立即激活一个等待线程,只是解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问
finally{
bankLock.unlock();
}
}
single方法: 则是随机解除等待集中某个线程的阻塞状态,这比解除所有线程的阻塞更加有效, 但也存在危险,容易发生死锁
5 synchronized关键字
锁和条件的关键之处:
-
锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
-
锁可以管理试图进入被保护代码段的线程
-
锁可以拥有一个或多个相关的条件对象
-
每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法,要调用该方法,线程必须获得内部的对象锁
内部对象锁只有一个相关条件,wait方法添加一个线程到等待集中,notify/notifyAll方法解除等待线程的阻塞状态
调用wait和notifyAll等价于await和singleAll
条件对象.await();
条件对象.singleAll();
使用 synchronized关键字来编写代码简洁很多,每一个对象有一个内部锁,并且锁有一个内部条件,由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程
静态方法声明为synchronized也是合法的,该方法获得相关的类对象的内部锁;若当该方法被调用时,该对象的锁被锁住,因此没有其他线程可以调用同一个类的这个或者其他的同步静态方法
内部锁和条件存在一些局限,包括:
-
不能中断一个正在试图获得锁的线程
-
试图获得锁时不能设定超时
-
每个锁仅有单一的条件,可能是不够的
Lock Condition 同步方法如何选择:
-
最好既不使用Lock/Condition(条件对象)也不使用synchronized关键字
-
尽量使用synchronized关键字
-
除非特别想使用Lock/Condition结构提供的独有特性时,才使用它
补充:sleep wait的区别:
-
sleep方法必须传入参数,参数就是时间,时间到了自动醒来;wait方法不传参数就直接等待,传参数就在指定的时间结束后等待
-
sleep方法在同步函数或同步代码块中不释放锁;wait方法释放
六 阻塞队列(可以理解为一种同步机制)
1当试图从队列添加元素而队列已满,或是想从队列移出到元素而队列为空的时候,阻塞队列导致线程阻塞。
2队列会自动的平衡负载,如果第一个线程集运行的比第二个慢,第二个线程在等待结果时会阻塞;如果第一个线程集运行的快,它将等待第二个队列集赶上来
3阻塞队列方法分为三类,取决于它们的相应方式:
-
如果将队列当做线程管理工具来使用,将用到put和take方法
-
当试图从满的队列中添加或从空的队列中移除元素时,add,remove,element操作抛出异常
-
在多线程程序中,队列会在任何时候空或满,所以一定要使用offer(添加一个元素并返回true),poll(移除并返回队列的头元素),peak(返回队列的头元素)方法来替代,这些方法不会抛出异常