三、Java并发编程基础

转:《Java并发编程的艺术》

1 线程简介

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作
系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

2 线程优先级

现代操作系统基本采用时分的形式调度运行的线程, 操作系统会分出一个个时间片, 线程会分配到若干时间片, 当线程的时间片用完了就会发生线程调度, 并等待着下次分配。 线程分配到的时间片多少也就决定了线程使用处理器资源的多少, 而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

在Java线程中,通过一个整型成员变量priority来控制优先级, 优先级的范围从1~10, 在线程构建的时候可以通过setPriority(int)方法来修改优先级, 默认优先级是5, 优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时, 针对频繁阻塞(休眠或者I/O操作) 的线程需要设置较高优先级, 而偏重计算(需要较多CPU时间或者偏运算) 的线程则设置较低的优先级, 确保处理器不会被独占。 在不同的JVM以及操作系统上, 线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定(因此,在程序中设置线程优先级的实践意义并不大,因为线程优先级的最终解释权在底层操作系统)。

3 并发与并行的区别

如果某个系统支持两个或者多个操作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。

在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

用一个极其简单的生活实例来解释如下:
  你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
  并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。

4 线程状态及其切换

下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻。


  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程队列中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会再次转到运行状态。

阻塞的情况分三种:
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中(wait会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
其他阻塞运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态(注意,sleep不会释放线程所持有的锁)

  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java线程在运行的生命周期中可能处于下图表中所示的6种不同的状态, 在给定的一个时刻,线程只能处于其中的一个状态。

下面使用jstack工具(可以选择打开终端, 键入jstack或者到JDK安装目录的bin目录下执行命令), 尝试查看示例代码运行时的线程信息, 更加深入地理解线程状态。测试代码如下:

public class ThreadState {
    // 该线程不断地进行睡眠
    static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(100);
            }
        }
    }

    // 该线程在Waiting.class实例上等待
    static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    // 该线程在Blocked.class实例上加锁后, 不会释放该锁
    static class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.second(100);
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TimeWaiting (), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        // 使用两个Blocked线程, 一个获取锁成功, 另一个被阻塞
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }
}

上述示例中使用的SleepUtils代码如下:

public class SleepUtils {

    public static final void second(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds) ;
        } catch (InterruptedException e) {
        }
    }
}

运行该示例, 打开终端或者命令提示符, 键入“jps”, 输出如下:

16544 Jps
13700
10156 ThreadState

可以看到运行示例对应的进程ID是10156,接着再输入“jstack 10156”,部分输出如下:

//BlockedThread-2线程阻塞在获取Blocked.class示例的锁上
"BlockedThread-2" #13 prio=5 os_prio=0 tid=0x00000000180ad800 nid=0x108c waiting for monitor entry [0x0000000018e8f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - waiting to lock <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//BlockedThread-1线程获取到了Blocked.class的锁,处于睡眠状态
"BlockedThread-1" #12 prio=5 os_prio=0 tid=0x00000000180ad000 nid=0x2740 waiting on condition [0x0000000018d8e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - locked <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//WaitingThread线程在Waitting实例上等待
"WaitingThread" #11 prio=5 os_prio=0 tid=0x00000000180a6000 nid=0x93c in Object.wait() [0x0000000018c8f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Object.wait(Object.java:502)
        at ThreadState$Waiting.run(ThreadState.java:21)
        - locked <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Thread.run(Thread.java:745)
//TimeWaitingThread线程处于超时等待
"TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x00000000180a5000 nid=0x8c4 waiting on condition [0x0000000018b8f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$TimeWaiting.run(ThreadState.java:8)
        at java.lang.Thread.run(Thread.java:745)

通过示例, 我们了解到Java程序运行中线程状态的具体含义。 线程在自身的生命周期中,并不是固定地处于某个状态, 而是随着代码的执行在不同的状态之间进行切换。

下面是一张更详细的线程状态迁移图:

从图中可以看到, 线程创建之后, 调用start()方法开始运行(这里的运行状态其实是就绪态和运行态的合集)。 当线程执行wait()方法之后, 线程进入等待状态。 进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态, 而超时等待状态相当于在等待状态的基础上增加了超时限制, 也就是超时时间到达时将会返回到运行状态。 当线程调用同步方法时, 在没有获取到锁的情况下, 线程将会进入到阻塞状态。 线程在执行Runnable的run()方法之后将会进入到终止状态。

注意:阻塞状态是线程在进入synchronized关键字修饰的方法或代码块(尝试获取锁) 时没有拿到锁的状态,但是阻塞在java.concurrent包中Lock接口 的线程状态却是等待状态, 因为java.concurrent包中 Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

5 线程的启动、中断与终止

线程对象在初始化完成之后, 调用start()方法就可以启动这个线程。 线程start()方法的含义是: 当前线程(即parent线程) 同步告知Java虚拟机, 只要线程规划器空闲, 应立即启动调用start()方法的线程。

注意:启动一个线程前, 最好为这个线程设置线程名 称, 因为这样在使用jstack分析程序或者进行问题排查时, 就会给开发人员提供一些提示, 自定义的线程最好能够起个名字。

中断可以理解为线程的一个标识位属性, 它表示一个运行中的线程是否被其他线程进行了中断操作。 中断好比其他线程对该线程打了个招呼, 其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应, 线程通过方法isInterrupted()来进行判断是否被中断, 也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。 如果该线程已经处于终结状态, 即使该线程被中断过, 在调用该线程对象的isInterrupted()时依旧会返回false。

调用某个线程的interrupt()方法,将会设置该线程为中断状态,即设置为true。线程中断后的结果是死亡、还是等待新的任务或是继续运行至下一步,取决于这个程序本身。线程可以不时地检测这个中断标识位,以判断线程是否应该被中断(中断标志是否为true)。它并不像stop方法那样会真的会粗暴地打断一个正在运行的线程。

从Java的API中可以看到, 有许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法), 这些方法在抛出InterruptedException之前, Java虚拟机会先将该线程的中断标识位清除, 然后抛出InterruptedException, 此时调用isInterrupted()方法将会返回false。

测试代码如下,首先创建了两个线程, SleepThread和BusyThread, 前者不停地睡眠, 后者一直运行, 然后对这两个线程分别进行中断操作, 观察二者的中断标识位。

public class Interrupted {

    public static void main(String[ ] args) throws Exception {
        // sleepThread不停的尝试睡眠
        Thread sleepThread = new Thread(new SleepRunner() , "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread不停的运行
        Thread busyThread = new Thread(new BusyRunner() , "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();

        // 休眠5秒, 让sleepThread和busyThread充分运行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());

        // 防止sleepThread和busyThread立刻退出
        SleepUtils. second(2);

    }

    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils. second(10) ;
            }
        }
    }

    static class BusyRunner implements Runnable {
        @Override
        public void run() {
            while (true) {

            }
        }   
    }
}

输出如下:

SleepThread interrupted is false
BusyThread interrupted is true

从结果可以看出, 抛出InterruptedException的线程SleepThread, 其中断标识位被清除了,而一直忙碌运作的线程BusyThread, 中断标识位没有被清除。

中断状态是线程的一个标识位, 而中断操作是一种简便的线程间交互方式, 而这种交互方式最适合用来取消或停止任务。 除了中断以外, 还可以利用一个boolean共享变量来控制是否需要停止任务并终止该线程,这是最受推荐的终止一个线程(就是让一个线程彻底停止运行)的方式,使用共享变量(shared variable)来发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地停止任务。测试代码如下:

public class Shutdown {

    public static void main(String[ ] args) throws Exception {
        Runner one = new Runner() ;
        Thread countThread = new Thread(one, "CountThread") ;
        countThread.start() ;

        // 睡眠1秒,main线程对Runner one进行中断, 使CountThread能够感知中断标识位的置位而结束
        TimeUnit.SECONDS.sleep(1) ;
        countThread.interrupt() ;
        Runner two = new Runner() ;
        countThread = new Thread(two, "CountThread") ;
        countThread.start() ;
        // 睡眠1秒,main线程对Runner two进行取消, 使CountThread能够感知on为false而结束
        TimeUnit.SECONDS.sleep(1) ;
        two.cancel() ;
    }

    private static class Runner implements Runnable {

        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && ! Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false; 
        }
    }       
}

输出结果如下(多次运行结果可能不同):

Count i = 543487324
Count i = 540898082

示例在执行过程中, main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源, 而不是武断地将线程停止, 因此这种终止线程的做法显得更加安全和优雅。

注意:suspend()、resume()和stop()方法也可以完成线程的暂停、恢复和终止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容