多线程

多线程

[TOC]

继承Thread类

使用继承Thread类的方法来创建线程类时,多个线程间无法共享线程类的实例变量

步骤:

  1. 定义Thread类的子类
  2. 重写Thread类的run方法,此方法的方法体代表了线程需完成的任务
  3. 创建Thread类子类的实例,如new FirstThread()
  4. 调用实例的start()方法启动线程

由上面步骤可知,由于每次创建线程都需要创建一个新的对象,所以每个线程的实例变量不一样

实现Runable接口

使用Runable接口的方式创建的多线程可以共享线程类的实例变量

步骤:

  1. 定义Runable接口的实现类
  2. 重写Runable接口的run方法
  3. 创建Runable实现类的实例,并将此实例作为Thread的target来创建Thread对象
  4. 调用该Thread对象的start方法

由上面步骤可知,程序创建的Runable对象只是Thread的target而已,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类的实例变量

使用Callable和Future创建线程

使用Callable和Future创建线程可以共享线程类的实例变量

步骤:

  1. 创建Callable接口的实现类
  2. 实现call()方法,也可以使用Lambda表达式创建Callable对象
  3. 使用FutrueTask类包装Callable对象
  4. 将FutureTask对象作为Thread对象的target启动新线程
  5. 调用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
2
3
synchronized(obj) {
...
}

语法格式中,synchronized后括号中的obj就是同步监视器,含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定

==推荐使用共享资源充当同步监视器==

  • 使用同步方法

语法格式:

1
2
3
public synchronized void Xxx() {
....
}

对于synchronized修饰的实例方法而言,没必要显式指定同步监视器,被修饰的实例方法叫做同步方法,而同步方法的同步监视器是this,也就是调用该方法的对象。

如:

1
2
3
4
5
6
7
8
9
10
11
public class Account {
public synchronized void draw() {
....
}

public void main(String[] args) {
Account account = new Account();
account.draw();
}

}

上述代码中,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()来关闭==

步骤:

  1. 调用Executors类的静态工厂方法创建一个ExecutorService或者ScheduledExecutorService对象,该对象代表一个线程
  2. 创建Runnable实现类或者Callable实现类实例,作为线程任务执行体
  3. 用ExecutorService或者ScheduleExecutorService对象的submit()方法提交Runnable实例或者Callable实例,这样让Runnable和Callable实例放入线程池里
  4. 不想提交任何线程时,调用ExecutorService的shutdown()方法关闭

ForkJoinPool

ForkJoinPool是ExecutorService的实现类

描述:将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。

ForkJoinPool构造器:

  • ForkJoinPool(int parallelism): 创建一个包含parallelism个并行线程的ForkJoinPool
  • ForkJoinPool(): 默认使用Runtime。availableProcessors()方法返回值作为parallelism参数创建ForkJoinPool。就是可用的处理器吧,如4核CPU,然后parallelism就是4

==重点了解一下ForkJoinTask==

ForkJoinTask是一个抽象类,其下还有两个抽象类:

  • RecursiveAction:代表有返回值的任务
  • RecursiveTask: 代表没有返回值的任务

步骤:

  1. 创建ForkJoinPool实例
  2. 调用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 c)
synchronizedList(List list)
synchronizedMap(Map<K, V> m)
synchronizedSet(Set s)
synchronizedSortedMap(SortedMap<K,V> m)
synchronizedSortedSet(SortedSet<K,V> m)

1
HashMap m = Collection.synchronizedMap(new HashMap());

线程安全集合

本知识不多做介绍,详细可以参考疯狂Java讲义 760页

  • 以Concurrent开头的集合类:代表了支持并发访问的集合
  • 以CopyOnWrite开头的集合类:略

一般使用Concurrent开头集合类,而CopyOnWrite开头的性能较差

-------------本文结束感谢您的阅读-------------