Thread
大部分时候我们都做着单线程的编程,都只有==一条顺序执行流==:程序从main方法开始执行,依次向下执行每行代码,如果程序执行某行代码遇到了阻塞,程序将会停滞在此处。
==单线程的程序只有一个顺序执行流,多线程的程序则可以包括多个顺序执行流,多个顺序流之间互不干扰。==
可以这样理解:单线程程序如果只有一个服务员的餐厅,他必须做完一件事之后才可以做下一件事;多线程程序则如同有多个服务员的餐厅,他们可以同时进行着许多事情。
1.线程概述
一个操作系统里可以有多个进程,而一个进程里可以有多个线程。
进程
几乎所有操作系统都支持==进程==的概念,所有运行中的任务通常对应这一条进程(Process)。当一个程序进入内存(存储正在运行的程序和数据)运行时,就变成了一个进程。
进程是处于运行过程中的程序,并具有一定独立功能,是系统进行资源分配和调度的一个独立单位。
进程的特征:
独立性
进程是系统中独立存在的实体,可以拥有自己独立的资源,每个进程都有自己私有的地址空间(独立的代码和数据空间)。进程间的切换会有较大的开销。
动态性
进程与程序的区别在于:程序只是一个==静态==的指令集合,而进程是一个正在系统中活动==(动态)==的指令集合。
在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,而这些概念在程序中都是不具备的。
并发性(concurrency)
多个进程可以在单个处理器上并发执行,多个线程之间不会互相影响。
注意区分并发性(concurrency)和并行性(parallel)这两个概念:
并行(parallel):指在同一时刻,有==多条==指令在==多个处理器==上==同时==执行。
并发(Concurrency): 指在同一时刻,只能有==一==条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
举个例子:现代的操作系统几乎都支持同时运行多个任务:一边开着ide写代码,一边开着网页在查API,同时还在听音乐,用markdown作笔记…这些进程开上去像是在同时工作。
但是真相是:==对于一个CPU而言,它在某一个时间点上只能执行一个程序,也就是说只能运行一个进程。==*CPU不断的在这些进程之间快速轮换执行,那么为什么感觉不到任何中断现象呢?这是因为CPU的执行速度相对我们的感觉来说实在是太快了。*如果启动的程序(进程)足够多,我们依然可以感觉程序的运行速度下降(电脑变卡)。
线程
多线程扩展了多进程的概念,使得同一个进程(注意这里是限于一个进程里!)可以同时并发处理多个任务。
==线程(Thread)==也被称作 ==轻量级进程(Lightweight Process)==。线程(Thread)是进程(Process)的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化之后,主线程就被创建了。对于大多数的应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是Thread,每条Thread也是互相独立的。
线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程。
一个线程可以拥有自己的堆、栈、自己的程序计数器(PC)和自己的局部变量,==但不再拥有系统资源,它与父进程的其他线程共享该进程(Process)所拥有的全部资源。==
因为多个线程共享父进程的全部资源,因此编程更加方便;但必须注意的是:==必须确保一个线程不会妨碍同一进程里的其他线程!==
线程可以完成一定的任务,可与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。
线程是独立运行的,==它并不知道进程中是否还有其他线程的存在==。线程的运行是==抢占式==的 ———> 当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。
一个线程可以创建和撤销另一个线程(例如在main方法这个主线程里创建另一个线程),同一个进程(Process)的多个线程(Thread)之间可以并发执行(concurrency,多个线程之间快速切换,快到让人感觉是在同时执行)
从逻辑角度来看:多线程存在于一个应用程序中,让一个应用程序中可以有==多个执行部分同时==执行;但操作系统无需将多个线程看做多个独立的应用,对多线程实现调度和管理、资源分配由==进程==本身负责完成。
总结:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要有一个线程(主线程)
多线程的优势
线程的划分尺度小于进程,使得多线程程序的==并发性高==。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序的效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程经共享同一个进程的虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。
系统创建进程必须为该进程分配独立的内存空间,并分配大量相关资源,但创建一个线程则简单的得多。
1.创建和启动
java使用Thread类代表线程,所有的线程都必须是Thread类或其子类。
每条线程的作用是:==完成一定的任务,实际上就是执行一段程序流。==*java使用run
方法来封装这样一段程序流,run
方法也被成为==线程执行体==*
1.1 继承Thread类创建线程类
步骤:
定义Thread类的子类,重写该类
run
方法。==run方法的方法体代表了该线程需要完成的任务==(想要它做什么,就往run方法里面写什么)。创建线程对象
用线程对象的start方法来启动线程。
例:
package thread.createThread;
public class FirstThread extends Thread {
private int i;
public void run() {
for (; i < 20; i++) {
// shiyong getName()方法来返回当前线程的名字
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
// 使用Thread类的静态方法 currentThread() 来获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 10) {
new FirstThread().start();
}
if (i == 15) {
new FirstThread().start();
}
}
}
}
上面的程序只显式的启动了两条线程,但实际上有==3条线程==,因为还有包含main方法的主线程。主线程的线程体不是有run方法确定的,而是由main方法的方法体来确定。
上面还用到了线程类的两个方法:
Thread.currentThread()
:返回当前正在执行的线程对象getName()
:返回调用该方法的线程的名字。程序可以通过
setName(String name)
方法来为线程设置名字。默认情况下下主线程的名字为main,用户启动的多条线程名字依次为Thread-0, Thread-1…
上面程序Thread-0和Thread-1输出的i并不连续,这是因为i是实例属性,程序每次创建线程对象都需要创建一个FirstThread对象,Thread-0和Thread-1不能共享i。(但如果把i设成static就可以)
使用继承Thread类的方法来创建线程,多条线程之间无法共享实例变量。
1.2 实现Runnable接口创建线程类
步骤:
定义实现Runnable接口的类,重写
run
方法public class SecondThread implements Runnable
创建Runnable实现类的对象,并以此作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象。
SecondThread st = new SecondThread(); new Thread(st, "NewThread");
Runnable对象仅仅作为Thread对象的Target(在创建Thread对象时作为参数传进构造方法,Runnale实现类里包含的
run
方法仅仅作为线程执行体。==而实际的线程对象依然是Thread类的实例,只是该Thread线程负责执行其Target的run
方法而已==)调用线程对象的
start
方法来启动该线程
例:
package thread.createThread;
public class SecondThread implements Runnable {
private int i;
public void run() {
for (; i < 20; i++) {
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()
+ " " + i);
SecondThread st = new SecondThread();
if (i == 10) {
new Thread(st, "newThread1").start();
}
if (i == 15) {
new Thread(st, "newThread2").start();
}
}
}
}
- 这时两个线程的i变量是连续的,因为程序所创建的Runnable对象只是线程的target,而多条线程可以共享同一个target,也就是说可以共享同一个target的所有实例变量。
两种方式对比
优缺点 | 继承Thread | 实现Runnable |
---|---|---|
优点 | 简单,直接使用this.getName()来获取当前线程(因为本身是一个线程类对象) | 1.只是实现了Runnable接口,还可以继承其他类 2.多个线程共享一个target对象,非常适合多个线程来处理同一份资源的情况 |
缺点 | 因为线程类已经继承了Thread,所以不能再继承其他父类了 | 略微复杂,要使用Thread.currentThread() 来获取当前线程 |
==几乎所有的多线程应用都采用实现Runnable接口的方式。==
2.线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过
- 新建(new)
- 就绪(runnable)
- 运行(running)
- 阻塞(blocked)
- 死亡(dead)
五种状态。尤其是线程启动以后,它不能一直”霸占“着CPU独自运行,CPU需要在多条线程之间切换,所以线程状态也会多次在运行、阻塞之间切换。
新 就 运 死
--start()--> --获得处理器资源--> --run执行完--------->
<--失去处理器资源-- --Error/Exception-->
建 绪 行 亡
^ |
| |
|______ blcok ______|
2.1 新建、就绪状态
新建(new)
当程序使用new关键字创建了一个线程之后,该线程就处于==新建(new)==状态,此时它和其他java对象一样,仅仅由java虚拟机为它分配了内存,并初始化了其成员变量的值。==(仅仅就是一个普通的对象)==
就绪(runnable)
当线程对象调用了start()
方法之后,该线程处于==就绪(runnable)==状态,java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程==并没有开始==运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
注意:
==启动线程使用
start()
方法,而不是run()
方法!!!!==永远不要调用线程对象的
run()
方法!!!!
永远不要调用线程对象的run()
方法!!!!
永远不要调用线程对象的run()
方法!!!!(重要的事情说三次。。。)
- 调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。多个线程之间可以并发执行
- 但是如果直接调用线程对象的run方法,run方法会被立刻执行,而且在run方法返回之前其他线程无法并发执行。(变成了普通的方法调用!!!)
不要对已经启动的线程再次调用start方法,否则会引发
IllegalThreadStateException
线程调度器切换线程由底层平台控制,具有一定随机性
如果希望调用子线程的strat方法立刻执行子线程,可以使用
Thread.sleep(1)
来是当前运行的线程(主线程)睡眠一个毫秒,这一毫秒内CPU不会空闲,它会立刻去执行一条就绪的线程。
2.2 运行、阻塞
运行
处于就绪状态(runnable)的线程获得了CPU,开始执行run方法的线程执行体。
如果计算机只有一个CPU,在任何时候==只有一条线程==处于运行状态。在多CPU的机器上将会有多个线程并行(parallel)执行;担当线程数多于处理器数时,依然会有多条线程在同一个CPU上轮换的现象。
当一条线程开始运行后,它不可能一直处于运行状态(除非线程执行体足够短,瞬间就执行完了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。
==抢占式调度策略==(所有现代的桌面和服务器操作系统采用)
系统会给每个可执行的线程一小段的时间来处理任务;当该时间段使用完,系统就会剥夺该线程所占据的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所导致。
线程死亡
线程会以以下三种方式之一结束,结束后处于死亡状态:
- run方法执行完成,线程正常结束
- 线程抛出一个未捕获的Exception或Error
- 直接调用该线程的stop方法来结束该线程(容易导致死锁,不推荐使用!)
注意:
当主线程结束的时候,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响(如前面所说,线程之间是相互独立的)。
为了测试某条线程是否已经死亡,可以调用线程对象的isAlive方法。
- 当线程处于就绪、运行、阻塞三种状态时,该方法返回true
- 处于新建、死亡两种状态时,返回false。
不可以对一个已经死亡的线程调用start方法,否则会引发IllegalThreadStateException
(不能对已经死亡或者已经启动的线程调用start方法,只能对新建状态的线程调用)
3.控制线程
3.1 join
当在某个程序执行流中A调用其他线程的join方法,A(调用join方法的那个线程)将被阻塞,知道join方法加入的join线程完成为止。
join方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题配一个线程,当所有的小问题得到处理后,再调用主线程来进一步操作。
例:
package thread.controlThread;
public class Join {
public static void main(String[] args) {
JoinThread jt = new JoinThread();
new Thread(jt, "HelloWorld").start();
for (int i = 0; i < 100; i++) {
if (i == 20) {
Thread joinThread = new Thread(jt, "joinThread");
joinThread.start();
try {
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + i );
}
}
}
class JoinThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
上面程序一共有3条线程:
主线程开始之后启动了名为“HelloWorld”的线程,该子线程将会和main线程并发执行
当主线程的循环变量i等于20时,启动了名为“joinThread”的线程,然后这个线程join进了main线程。注意:==此时“joinThread”不会和main线程并发执行,而是main线程必须等该线程执行结束后才可以向下执行==。在“joinThread”执行时,实际上只有两条子线程(“HelloWorld” 和 “joinThread”)并发执行,而main线程处于等待(阻塞)状态知道“joinThread”执行完。
3.2 后台程序(Daemon Thread)
指 在后台运行的 线程,任务是为其他的线程提供服务。 JVM的垃圾回收线程就是典型的后台线程。
==特征:如果所有的前台线程都死亡,那么后台线程会自动死亡。==当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。
设置指定线程为后台线程: ==调用Thread对象的
setDaemon()
方法==前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程
例:
package thread.controlThread;
public class DaemonThread {
public static void main(String[] args) {
Daemon d = new Daemon();
Thread t = new Thread(d, "DaemonThread");
t.setDaemon(true);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
class Daemon implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
上面代码在main方法里先将t设置为后台线程,然后启动该线程(==要将某个线程设置为后台线程,必须在该线程启动之前设置,setDaemon(true)
必须在start()
之前调用,否则会引发IllegalThreadStateException
==)。本来该线程和ing该执行到i = 99才会结束,但是实际上它无法运行到99,因为主线程(程序中唯一的前台线程)运行结束后,JVM会自动退出,后台线程也就自动死亡了。
3.3 线程睡眠:sleep
sleep方法:Thread类的静态方法,让当前正在执行的线程暂停一段时间,并且进入阻塞状态。当当前线程调用sleep方法进入阻塞状态之后,在它sleep的时间里,它不会获得执行的机会。就算系统中没有其他可运行的程序,处于sleep的线程也不会运行,因此sleep方法常用于暂停程序的运行。
sleep有两种重载方法:
static void sleep(long millis)
让当前正在执行的线程暂停millis个毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器精度和准确度的影响。
static void sleep(long millis, int nanos)
和前面一样,很少使用。
3.4 线程让步:yield
和sleep有点类似,也是Thread类的一个静态方法。它也可以让当前正在执行的线程暂停,但不会使线程阻塞,只是将线程转入就绪状态。
——> yield只是让当前线程暂停一下下,让系统的线程调度器重新调度一次。完全可能出现的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将它调度出来执行。
实际上,当某个线程调用了yield之后,只有优先级与当前线程==相同==或者==比当前线程高==的==就绪状态==的线程才会获得执行的机会。也就是说,如果当前线程优先级设成了Thread.MAX_PRIORITY
的话,它yield之后其他线程不会获得执行机会(除非其他线程中也有MAX——PRIORITY
的线程),线程调度器又把它调出来运行。
yield的意思是屈服, 顾名思义,只有面对和它优先级相同或者比它更高的线程才能屈服,暂停自己让别人运行,不可能屈服于比自己优先级低的线程。
sleep和yield的区别
sleep | yield |
---|---|
sleep暂停当前线程之后,会给其他线程执行机会,并不考虑线程优先级 | yield方法暂停当前线程之后,==只有和当前线程优先级相同或者更高的处于就绪状态(runnable)的线程才能有执行的机会== |
sleep方法会将线程转入阻塞状态,知道经过了设定的阻塞时间才会转到就绪状态 | yield方法不会将线程转入阻塞状态,它只是强制让当前线程从运行状态(runnig)转到就绪状态(runnable)。因此完全有可能某个线程调用yield暂停之后又马上获得CPU资源被执行 |
sleep方法会抛出InterruptedException 异常 | 不抛异常 |
sleep方法比yiled方法具有更好的移植性 | 通常不要依靠yield来控制并发线程的执行 |
3.5 改变线程的优先级
每个线程执行时都有一定的优先级,优先级高的线程获得较多的执行机会,优先级低的获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,默认情况下,main线程的具有普通的优先级,它创建的子线程也具有普通的优先级
通过`setPriority(int newPriority)来设置指定线程的优先级,参数可以是一个1 ~ 10之间的整数,也可以使用Thread类的三个静态常量:
- MAX_PRIORITY : 10
- MIN_PRIORITY : 1
- NORM_PRIORITY : 5
虽然java提供了10个优先级别,但是这些优先级需要操作系统的支持。不同的操作系统上优先级并不相同,而且也不能很好的和java中的10个优先级对应。——> ==所以应该金莲高避免直接为线程指定优先级,而是使用三个经常常量来设置优先级,这样才可以保证程序具有最好的可移植性。==
通过
getPriority()
来返回当前线程的优先级
4.线程的同步
多线程编程容易突然出现错误,这是因为系统的线程调度具有一定的随机性,也说明了编程不当。在使用多线程的时候,必须要保证线程安全。
4.1 线程安全
考虑一个经典的问题:银行取钱,基本流程分为如下几个步骤:
用户输入账户、密码,系统判断用户的账户和密码是否匹配
用户输入取款金额
系统判断用户余额是否大于取款金额
余额 > 取款金额,取款成功,用户取出钞票;否则取款失败。
现在模拟最后三步操作,使用两条线程来模拟两个人分别在不同的取款机上对同一个账户并发进行取款。
代码如下:
- Account
package thread.synchronize;
public class Account {
private String accountNr;
private double balance;
public Account() {
}
public Account(String accountNr, double balance) {
this.accountNr = accountNr;
this.balance = balance;
}
public String getAccountNr() {
return accountNr;
}
public void setAccountNr(String accountNr) {
this.accountNr = accountNr;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public int hashCode() {
return this.accountNr.hashCode();
}
public boolean equals(Account account) {
return account.getAccountNr().equals(this.accountNr);
}
}
- DrawThread:
package thread.synchronize;
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double amount) {
super(name);
this.account = account;
this.drawAmount = amount;
}
public void run() {
if (this.account.getBalance() >= this.drawAmount) {
System.out.println(this.getName() + " : Success! draw : " + this.drawAmount);
try {
this.sleep(1);
} catch(InterruptedException e) {
e.printStackTrace();
}
this.account.setBalance(this.account.getBalance() - this.drawAmount);
System.out.println("balance : " + this.account.getBalance());
} else {
System.out.println(this.getName() + " : Fail...Not enough balance");
}
}
}
- TestDraw:
package thread.synchronize;
public class TestDraw {
public static void main(String[] args) {
Account account = new Account("1568856", 1000);
new DrawThread("A", account, 800).start();;
new DrawThread("B", account, 800).start();;
}
}
结果:
B : Success! draw : 800.0
A : Success! draw : 800.0
balance : 200.0
balance : 200.0
这个结果明显是不符合预期的,取了两次800,账户却只扣除了一次。为什么会出现这种情况呢?这是因为run方法的方法体不具有同步安全性:程序中由两条并发的线程修改Account对象,而且系统恰好在run方法里
this.sleep(1)
这行代码处进行线程切换:
Takt | A | B |
---|---|---|
1 | balance = 1000,取出钱 | nop |
2 | sleep(1) | balance = 1000,取出钱 |
3 | balance = 1000 - 800 = 200 | sleep(1) |
4 | nop | balance = 1000 - 800 = 200 |
所以最后A和B各取了1000,账户只扣了800.
为了解决这个问题,java的多线程引入了==同步监视器==来解决这个问题。
使用同步监视器的通用方法就是==同步代码块==,语法如下:
synchronized(obj) {
//bla bla bla
}
synchronized后括号里的obj就是同步锁定器。上面代码的含义是:==线程开始执行同步代码块之前,必须先获得对同步锁定器的锁定。==
使用同步监视器的目的是:防止两条线程对同一个共享资源进行并发访问,因此==通常用可能被并发访问的共享资源当同步监视器==(比如上面的Account对象)。
使用同步监视器的逻辑是:
==加锁 ——> 修改 ——> 修改完成 ——> 释放锁==
任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,修改完成后,该线程释放对资源的锁定,然后其他线程才能访问或修改这个资源。通过这种方式就可以==保证在任一时刻只有一条线程可以进入修改共享资源的代码区(==同步代码块,也被称作临界区),所以同一时刻仅仅有一条线程处于临界区内,从而保证了线程的安全性。
依照这个思路,修改上面的代码:
package thread.synchronize;
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double amount) {
super(name);
this.account = account;
this.drawAmount = amount;
}
public void run() {
synchronized (this.account) {
if (this.account.getBalance() >= this.drawAmount) {
System.out.println(this.getName() + " : Success! draw : "
+ this.drawAmount);
try {
this.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.setBalance(this.account.getBalance()
- this.drawAmount);
System.out.println("balance : " + this.account.getBalance());
} else {
System.out.println(this.getName()
+ " : Fail...Not enough balance");
}
}
}
}
4.2 同步方法
同步方法就是使用
synchronized
关键字来修饰某个方法,该方法称为同步方法。对于同步方法而言,无需显式指定同步监视器,==同步方法的同步监视器就是this,也就是对象本身==。
synchronized
关键字可以修饰方法,可以修饰代码块,但是不能修饰构造器,属性等。
例:将上面的例子使用同步方法来保证线程安全:
修改Account,增加一个用synchronized
关键字修饰的draw
方法
package thread.synchronize;
public class Account {
private String accountNr;
private double balance;
public Account() {
}
public Account(String accountNr, double balance) {
this.accountNr = accountNr;
this.balance = balance;
}
public String getAccountNr() {
return accountNr;
}
public double getBalance() {
return balance;
}
public synchronized void draw(double drawAmount) {
if (this.balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + " : Success! draw : " + drawAmount);
try {
Thread.sleep(1);
} catch(InterruptedException e) {
e.printStackTrace();
}
this.balance -= drawAmount;
} else {
System.out.println(Thread.currentThread().getName()
+ " : Fail...Not enough balance");
}
}
public int hashCode() {
return this.accountNr.hashCode();
}
public boolean equals(Account account) {
return account.getAccountNr().equals(this.accountNr);
}
}
上面的代码还删去了balance属性的setter方法,因为账户余额不能随便修改。
同步方法的同步监视器是this,==因此对于同一个Account对象而言,任意时刻只能有一条线程获得对Account的锁定,然后进入draw方法执行取钱操作==,这样也可以保证多条线程并发取钱的线程安全。
因为Account类已经提供了draw方法,而且取消了setBalance()方法,所以还得修改DrawThread类:该类只需直接调用Account对象的draw方法来执行取钱操作。
package thread.synchronize;
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double amount) {
super(name);
this.account = account;
this.drawAmount = amount;
}
public void run() {
this.account.draw(this.drawAmount);
}
}
因为已经在Account类中使用synchronized
保证了draw方法的线程安全性,所以在run方法里直接调用也没有任何问题。
这时的程序把draw方法定义在Account里,而不是在run方法里实现取钱逻辑,这种做法更符合面向对象。在面向对象里有一种流行的设计方式:==Domain Drive Design(领域驱动设计,DDD),这种方式认为每个类都应该是完备的领域对象。==例如Account代表账户,应该提供账户的相关方法,例如通过draw方法来执行取钱操作,而不是直接将setBalance方法暴露出来任人操作,这样才可以更好的保证Account对象的完整性、一致性。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少保证线程安全而带来的负面影响,可以采取以下策略:
不要对线程安全类的所有方法都进行同步(都用
synchronized
修饰),只对那些会改变竞争资源(共享资源)的方法进行同步。例如上面Account类的hashCode和equals等方法无需同步如果可变类有两种运行环境:单线程和多线程环境,则应该为该类提供两种版本:线程不安全版本(用于在单线程环境中以保证性能),线程安全版本(在多线程环境中保证线程安全)
4.3 释放同步监视器的锁定
任何线程进入同步代码块,同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
程序无法显式的释放对同步监视器的锁定,线程会在如下几种情况释放:
当前线程的同步方法、同步代码块执行结束
当前线程的同步方法、同步代码块遇到**break、retur**n终止了该方法的继续执行
当前线程的同步方法、同步代码块出现了未处理的Error或Exception,导致了该方法、代码块异常结束
==当前线程的同步方法、同步代码块执行了同步监视器对象的wait方法==
在下面情况下,线程不会释放同步监视器:
当前线程执行同步方法、同步代码块时,程序调用
Thread.sleep()
,Thread.yield()
方法来暂停当前线程的执行当前线程执行同步代码块、同步方法时,其他线程调用了当前线程的suspend方法将它挂起(当然我们应该尽量避免使用suspend和resume方法)
4.4同步锁(Lock)
另一种线程同步的机制:它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象更适合。
Lock提供了比synchronized
方法和代码块更广泛的锁定操作,Lock对象实现允许更灵活的结构,可以具有差别很大的属性,并可以支持多个相关的Condition对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次==只能有一个线程==对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源进行并发访问,比如ReadWriteLock
。
在实现线程安全的控制中,通常用ReentrantLock
,使用该Lock对象可以显式的加锁、释放锁。
语法如下
class X {
// 定义锁对象
private final ReentrantLock lock = new ReentranLock();
// 定义需要保证线程安全的方法
public void m() {
// 加锁
lock.lock();
try {
//需要保证线程安全的代码
// body
} finally {
// 使用finally来保证释放锁
lock.unlock();
}
}
}
例:根据Lock来修改上面的Account类:
package thread.synchronize;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private ReentrantLock lock = new ReentrantLock();
private String accountNr;
private double balance;
public Account() {
}
public Account(String accountNr, double balance) {
this.accountNr = accountNr;
this.balance = balance;
}
public String getAccountNr() {
return accountNr;
}
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
lock.lock();
try {
if (this.balance >= drawAmount) {
System.out.println(Thread.currentThread().getName()
+ " : Success! draw : " + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance -= drawAmount;
} else {
System.out.println(Thread.currentThread().getName()
+ " : Fail...Not enough balance");
}
} finally {
lock.unlock();
}
}
public int hashCode() {
return this.accountNr.hashCode();
}
public boolean equals(Account account) {
return account.getAccountNr().equals(this.accountNr);
}
}
使用Lock与使用同步方法有点相似,只是使用Lock时==显式==使用Lock对象作为同步锁,而使用同步方式时系统==隐式==的使用当前对象作为同步监视器,同样都符合“加锁 –> 访问 –> 释放锁”的模式;而且==使用Lock对象时每个Lock对象对应一个Account对象==,同样能保证对于同一个Account对象,同一时刻只有一条线程进入临界区。
Lock提供了同步方法和同步代码块没有的其他功能,包括用于非块结构的
tryLock
方法、试图获取可中断锁lockInterruptibly
方法、获取超时失效锁的tryLock(long, TimeUnit)方法
ReentrantLock具有==重入性==,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个==计数器==来追踪lock方法的嵌套调用。==线程在每次调用lock方法加锁后,必须显式的使用unlock来解锁。==
4.5 死锁(DeadLock)
==当两个线程相互等待对方释放同步监视器时就会发生死锁==,java虚拟机没有检测,也没有采用措施来处理死锁情况,一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续运行。
死锁是很容易发生的,尤其是出现多个同步监视器的时候。
例:
- A类:
package thread.deadLock;
public class A {
public synchronized void foo(B b) {
System.out.println(Thread.currentThread().getName() + " :entering method foo of A...");
try {
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :try to enter method last of B...");
b.last();
}
public synchronized void last() {
System.out.println("Got into method last of A");
}
}
- B类:
package thread.deadLock;
public class B {
public synchronized void bar(A a) {
System.out.println(Thread.currentThread().getName() + " :entering method bar of B...");
try {
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :try to enter method last of A...");
a.last();
}
public synchronized void last() {
System.out.println("got into methood last of B");
}
}
- DeadLock:
package thread.deadLock;
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("main Thread");
a.foo(b);
System.out.println("After starting main Thread...");
}
@Override
public void run() {
Thread.currentThread().setName("vice Thread");
b.bar(a);
System.out.println("After entering vice Thread...");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
运行结果:
main Thread :entering method foo of A...
vice Thread :entering method bar of B...
main Thread :try to enter method last of B...
vice Thread :try to enter method last of A...
从结果中可以看出:程序既无法向下执行,也不会抛出任何异常,只是都卡在两个对象的last方法那里,僵持着无法向下执行。
这是因为:
上面程序中A对象和B对象的方法都是同步方法,即:==A对象和B对象都是同步锁==。
程序中有两条线程在执行,一条线程的执行体是DeadLock类是run方法(副线程),另一条线程的执行体是DeadLock的init方法(主线程调用了init方法)。run方法让B对象调用bar方法,而init方法让A对象调用foo方法。
init方法首先执行,调用了A对象的foo方法,因为foo方法是一个同步方法,所以进入foo方法之前,主线程会首先对A对象加锁。执行到
Thread.sleep(200);
时,主线程休眠200个毫秒然后副线程开始执行,调用B对象的bar方法,同样的,由于bar方法也是一个同步方法,所以进入bar方法之前,副线程会首先对B对象加锁。执行到
Thread.sleep(200);
时,副线程休眠200个毫秒接下来主线程继续向下执行直到
b.last();
,此处希望调用b的last方法;由于last方法也是一个同步方法,所以主线程在执行之前必须先对B对象加锁,==但此时副线程正保持着B对象的锁,主线程无法对B对象加锁==。所以b.last();
无法执行,主线程进入阻塞状态。接着副线程醒过来继续向下执行直到
a.last();
,此处希望调用a的last方法,由于last方法也是一个同步方法,所以在副线程执行之前必须先对A对象加锁,==但此时主线程正保持着A对象的锁,副线程无法对A对象加锁==。所以a.last();
无法执行,副线程也进入阻塞状态。此时的状况就是:==主线程保持着A对象的同步锁,等待B对象解锁之后执行
b.last();
;副线程保持着B对象的同步锁,等待A对象解锁之后执行a.last()
,两条线程互相等待对方先释放锁,谁也不让谁,所以就出现了死锁!==
5.线程通信
5.1 线程的协调运行
假设现在系统中由两条线程,分别代表存款者、取款者——现在有一个需求:要求存款者和取款者不断重复存款、取款两个操作,并且要求每当存款者将钱存入账户后,取款者马上从账户中取走这笔钱。不允许存款者、取款者连续两次存款、取款。
为了实现这个功能,可以使用Object类提供的wait()
,notify()
,notifyAll()
三个方法。注意:这三个方法并不属于Thread类,而是属于Object类。但这三个方法必须由同步监视器对象来调用,可分成两种情况:
对于使用
synchronized
来修饰的同步方法,对象本身就是同步监视器,所以可以在同步方法中直接使用这三个方法。对于使用
synchronized
修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象来调用这三个方法。
三个方法:
wait()
:导致当前线程等待,知道其他线程调用该同步监视器的
notify()
或者notifyAll()
来唤醒该线程。三种重载:- 无参数的wait,一直等待,知道其他线程通知唤醒
- 带毫秒参数的wait
- 带毫秒、微秒参数的wait
后面两种方法都是等待制定时间后==自动苏醒==过来。
==调用wait方法的线程会释放对该同步监视器的锁定!==
notify()
:唤醒在此同步监视器等待的单个线程。如果所有线程都在此同步监视器上等待,那么会随机唤醒一条线程。但并不是说唤醒之后就执行被唤醒的线程,而是当前线程放弃对该同步监视器的锁定后(使用wait方法),才可以执行被唤醒的线程。(因为任意时刻只能有一条线程锁定同步监视器)
notify()All
:唤醒在此同步监视器上等待的所有线程。同样的,只有当前线程放弃对该同步监视器的锁定后(使用wait方法),才可以执行被唤醒的线程。
思路:在Account类中可以设一个Tag isDeposited
来标记账户中是否已经有存款:
当tag是false,表明账户中没有存款,存款者可以继续向下执行;存款者把钱存入账户后,tag设为true(已经存入款项),并调用notifyAll来唤醒其他线程;当存款者线程进入线程体后,如果tag是true(已经有存款,不能重复存),那么就调用wait方法来阻塞它。
当tag是true,表明账户中已经存入了钱,则取款者可以继续向下执行,当取款者把钱从账户中取出后,将tag设为false(钱已取出,账户中没有存款了),tag设为true,并调用notifyAll来唤醒其他线程;当存款者线程进入线程体后,如果tag是false(账户中没有存款,自然就不能取款了),那么就调用wait方法来阻塞它。
代码如下:
- Account:
package thread.threadCommunitcation;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private ReentrantLock lock = new ReentrantLock();
private String accountNr;
private double balance;
// use isDeposited to indicate whether the account is deposited
private boolean isDeposited;
public Account() {
}
public Account(String accountNr, double balance) {
this.accountNr = accountNr;
this.balance = balance;
}
public String getAccountNr() {
return accountNr;
}
public double getBalance() {
return balance;
}
public synchronized void draw(double drawAmount) {
try {
if (!isDeposited) {
this.wait();
} else {
System.out.println(Thread.currentThread().getName()
+ " draw : " + drawAmount);
this.balance -= drawAmount;
System.out.println("Now the balance is: " + this.balance + "\n");
// After drawing set isDeposited to false
this.isDeposited = false;
this.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(double depoAmount) {
try {
if (isDeposited) {
this.wait();
} else {
System.out.println(Thread.currentThread().getName() + " deposit : " + depoAmount);
this.balance += depoAmount;
System.out.println("Now the balance is: " + this.balance + "\n");
// After depositing set isDeposited to true
this.isDeposited = true;
notifyAll();
}
} catch(InterruptedException e) {
e.printStackTrace();
}
}
public int hashCode() {
return this.accountNr.hashCode();
}
public boolean equals(Account account) {
return account.getAccountNr().equals(this.accountNr);
}
}
因为draw和deposit两个方法会并发修改Account的balance属性,所以要设成==synchronized==来保证线程安全。
- 取款者线程:
package thread.threadCommunitcation;
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double amount) {
super(name);
this.account = account;
this.drawAmount = amount;
}
public void run() {
for(int i = 0; i < 20; i++) {
this.account.draw(this.drawAmount);
}
}
}
- 存款者线程:
package thread.threadCommunitcation;
public class DepositThread extends Thread{
private Account account;
private double depoAmount;
public DepositThread(String name, Account account, double depoAmount) {
super(name);
this.account = account;
this.depoAmount = depoAmount;
}
public void run() {
for (int i = 0; i < 20; i++) {
this.account.deposit(this.depoAmount);
}
}
}
对存款者线程而言,进入Account类的deposit方法后,如果isDeposited == true
,表明账户中已经有存款,不能重复存款,所以调用wait方法阻塞;否则(isDeposited == flase
)程序向下执行存款操作,存款操作完成后,将isDeposited设成true,然后调用notifyAll来唤醒其他被阻塞的线程——如果系统中有其他存款者线程,也会被唤醒,但被唤醒的存款者线程执行到if (isDeposited)
这一句时,因为之前的存款者线程已经将isDeposited
设成了true,即此时Account中已有存款,所以该存款者线程会再次进入阻塞(调用wait);只有执行draw方法的取款者线程才可以继续向下执行。同理,取款者线程的流程也是如此。
- 测试类:
假设有三个存款者和一个取款者
package thread.threadCommunitcation;
public class Demo {
public static void main(String[] args) {
Account account = new Account("!568856", 0);
new DrawThread("draw1", account,800).start();
new DepositThread("depo1", account, 800).start();;
new DepositThread("depo2", account, 800).start();;
new DepositThread("depo3", account, 800).start();;
}
}
运行程序可以看到:存款者线程、取款者线程交替存、取钱(三个存款者线程随机向账户中存钱)。
执行到最后会看到这样的结果:
draw1 draw : 800.0
Now the balance is: 0.0
depo1 deposit : 800.0
Now the balance is: 800.0
draw1 draw : 800.0
Now the balance is: 0.0
depo2 deposit : 800.0
Now the balance is: 800.0
程序最后阻塞无法继续向下运行,这是因为:3个存款者线程,一共会存款 3 * 20 = 60 次,而只有一个取款者线程,总共只取20次,所以当取款者线程执行完最后一次取款操作后,调用notifyAll来唤醒三个存款者线程,然后其中一个存款者线程往账户里存钱之后,调用notifyAll来唤醒其他线程,==但此时能唤醒的只有另外两个存款者线程(因为取款者线程已经执行完最后一次取款操作,以后不会再执行了)==,另外两个存款者线程进入deposit方法后,isDeposit都是true,所以都会调用wait方法,然后大家都wait在那里了,所以程序也就阻塞了,不能继续向下运行了。
注意这里的阻塞并不是死锁!在这里取款者线程已经执行结束,==而存款者线程只是在那里等待其他线程来取钱而已,而不是等待其他线程释放同步监视器==。切勿把死锁和阻塞混淆起来!
5.2 使用条件变量控制线程协调
如果程序不使用
synchronized
关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait、notify、notifyAll来协调进程的进行。当使用Lock对象来保证同步时,java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程
Condition将同步监视器方法wait、notify、notifyAll分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(Wait-set)。
——> Lock替代了同步方法、同步代码块,Condition替代了同步监视器。
Conditon实例被绑定在一个Lock对象,调用Lock对象的
newCondintion()
方法可以获得Condition的实例。Condition类提供了三个方法:
await()
类似于隐式同步监视器的wait方法,导致当前线程等待,直到其他线程调用signal或者signalAll方法来唤醒该线程。
该方法有更多的变体,具体查API
signal()
类似于notify方法,唤醒在此Lock对象上等待的多个线程。如果所有线程都在该Lock对象上等待,则会随机选择唤醒一个线程。只有当前线程放弃对该Lock对象的锁定后(使用await方法),才可以执行被唤醒的线程。
(与notify极其类似!!!)
signAll()
唤醒在此Lock对象上等待的所有线程。同样的,只有当前线程放弃对该Lock对象的锁定后(使用await方法),才可以执行被唤醒的线程。
使用Lock和Condition来修改Account类:
package thread.threadCommunitcation;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = this.lock.newCondition();
private String accountNr;
private double balance;
// use isDeposited to indicate whether the account is deposited
private boolean isDeposited;
public Account() {
}
public Account(String accountNr, double balance) {
this.accountNr = accountNr;
this.balance = balance;
}
public String getAccountNr() {
return accountNr;
}
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
this.lock.lock();
try {
if (!isDeposited) {
this.condition.await();
} else {
System.out.println(Thread.currentThread().getName()
+ " draw : " + drawAmount);
this.balance -= drawAmount;
System.out.println("Now the balance is: " + this.balance + "\n");
// After drawing set isDeposited to false
this.isDeposited = false;
this.condition.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.lock.unlock();
}
}
public void deposit(double depoAmount) {
this.lock.lock();
try {
if (isDeposited) {
this.condition.await();
} else {
System.out.println(Thread.currentThread().getName() + " deposit : " + depoAmount);
this.balance += depoAmount;
System.out.println("Now the balance is: " + this.balance + "\n");
// After depositing set isDeposited to true
this.isDeposited = true;
this.condition.signalAll();
}
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
this.lock.unlock();
}
}
public int hashCode() {
return this.accountNr.hashCode();
}
public boolean equals(Account account) {
return account.getAccountNr().equals(this.accountNr);
}
}
- Account类增加了属性condition
- deposit和draw两个方法不需要再用
synchronized
修饰 - 在两个方法的方法体里显式的加锁
this.lock.lock();
和解锁this.lock.unlock();
5.3使用管道流
前面介绍的两种方式可以称之为线程之间协调运行的控制策略。如果需要在两条线程之间进行更多的信息交互,可以考虑使用管道流进行通信。
管道流油三种形式:
管道字节流
- PipedInputStream
- PipedOutputStream
管道字符流
- PipedReader
- PepedWriter
新IO的管道Channel
- Pipe.SinkChannel
- Pipe.SourceChannel
==使用管道流实现多线程通信的步骤:==:
使用new字符分别创建管道输入流和管道输出流
使用管道输入流或管道输出流的connect方法把两个输入流和输出流连接起来
将管道输入流、管道输出流分别传入两个线程
两个线程分别依赖各自的管道输入流、管道输出流进行通信
例子:
- ReadThread
package thread.readAndWrite;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PipedReader;
public class ReadThread extends Thread {
private PipedReader pr;
private BufferedReader br;
public ReadThread(){
}
public ReadThread(PipedReader pr) {
this.pr = pr;
this.br = new BufferedReader(pr); // decorator
}
public void run() {
String read = null;
try {
while((read = this.br.readLine()) != null) {
System.out.println(read);
}
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if (this.br != null);
br.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
}
- WriteThread
package thread.readAndWrite;
import java.io.IOException;
import java.io.PipedWriter;
public class WriteThread extends Thread {
private String[] strArr = new String[] { "A", "B", "C", "D" };
private PipedWriter pw;
public WriteThread() {
}
public WriteThread(PipedWriter pw) {
this.pw = pw;
}
public void run() {
try {
for (int i = 0; i < 10; i++) {
pw.write(strArr[i % 4] + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (this.pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 测试类
package thread.readAndWrite;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class TestCommunication {
public static void main(String[] args) {
PipedWriter pw = null;
PipedReader pr = null;
try {
// 1. Create PipedWriter and PipedReader
pw = new PipedWriter();
pr = new PipedReader();
// 2. connect
pw.connect(pr);
// 3. Now is ready for communication between these two threads
new WriteThread(pw).start();
new ReadThread(pr).start();
} catch(IOException e) {
e.printStackTrace();
}
}
}
不难发现,其实跟IO并没有什么区别,关键在于通过pw.connect(pr);
这一句把PipedWriter和PipeReader连接起来实现线程通信。
通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,它们可以肥城方便的共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是使用管道流。
—–> 现在才来告诉我然而并没有什么卵用。。。。
5.4 线程组(ThreadGroup)
java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,对线程组的控制相当于同时控制这一批线程。
用户创建的所有线程都属于==指定线程组==。如果程序没有显式的制定线程属于哪个线程组,那么该线程属于默认线程组。默认情况下,子线程和创建它的父线程处于同一个线程组内:例如A创建了B并且没有指定B的线程组,那么B属于线程A所在的线程组
一旦某个线程加入了制定线程组之后,该线程将一直处于该线程组,知道该线程死亡,线程运行中途==不能改变它所属的线程组==。因此Thread类并没与提供setThreadGroup这样一个方法来改变线程所属的线程组,但是提供了一个getThreadGroup方法来返回该线程所属的线程组。
Thread类提供下面几个构造器来设置新家的线程属于哪个线程组:
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnnale target, String name)
Thread(ThreadGroup group, String name)
ThreadGroup类的构造方法:
ThreadGroup(String name)
Thread(ThreadGroup parent, String name)
以指定的名字和父线程组来创建一个新线程组
上面两个构造方法创建线程组实例时都必须为其制定一个名字,也就是线程组总是有一个字符串名字,改名字可调用ThreadGroup的
getName()
得到,==但不允许改变线程组的名字!==常用方法:
int activeCount()
返回此线程组中活动线程的数目
interrupt()
中断此线程组的所有线程
isDaemon()
判断该线程是否是后台线程组
setDaemon(boolean daemon)
把该线程组设厂后台线程组。后天线程组的特征是:当后条线程组的最后一个线程执行结束或最后一个线程被销毁,那么后台线程组将自动销毁
setMaxPriority(int pri)
设置线程组的最高优先级
例:
package thread.threadGroup;
public class TestThread extends Thread {
public TestThread(String name) {
super(name);
}
public TestThread(ThreadGroup group, String name) {
super(group, name);
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + " : i = " + i);
}
}
}
- 测试类:
package thread.threadGroup;
public class ThreadGroupTest {
public static void main(String[] args) {
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("Main ThreadGroup is : " + mainGroup.getName());
System.out.println("Is main ThreadGroup a daemon ThreadGroup ? " + mainGroup.isDaemon());
new TestThread("Thread of main ThreadGroup").start();
System.out.println();
ThreadGroup viceGroup = new ThreadGroup("Vice Group");
viceGroup.setDaemon(true);
System.out.println("Is Vice Group a daemon threadGroup ? " + viceGroup.isDaemon());
TestThread t1 = new TestThread(viceGroup, "Thread-1 from viceGroup");
t1.start();
new TestThread(viceGroup, "Thread-2 from viceGroup").start();
}
}
5.5 线程异常处理
ThreadGroup中还定义了一个很有用的方法:void uncaughtException(Thread t, Throwable e)
,该方法可以哦你过来处理该线程组内的线程所抛出的没有处理的异常。
如果线程执行过程中抛出了一个未处理的异常,JVM会在结束该线程之前自动查找是否有对应的
Thread.UncauchtExceptionHandler
对象,如果找到该处理器对象,就会调用该对象的void uncaughtException(Thread t, Throwable e)
来处理该异常Thread.UncauchtExceptionHandler
是Thread类的一个==内部公共静态接口。==这个接口内只有一个方法:void uncaughtException(Thread t, Throwable e)
,t代表出现异常的线程,e代表该异常抛出的异常。Thread类中提供了两个方法来设置异常处理器:
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
为该线程类的所有线程实例设置默认的异常处理器(它是static的)
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
为制定线程实例设置异常处理器
ThreadGroup类实现了
Thread.UncauchtExceptionHandler
接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理的异常时,JVM会首先查找该异常对应的异常处理器(即setUncaughtExceptionHandler设置的异常处理器)。如果找到该异常处理器,则调用该异常处理器来处理异常
如果没有找到,JVM将会带哦用该线程所属的线程组的对象的
void uncaughtException(Thread t, Throwable e)
来处理该异常。
==线程组处理异常的默认流程==:如果该线程组有父线程组,那么调用父线程组的
void uncaughtException(Thread t, Throwable e)
来处理该异常如果该线程实例所属的线程类有默认的异常处理器(
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
方法设置的异常处理器),那就调用该异常处理器来处理该异常- 如果该异常对象是ThreadDeath的对象,不做任何处理;否则将StackTrace的信息打印到System.err错误输出力,结束该线程
例:
下面自定义一个线程的异常处理器,然后和JVM默认的线程处理器对比一下
- 自定义的线程异常处理器
package thread.exceptionHandler;
public class MyExHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(e + " appears in " + t);
}
}
测试类:
package thread.exceptionHandler;
public class TestExHandler {
public static void main(String[] args) {
int a = 5 / 0;
}
}
毫无疑问main方法里仅有的一句代码会引发除0异常
输出的异常信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at thread.exceptionHandler.TestExHandler.main(TestExHandler.java:8)
然后我在int a = 5 / 0
之前设定main线程的默认异常处理器为我自定义的异常处理器:
package thread.exceptionHandler;
public class TestExHandler {
public static void main(String[] args) {
// set the new Handler
Thread.setDefaultUncaughtExceptionHandler(new MyExHandler());
int a = 5 / 0;
}
}
这样main线程的默认异常处理器就设成了我自定义的异常处理器
输出的异常信息:
java.lang.ArithmeticException: / by zero appears in Thread[main,5,main]
5.6 Callabe 和 Future
Callable是Runnable接口的增强版,Callable也提供了一个call()
方法作为线程执行体,但它比run方法更加强大牛逼:
call()
方法可以有返回值call()
方法可以声明抛出异常
类似于Runnable的用法,我们可以提供一个Callable对象作为Thread的target,而call()
方法作为该线程的线程执行体。问题是:Callable接口并不是Runnable接口的子接口,而Thread的构造方法里形参的类型是Runnable,所以Callable对象不能直接作为Thread的target;而且call方法还有一个返回值——但call方法不是直接调用,而是作为线程执行体被调用的。为了解决这几个问题,java提供了Future接口来代表Callable接口里call方法的返回值,并且为Future接口提供了一个FutureTask实现类,这个类实现了Future接口,也实现了Runnable接口(Adapter模式),这样就可以作为Thread类的target了。
在Future接口里定义了下面几个公共方法来控制它关联的Callable任务:
boolean cancel(boolean mayInterruptIfRunning)
试图取消Future里关联的Callable任务。
V get()
返回Callable里call方法的返回值,直接调用该方法将导致程序阻塞,必须等到子线程结束时才会得到返回值
V get(long timeout, TimeUnit unit)
返回Callable里call方法的返回值。该方法让程序最多阻塞
timeout
和unit
指定的时间。如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常boolean isCancelled
如果在Callable任务正常完成前被取消则返回true
boolean isDone()
如果Callable任务已经完成则返回true
使用Callable的步骤:
创建Callabe接口的实现类,并实现call方法。
注意Callable接口有泛型限制,Callable接口里的泛型形参类型与call方法返回值类型相同
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call方法的返回值
使用FutureTask对象作为Thread对象的target来创建、启动线程
调用FutureTask对象的方法来获得子线程执行结束之后的返回值
例:
- 创建Callabe接口的实现类,并实现call方法。
package thread.Callable;
import java.util.concurrent.Callable;
public class RunThread implements Callable<Integer>{
@Override
public Integer call() {
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " : i = " + i);
}
return i;
}
}
测试类:
package thread.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
// 2.1 create instance of RunThread
RunThread run = new RunThread();
// 2.2 create instance of FutureTask
// and use it to wrap the instance of RunThread
FutureTask<Integer> task = new FutureTask<Integer>(run);
for (int j = 0; j < 30; j++) {
System.out
.println(Thread.currentThread().getName() + " : j = " + j);
if (j == 20) {
try {
new Thread(task, "Vice Thread").start();
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
System.out.println("return value of Vice Thread is : "
+ task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
程序最后调用FutureTask对象的get方法;来返回call方法的返回值,该方法将导致主线程被阻塞,知道call方法结束并返回为止
5.7 Thread Pool(线程池)
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情况下,使用线程池可以很好的提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
==线程池在系统启动时就创建了大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束之后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。==
使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时会导致系统性能严重下降,甚至会导致JVM崩溃,而线程池的最大线程参数可以控制系统中并发线程不超过此数目。
java提供了一个Executors工厂类来生产线程池,包含下面几个==静态工厂方法==来创建线程池:
newCachedThreadPool()
创建一个具有缓存功能的线程池,系统根据需要创建线程,这线程会被缓存在线程池中
newFixedThreadPool(int nThreads)
创建一个可重用的、具有固定线程数的线程池
newSingleThreadExecutor()
创建一个只有单线程的线程池,相当于
newFixedThreadPool(int nThreads)
传入参数为1newScheduledThreadPool(int corePoolSize)
创建具有固定线程数的线程池,可以在指定延迟后执行线程任务。
corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池里
newSingleThreadScheduledExecutor()
创建只有固定线程的线程池,可以在指定延迟后执行线程任务。
前三个方法返回一个ExecutorService
对象,该对象代表一个线程池,可以执行Runnable或Callable对象所代表的线程。
ExecutorService
代表==尽快执行线程的线程池(只要池中有空闲线程就立即执行线程任务)==,程序只需要将一个Runnable或Callable对象提交给该线程池即可,该线程池就会尽快执行该任务(实现的细节对客户解耦)。
ExecutorService
提供了三个方法:
Future<?> submit(Runnable Task)
将一个Runnable对象提交给制定的线程池,线程池在由空闲线程的时候立刻执行Runnable对象所代表的任务。
Future对象代表Runnable任务的返回值,但是run方法没有返回值——所以Future对象在fun方法执行结束后返回null。*==但是可以调用Future的isDone,isCancelled方法来获得Runnable对象的执行状态。==*
<T> Future<T> submit(Runnable task, T result)
<T> Future<T> submit(Callable<T> task)
后两个方法返回一个ScheduledExecutorService
,是ExecutorService
的子类,可以在指定延迟后执行线程任务。
ScheduledExecutorService
代表==可以在制定延迟或周期性执行线程任务的线程池==,提供了以下四个方法:
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
指定的Callable任务将在delay后延迟执行
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
指定的Runnable任务将在delay后执行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
指定的Runnable任务将在delay后执行,并且以设定频率重复执行。
(在intialDealy后开始执行,依次在initialDelay + period、nitialDelay + period * 2 …… 处重复执行)
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
创建并执行一个在给定初始延迟后首次启用的定期操作,随后,==在每一次执行终止和下一次执行开始之间都存在给定的延迟。== 如果任务的任一此执行时遇到异常,就会取消后续执行。否则只能通过程序来显式的取消或终止任务。
当用完一个线程池,应该调用该线程池的shutdowm()
方法,该方法将启动线程池的关闭序列,调用了shutdown()
方法后的线程池==不再接受新任务,但会将以前所有已经提交的任务执行完成。当线程池中的所有任务都执行完成后,池中所有线程都会死亡==
另外也可以调用shutdownNow()
方法来关闭线程池,该方法视图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。(简单粗暴。。。)
使用线程池来执行线程任务的步骤:
调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
创建Runnable或Callable实现类的实例,作为线程任务
调用ExecutorService对象的submit方法来提交Runnable或Callable任务
当不想再提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池
例:
package thread.threadPool;
public class RunTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " : i = " + i);
}
}
}
package thread.threadPool;
import java.util.concurrent.Callable;
public class CallTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int j = 0;
for (; j < 20; j++) {
System.out.println(Thread.currentThread().getName() + " : j = " + j);
}
return j;
}
}
package thread.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
// 1.create threadPool
ExecutorService pool = Executors.newFixedThreadPool(6);
// 2. create tasks
// 3. submit tasks
pool.submit(new CallTask());
pool.submit(new RunTask());
// 4. shut down the pool after execution
pool.shutdown();
}
}
5.8 ThreadLocal
通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以很简洁的编写出优美的多线程程序。
ThreadLocal,是ThreadLocalVariable的意思。功能是:==为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本发生冲突。==从每一个线程的角度来看,好像每一个线程都==完全拥有该变量==。
ThreadLocal的用法非常简单,提供了三个public方法:
T get()
返回此线程局部变量中当前线程副本的值
void remove()
删除此线程局部变量中当前线程的值
void set(T value)
设置此线程局部变量中当前线程副本的值
例:
package thread.threadLocal;
public class Account {
private ThreadLocal<String> name = new ThreadLocal<String>();
public Account(String name) {
this.name.set(name);
System.out.println("----" + this.name.get());
// System.out.println(Thread.currentThread().getName());
}
public String getName() {
return this.name.get();
}
public void setName(String name) {
this.name.set(name);
}
}
package thread.threadLocal;
public class MyThread extends Thread {
private Account account;
public MyThread(Account account, String name) {
super(name);
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 6) {
this.account.setName(this.getName());
}
System.out.println(this.account.getName() + " : i = " + i);
}
}
}
测试类:
package thread.threadLocal;
public class TestThreadLocal {
public static void main(String[] args) {
Account account = new Account("initAccount");
new MyThread(account, "MyThread 1").start();;
new MyThread(account, "MyThread 2").start();
}
}
结果:
----initAccount----
null : i = 0
null : i = 1
null : i = 0
null : i = 2
null : i = 1
null : i = 3
null : i = 2
null : i = 4
null : i = 3
null : i = 5
null : i = 4
MyThread 1 : i = 6
null : i = 5
MyThread 1 : i = 7
MyThread 2 : i = 6
MyThread 2 : i = 7
MyThread 2 : i = 8
MyThread 2 : i = 9
MyThread 1 : i = 8
MyThread 1 : i = 9
上面的代码中,有两条线程,只有一个账户,即只有一个账户名
但由于账户名是ThreadLoacl类型,所以两条线程虽然有同一个Account,但每条线程都各有一个账户名的副本,两个副本毫无关系,互不影响
所以从
i == 6
开始,将看到里那个条线程访问同一个账户时会有不同的账户名。
总结:
==ThreadLocal和其他所有的同步机制都是为了解决多线程中对同一变量的访问冲突。==
在普通的同步机制中,是通过对对象加锁的方式来实现对同一变量的安全访问:
synchronized(obj)
- 把方法用
synchronized
修饰成同步方法 - 显式使用Lock对象的lock和unlock方法加锁、释放锁
在这种情形下,该变量是多个线程共享的,所以需要和细致的分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候需要释放该对象的锁。
在这种情况下,==系统并没有将这份同步资源复制多分,只是用了安全机制来控制对这份资源的访问而已。==
ThreadLocal从另一个角度来解决多线程的并发访问:**==ThreadLocal将需要并发访问的资源复制出多份来,每个线程拥有一份资源,每一个线程都拥有自己独立的资源副本,从而就没有必要对该变量进行同步了。==**ThreadLocal提供了线程安全的共享对象,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态用ThreadLocal保存。
但是必须注意的是:ThreadLocal并不能替代同步机制!两者面向的领域不同。
同步机制是为了同步多个线程对相同资源的并发访问,是==多个线程之间进行通信==的有效方式。(想一下之前两个人取同一个账户的钱,当一个取完800元之后,账户只剩下200元,另一个人相取800元会想提示取款失败,这就涉及到了线程之间的通信)
而ThreadLocal是==隔离多个线程的数据共享==(每个线程拥有一份独立的副本,各个线程对自己的副本操作,而不是对同步资源操作),从根本上避免了多个线程之间共享资源(变量),从而也就不需要对多个线程进行同步了。
所以:
如果需要进行多个线程之间共享资源,达到线程之间的通信功能 ——> 用==同步机制==
如果仅仅需要隔离多个线程之间的共享冲突,可以用==ThreadLocal==
5.9 包装线程不安全的集合
ArrayList、LinkedList、HashSet、HashMap等都是==线程不安全==的,也就是有可能当多个线程向这些集合放入一个元素时,可能会破坏集合数据的完整性。
可以使用Collections类提供的静态方法来把这些集合包装成线程安全的集合:
<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)
返回指定Map对应的线程安全的Map
static <T> Set<T> synchronizedSet(Set<T> s)
返回指定Set对应的线程安全的set
static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> m)
返回指定SortedMap对应的线程安全的SortedMap
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
返回指定SortedSet对应的线程安全的SortedSet
==如果需要把某个集合包装成线程安全的集合,应该在创建之后立即包装!==
例:
HashMap m = Collections.sychronizedMap(new HashMap());
5.10 线程安全的集合类
JDK1.5后再java.util.concurrent包下提供了==ConcuurentHashMap==和==ConcurrentLinkedQueue==支持并发访问的集合。
默认情况下都支持多条线程并发写访,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。
当多个线程共享访问一个公共集合时,==ConcurrentLinkedQueue==是一个合适的选择:
不允许使用null元素
实现了多线程的高效访问,多条线程访问==ConcurrentLinkedQueue==时无需等待。
与HashMap和普通集合不同的是,ConcuurentHashMap和ConcurrentLinkedQueue支持多线程并发访问,所以==当使用迭代器来遍历元素时,该迭代器可能不能反应出创建迭代器之后做的修改,但程序不会抛异常。==
使用java.util包下的Collection作为集合对象时,==如果该集合对象创建迭代器后集合元素发生改变,将引发
ConcurrentModificationException
==