多进程与多线程本质的区别:每个进程都拥有自己的一整套变量,而线程则共享数据。
12.1 什么是线程
- 将执行这个任务的代码放在一个类的run方法中,这个类要实现Runnable接口。
Runnable接口非常简单,只有一个run方法:
public interface Runnable
{
void run();
}
- 从这个Runnable构造一个Thread对象:
var t = new Thread(r);
- 启动线程:
t.start(); //start在新线程调用run方法
调用Thread.start方法,这会创建一个执行run方法的新线程。不要调用Thread类或Runnable对象的run方法。直接调用run方法只会在同一个线程中执行这个任务——而没有启动新的线程。
12.2 线程状态
抢占式调度系统 :给每一个可运行线程一个时间片来执行任务。当时间片用完时,操作系统剥夺该线程的运行权,并给另一个线程一个机会来运行。
现在所有的桌面以及服务器操作系统都使用抢占式调度。但是,像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用yield方法或者被阻塞或等待时才失去控制权。
12.2.1 新建线程
用new操作符创建一个新线程时,如newThread®,这个线程还没有开始运行。这意味
着它的状态是新建。
12.2.2 可运行线程
一旦调用start方法,线程就处于可运行(runnable)状态。一个可运行的线程可能正在
运行也可能没有运行。要由操作系统为线程提供具体的运行时间。
在有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当
然,如果线程的数目多于处理器的数目,调度器还是需要分配时间片。
12.2.3 阻塞和等待线程
要由线程调度器重新激活这个线程。具体细节取决于它是怎样到达非活动状态的。
- 当一个线程试图获取一个内部的对象锁,这个锁目前被其他线程占有,该线程就会被阻塞。当所有其他线程都释放了这个锁,并且线程调度器允许该线程持有这个锁时,它将变成非阻塞状态。
- 当线程等待另一个线程通知调度器出现一个条件时,这个线程会进人等待状态。
- 有几个方法有超时参数,调用这些方法会让线程进人计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep 和计时版的 Object.wait, Thread.join, Lock.tryLock 以及 Condition.await。
当一个线程阻塞或等待时(或终止时),可以调度另一个线程运行。当一个线程被重新激活(例如,因为超时期满或成功地获得了一个锁),调度器检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器会剥夺某个当前运行线程的运行权,选择一个新线程运行。
12.2.4 终止线程
线程会由于以下两个原因之一而终止:
- run方法正常退出,线程自然终止。
- 因为一个没有捕获的异常终止了 run方法,使线程意外终止。
12.3线程属性
12.3.1 中断线程
-
除了已经废弃的stop方法,没有办法可以强制线程终止。不过,interrupt方法可以用来请求终止一个线程。
-
要想得出是否设置了中断状态,首先调用静态的Thread. currentThread方法获得当前线程,然后调用islnterrupted方法:
while (! Thread. currentT read (). islnterrupted () && more work to do)
{
do more work
}
-
如果线程被阻塞,就无法检查中断状态。这里就要引人InterruptedException异常。
当在一个被sleep即或wait调用阻塞的线程上调用interrupt方法时,那个阻塞调用将被一个InterruptedException异常中断。 -
**没有任何语言要求被中断的线程应当终止。**中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。某些线程非常重要,所以应该处理这个异常,然后再继续执行。但是,**更普遍的情况是,线程只希望将中断解释为一个终止请求。**这种线程的run方法
具有如下形式:
Runnable r = () -> {
try
{
while (!Thread.currentThread().islnterrupted() && more work to do)
{
do more work
}
}
catch(InterruptedException e)
{
// thread was interrupted during sleep or wait
}
finally
{
cleanup, if required
}
II exiting the run method terminates the thread
};
- 如果在每次工作迭代之后都调用sleep方法(或者其他可中断方法),islnterrupted检查既没有必要也没有用处。如果设置了中断状态,此时倘若调用sleep方法,它不会休眠。实际上,它会清除中断状态(!)并抛出InterruptedException。因此,如果你的循环调用了sleep,不要检测中断状态,而应当捕获InterruptedException异常,如下所示:
Runnable r = () -> {
try {
while (more work to do) {
//do more work
Thread.sleep(delay);
}
} catch (InterruptedException e) {
// thread was interrupted during sleep
} finally {
//cleanup, if required
}
// exiting the run method terminates the thread
};
6.
12.3.2 守护线程
- 守护线程的唯一用途是为其他线程提供服务。计时器线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程,另外清空过时缓存项的线程也是守护线程。
- 当只剩下守护线程时,虚拟机就会退出。因为如果只剩下守护线程,就没必要继续运行程序了。
t.setDaemon(true);
12.3.3 线程名
用setName方法为线程设置任何名字。这在线程转储时可能很有用。
var t = new Thread(runnable);
t.setName(“Web crawler”);
12.3.4 未捕获异常的处理器
12.4 同步
12.4.1 竞态条件的一个例子
12.4.2竞态条件详解
12.4.3 锁对象(重要)
两种机制可防止并发访问代码块:
- Java语言提供了一个synchronized关键字,会自动提供一个锁以及相关的“条件”
- 另外Java5引人了ReentrantLock类。用ReentrantLock保护代码块的基本结构如下:
myLock.lock(); // a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock(); // make sure the lock is unlocked even if an exception is thrown
}
这个结构确保任何时刻只有一个线程进人临界区。一旦一个线程锁定了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们会暂停,直到第一个线程释放这个锁对象。
- ReentrantLock的例子
12.4.4 条件对象(重要)
通常,线程进人临界区后却发现只有满足了某个条件之后它才能执行。可以使用一个
条件变量来管理那些已经获得了一个锁却不能做有用工作的线程。
12.4.5 synchronized关键字(重要)
BrianGoetz创造了以下“同步格言”:“如果写一个变量,而这个变量接下来可能会被另一个线程读取,或者,如果读一个变量,而这个变量可能已经被另一个线程写入值,那么必须使用同步。”
12.4.6 同步块
- 线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁:即进人一个同步块。
- 客户端锁定是非常脆弱的,非常不推荐使用。
12.4.7监视器概念
Java的synchronized不太严格的实现了监视器。学习synchronized即可。
- 显式锁严格地讲,不是面向对象的。希望不要求程序员考虑显式锁就可以保证多线程的安全性,由此产生监视器。
- 监视器具有如下特性:
- 监视器是只包含私有字段的类。
- 监视器类的每个对象有一个关联的锁。
- 所有方法由这个锁锁定。换句话说,如果客户端调用obj.method(),那么obj对象的锁在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的字段是私有的,这样的安排可以确保一个线程处理字段时,没有其他线程能够访问这些字段。
- 锁可以有任意多个相关联的条件。
-
Java设计者以不太严格的方式采用了监视器概念,Java中的每一个对象都有一个内部锁和一个内部条件。如果一个方法用synchronized关键字声明,那么,它表现得就像是一个监视器方法。可以通过调用wait/notifyAll/notify来访问条件变量。
-
Java对象在以下3个重要方面不同于监视器,这削弱了线程的安全性:
- 字段不要求是private。
- 方法不要求是synchronized。
- 内部锁对客户是可用的。
12.4.8 volatile 字段
- volatile关键字为实例字段的同步访问提供了一种免锁机制。如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。
12.4.9 final 变量
多个线程安全地读取一个字段:
- 锁
- volatile变量
- final变量。
final保证accounts这个对象引用不会变,不可能指向另一个对象。但是,它的内容可能变。所以final变量是同步的,但映射的操作不是线程安全的。
12.4.10 原子性
见书
12.4.11 死锁
Java编程语言中没有任何东西可以避免或打破这种死锁。必须仔细设计程序,确保不会出现死锁。
12.4.12 线程局部变量
- 要为每个线程构造一个实例,可以使用以下代码:
public static final ThreadLocal\<SimpleDateFormat> dateFormat =ThreadLocal.withlnitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
- 要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new DateO);
- 应用场景:线程不安全的对象、多个线程等待共享同一个对象低效。
- 例如,SimpleDateFormat类不是线程安全的。假设有一个静态变量:
public static final SimpleDateFormat dateFormat = newSimpleDateFormat("yyyy-MM-dd");
如果两个线程都执行以下操作:StringdateStamp =d ateFormat.format(newDateO);
结果可能很混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大;或者也可以在需要时构造一个局部SimpleDateFormat对象,不过这也太浪费了。- 在多个线程中生成随机数也存在类似的问题。java.util.Random类是线程安全的,但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。
可以使用ThreadLoacl辅助类为各个线程提供一个单独的生成器,不过Java7还另外提供了一个便利类。只需要做以下调用:int random = ThreadLocalRandom.current().nextlnt(upperBound);
ThreadLocalRandom.current()调用会返冋特定于当前线程的Random类的实例。
12.4.13 为什么废弃stop和suspend方法
- stop方法是不安全的,该方法会终止所有未结束的方法,包括run方法。当线程被终止,它会立即释放被它锁定的所有对象的锁。这会导致对象处于不一致的状态。
假设一个Transfer在从一个账户向另一个账户转账的过程中被终止,钱已经取出,但还没有存人目标账户,现在银行对象就被破坏了。因为锁已经被释放,其他未停止的线程也会观察到这种破坏。
-
当一个线程要终止另一个线程时,它无法知道什么时候调用stop方法是安全的,而什么时候会导致对象被破坏。因此,该方法已经被废弃。希望停止一个线程的时候应该中断该线程,被中断的线程可以在安全的时候终止。
-
suspend会导致死锁。与stop不同,suspend不会破坏对象。但是,如果用suspend挂起一个持有锁的线程,那么,在线程恢复运行之前这个锁是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
12.5 线程安全的集合
12.5.1 阻塞队列
- 生产者线程向队列插人元素,消费者线程则获取元素。使用队列,可以安全地从一个线程向另一个线程传递数据。
考虑银行转账程序,转账线程将转账指令对象插人一个队列,而不是直接访问银行对象。另一个线程从队列中取出指令完成转账。只有这个线程可以访问银行对象的内部.因此不需要同步。
- 当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队 列将导致线程阻塞。
- 队列会自动地平衡负载。
如果第一组线程运行得比第二组慢,第二组在等待结果时会阻塞。如果第一组线程运行得更快,队列会填满,直到第二组赶上来。
- 当试图向满队列添加元素或者想从空队列得到队头元素时,add、remove和element操作会抛出异常。当然,在一个多线程程序中,队列可能会在任何时候变空或变满,因此,应当使用offer、poll和peek方法作为替代。如果不能完成任务,这些方法只是给出一个错误提示而不会抛出异常。
poll和peek方法返回null来指示失败。因此,向这些队列中插入null值是非法的。
- 如果使用队列作为线程管理工具,将要用到put和take方法。如果队列满,put方法阻塞;如果队列空,则take方法阻塞。它们与不带超时参数的offer和poll方法等效。
- 还有带有超时时间的offer方法和poll方法。
boolean success = q.offer(x, 100, Timellnit.MILLISECONDS);
尝试在100毫秒的时间内在队尾插入一个元素。如果成功返回true;否则,如果超时,则返回false。
- java.util.concurrent包提供了阻塞队列的几个变体。
理解:
- 生产者和消费者只在队满继续加元素或者队空要删除元素时,需要线程间通信。其它时间,互不干扰。
- add方法没有同步锁。offer和put方法有锁。add方法不是线程安全的。
- put方法采用条件变量实现了阻塞。offer方法不阻塞,返回false。
12.5.2 高效的映射、集和队列
个人理解:线程安全定义
线程安全的主题是对象。线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。深入理解Java虚拟机》的作者也认可这个观点。本人也认为这是一个恰当的定义,因为线程安全的主体是什么?是方法还是代码块?这里给出的主体是对象,这是非常恰当的,因为Java是纯面向对象的,Java中一切为对象。因此通过对象定义线程安全是恰当的。
但是,这里并不是说其他的方式定义不对(这里绝没有这个意思)。我们可以看一下其他的定义方式,进行一下对比:
————————————————
版权声明:本文为CSDN博主「凡尘炼心」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: