java并发(2)线程详解

什么是线程

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程由线程ID程序计数器(PC)[用于指向内存中的程序指令],寄存器集合[由于存放本地变量和临时变量]和堆栈[用于存放方法指令和方法参数等]组成。

线程状态

Java线程中,有一个内部枚举类 State,里面定义了Java线程的六种状态.

状态名 说明
NEW 初始状态,线程被构建,但是还没调用start()方法
RUNNABLE 处于可运行状态的线程正在Java虚拟机中执行,
但它可能正在等待来自操作系统(例如处理器)的其他资源。
对应操作系统中的就绪和运行状态
BLOCKED 阻塞状态, 线程正在处于等待锁的状态
WAITING 等待状态,表示当前线程需要等待其他线程唤醒或中断.
TIMED_WAITING 超时等待状态,不同于 WAITING,他可以在指定的时间内自行返回
TERMINATED 终止状态, 表示当前线程已经执行完毕
java线程状态.png

阻塞状态是线程阻塞在进入synchronized方法或方法块(等待获取锁)的状态, 但是阻塞在 JUC中 Lock接口的线程状态确实等待状态,因为JUC是使用LockSupport相关方法来实现阻塞.

线程优先级

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

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

线程的优先级在一些系统中可能不会生效. 即该方法不能保证线程的优先级,只能当做辅助.

public class Priority {
    public static void main(String[] args) throws Exception {

        Thread.currentThread().setPriority(10);
        Thread t = null;
        for (int i = 1; i < 10; i++) {
            t = new Thread(new JustWaitRunnable(), "thread priority " + i);
            t.setPriority(i);
            t.start();
        }
        t.join();
    }

    private static class JustWaitRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

使用 jps指令查找对应的java进程号.

使用 jstack pid 查看java进程具体的信息.

在windows上运行的jstack信息:

"main" #1 prio=10 os_prio=2 tid=0x00000000036b3800 nid=0x7fc4 in Object.wait() [0x00000000036af000]
   java.lang.Thread.State: WAITING (on object monitor)

"thread priority 9" #18 prio=9 os_prio=2 tid=0x000000001e262000 nid=0x7d7c waiting on condition [0x000000001f51f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 8" #17 prio=8 os_prio=1 tid=0x000000001e261800 nid=0xdfc waiting on condition [0x000000001f41f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 7" #16 prio=7 os_prio=1 tid=0x000000001e25c800 nid=0x7d94 waiting on condition [0x000000001f31f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 6" #15 prio=6 os_prio=0 tid=0x000000001e258000 nid=0x7edc waiting on condition [0x000000001f21e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 5" #14 prio=5 os_prio=0 tid=0x000000001e257000 nid=0x23d4 waiting on condition [0x000000001f11e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 4" #13 prio=4 os_prio=-1 tid=0x000000001e256800 nid=0x7c74 waiting on condition [0x000000001f01f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 3" #12 prio=3 os_prio=-1 tid=0x000000001e253800 nid=0x7a98 waiting on condition [0x000000001ef1e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 2" #11 prio=2 os_prio=-2 tid=0x000000001e23f800 nid=0x7e6c waiting on condition [0x000000001ee1f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 1" #10 prio=1 os_prio=-2 tid=0x000000001e23e800 nid=0x7fdc waiting on condition [0x000000001ed1f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

从上面可以看出,在windows平台上, java优先级和对应的内核优先级的关系.

Java线程优先级 内核线程对应的优先级
1,2 -2
3,4 -1
5,6 0
7,8 1
9,10 2

在mac上的jstack信息

"main" #1 prio=10 os_prio=31 tid=0x00007fea1c800800 nid=0x1103 in Object.wait() [0x00007000016d2000]
   java.lang.Thread.State: WAITING (on object monitor)

"thread priority 9" #17 prio=9 os_prio=31 tid=0x00007fea1c83b000 nid=0x5803 waiting on condition [0x0000700002e1a000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 8" #16 prio=8 os_prio=31 tid=0x00007fea1b041800 nid=0xa703 waiting on condition [0x0000700002d17000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 7" #15 prio=7 os_prio=31 tid=0x00007fea1c83a000 nid=0xa803 waiting on condition [0x0000700002c14000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 6" #14 prio=6 os_prio=31 tid=0x00007fea1b040800 nid=0x5503 waiting on condition [0x0000700002b11000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 5" #13 prio=5 os_prio=31 tid=0x00007fea1c839800 nid=0x4503 waiting on condition [0x0000700002a0e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 4" #12 prio=4 os_prio=31 tid=0x00007fea1b040000 nid=0x4403 waiting on condition [0x000070000290b000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 3" #11 prio=3 os_prio=31 tid=0x00007fea1c838800 nid=0x4903 waiting on condition [0x0000700002808000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 2" #10 prio=2 os_prio=31 tid=0x00007fea1c026800 nid=0x4a03 waiting on condition [0x0000700002705000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"thread priority 1" #9 prio=1 os_prio=31 tid=0x00007fea1b853000 nid=0x4b03 waiting on condition [0x0000700002602000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java 线程对于优先级的设定。
从上面可以看出,在mac系统中, java线程优先级的设置是无效的, 对应的系统线程优先级都是 31.

另一个例子 :

public class Priority {
    private static volatile boolean notStart = true;
    private static volatile boolean notEnd   = true;

