1、线程概述
- 操作系统中所有运行中的任务通常对应一个进程(Process),当一个程序进入内存运行时,即变成一个进程
- 进程的三个特征:
独立性:每一个进程都拥有自己私有的地址空间,没有经过进程本身允许,不允许其他进程访问地址空间
动态性:相比起程序,进行加入了时间概念,具有自己的生命周期和各种不同的状态,而程序不具备
并发性:多个进行可以在单个处理器上并发执行,互不影响 - 并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一个时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
- 多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务;线程也被称作轻量级进程,线程是进程的执行单元
- 多线程的优势:
进程之间不能共享内存,但线程之间共享内容非常容易
系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
Java语言内置了多线程功能支持,而不是单纯地为底层操作系统的调度方式,从而简化了Java的多线程编程
2、线程的创建和启动
- 继承Tread类创建线程类
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务
调用线程对象的start()方法来启动该线程 - Thread.currentThread():currentThread()是Tread类的静态方法,该方法总是返回当前正在执行的线程对象
- getName():该方法是Tread类的实例方法,该方法返回调用该方法的线程名字
- 使用继承Tread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
- 实现Runnable接口创建线程类
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
创建Runnable实现类的实例,并以此实例作为Tread的target来创建Tread对象,该Tread对象才是真正的线程对象
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
},”ThreadName”).start();;
调用线程对象的start()方法来启动该线程 - 采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量
- 使用Callable和Futrue创建线程
Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大
call()方法可以有返回值
call()方法可以声明抛出异常
Callable接口不是Runnable接口的子接口,所以Callable对象不能直接作为Tread的target,需要使用Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,所以可以作为Tread类的target - 创建并启动有返回值的线程步骤:
创建Callable接口的实现类,并实现call()方法
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
使用FutureTask对象作为Tread对象的target创建并启动新线程
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 10;
}
});
new Thread(task,"").start();;
try {
System.out.println(task.get());
} catch (Exception e) {
e.printStackTrace();
}
- 三种创建方式的对比,采用实现Runnable、Callable接口的方式归为一类:
线程类只实现Runnable、Callable接口还可以继承其他类
在这种方式下,多个线程可以共享一个target对象,非常适合多个相同线程来处理同一份资源的情况
如果要访问当前线程,必须使用Tread.currentTread()方法,如果继承Tread类的话直接使用this即可获得当前线程
3、线程的生命周期
- 当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
- 当线程启动以后,CPU需要在多条线程之间切换,于是线程状态也会多次在云信、阻塞之间切换
- 新建和就绪状态
new一个线程之后,该线程处于新建状态,此时和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值,此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的执行体
当调用start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了,至于何时开始运行,取决于JVM里线程调度器的调度 - 运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
当发生如下情况时,线程将会进入阻塞状态:
线程调用了sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有
线程在等待某个通知
程序调用了线程的suspend()方法将线程挂起
当正在执行的线程被阻塞之后,其他线程就可以获得执行的机会
当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
调用sleep()方法的线程经过了指定时间
线程调用的阻塞式IO方法已经返回
线程成功地获得了试图取得的同步监视器
线程正在等待某个通知时,其他线程发出了一个通知
处于挂起状态的线程被调用了resume()恢复方法 - 线程死亡
线程会以如下三种方式结束,结束后就处于死亡状态
run()或call()方法执行完成,线程正常结束
线程抛出一个未捕获的Exception或Error
直接调用该线程的stop()方法来结束该线程-该方法容易导致死锁,不推荐使用
isAlive()方法可以测试某个线程是否已经死亡,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡状态时,返回false
若对已死亡的线程使用start()方法将会引发IllegalThreadStateException异常
4、线程控制
- join线程
当在某个程序执行流中调用其它线程的join()方法时,调用线程将被阻塞,直到被join的线程执行完毕为止
jion()的三种重载方法:
jion():等待被jion的线程执行完成
jion(long milis):等待被jion的线程的时间最长为millis毫秒
jion(long millis,int nanos):等待被jion的线程的时间最长为millis毫微秒 - 后台线程(Deamon Thread)
后台线程在后台运行,任务是为其它的线程提供服务
如果所有的前台线程都死亡,后台线程会自动死亡
setDeamon(true)方法可将指定线程设置成后台线程
isDeamon()方法可以判断指定线程是否为后台线程 - 线程睡眠:sleep
Thread.sleep()方法可以让正在执行的线程暂停一段时间,并进入阻塞状态
sleep()的两种重载形式:
static void sleep(long millis)
static void sleep(long millis,int nanos)
在线程睡眠期间,将不会获得执行机会,即使系统中没有其它可执行的线程也不会执行 - 线程让步:yield
Thread.yield()方法可以让正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态,可能很快又会继续执行 - 改变线程优先级
Tread.setPriority(int newPriority)、getPriority()方法可以设置和返回指定线程的优先级,范围是1~10之间
5、线程同步
- 可以使用关键字synchronized修饰代码块
synchronize(account){
}
- 可以使用关键字synchronize修饰方法
public synchronize void draw(){
}
- 释放同步监视器的锁定
当前线程的同步方法、同步代码块执行结束时释放
遇到break、return终止了该代码块、该方法的继续执行时释放
出现了未处理Error或Exception,导致异常结束时释放
程序执行了同步监视器对象的wait()方法时释放 - 不释放
程序调用Tread.sleep()、Thread.yield()方法来暂停当前线程的执行
其它线程调用了该线程的suspend()方法将该线程挂起 - 死锁
当两个线程相互等打死对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况
由于Tread类的suspend()方法也很容易导致死锁,所以不推荐使用
6、线程通信
- 传统的线程通信
通过成员变量、synchronized、wait()、notify()、notifyAll()方法实现
假设有存钱和取钱动作,要求每当存款者存款后取款者就立即取出该笔钱,不允许存款者连续两次存钱,也不允许取款者连续取钱
实现思路:通过booelan flag判断是否应该存钱或取钱,如果账户已存款flag=true,并调用notifyAll()方法唤醒其它线程,其它当存款者调用wait()方法进入等待,此时取款者可以顺利取款,取款后将flag=false,同样调用notifyAll()方法唤醒其它线程,如此类推
7、使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式同步监视器,也就不能使用wait、notify、notifyAll方法进行线程通信了,这个时候可以使用Condition对象替代
Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();
cond.await():类似wait()
cond.signal():类似notify()
cond.signalAll():类似notifyAll()
8、使用阻塞队列(BlockingQueue)控制线程通信
- BlockingQueue是Queue的子接口,主要用途并不是作为容器,而是作为线程同步工具
- 当线程试图往BlockingQueue中放入元素时,如果已满则线程被阻塞;当从BlockingQueue中取出元素时,如果已空则线程被阻塞
- 两个阻塞方法:
put(E e)
take() - 常用方法:
- BlockingQueue与实现类之间的类图
- BlockingQueue的5个实现类
ArrayBlockingQueue:基于数组
LinkedBlockingQueue:基于链表
PriorityBlockingQueue:首先取出的元素是最小的元素
SynchronousQueue:同步队列,存和取必须是交替进行
DelayQueue:根据getDalay()方法的返回值进行排序
9、线程组
- Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,如果程序没有显式指定线程属于哪个线程组则默认属于默认线程组
- 如果A线程创建了B线程,则默认B线程属于A线程所在的线程组
- 一旦某个线程加入了指定线程组之后,就一直属于该线程组,直到该线程死亡
10、线程异常
- 如果线程执行过程中抛出一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常
- 例子:
public class ExHandler {
public static void main(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
int a = 5/0;
// 虽然制定了异常处理器对未捕获的异常进行处理,但程序依然不会正常结束,这和catch不同
System.out.println("程序正常结束!");
}
}
class MyExHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t+"线程出现了异常:"+e);
}
}
11、线程池
- 当程序中需要创建大量生存期很短的线程时,应当要考虑使用线程池
- Java5开始内建线程池,新增了一个Executors工厂类来产生线程池,常用API:
ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中
ExecutorService newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池
ExecutorService newSingleThreadExecutor():创建一个只有单线程的线程池,相当于调用newFixedThreadPool()方法时传入参数
ScheduledExcutorService newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务,corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内
ScheduledExcutorService newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务
ExecutorService newWorkStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争
ExecutorService newWorkStealingPool():同上,如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于上一个方法传入4作为参数
- ExecutorService代表尽快执行线程的线程池(只要线程池中有闲置线程,就立即执行线程任务),程序只要将一个
Runnable对象或Callable对象(代表线程任务)提交给该线程池,该线程池就会尽快执行该任务,常用API:
Future<?> submit(Runnable task):将一个Runnable对象提交指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务;其中Future对象代表Runnable任务的返回值-但run()方法没有返回值,所以Future对象将在run()方法执行结束后返回null;但可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态
<T> Future<T> submit(Runnable task,T result):同上,其中result显式指定线程执行结束后的返回值
<T> Future<T> submit(Callable<T> task):同上,参数换成了Callable对象,其中Future代表Callable对象里call()方法的返回值
ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit):指定callable任务将在delay延迟后执行
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit):指定command任务将在delay延迟后执行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):同上,而且以设定频率重复执行,在initialDelay后开始执行,依次在initialDelay+period、initialDelay+2*period...处重复执行,依次类推
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):创建并执行一个在给定初始延迟后首次启动的定期操作,随后在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务在任一次执行时遇到异常,就会取消后续执行;否则,只能通过程序来显式取消或终止该任务
- 用完一个线程池后,应该调用线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程不再接收新任务,但会将以前所有已提交任务执行完成。当线程池中的所有任务都执行完成后,池中的所有线程都会死忙;另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表
- 使用线程池来执行线程任务的步骤:
调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
创建Runnable实现类或Callable实现类的实例,作为线程执行任务
调用ExecutorService对象或Callable实现类的实例,作为线程执行任务
当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池
ExecutorService pool = Excutors.newFixedThreadPool(6);
Runnable target = ()->{
system.out.println();
}
pool.submit(target);
pool.submit(target);
pool.shutdown();
- Java7提供了ForkJoinPool来支持将一个任务拆分成多个小人物并行计算,在把多个小人物的结果合并成总的计算结果,构造器:
ForkJoinPool(int parallelism):创一个一个包含parallelism个并行线程的ForkJoinPool
ForkJOinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPookl
Java8进一步扩展了ForkJoinPool功能:
ForkJoinPool commonPool():返回一个通用池,运行状态不会受shutdown()或shutdownNow()方法的影响
int getCommonPoolParallelism():该方法返回通用池的并行级别
创建了ForkJoinPool实例之后,uji可以调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务;ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction和RecursiveTask;其中RecursiveTask代表有返回值的任务,而RecursiveAction代表没有返回值的任务
例:
class PrintTask extends RecursiveAction{
int middle = (start + end)/2;
PrintTask left = new PrintTask(start,middle);
PrintTask right = new PrintTask(middle,end);
left.fork();
right.fork();
}
class CalTask extends RecursiveTask<Integer>{
int middle = (start + end)/2;
CalTask left = new CalTask(arr,start,middle);
CalTask right = new CalTask(arr,middle,end);
left.fork();
right.fork();
return left.join() + right.join();
}
- ThreadLocal是线程局部变量的意思,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变的副本,而不会和其它线程的副本冲突
T get():返回此线程局部变量中当前线程副本中的值
void remove():删除此线程局部变量中当前线程的值
void set(T value):设置此线程局部变量中当前线程副本中的值
ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免了多个线程之间对共享资源(变量)的竞争,也就不需要多多个线程进行同步了
- 包装线程不安全的集合
Java集合中ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,当多个并发线程向这些集合中存、取元素时,就会破坏这些集合的数据完整性
Colection提供的类方法可以把这些集合包装成线程安全的集合:
<T> Collection<T> synchronizedCollection(Collection<T> c):返回指定collection对应的线程安全的collection
static <T> List<T> synchronizedList(List<T> list):返回指定List对象对应的线程安全的List对象
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):类似上
static <T> Set<T> synchronizedSet(Set<T> s):类似上
static <K,V> SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V> m):类似上
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):类似上
HashMap m = Collections.synchronizedMap(new HashMap());