第12章 并 发

多进程与多线程本质的区别:每个进程都拥有自己的一整套变量,而线程则共享数据。

12.1 什么是线程

  1. 将执行这个任务的代码放在一个类的run方法中,这个类要实现Runnable接口。
    Runnable接口非常简单,只有一个run方法:
public interface Runnable
{
void run();
}
  1. 从这个Runnable构造一个Thread对象:
var t = new Thread(r);
  1. 启动线程:
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 中断线程

  1. 除了已经废弃的stop方法,没有办法可以强制线程终止。不过,interrupt方法可以用来请求终止一个线程。

  2. 要想得出是否设置了中断状态,首先调用静态的Thread. currentThread方法获得当前线程,然后调用islnterrupted方法:

while (! Thread. currentT read (). islnterrupted () && more work to do)
{
do more work
}
  1. 如果线程被阻塞,就无法检查中断状态。这里就要引人InterruptedException异常。
    当在一个被sleep即或wait调用阻塞的线程上调用interrupt方法时,那个阻塞调用将被一个InterruptedException异常中断。

  2. **没有任何语言要求被中断的线程应当终止。**中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。某些线程非常重要,所以应该处理这个异常,然后再继续执行。但是,**更普遍的情况是,线程只希望将中断解释为一个终止请求。**这种线程的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
}
  1. 如果在每次工作迭代之后都调用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 守护线程

  1. 守护线程的唯一用途是为其他线程提供服务。计时器线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程,另外清空过时缓存项的线程也是守护线程。
  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 锁对象(重要)

两种机制可防止并发访问代码块:

  1. Java语言提供了一个synchronized关键字,会自动提供一个锁以及相关的“条件”
  2. 另外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时,它们会暂停,直到第一个线程释放这个锁对象。
在这里插入图片描述

  1. ReentrantLock的例子
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

12.4.4 条件对象(重要)

通常,线程进人临界区后却发现只有满足了某个条件之后它才能执行。可以使用一个
条件变量来管理那些已经获得了一个锁却不能做有用工作的线程。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

12.4.5 synchronized关键字(重要)

BrianGoetz创造了以下“同步格言”:“如果写一个变量,而这个变量接下来可能会被另一个线程读取,或者,如果读一个变量,而这个变量可能已经被另一个线程写入值,那么必须使用同步。”
在这里插入图片描述
在这里插入图片描述

12.4.6 同步块

  1. 线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁:即进人一个同步块。
  2. 客户端锁定是非常脆弱的,非常不推荐使用
    在这里插入图片描述

12.4.7监视器概念

Java的synchronized不太严格的实现了监视器。学习synchronized即可。

  1. 显式锁严格地讲,不是面向对象的。希望不要求程序员考虑显式锁就可以保证多线程的安全性,由此产生监视器。
  2. 监视器具有如下特性:
  • 监视器是只包含私有字段的
  • 监视器类的每个对象一个关联的锁
  • 所有方法由这个锁锁定。换句话说,如果客户端调用obj.method(),那么obj对象的锁在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的字段是私有的,这样的安排可以确保一个线程处理字段时,没有其他线程能够访问这些字段。
  • 锁可以有任意多个相关联的条件
  1. Java设计者以不太严格的方式采用了监视器概念,Java中的每一个对象都有一个内部锁和一个内部条件。如果一个方法用synchronized关键字声明,那么,它表现得就像是一个监视器方法。可以通过调用wait/notifyAll/notify来访问条件变量。

  2. Java对象在以下3个重要方面不同于监视器,这削弱了线程的安全性:

  • 字段不要求是private。
  • 方法不要求是synchronized。
  • 内部锁对客户是可用的。

12.4.8 volatile 字段

  1. volatile关键字为实例字段的同步访问提供了一种免锁机制。如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。

在这里插入图片描述

12.4.9 final 变量

多个线程安全地读取一个字段:

  1. volatile变量
  2. final变量。
    在这里插入图片描述

final保证accounts这个对象引用不会变,不可能指向另一个对象。但是,它的内容可能变。所以final变量是同步的,但映射的操作不是线程安全的。

12.4.10 原子性

见书

12.4.11 死锁

Java编程语言中没有任何东西可以避免或打破这种死锁。必须仔细设计程序,确保不会出现死锁。

