Java并发编程(二) - 线程基础

2020-10-13  本文已影响0人  未子涵

并发不一定要依赖多线程(如PHP中很常见的多进程并发),但是在Java里面谈论并发,大多数都与线程脱不开关系,因此,就让我们从Java线程在虚拟机中的实现开始讲起。

线程的实现

主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理。我们注意到Thread类与大部分的Java API有着显著差别,它的所有关键方法都是声明为Native的。这就说明,有关线程的操作,底层都是与平台相关的。

实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

1.使用内核线程实现

内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。

程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程。每个轻量级进程都由一个内核线程支持,这种关系称为 一对一的线程模型

图1-轻量级进程与内核线程之间1-1的关系.jpg
轻量级进程的局限性

2.使用用户线程实现

广义

从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread,UT)。从这个定义上来看,轻量级进程也属于用户线程,但其实现始终是建立在内核线程之上的,许多操作都要进行系统调用,效率会受限。

狭义

这种进程与用户线程之间1:N的关系称为一对多的线程模型

图2-进程与用户线程之间1-N的关系.jpg
用户线程的劣势

不需要系统内核的支援,带给了用户线程一些优势,但同时也带来了一些劣势。

除了以前在不支持多线程的操作系统中(如DOS)的多线程程序与少数有特殊需求的程序外,现在使用用户线程的程序越来越少了,Java、Ruby等语言都曾经使用过用户线程,最终又都放弃使用它

3.使用用户线程加轻量级进程混合实现

这是一种将内核线程与用户线程一起使用的实现方式。

在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系,这就是 多对多的线程模型

图3-用户线程与轻量级进程之间N-M的关系.jpg

4.Java线程的实现

对于Sun JDK来说,其Windows版与Linux版都是使用的”一对一的线程模型“实现的,一条Java线程就映射到一条轻量级进程之中。

而在Solaris平台中,由于操作系统同时支持一对一及多对多的线程模型,因此在Solaris版的JDK中也对应提供了两个平台专有的虚拟机参数来明确指定虚拟机使用哪种线程模型。

Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要有:协同式线程调度抢占式线程调度

协同式

线程执行时间由自己控制:本身任务结束后,主动通知系统切换线程。

优点
缺点

抢占式

线程执行时间由系统分配,线程切换也由系统控制。

线程可以通过 Thread.yield() 主动让出执行时间,但却无法主动获取执行时间。

优点

给系统的线程调度提“建议”

虽然Java线程调度是系统自动完成的,但在孰多孰少上,我们也可以给系统提“建议”。这个操作可以通过设置 线程优先级 来完成。

如何理解“线程优先级”

扩展:线程优先级“不靠谱”的直接原因

  • 一些系统中不同的优先级实际会变得相同
    虽然多数操作系统都提供了线程优先级的概念,但并不一定都能与Java线程的优先级一一对应,比如Solaris有“2的32次方”种优先级,而Windows只有7种。当操作系统的线程优先级少于Java的线程优先级时,就不得不出现几个优先级相同的情况了。
  • 优先级可能会被系统自行改变
    比如Windows中的“优先级推进器”(可以被关闭),它的大致作用是当系统发现一个线程执行得特别”勤奋努力“的话,可能会越过线程优先级去为它分配执行时间。
Java线程优先级与Windows线程优先级之间的对应关系
Java线程优先级 Windows线程优先级
1(Thread.MIN_PRIORITY) THREAD_PRIORITY_LOWEST
2 THREAD_PRIORITY_LOWEST
3 THREAD_PRIORITY_BELOW_NARMAL
4 THREAD_PRIORITY_BELOW_NARMAL
5(Thread.NORMAL_PRIORITY) THREAD_PRIORITY_NORMAL
6 THREAD_PRIORITY_ABOVE_NORMAL
7 THREAD_PRIORITY_ABOVE_NORMAL
8 THREAD_PRIORITY_HIGHEST
9 THREAD_PRIORITY_HIGHEST
10(Thread.MAX_PRIORITY) THREAD_PRIORITY_CRITICAL

