并发
本文档建立在之前博客基础上。
- 直接调用
Thread
类或Runnable
对象的run()
方法,只会执行统一线程中的任务,而不会启动新线程。调用start()
方法,此方法将创建一个执行run
方法的新线程。
中断线程
-
调用
interrupt()
方法,可以请求终止线程。调用Thread.currentThread()
得到当前线程,再使用isInterrupt()
来判断线程是否被终止。 -
若线程被阻塞(调用
sleep
或wait
),则无法检测中断状态。• void interrupt() 向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个sleep调用阻塞,那么,InterruptedException 异常被抛出。 • static boolean interrupted() 测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用—它将当前线程的中断状态重置为 false。 • boolean islnterrupted() 测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。 • static Thread currentThread() 返回代表当前执行线程的 Thread 对象。
线程状态
- 线程可以有如下 6 种状态:
-
New (新创建)
-
Runnable (可运行)
-
Blocked (被阻塞)
-
Waiting (等待)
-
Timed waiting (计时等待)
-
Terminated (被终止)
- 要确定一个线程的当前状态,调用
getState()
方法。
抢占式调度和协作式调度
- 抢占式调度系 统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行 权, 并给另一个线程运行机会当选择下一个线程时, 操作系统考虑线程的优先级。 现在所有的桌面以及服务器操作系统都使用抢占式调度 。
- 像手机这样的小型设备 可能使用协作式调度。在这样的设备中,一个线程只有在调用 yield 方法、 或者被阻塞或等 待时,线程才失去控制权。
在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什 么将这个状态称为可运行而不是运行
被阻塞线程和等待线程
- 当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。
线程属性
线程优先级、守护线程、 线程组以及处理未捕 获异常的处理器。
-
线程优先级
- 在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下, 线程继承它的父 线程的优先级。可以用
setPriority
方法提高或降低任何一个线程的优先级。 - 每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。但是,线程 优先级是高度依赖于系统的。
- 不要将程序构建 为功能的正确性依赖于优先级。
- 在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下, 线程继承它的父 线程的优先级。可以用
-
守护线程
- 可以通过调用
t.setDaemon(true)
; 将线程转换为守护线程。 这一方法必须在线程启动之前调用。 守护线程的唯一用途 是为其他线程提供服务。 当只剩下守护线程时, 虚拟机就退出了 。 - 守护线程应该永远不去访问固有资源, 如文件、 数据库,因为它会在任何时 候甚至在一个操作的中间发生中断。
- 可以通过调用
-
未捕获异常处理器
-
线程的 run方法不能抛出任何受查异常(必须在内部使用
try...catch
来解决发生的任何必须处理异常), 但是,非受査异常(典型代表:RuntimeException
)会导致线程终止。在这种情 况下,线程就死亡了。 -
在线程死亡之前, 异常被传递到一个用于未捕获异常的处理器。 该处理器必须属于一个实现
Thread.UncaughtExceptionHandler
接口的类。这个接口只有一个方法:void uncaughtException(Thread t, Throwable e)
-
可以用
setUncaughtExceptionHandler
方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法setDefaultUncaughtExceptionHandler
为所有线程安装一个默认的处理器。 -
如果不安装默认的处理器, 默认的处理器为空。但是, 如果不为独立的线程安装处理 器,此时的处理器就是该线程的
ThreadGroup
对象。Thread aThread = new Thread(() -> { System.out.println("执行开始"); int a = 10 / 0; System.out.println("执行结束"); }, "A"); aThread.start(); // 默认线程组处理结果 /* 执行开始 Exception in thread "A" java.lang.ArithmeticException: / by zero at BaseLearn.multithreadingTest.Test6.Main.lambda$main$0(Main.java:11) at java.lang.Thread.run(Thread.java:748) */ Thread aThread = new Thread(() -> { System.out.println("执行开始"); int a = 10 / 0; System.out.println("执行结束"); }, "A"); aThread.setUncaughtExceptionHandler((Thread t, Throwable e) -> { System.out.println("发现错误了"); System.out.println(e.toString()); }); aThread.start(); // 添加自定义异常处理 Thread aThread = new Thread(() -> { System.out.println("执行开始"); int a = 10 / 0; System.out.println("执行结束"); }, "A"); aThread.setUncaughtExceptionHandler((Thread t, Throwable e) -> { System.out.println("发现错误了"); e.printStackTrace(); }); aThread.start(); /* 执行开始 java.lang.ArithmeticException: / by zero 发现错误了 at BaseLearn.multithreadingTest.Test6.Main.lambda$main$0(Main.java:11) at java.lang.Thread.run(Thread.java:748) */
-
线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组, 但是, 也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作, 所以建议不要在自己的程序中使用线程组。
-
同步
-
在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。若线程调用了一个修改该对象状态的方法,根据各线程访问数据的次序,可能会产生讹误的对象。这样一个情况通常称为竞争条件。
竞争条件例子:(示例来自Java核心技术卷14.5.1,643页)
模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱 款的交易。每一个账户有一个线程。每一笔交易中, 会从线程所服务的账户中随机转移一定 数目的钱款到另一个随机账户。
// 第一部分截取 public void transfer(int from, int to, double amount { System.out.print(Thread,currentThread0); // 从accounts的第from账户中扣钱 accounts[from] -= amount; System.out.printff" X10.2f from Xd to Xd", amount, from, to); // 加给to账户 accounts[to] += amount; System.out.printf("Total Balance: X10.2fXn", getTotalBalance()); } // 第二部分截取 Runnable r = () -> { try { while (true) { // 转义账户随机生成 int toAccount = (int) (bank.sizeO * Math.random()); // 转义钱数 double amount = MAX_AMOUNT * Math.random(); // 转移 bank.transfer(fromAccount, toAccount, amount); // 暂停一会 Thread,sleep((int) (DELAY * Math.randomO)); } } catch (InterruptedExeeption e){ } } ;
示例程序源代码存放在个人码云示例中点击访问
执行结果:
Thread[Thread-26,5,main] 82.57 from 26 to 35 Total Balance: 100000.00 Thread[Thread-44,5,main] 819.26 from 44 to 49 Total Balance: 100000.00 Thread[Thread-37,5,main] 966.78 from 37 to 40 Total Balance: 100000.00 Thread[Thread-5,5,main] 811.87 from 5 to 89 Total Balance: 100000.00 Thread[Thread-81,5,main] 344.01 from 81 to 3 Total Balance: 100000.00 Thread[Thread-14,5,main] 730.87 from 14 to 17 Total Balance: 100000.00 Thread[Thread-87,5,main] 965.71 from 87 to 97 Total Balance: 100000.00 Thread[Thread-6,5,main]Thread[Thread-62,5,main]Thread[Thread-39,5,main] 313.93 from 39 to 56 Total Balance: 99331.28 Thread[Thread-57,5,main] 31.16 from 57 to 36 Total Balance: 99331.28 Thread[Thread-77,5,main]Thread[Thread-57,5,main] 523.69 from 57 to 19 Total Balance: 98750.24 Thread[Thread-17,5,main] 537.68 from 17 to 22 Total Balance: 98750.24 581.04 from 77 to 13 Total Balance: 99331.28 Thread[Thread-13,5,main] 212.00 from 13 to 46 Total Balance: 99331.28 Thread[Thread-91,5,main] 98.60 from 91 to 34 Total Balance: 99331.28 Thread[Thread-20,5,main] 898.50 from 20 to 95 Total Balance: 99331.28 15.54 from 62 to 70 Total Balance: 99346.82 653.18 from 6 to 34 Total Balance: 100000.00 Thread[Thread-20,5,main] 777.97 from 20 to 14 Total Balance: 100000.00 Thread[Thread-13,5,main] 194.07 from 13 to 40 Total Balance: 100000.00 Thread[Thread-74,5,main] 815.60 from 74 to 58 Total Balance: 100000.00 Thread[Thread-70,5,main] 844.52 from 70 to 73 Total Balance: 100000.00
由结果可以看出。一段时间之后, 错误不知不觉地出现了,总额要么增加, 要么变少。当两个线程试图同时更新同一个账户的时候, 这个问题就出现了。
-
个人分析:因为线程执行时有抢占资源的问题,假定现在Thread-1线程为A向B转钱,A账户钱数减少时,Thread-2线程此时抢占到了资源开始运行,此时A的钱数少了,所以银行总钱数减少,但是后续线程A又拿到了运行权,此时才会继续执行下面的增加B账户钱数的运算。所以后面钱数又回归正常了。
-
官方分析:假定两个线程同时执行指令
accounts[to] += amount;
问题在于这不是原子操作。该指令可能被处理如下:
- 将 accounts[to] 加载到寄存器。
- 增加 amount。
- 将结果写回 accounts[to]。
现在,假定第 1 个线程执行步骤 1 和 2, 然后, 它被剥夺了运行权。假定第 2 个线程被 唤醒并修改了 accounts 数组中的同一项。然后,第 1 个线程被唤醒并完成其第 3 步。 这样, 这一动作擦去了第二个线程所做的更新。于是, 总金额不再正确。(见图 14-4J 我们的测试程序检测到这一i化误。(当然, 如果线程在运行这一测试时被中断,也有可能 会出现失败警告!)
锁对象
-
有两种机制防止代码块受并发访问的干扰。Java语言提供一个
synchronized
关键字达 到这一目的,并且 Java SE 5.0引入了ReentrantLock
类。synchronized
关键字自动提供一个 锁以及相关的“ 条件”, 对于大多数需要显式锁的情况, 这是很便利的。 -
用 ReentrantLock 保护代码块的基本结构如下:
myLock.lock(); try { critical section } finally { myLock.unlockO; }
-
这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象, 其他任 何线程都无法通过 lock语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放 锁对象。
-
把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常, 锁必须被释放。否则, 其他线程将永远阻塞。
-
如果使用锁, 就不能使用带资源的 try语句。
修改示例:
public class Bank { private Lock bankLock = new ReentrantLock0; public void transfer(int from, int to, int amount) t bankLock.lock(); try { System.out.print(Thread.currentThread0); accounts[from] -= amount; System.out.printf(" X10.2f from %A to Xd", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: X10.2fXn", getTotalBalanceO); } finally { banklock.unlock(); } }
结果:
101.10 from 93 to 68 Total Balance: 100000.00 Thread[Thread-66,5,main] 20.39 from 66 to 41 Total Balance: 100000.00 Thread[Thread-5,5,main] 905.92 from 5 to 89 Total Balance: 100000.00 Thread[Thread-87,5,main] 608.07 from 87 to 41 Total Balance: 100000.00 Thread[Thread-38,5,main] 942.93 from 38 to 84 Total Balance: 100000.00 Thread[Thread-39,5,main] 757.74 from 39 to 32 Total Balance: 100000.00 Thread[Thread-62,5,main] 503.86 from 62 to 16 Total Balance: 100000.00 Thread[Thread-4,5,main] 417.09 from 4 to 51 Total Balance: 100000.00 Thread[Thread-67,5,main] 781.40 from 67 to 67 Total Balance: 100000.00 Thread[Thread-56,5,main] 972.45 from 56 to 74 Total Balance: 100000.00 Thread[Thread-91,5,main] 102.22 from 91 to 65 Total Balance: 100000.00 Thread[Thread-58,5,main] 112.92 from 58 to 51 Total Balance: 100000.00 Thread[Thread-61,5,main] 375.14 from 61 to 64 Total Balance: 100000.00 Thread[Thread-1,5,main] 275.49 from 1 to 40 Total Balance: 100000.00 Thread[Thread-33,5,main] 368.64 from 33 to 53 Total Balance: 100000.00 Thread[Thread-25,5,main] 875.35 from 25 to 86 Total Balance: 100000.00 Thread[Thread-30,5,main] 104.90 from 30 to 21 Total Balance: 100000.00
-