12.4.12 线程局部变量

  1. 要为每个线程构造一个实例,可以使用以下代码:
public static final ThreadLocal\<SimpleDateFormat> dateFormat =ThreadLocal.withlnitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
  1. 要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new DateO);
  1. 应用场景:线程不安全的对象、多个线程等待共享同一个对象低效。
  • 例如,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方法

  1. stop方法是不安全的,该方法会终止所有未结束的方法,包括run方法。当线程被终止,它会立即释放被它锁定的所有对象的。这会导致对象处于不一致的状态。

假设一个Transfer在从一个账户向另一个账户转账的过程中被终止,钱已经取出,但还没有存人目标账户,现在银行对象就被破坏了。因为锁已经被释放,其他未停止的线程也会观察到这种破坏。

  1. 当一个线程要终止另一个线程时,它无法知道什么时候调用stop方法是安全的,而什么时候会导致对象被破坏。因此,该方法已经被废弃。希望停止一个线程的时候应该中断该线程,被中断的线程可以在安全的时候终止。

  2. suspend会导致死锁。与stop不同,suspend不会破坏对象。但是,如果用suspend挂起一个持有锁的线程,那么,在线程恢复运行之前这个锁是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。

12.5 线程安全的集合

12.5.1 阻塞队列

  1. 生产者线程向队列插人元素,消费者线程则获取元素。使用队列,可以安全地从一个线程向另一个线程传递数据。

考虑银行转账程序,转账线程将转账指令对象插人一个队列,而不是直接访问银行对象。另一个线程从队列中取出指令完成转账。只有这个线程可以访问银行对象的内部.因此不需要同步。

  1. 当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队 列将导致线程阻塞。
  2. 队列会自动地平衡负载。

如果第一组线程运行得比第二组慢,第二组在等待结果时会阻塞。如果第一组线程运行得更快,队列会填满,直到第二组赶上来。

  1. 当试图向满队列添加元素或者想从空队列得到队头元素时,add、remove和element操作会抛出异常。当然,在一个多线程程序中,队列可能会在任何时候变空或变满,因此,应当使用offer、poll和peek方法作为替代。如果不能完成任务,这些方法只是给出一个错误提示而不会抛出异常。

poll和peek方法返回null来指示失败。因此,向这些队列中插入null值是非法的。

  1. 如果使用队列作为线程管理工具,将要用到put和take方法。如果队列满,put方法阻塞;如果队列空,则take方法阻塞。它们与不带超时参数的offer和poll方法等效。
  2. 还有带有超时时间的offer方法和poll方法。

boolean success = q.offer(x, 100, Timellnit.MILLISECONDS);尝试在100毫秒的时间内在队尾插入一个元素。如果成功返回true;否则,如果超时,则返回false。

  1. java.util.concurrent包提供了阻塞队列的几个变体。在这里插入图片描述

理解:

  • 生产者和消费者只在队满继续加元素或者队空要删除元素时,需要线程间通信。其它时间,互不干扰。
  • add方法没有同步锁。offer和put方法有锁。add方法不是线程安全的。
  • put方法采用条件变量实现了阻塞。offer方法不阻塞,返回false。

12.5.2 高效的映射、集和队列

个人理解:线程安全定义

线程安全的主题是对象。线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。深入理解Java虚拟机》的作者也认可这个观点。本人也认为这是一个恰当的定义,因为线程安全的主体是什么?是方法还是代码块?这里给出的主体是对象,这是非常恰当的,因为Java是纯面向对象的,Java中一切为对象。因此通过对象定义线程安全是恰当的。

但是,这里并不是说其他的方式定义不对(这里绝没有这个意思)。我们可以看一下其他的定义方式,进行一下对比:
————————————————
版权声明:本文为CSDN博主「凡尘炼心」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:

12.5.3 映射条目的原子更新在这里插入图片描述

12.5.4 对并发散列映射的批操作

12.5.5 并发集视图

12.5.6 写数组的拷贝

12.5.7 并行数组算法

12.5.8 较早的线程安全集合

在这里插入图片描述

12.7 异步计算

12.7.1 可完成Future

12.7.2 组合可完成Future

12.7.3 用户界面回调中的长时间运行任务

12.8 进程

12.8.1 建立一个进程

12.8.2 运行一个进程

12.8.3 进程句柄

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值