状态转换

Java定义了以下5种线程状态,它们在遇到特定事件时将会互相转换,但是在任意一个时间点,一个线程只能有且只有其中一种状态

1.新建(New)

创建后尚未启动

2.运行(Runnable)

Runnable 包括了操作系统线程状态中的 RunningReady ,也就是可能正在执行,也可能正在等待 CPU 为它分配执行时间。

3-1.无限期等待(Waiting)

不会被分配 CPU 执行时间,直到被其他线程显式地唤星。以下方法可以让线程进入该状态:

3-2.限期等待(Timed Waiting)

不会被分配 CPU 执行时间,但也不是非要等待被其他线程显式地唤醒,在一定时间之后会由系统自动唤醒。以下方法可以让线程进入该状态:

4.阻塞(Blocked)

线程被阻塞了。在程序将进入同步区域的时候,线程将进入这种状态。

”阻塞状态“与”等待状态“的区别是:</br>
”阻塞状态“在等待获取到一个排他锁,这将在另一个线程放弃这个排他锁时发生;</br>
”等待状态“在等待一段时间,或者唤醒动作的发生。

5.结束(Terminated)

线程已结束执行。

线程状态转换关系

线程状态转换关系.jpg

线程状态的基本操作

join()

如果某个线程的输入需要依赖另一个线程的输出,则可以通过 join() 实现。比如在线程A中调用线程B的join方法,意思就是:线程A会等待线程B执行结束后再继续。可见,join() 可以简单地实现多线程排序。

我们看下面这段测试代码:

