【译文】线程和锁17.1—17.3

原文地址说明
线程和锁
  • Java虚拟机可以支持多个线程同时执行,但我们在前边章节里,讨论的大多数都只是一个线程在同一时刻执行一个代码段的行为。
    在Java里, 多个线程可以在一个共享的主内存里,同时执行代码去操作这些共享内存里的变量和对象。拥有多个硬件处理器、单个硬件处理器的时间切片或者多个硬件处理器的时间切片,均可以实现多线程。
  • 线程通过Thread 类表示,用户为一个对象创建一个线程的唯一方式是去创建一个Thread类的对象。每一个线程与这个对象相关联,当start()方法被对应的对象调用时,线程启动。
  • 在没有正确进行同步的前提下,线程的表现行为通常让人感觉困惑和反常。在这个章节,我们将解读多线程编程的语义。这些语义包括:在共享内存里,多个线程更新数据之后,这些数据能够被一个线程读取的规则。由于多个不同硬件体系结构的内存模型和这些规则很类似,我们将这些语义统称为:Java编程语言的内存模型(JMM). 在没有上下文语义混淆时,我们简单的称之为:内存模型。
  • 实际上,这些语义并没有规定多线程程序应该如何被执行。反之,他们仅仅描述了多线程程序允许的表现行为,只有一个被允许的执行策略才可以被生成。
Synchronization 同步
  • Java语言为多线程之间的通信提供了多种机制,最为基础常用的方法,就是通过使用monitor监视器实现的同步。每一个Java对象都关联这一个monitor监视器,一个线程可以对此monitor进行加锁或者解锁。在同一个时间,只能有一个线程可以获取监视器的锁,任何其他试图去获取monitor锁的线程将会被阻塞,知道他们可以获得锁。一个线程 t 可以多次获取同一个monitor锁,而它对应的解锁操作则与获取锁相反。
  • 一个Synchronized代码块会计算它对一个对象产生的引用,从而试图对这个对象执行monitor加锁的操作,在加锁成功前,不会有别的进一步操作。在加锁完成后,被Synchronized修饰的代码块将会被执行。如果这段代码被执行结束,不管是正常结束还是异常结束,在同一个monitor对象上,会自动的执行解锁操作。
  • 一个Synchronized方法被调用的时候会执行加锁操作,在加锁完成后,方法会被执行。如果这个方法是实例方法,被锁定的是与此实例方法相关联的监视器monitor—对象锁。如果这个方法是静态方法,被锁定的则是与定义此方法相关联的class对象的monitor—类锁。如果一个方法体被执行结束,不管是正常还是异常,在同一个monitor对象上会自动执行解锁操作。
  • Java变成语言既不要求检测死锁条件,但也不阻止检测。线程直接或者间接在多个对象上持有锁的情况,将采用传统的死锁避免技术去处理:如有必要,创建一个更高级别的锁。
  • 其他机制提供了其他的同步方法,比如:Java.util.concurrent包里的类;volatile变量的读写。
等待和通知
  • 每个对象,除了拥有一个相关联的monitor意外,还有一个相关联的wait set。一个wait set 是一组线程集合。
  • 当一个对象被首次创建时,关联的wait set 为空。向这个set 新增或者移除线程的操作是原子的,这个操作,只能通过方法:Object.wait、Object.notify 和Object.notifyAll.
  • 一个线程的中断状态或者一个线程类处理中断时也可能影响wait set。另外,当线程类方法sleep或者join其他线程时,具有从wait和notification操作派生的属性。
等待
  • 等待操作发生在方法wait()被调用时,或者定时等待wait(long millisecs)和wait(long millisecs,int nanosecs)。当wait(0),或者wait(0,0)时,等同于wait().当线程没有产生中止异常时,它往往从一个等待中成功返回。
  • 假设一个线程 t 在一个对象 m 上执行等待方法,然后 n 是t在m上未被匹配出来解锁的加锁数量,这时候,则会发生以下状况:
  • 如果n=0,即线程t 还未获得m的锁,这时候,IllegalMonitorStateException异常抛出。如果这是一个定时等待,并且nanosecs参数不在0-999999的范围内,或者millisecs参数为负,则抛出IllegalArgumentException。如果线程t被中断,则抛出InterruptedException,并将t的中断状态设置为false。
  • 除此之外,将会出现以下顺序:
  • A:线程t 被添加到m 的wait set,并且对m执行 n个解锁操作。
  • B:t 不会执行任何进一步的操作指导它被移除m的wait set。这个移除操作可能发生在以下任何一个场景中,并且可能会在之后某个时间恢复:
  • 1,在m上执行了notify;2,在m上执行了notifyAll;3,t被执行了中断操作。
  • 如果这是一个定时等待,在执行等待至少millisecs+milliseconds和nanosecs+nanoseconds后,会有一个内部操作去将 t从m的等待集里边移除。我们并不鼓励使用内部操作,但允许执行“spurious wake-ups”,即:将线程从wait set中移除,使他能够再没有明确的指令操作中被恢复。注意,这个规定,使我们在运用Java变成的时候,只能在线程等待某些逻辑条件而终止的loop循环中使用wait。
  • 每个线程都必须有确定的事件执行顺序,这些事件可能导致它被移除wait set。不必和其他顺序相关联,但线程必须按照这些事件的顺序去执行相对应的行为。举个栗子:如果 t 在m 的wait set 集中,这时候同时产生两个事件,第一,t 中断;第二,m被唤醒。这时候,对于此两个时间必须有一个顺序,如果中断先于唤醒,这时候t将抛出中断异常,并且其他在m的wait set里 的线程必须能接收到m的唤醒通知。如果唤醒通知先于中断,那么t将被正常从wait 中唤醒,并且中断挂起。
  • C:线程t对m执行n个锁操作
  • D:如果在步骤2中由于中断而从m的wait集中删除了线程t,那么t的中断状态设置为false,wait方法抛出InterruptedException。
