代码改变世界

Java多线程

2013-01-31 22:28  ggzwtj  阅读(399)  评论(0编辑  收藏  举报

多线程

看一段简单的代码:

void func(){
    // workA
    {
        // do something
    }
    // workB
    {
        // do something
    }   
}

  其中workAworkB分别表示用来完成两个任务的代码块。在执行workA的时候使用goto(当然还有很多其他的方法)就可以跳转去执行workB的代码了,在执行一段时间之后可以跳回来。那么这段代码是不是有点像线程之间的切换?所以,在CPU看来,代码跟代码(指令跟指令)之间没有多大区别,我们把内容分给了各个任务,其实在CPU看起来都是一样的:执行指令寄存器位置的指令。不过现在编写多线程完全不需要考虑这么多的细节。

线程的实现

  使用内核线程来实现

  1:1的实现方案,也就是一个Java线程对应一个内核线程,这样线程创建、销毁和调度的工作就可以交给内核了,不过有以下缺点:

  1. 线程的创建、销毁等操作都需要系统调用,代价比较大;
  2. 会消耗一定的内核资源;

  如果没有记错的话,pthread用的应该是这种方案:虽然这样会导致有大量的内核线程,但是对于调度器来说完全没有压力,甚至能在O(1)的复杂度内完成调度,再说了就算是CFS(复杂度为LogN)也完全没问题。ps:在哪里看到一篇文章大意是要这样实现pthread还得先去说服linus。

  使用用户线程来实现

  1:N的实现方案,多个“用户线程”只对应一个内核线程,而CPU完全不知道这些“用户线程”的存在。线程的创建、销毁等各种操作都在用户态完成,而且可以支持规模更大的线程数(因为没有了内核对线程数的限制)。但是还是有以下缺点:

  1. 阻塞、将线程映射到其他处理器等操作变得非常困难甚至无法完成;
  2. 需要用户处理线程调度等操作,实现起来也非常麻烦;

  在我们现在看来在用户态实现线程的切换似乎是件比较麻烦的事情,其实不是这样的,据说只要9行汇编。

  混合实现

  N:M的实现,其实可以理解为1:1和1:N的结合。用户线程还是完全在用户态,那么线程的创建、销毁等操作的开销还是非常小的。而操作系统提供的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能以及处理器映射,并且用户线程的系统调用通过轻量级线程来完成,大大降低了进程被阻塞的风险。

  Java线程的实现

  Windows和Linux版本都是使用1:1的模型(因为它们提供的线程模型就是1:1的),在Solaris中,由于操作系统的线程特性可以同时支持1:1及N:M的线程模型。

然后来总结一下Java中的多线程编程(不然太没意思了,剩下的东西在文章尾部给出)。

Java线程的创建

看很多别人写的文章都说是两种创建方法:Thread和Runnable,但总是感觉着俩只能算是一种。。

Runnable的写法:

    public static void main(String[] args) {
        Runnable runner = new Runnable() {
            public void run() {
                System.out.print("hello");
            }
        };
        new Thread(runner).start();
    }

Thread的写法:

    public static void main(String[] args) {
        new Thread() {
            public void run() {
                System.out.print("hello");
            }
        }.start();
    }

Java中定义了5中线程状态:新建、运行、无限期等待、限期等待、阻塞、结束。

  1. 新建:创建但没有调用start()。
  2. 运行:这里指的是可运行,实际上在这个状态上线程可能并没有执行(比如在等待CPU给它分配执行时间)。
  3. 无限期等待:需要其他线程唤醒(不然不会给它分配执行时间):
    1. Object.wait();
    2. Thread.Join();
    3. LockSupport.park();
  4. 限期等待:不需要其他线程唤醒,在一定时间之后会由系统唤醒:
    1. Thread.sleep();
    2. Object.wait();
    3. Thread.join();
    4. LockSupport.parkNanos();
    5. LockSupport.parkUntil();
  5. 阻塞:等待获取一个排他锁。
  6. 结束:线程已经结束执行。

多个线程读写同一块内存的时候会导致一些问题,具体原因这里就不说啦。下面来具体看一下同步的方法:

synchronized

  synchronized经过编译之后会在同步块的前后分别形成monitorentermonitorexit指令,这两个指令需要指定加锁和解锁的对象。分两种情况:

  1. 程序中的synchronized明确指定了对象参数,那就使用这个对象作为加锁和解锁的对象。
  2. 没有明确指定,根据synchronized修饰的是实例方法还是类方法来确定是对象实例为锁对象还是Class对象实例为锁对象。

用法很简单,常见的有以下几种:

    public synchronized void method1() {
        // do something.
    }
    public void method2() {
        synchronized (this) {
            // do something.
        }
    }
    public void method3(Object o) {
        synchronized (o) {
            // do something.
        }
    }
    public synchronized static void method4() {
        // do something.
    }

其实从synchronized的原理上就能知道一些特性,需要注意一下:

  1. 两个线程需要调用同一个对象的synchronized修饰的方法,同一时间只能有一个线程执行;
  2. 一个线程访问一个对象的synchronized(this)修饰的代码块时,另一个线程可以访问该对象的非synchronized(this)修饰的代码块;
  3. 一个线程访问一个对象的synchronized(this)修饰的代码块时,另一个线程无法访问该对象的其他的synchronized(this)修饰的代码块;

PS:需要注意的一点是,synchronized同步块对同一条线程是可重入的,不会出现自己吧自己锁死的问题。因为阻塞和唤醒一条线程需要操作系统帮忙完成,所有消耗还是比较大的,慎用。

java.util.concurrent

AQS作为一个并发架构需要提供以下几个功能:

  1. 不阻塞的情况下尝试获取同步资源;
  2. 允许设置等待时间;
  3. 可以取消;

其中,获取同步资源的操作(acquire)可能是这样:

while (synchronization state does not allow acquire) {
    // enqueue current thread if not already queued;
    // possibly block current thread;
}
// dequeue current thread if it was queued;

释放同步资源的代码(release)可能是这样:

// update synchronization state;
if (state may permit a blocked thread to acquire)
    // unblock one or more queued threads;

实现这些需要三个条件:

  1. 原子地操作state;
  2. 阻塞和唤醒线程;
  3. 管理队列;

state

很容易想到:即使是使用了volatile来修饰state字段,也是无法满足要求的。而且可以发现其实在修改状态的时候需要的都是一次CAS操作(compare-and-swap),看代码也可以发现在compareAndSetState中使用的正是Unsafe.compareAndSwapInt,直观上的感觉就是:CAS操作时需要锁住的做小的一个操作,其他更复杂的功能可以在此基础上完成。可以看一下的LockableObject.tryAquire的代码,如下:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            owner = current;
            return true;
        }
    }else if (current == owner) {
        setState(c + acquires);
        return true;
    }
    return false;
}

blocking

这里的block&unblock是调用LockSuport.parkLockSuport.unpark来实现的,而这两个方法最终调用的是Unsafe.parkUnsafe.unpark,比如:

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    unsafe.park(false, 0L);
    setBlocker(t, null);
}

queues

这部分是AQS框架的核心:用来维护blocked线程。队列节点的定义如下:

static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;    
}

AbstractQueuedSynchronizer会有headtail来维护。

 

 

 

condition queues