    public static void main(String[] args) throws Exception {
        List<Job> jobs = new ArrayList<Job>();
        for (int i = 1; i < 11; i++) {
            Job job = new Job(i);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread:" + i);
            thread.setPriority(i);
            thread.start();
        }
        notStart = false;
        Thread.currentThread().setPriority(8);
        System.out.println("done.");
        TimeUnit.SECONDS.sleep(5);
        notEnd = false;

        jobs.sort(Comparator.comparingInt(o -> o.jobCount));
        for (Job job : jobs) {
            System.out.println("Job Priority : " + job.priority + ", Count : " + job.jobCount);
        }
    }

    static class Job implements Runnable {
        private final int priority;
        private       int jobCount;

        public Job(int priority) {
            this.priority = priority;
        }

        public void run() {
            while (notStart) {
                Thread.yield();
            }
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

在windows上运行:

Job Priority : 2, Count : 102
Job Priority : 1, Count : 318
Job Priority : 3, Count : 10518
Job Priority : 4, Count : 10667
Job Priority : 6, Count : 1113314
Job Priority : 5, Count : 1113323
Job Priority : 7, Count : 1874817
Job Priority : 8, Count : 1875519
Job Priority : 10, Count : 2133667
Job Priority : 9, Count : 2135421

在Mac上运行:

Job Priority : 5, Count : 417774
Job Priority : 2, Count : 417997
Job Priority : 3, Count : 418077
Job Priority : 1, Count : 418094
Job Priority : 4, Count : 418203
Job Priority : 9, Count : 418218
Job Priority : 8, Count : 418237
Job Priority : 7, Count : 418299
Job Priority : 6, Count : 418416
Job Priority : 10, Count : 418463

在Android手机中运行:

Job Priority : 2, Count : 1332
Job Priority : 1, Count : 1827
Job Priority : 4, Count : 4250
Job Priority : 3, Count : 24189
Job Priority : 5, Count : 501642
Job Priority : 6, Count : 709992
Job Priority : 7, Count : 1181706
Job Priority : 8, Count : 1906252
Job Priority : 9, Count : 2342730
Job Priority : 10, Count : 2967839

Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。

可以通过调 用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner());
        thread.setDaemon(true);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " thread run finished");
    }

    static class DaemonRunner implements Runnable {
        @Override
        public void run() {
            try {
                SleepUtils.second(100);
            } finally {
                System.out.println("DaemonThread run finally");
            }
        }
    }
}
// 运行结果
// main thread run finished

从例子中可以看出,当Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即 终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。

在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源
的逻辑。因此, io处理,数据库访问,持有资源需要及时释放的工作不能放在Daemon线程中处理.

我们开发者很少会用到它, 它主要是用来做内存清理,对象释放等, 如JVM中GC操作等使用的就是Daemon进程.

捕获线程未处理的异常

