欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java线程总结 博客分类: Java Java多线程 

程序员文章站 2024-03-21 15:41:10
...

前言

最近又仔细钻研了一下Java线程,主要参考了O‘Reilly的《Java线程》一书第二版,我在这里对关键点做一下总结,也算是抛砖引玉吧。(注意:本文不适合初涉线程的Java程序员,需要对线程有初步的理解和实践经验。如果要了解线程的详细内容请阅读《Java线程》,这是一本相当不错的书)

 

 

1. 同步

2. 等待和通知

3. 线程调度

4. 多处理器上的多线程

 

-----------------------------------------------------------------------------------------------

 

 

同步

-----------------------------------------------------------------------------------------------

在线程同步技术中非常重要的一个概念是“原子性”,简单讲就是在执行过程中不能被中断的一组操作。

在线程之间共享数据并且对共享数据的操作不是“原子”的就会导致竞态条件(race condition)的产生,竟态条件会造成程序运行错误(或者说数据错误),因此需要对线程进行同步来解决这个问题。

Java提供synchronized关键字,并通过获取对象的互斥锁来实现线程同步。每一个Java对象都会创建一个锁。

 

 

Java规范保证对变量进行赋值的操作都是原子性的,除了long和double变量之外。

 

同步是基于实际对象而不是引用的,不要选择一个可能会在锁的作用域中改变值的实例变量作为锁对象,比如一个可能会被赋值为null的对象引用。通常可以用synchrnized(this)或者建立一个锁对象Object lock = new Object()来实现。

 

 

预防死锁:

两个或者多个线程试图获取对方的锁,就会发生死锁的状况。比如某一时刻线程A已获得了锁L1,线程B已获得了锁L2,下一时刻线程A试图获取锁L2,线程B试图获取锁L1,于是两个线程互相等待对方释放锁,产生了死锁。

 

Java虚拟机本身不会检测死锁,也没有很好的工具可以检测死锁,仔细的检查代码是唯一检测死锁的办法。

为了尽量避免死锁,可以考虑遵循以下原则:

 

 

  • 最简单的原则:一个同步方法永远不要调用另外一个同步方法。但是这个要求过于严格,且很多时候无法做到,有太多的Java方法是同步的。
  • 对与我们要使用的对象相关联但是更高一级的对象加锁。这个原则的问题是扩大了锁的粒度,这与引入多线程的初衷多少有点相违背。
  • 避免死锁的最有用原则是:确保以同样的顺序来获取锁。也就是说存在着一种锁层次关系,在做详细设计的时候最好定义一个队列形的锁层次关系,迫使编码按照锁层次的顺序来获取锁。

 

按顺序获取锁虽然能够有效解决死锁问题,但是却使程序的并行度降低了,在《java线程》一书中定义了一个BusyFlag类用以在预防死锁的同时,提高程序并行度,有兴趣可以参阅。

 

 

 

 

等待和通知

-----------------------------------------------------------------------------------------------

 

当线程需要等待某种条件的发生时,使用wait()方法;当某种条件已经发生了,则另外的线程调用notify()方法通知等待的线程。wait()和notify()方法在使用前,必须先获得使用这些方法的对象的锁,否则可能会产生静态条件。如:

 

synchronized (this) {
    ...
    this.wait();
    ...
}

wait()方法调用之后,线程进入阻塞状态并暂时释放在该对象上的锁,让其他线程获得锁。

如果有多个线程在等待,那么notify()方法调用的时候会通知到哪个线程呢?Java并没有明确定义哪个线程会收到通知,哪个都有可能,因此可以转而使用notifyAll()方法来通知所有线程。

 

在《Effective Java》一书中Bloch建议:“永远不要在循环之外调用wait()方法“,这是非常正确的,因为被唤醒时并不意味着等待的条件一定是满足了的,有可能其他线程错误的调用了notify()。其他的可能性参考《Effective Java》。

 

synchronized (obj) {
    while(<条件>) {
        obj.wait();
    }
    ......
}
 

 

 

