操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。
实现多任务的方法,有以下几种:
多进程模式(每个进程只有一个线程):
1 | ┌──────────┐ ┌──────────┐ ┌──────────┐ |
多线程模式(一个进程有多个线程):
1 | ┌────────────────────┐ |
多进程+多线程模式(复杂度最高):
1 | ┌──────────┐┌──────────┐┌──────────┐ |
创建线程
要实例化一个Thread
实例,然后调用它的start()
方法:
1 | public class Main { |
这个线程启动后实际上什么也不做就立刻结束了。我们希望新线程能执行指定的代码,有以下几种方法:
方法一:从Thread
派生一个自定义类,然后覆写run()
方法:
1 | // 多线程 |
start()
方法会在内部自动调用实例的run()
方法。
方法二:创建Thread
实例时,传入一个Runnable
实例:
1 | // 多线程 |
线程的优先级
可以对线程设定优先级,设定优先级的方法是:
1 | Thread.setPriority(int n) // 1~10, 默认值5 |
线程的状态
Java线程的状态有以下几种:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行
run()
方法的Java代码; - Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待; - Terminated:线程已终止,因为
run()
方法执行完毕。
状态转移图表示如下:
1 | ┌─────────────┐ |
一个线程还可以等待另一个线程直到其运行结束:
1 | // 多线程 |
join
就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main
线程先打印start
,t
线程再打印hello
,main
线程最后再打印end
。
通过对另一个线程对象调用
join()
方法可以等待其执行结束;可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用
join()
方法会立刻返回。
线程中断
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能立刻结束运行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
1 | // 中断线程 |
interrupt()方法仅仅向
t线程发出了“中断请求”,至于
t线程是否能立刻响应,要看具体代码。而
t线程的
while循环会检测
isInterrupted(),所以上述代码能正确响应
interrupt()请求,使得自身立刻结束运行
run()方法。
守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
JVM退出时,不必关心守护线程是否已结束。
创建守护线程,在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
1 | Thread t = new MyThread(); |
守护线程不能持有任何需要关闭的资源
线程同步
任何时候临界区最多只有一个线程能执行。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。
Java程序使用synchronized
关键字对一个对象进行加锁:
1 | synchronized(lock) { |
两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。
VM规范定义了几种原子操作:
- 基本类型(
long
和double
除外)赋值,例如:int n = m
; - 引用类型赋值,例如:
List<String> list = anotherList
。
同步方法
Java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。
更好的方法是把synchronized
逻辑封装起来。例如,我们编写一个计数器如下:
1 | public class Counter { |
上述代码,线程调用add()、dec()方法时,不必关心同步逻辑。因为synchronized代码块在方法内部。并且, synchronized
锁住的对象是this
,即当前实例,这又使得创建多个Counter
实例的时候,它们之间互不影响,可以并发执行:
1 | var c1 = Counter(); |
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter
类就是线程安全的。Java标准库的java.lang.StringBuffer
也是线程安全的。
还有一些不变类,例如String
,Integer
,LocalDate
,它们的所有成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList
,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList
是可以安全地在线程间共享的。
没有特殊说明时,一个类默认是非线程安全的。
发布时间: 2019-11-13
最后更新: 2019-12-03
本文链接: https://juoyo.github.io/posts/8625f16f.html
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!