通知
  • 通知操作在调用notify和notifyAll方法时发生。
  • 设thread t是在对象m上执行这些方法之一的线程,设n是m上未被解锁操作匹配的t的锁操作数。这是可能发生发生以下操作之一:1,如果n为零,则抛出IllegalMonitorStateException。在这种情况下,线程t尚未拥有目标m的锁。2,如果n大于零,并且这是一个notify操作,那么如果m的wait set不为空,那么将选择属于m的当前wait set成员的线程u,并将其从等待集中移除。不能保证在等待集中选择了哪个线程。从等待集中的这种删除使u能够在等待操作中恢复。但是,请注意,u在恢复后无法进行成功的锁操作,直到t完全解锁m的监视器之后。3,如果n大于零,并且这是一个notifyAll操作,那么将从m的等待集中删除所有线程,然后继续执行。但是,请注意,在恢复等待的同一时间,只能有一个线程锁定其监视器。
中断
  • 线程中断,发生在方法Thread.interrupt调用时,以及依次调用它而定义的方法,例如ThreadGroup.interrupt.
  • 设t是调用u.interrupt的线程,对于某些线程u,其中t和u可能相同。此操作将使u的中断状态设置为true。
    另外,如果存在等待集包含u的对象m,则从m的等待集中移除u。这使得u可以在等待操作中继续,在这种情况下,在重新锁定m的监视器之后,这个等待将抛出InterruptedException。
  • 调用Thread.isInterrupted可以确定线程的中断状态。静态方法Thread.interrupted可由线程调用以观察和清除其自身的中断状态。
等待、通知和中断的交替应用
  • 以上规范允许我们确定与等待、通知和中断的交互有关的几个属性。
  • 如果线程在等待时同时被通知和中断,它可以:1,通常情况下,从等待中被唤醒,等待中断—Thread.interrupted返回true。2,通过抛出InterruptedException从wait返回。线程可能不会重置其中断状态并从等待调用正常返回。
  • 同样,通知也不会因中断而丢失。假设一组线程位于对象m的等待集s中,另一个线程对m执行通知。然后:1,s中至少有一个线程必须从wait正常返回,或者s中的所有线程都必须通过抛InterruptedException退出wait。
  • 请注意,如果一个线程同时被中断并通过notify唤醒,并且该线程通过抛出InterruptedException从wait返回,那么必须通知等待集中的其他线程。
sleep 和 yield
  • Thread.sleep 使当前正在执行的线程为了某个特定的时间范围内进入sleep状态,中断其当前的执行。在这个时间范围内,线程不会丢失任一个monitor的所有权。线程的恢复取决于调度和执行线程处理器的可用性。
  • 值得注意的是:Thread.sleep 或者 Thread.yield都没有任何Synchronized的同步语义。特别是,在方法调用之前,编译器不会将寄存器中缓存的写入刷新到共享内存,也不会在调用后重新加载在寄存器中缓存的值。
  • 例如,在下面的代码片段中,假设this.donw是一个非volatile的布尔型字段:
while (!this.done)
    Thread.sleep(1000);
  • 编译器可以只需要自由读取一次this.done字段,并在每次循环执行中重用缓存值。这意味着循环永远不会终止,即使另一个线程更改了this.done的字段值。
尴尬的瞬间
  • 以前好像有人问我,说wait和notify哪一个操作是在持有锁,我说wait。唉,有时候的经历,就跟遭受了冷暴力一样。
  • 我问一个我觉得不是憨憨的人同样的问题:wait和notify,哪一个是获取锁,哪一个是释放锁?他说:wait 休眠当前线程,notify 是随机唤醒一个线程,和 锁没啥关系。然后,我就从我说wait持有锁这件事里边超脱出来了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值