中断线程

Thread类的interrupt()方法可以中断线程的运行,如果被中断线程处于sleep(), wait()阻塞状态,则线程进入非阻塞状态。

但是iterrupt()能否中断I/O阻塞的线程呢?这就不一定了,这取决与虚拟机的实现,不同平台的实现会有不同的结果,比如Windows平台的虚拟机就不能中断。因此不能依赖与这个方法来中断I/O阻塞的线程。正确的方法应该是先关闭该线程阻塞的I/O,在终止线程。

 

 

 

 

 

线程调度

-----------------------------------------------------------------------------------------------

在任一时刻,一个CPU上只能运行一个线程,因此Java线程调度就是决定在某一时刻运行哪一个线程。Java规范并没有定义Java虚拟机实现要用什么样的方式来调度线程,只规定调度是基于线程优先级的,但是不同的虚拟机以不同的方式来实现这个方针。

 

对于程序员来讲,只有当一个或者多个线程在很长一段时间内都是占用CPU较多时才需要考虑Java线程调度。那些运行时间很短的;周期性使用CPU来定时完成任务的;非经常性竞争CPU的线程,都不需要考虑线程调度的问题。

 

在Java虚拟机中每个线程都处于四种状态之一:

 

 

  • 初始状态
  • 可运行状态
  • 阻塞状态
  • 退出状态

Java虚拟机不会改变线程的优先级,优先级只能由程序员改变。线程调度的基本原则是:当前运行线程应当是所有处于可运行状态的线程中拥有最高优先级的线程。

Java虚拟机实现的调度程序是“抢占”式的,也就是说高优先级的线程会中断正在运行的低优先级线程,以使的高优先级线程成为当前运行线程。

 

某个线程由于优先级低于其他线程,很难成为当前运行线程,这种那个情况被成为CPU饥饿。Java虚拟机通常不会因为CPU饥饿而调整线程的优先级(有些OS可能会这么做),程序员要保证不会出现CPU饥饿。

 

在一个常用的Java虚拟机实现方法中,每一个优先级对应有一个链表(假设是链表),初始状态、阻塞状态和退出状态各一个链表。对于某优先级的链表,虚拟机在没有更高优先级的前提下挑选该优先级链表中第一个线程运行,直到有更高优先级的线程需要成为当前运行线程,这个被中断的线程会被移到链表的尾部。等到高优先级的线程重新进入阻塞状态,链表中的下一个线程会成为链表第一个线程,并成为当前运行线程。于是,该优先级的所有线程会随着更高优先级的线程的状态改变而轮流成为当前运行线程。换句话说,同一优先级的线程不会抢占正在运行的其他线程,除非有更高优先级的线程进行干预。

 

并不是每个Java虚拟机都会按章上面的方式重排链表中的线程,一个实时系统中的线程在被中断后是不会被重新排序的。

 

具有同样优先级的线程互相抢占的情况称为循环调度(round-robin scheduling),Java规范既没要求也没有禁止虚拟机实现采用这种方式实现,特别是非Windows的虚拟机。

 

 

多处理器上的多线程

-----------------------------------------------------------------------------------------------

与单处理器系统不同的是,对于多处理器上的系统来说,可能每一个CPU上都有正在运行的线程。这导致了在单处理器系统中的一些假设不再成立:

 

 

  • 不能再认定当前运行的线程具有最高的优先级。
  • 不能再认定低优先级的线程没法运行。
  • 不能再认定不同优先级的线程无法同时运行。
  • 有一些单处理器上不会发生的竞态条件,有可能会发生。

 

 

 

附录:

-----------------------------------------------------------------------------------------------

InterruptedException

表明方法比预期时间提前返回

 

InterruptedIOException

 

 

NoSuchMethodError

......

 

IllegalThreadStateException

 

IllegalArgumentException

 

IllegalMonitorStageException

 

SecurityException

 

 

 

 

相关标签: Java 多线程