public class JoinTester {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }

        System.out.println(Thread.currentThread().getName() + " terminated.");
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                // 试试打开下面两行代码看看运行结果
//              Thread.sleep(10);   // 这里完全可以不用sleep,只是为了更容易看到效果
//              System.out.println("I'm [" + Thread.currentThread().getName() + "], " +
//                      "I need to wait [" + thread.getName()  +"] to terminate.");
                thread.join();
                System.out.println(Thread.currentThread().getName() + " terminated.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其输出如下:

main terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-2 terminated.
Thread-3 terminated.
Thread-4 terminated.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.
Thread-9 terminated.

可见,确实实现了排序的效果,其实,细心的你可以再多思考一下:这整个运行过程的细节是怎样的?实际的排序效果是何时发生的?

为了更准确地理解整个运行过程,可以将上述代码中被注释的两行打开,再看看运行结果:

main terminated.
I'm [Thread-0], I need to wait [main] to terminate.
Thread-0 terminated.
I'm [Thread-2], I need to wait [Thread-1] to terminate.
I'm [Thread-1], I need to wait [Thread-0] to terminate.
Thread-1 terminated.
Thread-2 terminated.
I'm [Thread-4], I need to wait [Thread-3] to terminate.
I'm [Thread-3], I need to wait [Thread-2] to terminate.
Thread-3 terminated.
I'm [Thread-9], I need to wait [Thread-8] to terminate.
I'm [Thread-5], I need to wait [Thread-4] to terminate.
Thread-4 terminated.
I'm [Thread-8], I need to wait [Thread-7] to terminate.
I'm [Thread-7], I need to wait [Thread-6] to terminate.
I'm [Thread-6], I need to wait [Thread-5] to terminate.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.
Thread-9 terminated.

可见,join() 并非是说“线程B执行结束后,才开始执行线程A( run() 方法)”,其实线程A已经运行起来了,只是它在等待B运行结束而已。也就是说,真正被有序执行的代码,是 join() 之后的那部分代码,而 join() 之前的所有代码,各个线程都是并行的,没有固定的执行顺序。

我们再看一段示例代码:

public class JoinTester2 {
    public static void main(String[] args) {
        int size = 5;
        MyThread[] arr = new MyThread[size];
        for (int i = 0; i < size; i++) {
            arr[i] = new MyThread(i);
            arr[i].start();
        }
        String output = "";
        try {
            arr[0].join();
            output += arr[0].get();
            arr[1].join();
            output += arr[1].get();
            arr[2].join();
            output += arr[2].get();
            arr[3].join();
            output += arr[3].get();
            arr[4].join();
            output += arr[4].get();
            System.out.println("Result: " + output);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class MyThread extends Thread {
        private int i;

        public MyThread(int i) {
            setName("T-" + i);
            this.i = i;
        }

        @Override
        public void run() {
            this.i++;
            System.out.println("I'm [" + getName() + "], i is now " + this.i);
        }

        public int get() {
            return this.i;
        }
    }
}

输出如下:

I'm [T-0], i is now 1
I'm [T-1], i is now 2
I'm [T-3], i is now 4
I'm [T-4], i is now 5
I'm [T-2], i is now 3
Result: 12345

可见,线程体的执行(i自增)是完全并行的,而有序输出自增后的结果,这里的“有序”是在最后的结果输出阶段才做了排序拼接。

sleep() 和 wait()

sleep()wait() 都是让线程等待,但它们有以下区别:

我们看下面这段代码:

public class SleepWaitTester {
    private static byte[] lock = new byte[0];

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            doWork();
        }

        private void doWork() {
            synchronized (lock) {
                try {
                    System.out.println(getName() + " start running");
                    Thread.sleep(1000); // 换成lock.wait(1000)试试看
                    System.out.println(getName() + " stop running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

当使用 sleep() 时,运行结果如下:

Thread-0 start running
Thread-0 stop running
Thread-1 start running
Thread-1 stop running

如果将 sleep() 换成 wait() ,运行结果就成了:

Thread-0 start running
Thread-1 start running
Thread-1 stop running
Thread-0 stop running
yield()

yield() 定义如下:

public static native void yield();

可见,yield() 其实是一个native方法,它有以下特点:

来看以下代码:

public class YieldTester {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            doWork();
        }

        private void doWork() {
            System.out.println(getName() + " start running");
            yield();
            System.out.println(getName() + " stop running");
        }
    }
}

运行结果为:

Thread-0 start running
Thread-1 start running
Thread-2 start running
Thread-0 stop running
Thread-3 start running
Thread-1 stop running
Thread-4 start running
Thread-3 stop running
Thread-4 stop running
Thread-2 stop running

守护线程(Deamon Thread)

接下来,我们再来了解一下“守护线程”的概念。Java将线程分为 User线程Daemon线程 两种。 Daemon 线程即守护线程。关于守护线程,有以下要点:

通过下面一段代码,可以很清楚地说明daemon的作用。

public class DeamonThreadTest {
    public static void main(String[] args) {
        // 验证一:Deamon线程的finally块,不保证执行,
        // 若执行到finally之前所有user线程都结束,则deamon线程也会随着jvm退出而结束,不会执行finally
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("DeamonThread finally run.");
                }
            }
        });
        thread.setDaemon(true);
        thread.start();

        // 验证二:main线程结束,deamon线程即终止,不会等待执行完毕
        Runnable r = new Runnable() {
            public void run() {
                for (int time = 10; time > 0; --time) {
                    System.out.println("Time #" + time);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t = new Thread(r);
        t.setDaemon(true);  // try to set this to "false" and see what happens
        t.start();

        System.out.println("Main thread waiting...");
        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread exited.");
    }
}

运行结果是:

Main thread waiting...
Time #10
Time #9
Time #8
Main thread exited.

从这个结果验证了两点:一、Deamon线程的finally块,不保证执行。二、当所有User线程(这里的main线程)结束时,Jvm即退出,不会等待 deamon 线程执行完毕。

我们将线程 t 设置为 User 线程(即setDeamon为false)后,再运行一下:

Main thread waiting...
Time #10
Time #9
Time #8
Main thread exited.
Time #7
Time #6
Time #5
Time #4
Time #3
Time #2
Time #1
DeamonThread finally run.

这一次,作为 User 线程时,就能数完10个数字了。

Java线程总结

最后,我们来总结一下关于Java线程的基础知识点:

上一篇下一篇

猜你喜欢

热点阅读