多线程
[TOC]
继承Thread类
使用继承Thread类的方法来创建线程类时,多个线程间无法共享线程类的实例变量
步骤:
- 定义Thread类的子类
- 重写Thread类的run方法,此方法的方法体代表了线程需完成的任务
- 创建Thread类子类的实例,如new FirstThread()
- 调用实例的start()方法启动线程
由上面步骤可知,由于每次创建线程都需要创建一个新的对象,所以每个线程的实例变量不一样
实现Runable接口
使用Runable接口的方式创建的多线程可以共享线程类的实例变量
步骤:
- 定义Runable接口的实现类
- 重写Runable接口的run方法
- 创建Runable实现类的实例,并将此实例作为Thread的target来创建Thread对象
- 调用该Thread对象的start方法
由上面步骤可知,程序创建的Runable对象只是Thread的target而已,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类的实例变量
使用Callable和Future创建线程
使用Callable和Future创建线程可以共享线程类的实例变量
步骤:
- 创建Callable接口的实现类
- 实现call()方法,也可以使用Lambda表达式创建Callable对象
- 使用FutrueTask类包装Callable对象
- 将FutureTask对象作为Thread对象的target启动新线程
- 调用FutureTask对象的get()方法获得子线程执行并结束后的返回值
由上面步骤可知,一个Callable表示一个执行体,而FutureTask是用来去除Callable里的返回值,所以当有两个FutrueTask使用同一个Callable时,这时就会出现和Runable的情况
线程运行阻塞
进入阻塞 | 解除阻塞 |
---|---|
调用sleep方法 | 调用sleep方法超过了指定时间 |
调用了一个阻塞式IO方法 | 阻塞式IO方法已经返回 |
试图获取一个同步监视器 | 成功获取同步监视器 |
等待某个通知(notify) | 正在等待某个通知,其他线程发出了一个通知 |
调用了线程的suspend方法将其挂起 | 线程被调用了resume方法 |
线程从阻塞状态只能进入就绪状态,无法直接进入运行状态
线程死亡
- run()或call()方法完成,线程结束
- 线程抛出一个未捕获的Exception或者Error
- 直接调用stop()方法结束——不推荐使用,容易造成死锁
isAlive():判断是否已经死亡。(实例方法)
不要对已经死亡的线程重新start(),因为死亡就是死亡了,无法再运行了
控制线程
join()
描述:让一个线程等待另一个线程完成。当一个线程(调用者)调用了其他线程的join的方法(被join线程),调用者将被阻塞,直到被join线程执行完为止
形式:
- join(): 等待被join线程完成
- join(long millis): 等待被join线程完成时间为millis毫秒。若在millis毫秒内没完成,则不再等待。
- join(long millis, int nanos): 等待被join线程的时间最长为millis毫秒加nanos毫微秒。
后台线程
描述:后台运行,为其他线程提供服务。又称“守护线程”,“精灵线程”。如JVM垃圾回收线程
特征:如果前台所有线程死亡,后台线程自动死亡。
具体操作:调用Thread对象的setDaemon(true)方法指定线程为后台线程
setDaemon(true)必须在start()方法前调用
sleep()
静态方法
描述:让当前正在执行的线程暂停一段时间,并进入阻塞状态
形式:
- static void sleep(long millis): 暂停millis毫秒
- static void sleep(long millis, int nanos): 暂停millis毫秒 + nanos毫微秒
yield()
静态方法
描述:让当前正在执行的线程暂停一段时间,并进入就绪状态,并等待CPU调度
具体操作:调用Thread类的yield方法
注意:当某个线程调用了yield方法暂停后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态线程才会获得执行机会
sleep和yield区别
sleep | yield |
---|---|
暂停线程,给其他线程执行机会,不理会优先级 | 暂停线程,只给优先级相同或更高的线程执行机会 |
暂停后转入阻塞状态 | 强制当前线程进入就绪状态 |
声明抛出InterruptException异常 | 不声明抛出任何异常 |
比yield更好移植性 | 不建议使用yield控制并发线程执行 |
线程同步
synchronized
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器,成员变量等
- 使用同步代码块
语法格式:
1 | synchronized(obj) { |
语法格式中,synchronized后括号中的obj就是同步监视器,含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定
==推荐使用共享资源充当同步监视器==
- 使用同步方法
语法格式:
1 | public synchronized void Xxx() { |
对于synchronized修饰的实例方法而言,没必要显式指定同步监视器,被修饰的实例方法叫做同步方法,而同步方法的同步监视器是this,也就是调用该方法的对象。
如:
1 | public class Account { |
上述代码中,synchronized修饰了Account的draw这个实例方法,同时将Account这个对象当做同步监视器。原因就是因为draw是实例方法,而只有Account对象能调用draw方法,所以同步监视器就是调用draw方法的对象,即Account对象。
特征:
- 该类的对象可以被多个线程安全地访问
- 每个线程调用该对象的任意方法之后都将得到正确结果,原因是因为,同步监视器就是该对象
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态,原因也是因为同步监视器就是该对象
由于线程安全是以降低运行效率为代价运行的,为了减少这个代价,应注意:
- 不要对线程安全类所有方法都进行同步,即不要所有的方法都加上synchronized关键字,只对会改变竞争资源的方法进行同步。
- 若可变类有两种运行环境:单线程和多线程,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。单线程中使用线程不安全版本保证性能,在多线程版本中使用线程安全版本保证安全
线程通信
传统线程通信
描述:传统线程通信,可以借助Object类的wait(),notify()和notifyAll()三个方法。
wait(), notify()和notifyAll()不属于Thread类,而是Object类,而且三个方法必须由同步监视器对象调用
- synchronized修饰的同步方法:由于同步方法的同步监视器就是该方法所在类的实例,所以可以在同步方法中直接调用
- synchronized修饰的同步代码块:由于synchronized括号后面才是同步监视器对象,所以必须使用该对象调用
wait()
描述:是当前线程等待,直到其他线程notify或者notifyAll。
形式:
- wait()
- wait(long millis)
- wait(long millis, int nanos)
notify()
描述:唤醒在此同步监视器上等待的单个线程。若所有线程都在等待,则随机唤醒一个
notifyAll()
描述:唤醒在此同步监视器上等待的所有线程
使用Lock-Condition控制线程通信
描述:若使用Lock保证同步的话,那么就得使用Condition类来保持通信
形式:Condition实例被绑定在一个Lock对象上,所以必须得通过Lock对象的newCondition方法返回一个Condition对象。
与传统线程通信对比:
传统线程通信 | Lock-Condition通信 |
---|---|
wait() | await() |
notify() | signal() |
notifyAll() | signalAll() |
使用BlockingQueue控制线程通信
描述:Java里有一个BlockingQueue接口,作为线程同步的工具。
特征:当生产者线程视图向BlockingQueue中放入元素时,如果该队列已满,则该生产者线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则消费者线程被阻塞
实现类:
- ArrayBlockingQueue: 数组
- LinkedBlockingQueue:链表
- PriorityBlockingQueue:根据大小自然排序
- SynchronusQueue:同步队列
- DelayQueue:
内部方法:
抛出异常 | 不同返回值 | 阻塞线程 | 指定超时时长 | |
---|---|---|---|---|
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
队头删除元素 | remove() | poll() | take() | poll(time, unit) |
获取但不删除元素 | element() | peek() | 无 | 无 |
线程组
描述:Java使用ThreadGroup表示线程组,可以对一批线程进行分类管理。对线程组的控制相当于控制这一批线程。默认情况下,子线程和创建它的父线程在同一个线程组里。如main线程创建了B线程,但没有指定B线程的线程组,那么B线程则属于main线程所在的线程组
特征:一旦某个线程加入某个线程组,直到死亡,这个线程都是属于这个线程组,中途不可以改变,所以Thread类没有提供setThreadGroup()改变线程组
形式:
可以通过Thread类的构造器设置线程所属的线程组
- Thread(ThreadGroup group, Runable target): 指定属于group线程组
- Thread(ThreadGroup group, Runable target, String name): 指定属于group线程组,并命名name
- Thread(ThreadGroup group, String name)
ThreadGroup构造器:
- ThreadGroup(String name): 指定线程组名字
- ThreadGroup(ThreadGroup parent, String name): 指定线程组名字,指定父线程组
ThreadGroup方法:
- int activeCount(): 返回组中活动线程数目
- interrupt(): 中断组中所有线程
- isDaemon(): 判断此线程组是否是后台线程组
- setDaemon(): 把线程组设置为后台线程组
- setMaxPriority(int priority): 设置线程组最高优先级
线程池
描述:在系统启动时即创建大量空闲线程。当该线程结束任务后不是死亡,而是再次返回线程池成为空闲状态
启动:
使用Executors工厂类产生线程池
Executors静态工厂类:
- ExecutorService newCachedThreadPool(): 具有缓存功能线程池
- ExecutorService newFixedThreadExecutor(): 可重用,固定线程数的线程池
- ExecutorService newSingleThreadExecutor(): 单线程线程池
- ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池, 并且可以指定延迟
- ScheduledExecutorService newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池,可以指定延迟执行
- ExecutorService newWorkStealingPool(int parallelism): 创建持有足够线程的线程池来支持给定的并行级别
- ExecutorService newWorkStealingPool(): 前一个方法的简化版
如上所示,前三个返回ExecutorService,中间两个返回ScheduledExecutorService
==ExecutorService对象代表尽快执行线程的线程池==
- Future<?> submit(Runnable task): Runnable对象提交给线程池,线程池有空时执行任务
- Futrue
submit(Runnable task, T result): 同上,其中result显式指定线程执行结束后的返回值 - Future
submit(Callable task): Callable对象交给线程池,线程池将有空时执行,其中Future代表Callable对象里call()方法的返回值
==ScheduledExecutorService对象代表在指定延迟后或周期性地执行线程任务的线程池==
- ScheduledFuture
schedule(Callable callable, long delay, TimeUnit unit): 指定callable任务在延迟delay后执行 - ScheduleFuture<?> schedule(Runnable command, long delay, TimeUnit unit): 指定command任务在延迟delay后执行
- ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 指定command任务将在delay延迟后执行,并且设定频率(period)重复执行,即在initialDelay后开始执行,依次在initialDelay+period, initialDelay+2*period, ……处重复执行
- ScheduleFuture<?> scheduleWithFixedDelay(Runable command, long initialDelay, long delay, TimeUnit unit)
==用完一个线程池后,应调用shutdown()来关闭==
步骤:
- 调用Executors类的静态工厂方法创建一个ExecutorService或者ScheduledExecutorService对象,该对象代表一个线程
- 创建Runnable实现类或者Callable实现类实例,作为线程任务执行体
- 用ExecutorService或者ScheduleExecutorService对象的submit()方法提交Runnable实例或者Callable实例,这样让Runnable和Callable实例放入线程池里
- 不想提交任何线程时,调用ExecutorService的shutdown()方法关闭
ForkJoinPool
ForkJoinPool是ExecutorService的实现类
描述:将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。
ForkJoinPool构造器:
- ForkJoinPool(int parallelism): 创建一个包含parallelism个并行线程的ForkJoinPool
- ForkJoinPool(): 默认使用Runtime。availableProcessors()方法返回值作为parallelism参数创建ForkJoinPool。就是可用的处理器吧,如4核CPU,然后parallelism就是4
==重点了解一下ForkJoinTask==
ForkJoinTask是一个抽象类,其下还有两个抽象类:
- RecursiveAction:代表有返回值的任务
- RecursiveTask: 代表没有返回值的任务
步骤:
- 创建ForkJoinPool实例
- 调用ForkJoinPool的submit(ForkJoinTask task)或者invoke(ForkJoinTask task)方法执行指定任务。
ThreadLocal
描述:隔离多线程程序的竞争资源,又名线程局部变量
功能:为每一个使用变量的线程提供一个变量值的副本,使每一个线程可以独立改变自己副本,而不与其他线程冲突。
方法:
- T get(): 返回当前线程中线程局部变量的值
- void remove(): 删除当前线程中线程局部变量的值
- void set(T value): 设置线程中线程局部变量的值
ThreadLocal与同步机制区别:
同步机制 | ThreadLocal |
---|---|
同步机制是解决通信问题 | ThreadLocal是避免多线程间对共享资源的竞争 |
正面解决问题 | 侧面避免问题 |
包装线程不安全集合
线程不安全集合:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- TreeMap
- …..
上述可以使用Collections的类方法包装成线程安全集合
synchronizedCollection(Collection
synchronizedList(List
synchronizedMap(Map<K, V> m)
synchronizedSet(Set
synchronizedSortedMap(SortedMap<K,V> m)
synchronizedSortedSet(SortedSet<K,V> m)
如
1 | HashMap m = Collection.synchronizedMap(new HashMap()); |
线程安全集合
本知识不多做介绍,详细可以参考疯狂Java讲义 760页
- 以Concurrent开头的集合类:代表了支持并发访问的集合
- 以CopyOnWrite开头的集合类:略
一般使用Concurrent开头集合类,而CopyOnWrite开头的性能较差