线程中, 有两个方法可以捕获未处理的异常.Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler).

其中 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)非静态方法, 因此只能处理 该线程中的未处理异常捕获.
Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)是静态方法, 他可以处理进程内,所有没有自定义异常处理器的所有未处理异常.

对于这种异常处理, 建议只对 默认的处理机制做增强处理, 而不是完全替换为自己的实现.

应用 : Android中获取奔溃日志

public class LogExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler mParent = Thread.getDefaultUncaughtExceptionHandler();

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        extraHandle(t, e);
        if (mParent != null)
            mParent.uncaughtException(t, e);
    }

    private static void extraHandle(Thread t, Throwable e) {
        String threadName = t.getName();
        String trace      = AlException.fullTrace(e);
        // todo 保存奔溃日志
        System.out.println("Exception in thread \"" + threadName + "\" : " + trace);
    }
}

// 在应用入口初始化
Thread.setDefaultUncaughtExceptionHandler(new LogExceptionHandler());

线程中断

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

理解 interrupt()方法

当调用 interrupt()时:

如果线程在处于Object#wait(), Thread#join()或者Thread#sleep(long)情况时,则interrupt状态将会被重置, 即interrupt=false,但会抛出InterruptedException

如果线程通过java.nio.channels.InterruptibleChannel阻塞在IO操作中, 则interrupt=true,且抛出java.nio.channels.ClosedByInterruptException.

如果处于非阻塞状态, 则interrupt=true,且不会抛出异常,且不会停止线程执行.

过期的suspend()、resume()和stop()

为什么官方移除了, 直接停止线程的相关操作?

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

安全退出线程

除了中断以外,通常是利用一个boolean变量来控制是否需要停止任务并终止该线程。

使用boolean变量量退出循环,这个变量最好使用volatile修饰.

public class ExitThread {
    public static void main(String[] args) {
        // interrupt 的两种情况
        Thread busyThread = new Thread(new BusyRunnable(), "busyThread");
        busyThread.start();

        Thread sleepThread = new Thread(new SleepRunnable(), "sleepThread");
        sleepThread.start();

        // boolean 标志中断
        TagRunnable tagRunnable = new TagRunnable();
        Thread      tagThread   = new Thread(tagRunnable, "tagThread");
        tagThread.start();

        SleepUtils.second(2);
        // 中断
        busyThread.interrupt();
        sleepThread.interrupt();
        tagRunnable.stop();
    }

    private static class BusyRunnable implements Runnable {

        @Override
        public void run() {
            Thread t = Thread.currentThread();
            while (!t.isInterrupted()) {

            }
            System.out.println("busy thread end");
        }
    }

    private static class SleepRunnable implements Runnable {
        @Override
        public void run() {
            Thread t = Thread.currentThread();
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println("sleep thread end");
        }
    }

    private static class TagRunnable implements Runnable {
        private volatile boolean tag = true;

        void stop() {
            System.out.println("stop:" + Thread.currentThread().getName());
            tag = false;
        }

        @Override
        public void run() {
            System.out.println("run:" + Thread.currentThread().getName());
            while (tag) {
            }
            System.out.println("tag thread end");
        }
    }
}

yield()方法

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程重新竞争运行。

线程通信

共享内存变量-线程安全通信

  • synchronized
  • volatile
  • JUC的Lock

等待/通知机制

此处讲的等待通知机制,主要是指 Object.wait()/Object.notify()

方法名 描述
notify() 通知一个在对象上等待的线程,使其从wait()方法中返回.
返回的前提是 该线程获得了对象的锁
notifyAll() 通知所有等待在该对象上的线程
wait() 调用该放的线程进入WAITING状态,只有等待另外线程的通知或者中断操作才会返回,
调用wait()后,会释放持有对象的锁.
wait(long) 等待一段时间,如果超过long时间则直接返回

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

模型

等待方遵循如下原则:

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
  3. 条件满足则执行对应的逻辑。
synchronized(object) {
    while(条件不满足) {
        object.wait();
    }
    // todo 条件满足对应的处理
}

通知方遵循如下原则:

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

推荐阅读更多精彩内容