第一章 线程及线程安全

1.线程介绍##

线程,也称为轻量级进程。大多数现代操作系统把线程作为时序调度的基本单元。多线程的出现给程序处理带来了很多新的方式,但同时线程的使用也存在着不少风险。

线程的优点###

  • 多处理器的出现。程序调度的基本单元是线程,一个单线程应用程序一次只能运行在一个处理器上。因而多处理器的出现使得多线程的使用能够更充分地利用处理器的资源。
  • 对异步事件的处理。程序中有一些I/O或者等待资源的操作,单线程的情况下,整个程序都会停止来等待返回结果。而多线程的情况下,可以将这样耗时的操作作为异步的操作来同时进行。
  • 用户界面更好的响应。比如在GUI框架中,触发一个事件后,主线程会将事件交给独立的事件处理器线程来操作,从而使用户界面上不会出现卡顿或者没有反应的现象。

线程的风险###

安全危险

在多线程的情况下,如果程序没有进行充分的同步,程序的执行顺序是无法预测的。比如下面的类:

public class UnsafeSequence {
    private int value;
    
    public int getNext(){
        return value++;
    }
}

在单线程的情况下,调用getNext方法获得的结果是毋庸置疑的。但是在多线程的情况下,这个方法就是非线程安全的。虽然只有value++一个操作,但是自增操作并不是一个原子操作。getNext能否返回正确的值,取决于运行时线程交替执行的顺序,也称之为竞争条件(race condition)。

活跃性危险

多线程的使用可能会引起很多非必现的bug。比如线程A和线程B共同操作一个资源,如果线程B占有该资源一直不释放,则线程A需要一直等待,从而引起活跃度失败。然而由于很多bug是线程间执行时间的时序问题引发的,在开发和测试的过程中,往往难以发现。

性能危险

多线程的使用确实能并发地去执行多个任务,但多线程的创建销毁以及线程的切换仍然会带来一定程度的性能开销。如线程切换时保存当前线程的上下文以及恢复下一个线程的上下文。如果线程间共享数据,需要使用同步机制,这个机制同样会消耗额外的性能。

2.线程安全##

什么叫线程安全###

简单的来说,就是在多个线程访问一个类的时候,该类始终保持着正确的执行行为。

这里包括不用考虑线程在运行时的调度和交替执行的时序,以及不需要额外的同步或协调。

无状态对象永远是线程安全的###

如果两个线程不共享状态,即没有共享数据,则运行时相互之间没有任何的交互,也就不存在线程安全的问题。

原子性与竞争条件###

原子性,顾名思义,指的就是不可分割的操作,多个操作要么一起执行,要么就都不执行。原子性保证了程序的执行不会因为执行的时序问题而引发的线程安全问题。

而经常引起原子性问题的就是竞争条件。比如常见的检查再运行(check-then-act),当我创建一个文件夹时,会先判断文件夹是否存在,不存在再创建。这个在单线程的情况下不会出现问题,但是在多线程下,就可能会因为当我检查文件夹不存在后,另一个线程先创建了该文件夹,从而导致此线程创建文件错误。

public void mkdir(){
    File dest = new File("E://temp");
    if(!dest.exists()){
        dest.mkdir();
    }
}

很多复合操作在多线程下都会出现这样的问题,而解决这个问题的方法就是锁。

锁###

java提供了强制原子性的内置锁机制:synchronized块。synchronized是一个互斥锁,意味最多只有一个线程可以拥有,如果其他线程尝试获取,则必须等待或者阻塞,直到拥有锁的线程释放锁。回到刚才的创建文件夹的例子,我们就可以通过synchronized块解决其线程安全的问题。

public synchronized void mkdir(){
    File dest = new File("E://temp");
    if(!dest.exists()){
        dest.mkdir();
    }
}

内部锁是可重进入的,这里的重进入是对拥有锁的同一个线程。当线程试图获取它自己占有的锁时,请求会成功。这就意味着锁的请求时基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”。

重进入的实现是通过给每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录占有锁的线程,并且将请求计数置为1;如果同一线程再次请求锁,计数递增;线程每次退出同步块,请求计数减一;直到计数为0,线程释放锁。

重进入方便了锁行为的封装,比如子类覆写了父类synchronized类的方法,并且调用父类中的方法。

//父类
public class Father {
    public synchronized void doSomething(){
        //TODO
    }
}

//子类
public class Son extends Father{
    @Override
    public synchronized void doSomething() {
        System.out.println("son do something");
        super.doSomething();
    }
}

可以看到当执行子类synchronized方法时,需要调用父类的synchronized方法,如果内部锁不是可重进入的,此时便会出现死锁。

用锁来保护状态###

每个共享变量都需要唯一确定的锁来保护其状态。对于涉及多个变量的不变约束 ,需要同一个锁来保护其所有的变量。如Vector类本身每个方法都是同步的,但一旦涉及到多个方法的复合操作,还是需要锁对整个复合操作进行同步。

关于线程的简单介绍就到这里了,下一节来讨论关于多线程中对象的使用。

posted @ 2015-07-28 07:34  lntea  阅读(766)  评论(0编辑  收藏  举报