线程和锁

0
(0)

虽然前面章节的大部分讨论只涉及一次执行单个语句或表达式时的代码行为,也就是说,通过单个线程,Java虚拟机可以同时支持多个线程执行。这些线程独立地执行对共享主内存中的值和对象进行操作的代码。线程可以通过拥有多个硬件处理器、对单个硬件处理器进行时间切片或对多个硬件处理器进行时间切片来支持。
线程由类表示。用户创建线程的唯一方法是创建该类的对象;每个线程都与这样一个对象相关联。当在相应的线程对象上调用start()方法时,线程将启动。
线程的行为,特别是在没有正确同步的情况下,可能会令人困惑和违反直觉。本章描述了多线程程序的语义;它包括一些规则,这些规则的值可以被多个线程更新的共享内存的读取所看到。由于该规范类似于针对不同硬件架构的内存模型,因此这些语义称为Java编程语言内存模型。当不会产生混淆时,我们将简单地将这些规则称为“内存模型”。
这些语义并没有规定多线程程序应该如何执行。相反,它们描述了多线程程序允许展示的行为。任何只生成允许行为的执行策略都是可接受的执行策略。

同步(Synchronization)

Java编程语言为线程之间的通信提供了多种机制。这些方法中最基本的是同步,它是使用监视器实现的。Java中的每个对象都与监视器相关联,线程可以锁定或解锁监视器。一次只能有一个线程持有监视器上的锁。任何其他试图锁定该监视器的线程都会被阻塞,直到它们获得该监视器上的锁。线程t可以多次锁定特定的监视器;每个解锁都反转了一个锁定操作的效果。
sychronization 语法计算一个对象的引用;然后,它尝试在该对象的监视器上执行锁定操作,直到锁定操作成功完成才继续执行。执行锁操作后,执行语句体。如果身体的执行完成了(不管是正常的还是突然的),解锁动作就会在同一个监视器上自动执行。
同步方法(§8.4.3.6)在被调用时自动执行一个锁动作;直到锁定操作成功完成,它的主体才会被执行。如果该方法是一个实例方法,它将锁定与它被调用的实例相关联的监视器(即,在方法体执行期间称为this的对象)。如果方法是静态的,则它锁定与表示定义方法的类的类对象相关联的监视器。如果方法主体的执行完成了(正常或突然地),就会执行解锁操作。
Java编程语言既不防止也不要求检测死锁条件。线程(直接或间接)持有多个对象上的锁的程序应该使用避免死锁的传统技术,如有必要,创建不会死锁的高级锁原语。
其他机制,如volatile变量的读写和java.util中类的使用。并发包,提供替代的同步方式。

等待集和通知

每个对象除了有一个相关联的监视器外,还有一个相关联的等待集。等待集是一个线程的集合。
第一次创建对象时,它的等待集为空。向等待集中添加线程和从等待集中删除线程的基本操作是原子操作。等待集仅通过methods Object.wait, Object.notify, and Object.notifyAll 操作。
等待集操作也可能受到线程的中断状态以及thread类处理中断的方法的影响。此外,Thread类用于睡眠和连接其他线程的方法具有从等待和通知操作派生的属性。

wait

等待操作在调用 wait(), or the timed forms wait(long millisecs) and wait(long millisecs, int nanosecs). 时发生。
参数为0的 wait(long millisecs) 调用或参数为2个0的wait(long millisecs, int nanosecs) 调用等价于wait()调用。
如果线程返回时没有抛出InterruptedException异常,则它通常从等待状态返回。
设线程t是在对象m上执行wait 方法的线程,设n是t在m上没有与解锁操作匹配的锁定操作的数量。下面的一种情况将会发生:

  • 如果n为0(即,线程t还没有拥有目标m的锁),那么抛出一个IllegalMonitorStateException。
  • 如果这是一个定时等待,并且nanosecs参数不在0-999999范围内,或者毫秒参数为负,则抛出IllegalArgumentException。
  • 如果线程t被中断,则抛出InterruptedException,并将t的中断状态设置为false。
  • 否则,发生下列顺序:
    1. 线程t添加到等待的对象集m,并执行m上的n解锁动作。
    2. 解锁操作线程t不执行任何进一步的指令,直到它从m的等待集合中删除。线程t在从m的等待集中被删除之前不会执行任何进一步的指令。线程可能会因为以下任何一个操作被从等待集中删除,并在之后的某个时间恢复:
      • 在m上执行了 notify,其中t被选择从等待集中删除。
      • 在 m 上执行了 notifyAll。
      • 正在执行一个中断操作在t。
      • 如果这是一个定时等待,那么从m的等待集中删除t的内部操作将在至少等待指定时常之后发生。
      • 实现的内部动作。允许(虽然不鼓励)实现执行“伪唤醒”,也就是说,从等待集中删除线程,从而在没有明确指令的情况下恢复线程。
        注意,这种规定要求仅在在循环中使用 wait的Java编码实践,只有当线程正在等待的某些逻辑条件保持时才会终止循环。
        每个线程必须确定事件的顺序,这些事件可能会导致它从等待集中被删除。这个顺序不必与其他顺序一致,但线程必须表现得就像这些事件按照那个顺序发生一样。
        例如,如果线程t在m的等待集中,然后t的中断和m的通知同时发生,那么这些事件必须有一个顺序。如果中断被认为是首先发生的,那么t最终将通过抛出InterruptedException从wait返回,并且m的等待集中的其他线程(如果在发出通知时存在任何线程)必须接收通知。如果通知被认为是首先发生的,那么t最终将从wait正常返回,此时中断仍然挂起。
    3. 线程t对m执行n个锁操作。
    4. 如果线程t在步骤2中由于中断被从m的等待设置中删除,那么t的中断状态被设置为false,并且等待方法抛出InterruptedException。

