Java线程
什么是线程
线程,有时候被称为轻量进程,是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针,寄存器集合和堆栈组合。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,只是线程在运行中呈现间断性。
单线程程序
先来看一个单线程程序
1 | package demo; |
Java程序在执行时,至少会有一个线程在运行。上述代码中运行的就是被称作主线程的线程。主线程会执行Main类的main方法。在main方法中所有的处理都执行完毕之后,主线程也就终止了。
实现多线程
在Java中,有两种方法是可以实现多线程的。一种是继承Thread类,另外一种是实现Runnable接口。
继承Trhead
先来看一段代码。
MyThread类
1 | package demo; |
Main类
1 | package demo; |
执行Main类中的main 方法后,就“可能”得到如下输出:
之所以说是可能,是因为线程被调度的时间点是不确定的,它可能在主线程执行完毕之后才调度,也可能在主线程执行了一半才调度。
线程的操作都写在run方法中,当run方法执行结束时,线程也会跟着终止。然而,要启动一个线程,还必须调用它的start方法。
实现Runnable接口
还是先看一段代码
Mythread1类
1 | package demo; |
Main类
1 | package demo; |
执行结果
线程的启动
其实线程的启动已经在上一章节说的差不多了,主要有两种方式:
1、 利用Thread类的子类的实例启动线程;
2、 利用Runnable接口的实现类的实例启动线程。
线程的暂停
线程Thread类中的静态方法sleep能够暂停线程运行。
1 | package demo; |
执行上面的代码后,就会发现程序会以一秒的间隔输出一句Thread
在sleep方法中,停止时间也可以制定到纳秒单位。
1 | Thread.sleep(毫秒,纳秒) |
线程的互斥
多线程程序中的各个线程都是自由运行的,所有它们有时候就会同时操作同一个实例。这在某些情况下会引发问题。例如,从银行账户取款时,余额确认部分的代码应该是像下面这样的:
if (可用余额大于等于取款金额) {
从可用余额上减去取款金额
}
首先确认可用余额,确认是否允许存款。如果允许,则从可用余额上减去取款金额,这样才不会导致可用余额变为负数。
但是,如果两个线程同时执行这段代码,那么可用余额就有可能变为负数。
假设可用余额为100块,需要取款80块,那么在下面这种情况下,余额就可能变为负数:
线程A 线程B
可用余额是否大于取款金额
是的
《在此时切换至线程B》
可用余额是否大于取款金额
是的
从可用余额上减去取款金额
可用余额变为20块
《在此时切换回线程A》
从可用余额上减去取款金额
可用余额变为-60块
这种线程A和线程B之间互相竞争而引起的与预期相反的情况称为数据竞争或者竞态条件。
这个时候就需要有一种“交通管制”来协助防止发生数据金正。例如,一个线程正在执行某一部分操作,那么其他线程就不可以再执行这部分操作。这种类似于交通管制的操作通常称之为互斥。
Java用synchronized关键字来执行线程的互斥处理。
synchronized方法
如果声明一个方法时,在前面加上关键字synchd,那么这个方法就只能由一个线程运行。只能由一个线程运行是每次只能由一个线程运行的意思,并不是说仅能让某以特定线程运行。这种方法称之为synchronized方法,有时也称之为同步方法。
那么存取款这个过程就可以改为:
1 | package demo; |
对每个Bank实例来说,它们都拥有一个独立的说,因此,实例1和实例2之间是互相不影响的。但是在实例内部,如果正在执行加锁的方法,deposit,那么就不能执行withdraw方法,因为此时锁正被方法deposit持有,withdraw方法只能阻塞直至获取锁为止。但是,没有加上synchronized关键字的getName方法,是可以随便调用的,调用它不需要任何条件。
synchronized代码块
如果只是想让方法中的某一部分由一个线程运行,而非整个方法,则可以使用synchronized代码块,格式如下所示:
1 | synchronized (表达式) { |
synchronized代码块用于精确控制互斥处理的执行范围。
synchronized实例方法和synchronized代码块
假设有如下synchronized示例方法。
1 | synchronized void method() { |
这根下面将方法体用synchronized代码块包围起来是等效的,
1 | void method() { |
也就是说,synchronized示例方法是使用this的锁来执行线程的互斥处理的。
synchronized静态方法和synchronized代码块
假设有如下synchronized静态方法。synchronized静态方法每次只能由一个线程运行,这一点和synchronized实例方法相同。但synchronized静态方法使用的锁和synchronized实例方法使用的锁是不一样的。
1 | class Something { |
这跟下面将方法体用synchronized代码块包围起来是等效的。
1 | class Something { |
也就是说,synchronized静态方式是使用该类的类对象的锁来执行线程的互斥处理的。Something.class是Something类对应的java.lang.class类的实例。
线程的协作
等待队列
所有的实例都拥有一个等待队列,他是在实例的wait方法执行后停止操作的线程的队列。
当执行wait方法后,线程便会暂停操作,进入等待队列。除非发现下列某一种情况,否则线程会一直在等待队列中休眠。当下列任意一种情况发生时,线程便会退出等待队列。
- 有其他线程的notify方法来唤醒线程
- 有其他线程的notifyAll方法来唤醒线程
- 有其他线程的interrupt方法来唤醒线程
- wait方法超时
注意:等待队列是一个虚拟的概念。它既不是实例中的字段,也不是用于获取正在实力上等待的线程的列表的方法。
wait方法
wait方法会让线程进入等待队列。假设执行了下面这条语句
1 | obj.wait() |
那么当前线程便会暂停运行,并进入obj实例的等待队列中,这叫做“线程正在obj上wait”。
如果实例方法中有如下语句,有下面代码(1)处语句,由于其含义等同于(2)处语句,所以执行了wait()的线程将会进入this的等待,队列中,这可以说“线程正在this上wait”。
1 | wait(); // 1 |
若要执行wait方法,线程必须持有锁。但如果线程进入等待队列,便会释放其占有的实例的锁。
notify方法
notify(通知)方法会将等待队列中的一个线程取出。假设我们执行里下面的这条语句。
1 | obj.notify() |
那么obj的等待队列中的一个线程便会选中和唤醒,然后就会退出等待队列。
notify唤醒的线程并不会在执行notify的一瞬间重新运行。因为在执行notify的那一瞬间,执行notify的线程还持有着锁,所以其他线程还无法获取这个实例的锁。
加入在执行notify方法时,正在等待队列中等待的线程不止一个,对于“这时该如何来选择线程”这个问题规范中并没有做出规定。就近视选择最先wait的线程还是随机选择,或者采用其他方法要取决于Java平台运行环境。因此编写程序是需要注意,最好不要编写依赖于所选线程的程序。
notifyAll方法
notifyAll方法会将等待队列中的所有线程都取出来。例如,执行下面这条语句之后,在obj实例的等待队列中休眠的所有线程都会被唤醒。
1 | obj.notifyAll() |
如果简单地在示例方法中写成下面代码(1)处这样,那么由于其含义等同于(2),所以该语句所在方面的实例(this)的等待队列中所有线程都会退出等待队列。
1 | notifyAll() // 1 |
注意:
- 如果未持有锁的线程调用wait、notify或者notifyAll,异常java.lang.IllegalMonitorStateException会被抛出。
- 由于notify唤醒的线程较少,所以处理速度要比使用notifyAll时快
- 但使用notify时,如果处理不好,程序便可能会停止。一般来说,使用notifyAll的代码比使用notify时更为健壮。
- 除非开发人员完全理解代码的含义和范围,否则使用notifyAll更为稳妥。
notify方法仅欢迎一个线程,而notifyAll则唤醒所有线程,这是两者之间的唯一区别。
线程的状态转移
具体见下图,括号中的单词是Thread.State中定义的Enum名。
引用
百度百科-线程:https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B
《图解Java多线程设计模式》