notify

通知操作在调用notify和notifyAll方法时发生。

设线程t是在对象m上执行这两种方法中的任何一种的线程,设n是t在m上没有与解锁操作匹配的锁定操作的数量。出现下列动作之一:

  • 如果n为0,则抛出IllegalMonitorStateException。
    在这种情况下,线程t还没有拥有目标m的锁。
  • 如果n大于0,这是一个通知操作,那么如果m的等待集不是空的,一个线程u是m当前等待集的成员,将被选中并从等待集中移除。

    不能保证选择了等待集中的哪个线程。从等待集中删除后,u可以在等待动作中恢复。但是,请注意,u在恢复时的锁定动作不能成功,直到一段时间后,t完全解锁m的监视器 。
  • 如果n大于0,并且这是一个notifyAll动作,那么所有的线程都会从m的等待集中被删除,从而恢复。
    然而,请注意,在恢复等待期间,每次只有其中一个会锁定所需的监视器。

interruptions

中断操作会在调用Thread.interrupt时发生,也会在调用Thread.interrupt时发生,比如ThreadGroup.interrupt。

设t为调用u.interrupt的线程,对于某个线程u, t和u可能相同。此操作将导致u的中断状态设置为true。

另外,如果存在某个对象m的等待集包含了u,那么u就会被从m的等待集中移除。这使得u可以继续一个等待动作,在这种情况下,这个等待将在重新锁定m的监视器后抛出InterruptedException。

调用Thread.isInterrupted可以决定线程的中断状态。线程可以调用静态方法Thread.interrupted来观察和清除自己的中断状态。

wait、notify和interrupt的交互

以上规范允许我们确定与等待、通知和中断交互有关的几个属性。
如果一个线程在等待过程中同时被通知和中断,它可以:

  • 正常地从wait返回,同时仍然有一个挂起的中断(换句话说,调用Thread.interrupted将返回true)
  • 通过抛出InterruptedException从wait返回

线程可能不会重置它的中断状态,并从调用正常返回到wait

同样,通知也不会因为中断而丢失。假设在对象m的等待集中有一组线程s,而另一个线程对m执行一个通知,则可以:

  • s中至少有一个线程必须正常地从wait或返回
  • s中的所有线程都必须通过抛出InterruptedException退出wait

注意,如果一个线程通过notify中断和唤醒,并且该线程通过抛出InterruptedException从wait返回,那么必须通知等待集中的其他线程。

sleep和yield

Thread.sleep使当前执行的线程在指定的时间内处于睡眠状态(暂时停止执行),这取决于系统计时器和调度程序的精度和准确性。线程不会失去对任何监视器的所有权,执行的恢复将取决于调度和执行线程所依赖的处理器的可用性。

重要的是要注意这两个Thread.sleep和Thread.yield不具有任何同步语义。特别是,在调用Thread.sleep或Thread.yield之前,编译器不必将缓存在寄存器中的写操作刷新到共享内存。在调用Thread.sleep或Thread.yield之后,编译器也不需要重新加载缓存在寄存器中的值。

例如,在下面的代码片段中,假定this.done是一个非易失性布尔字段

while (!this.done)
    Thread.sleep(1000);

编译器可以自由地读取字段this.done只会执行一次,并在每次循环执行时重用缓存的值。这意味着即使另一个线程改变了this.done的值,循环也不会终止。

Synchronize原理图:

image

这篇文章有用吗?

平均评分 0 / 5. 投票数: 0

到目前为止还没有投票!成为第一位评论此文章。

很抱歉,这篇文章对您没有用!

让我们改善这篇文章!

告诉我们我们如何改善这篇文章?

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据