美文网首页
Java 基础 —— 多线程(读书笔记)「一」

Java 基础 —— 多线程(读书笔记)「一」

作者: 谢三弟 | 来源:发表于2016-10-07 09:30 被阅读128次

    多线程对于 Android 开发者来说是基础。而且这类知识在计算机里也是很重要的一环,所以很有必要整理一番。

    目录

    多线程的实现

    来上代码:

    // 最常见的两种方法启动新的线程
    public static void startThread() {
        // 覆盖 run 方法
        new Thread() {
            @Override
            public void run() {
                // 耗时操作
            }
        }.start();
    
        // 传入 Runnable 对象
        new Thread(new Runnable() {
            public void run() {
                // 耗时操作
            }
        }).start();
    }
    

    其实第一个就是在 Thread 里覆写了 run() 函数,第二个是给 Thread 传了一个 Runnable 对象,在 Runnable 对象 run() 方法里进行耗时操作。
    以前没有怎么考虑过他们两者的关系,今天我们来具体看看到底是什么鬼?

    Thread 源码

    进入 Thread 源码我们看看:

    public class Thread implements Runnable {
        /* What will be run. */
        private Runnable target;
    
        /* The group of this thread */
        private ThreadGroup group;
    
    
        public Thread() {
            init(null, null, "Thread-" + nextThreadNum(), 0);
        }
        public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
        }
    }
    

    源码很长,我进行了一点分割。一点一点的来解析看看。
    我们首先知道 Thread 也是一个 Runnable ,它实现了 Runnable 接口,并且在 Thread 类中有一个 Runnable 类型的 target 对象。

    构造方法里我们都会调用 init() 方法,接下来看看在该方法里做了如何的初始化配置。

        private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
        }
    
        private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
    
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        // group 参数如果为 null ,则获得当前线程的 group(线程组)
        if (g == null) {
                g = parent.getThreadGroup();
        }
        // 代码省略
    
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        // 设置 target( Runnable 类型 )
        this.target = target;
    
        }
    
    
        public synchronized void start() {
    
        // 将当前线程加入线程组
        group.add(this);
    
        boolean started = false;
        try {
            // 启动 native 方法启动新的线程
            start0();
            started = true;
        } finally {
            // 代码省略
        }
    
       private native void start0();
    
    
    
       @Override
       public void run() {
           if (target != null) {
            target.run();
           }
        }
    

    从上我们可以明白,最终被线程执行的任务是 Runnable ,Thread 只是对 Runnable 的一个包装,并且通过一些状态对 Thread 进行管理和调度。
    当启动一个线程时,如果 Thread 的 target 不为空,则会在子线程中执行这个 target 的 run() 函数,否则虚拟机就会执行该线程自身的 run() 函数。

    线程的几个重要的函数
    • wait()
      当一个线程执行到 wait() 方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁,使得其他线程可以访问。用户可以使用 notify 、notifyAll 或者指定睡眠时间来唤醒当前等待池中的线程。
      注意:wait() notify() notifyAll() 必须放在 synchronized block 中,否则会抛出异常。
    • sleep()
      该函数是 Thread 的静态函数,作用是使调用线程进入睡眠状态。因为 sleep() 是 Thread 类的静态方法,因此他不能改变对象的机锁。所以,当在一个 synchronized 块中调用 sleep() 方法时,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象。
    • join()
      等待目标线程执行完成之后继续执行。
    • yield()
      线程礼让。目前线程由运行状态转换为就绪状态,也就是让出执行权限,让其他线程得以优先执行,但其他线程能否优先执行未知。

    在源码中,查看 Thread 里的 State ,对几种状态解释的很清楚。

    NEW 状态是指线程刚创建,尚未启动

    RUNNABLE 状态是线程正在正常运行中,当然可能会有某种耗时计算 / IO 等待的操作 / CPU 时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep 等

    BLOCKED 这个状态下,是在多个线程有同步操作的场景, 比如正在等待另一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是这时线程在等待进入临界区

    WAITING 这个状态下是指线程拥有了某个锁之后,调用了他的 wait 方法,等待其他线程 / 锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作,这里要区分 BLOCKED 和 WATING ,一个是在临界点外面等待进入, 一个是在临界点里面 wait 等待别人 notify , 线程调用了 join 方法 进入另外的线程的时候, 也会进入 WAITING 状态,等待被他 join 的线程执行结束

    TIMED_WAITING 这个状态就是有限的 (时间限制) 的 WAITING, 一般出现在调用 wait(long), join(long) 等情况下,另外,一个线程 sleep 后, 也会进入 TIMED_WAITING 状态

    TERMINATED 这个状态下表示 该线程的 run 方法已经执行完毕了, 基本上就等于死亡了 (当时如果线程被持久持有, 可能不会被回收)

    Wait() 的实践

    我们来看一段,wait() 的用途和效果。

        static void waitAndNotifyAll() {
    
            System.out.println("主线程运行");
    
            Thread thread = new WaitThread();
            thread.start();
            long startTime = System.currentTimeMillis();
            try {
                synchronized (sLockOject) {
                    System.out.println("主线程等待");
                    sLockOject.wait();
                }
            } catch (Exception e) {
            }
    
            long timeMs = System.currentTimeMillis() - startTime;
            System.out.println("主线程继续 —-> 等待耗时:" + timeMs + " ms");
    
        }
    
        static class WaitThread extends Thread {
    
            @Override
            public void run() {
                try {
                    synchronized (sLockOject) {
                        System.out.println("进入子线程");
                        Thread.sleep(3000);
                        System.out.println("唤醒主线程");
                        sLockOject.notifyAll();
                    }
                } catch (Exception e) {
                }
            }
    
        }
    

    waitAndNotifyAll() 函数里,会启动一个 WaitThread 线程,在该线程中将会调用 sleep 函数睡眠 3 秒。线程启动之后在主线程调用 sLockOject 的 wait() 函数,使主线程进入等待状态,此时将不会继续执行。等 WaitThread 在 run() 函数沉睡了 3 秒后会调用 sLockOject 的 notifyAll() 函数,此时就会重新唤醒正在等待中的主线程,因此会继续往下执行。

    结果如下:

    主线程运行
    主线程等待
    进入子线程
    唤醒主线程
    主线程继续 —-> 等待耗时:3005 ms

    wait()、notify() 机制通常用于等待机制的实现,当条件未满足时调用 wait 进入等待状态,一旦条件满足,调用 notifynotifyAll 唤醒等待的线程继续执行。

    对于这里细节可能会有一些疑问。</br>

    在子线程启动的时候,run() 函数里面已经持有了该对象锁。</br>

    但是真实环境下,其实是主线程先持有对象锁,然后调用 wait() 进入等待区并且释放锁等待唤醒。

    这个问题涉及到 JNI 代码,目前我只能从理论上来解释这个问题。
    我们都知道一个线程 start() 并不是马上启动,而是需要 CPU 分配资源的,根据目前运行来看,分配资源的时间大于 Java 虚拟机运行指令的时间,所以主线程比子线程先拿到锁。
    我们还可以知道一点,控制台打印出的时间是 3005 ms ,在代码里我们只等待了 3s 多出来的 5ms (这个数字会浮动)我们可以推断是,子线程获取 CPU 的时间加上唤醒主线程的时间。

    上述只是自己的一个猜测,能力还有欠缺,准备深入学习。

    不过推荐大家看看这篇文章 Synchnornized 在 JVM 下的实现 - 简书

    Join() 的实践

    join() 的注释上面写着:

    Waits for this thread to die.

    意思是,阻塞当前调用 join() 函数所在的线程,直到接收线程执行完毕之后再继续。
    我们来看看实践代码:

    public class JoinThread {
    
        public static void main(String[] args) {
            joinDemo();
        }
    
        public static void joinDemo() {
            Worker worker1 = new Worker("work-1");
            Worker worker2 = new Worker("work-2");
            worker1.start();
            System.out.println("启动线程 1 ");
    
            try {
                // 调用 worker1 的 join 函数,主线,程会阻塞直到 woker1 执行完成
                worker1.join();
                System.out.println("启动线程 2");
                // 再启动线程 2 ,并且调用线程 2 的 join 函数,主线程会阻塞直到 woker2 执行完成
                worker2.start();
                worker2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("主线程继续执行");
        }
    
        static class Worker extends Thread {
            public Worker(String name) {
                super(name);
            }
    
            @Override
            public void run() {
    
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
    
                System.out.println("work in " + getName());
    
            }
        }
    }
    

    运行之后我们得到:

    启动线程 1
    work in work-1
    启动线程 2
    work in work-2
    主线程继续执行

    joinDemo() 方法里我们创建两个子线程,然后启动了 work1 线程,下一步调用了 woker1 的 join() 函数。此时,主线程会进入阻塞状态,直到 work1 执行完毕之后才开始继续执行。因为 Worker 的 run() 方法里会休眠 2 秒,因此线程每次调用了 join() 方法实际上都会阻塞 2 秒,直到 run() 方法执行完毕再继续。
    所以,上述代码逻辑其实就是:

    启动线程1 —-> 等待线程 1 执行完毕 —-> 启动线程2 —-> 等待线程 2 执行完毕 —-> 继续执行主线程代码

    Yield() 的实践

    yield() 是 Thread 的静态方法,注释上说:

    A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

    大致意思是说:当前线程让出执行时间给其他的线程。
    我们都知道,线程的执行是有时间片的,每个线程轮流占用 CPU 固定时间,执行周期到了之后让出执行权给其他线程。
    yield() 就是主动让出执行权给其他线程。

    来看看我们实践的代码:

    public class YieldThreadTest {
    
        public static void main(String[] args) {
            YieldTread t1 = new YieldTread("thread-1");
            YieldTread t2 = new YieldTread("thread-2");
            t1.start();
            t2.start();
        }
    
        public static class YieldTread extends Thread {
    
            public YieldTread(String name) {
                super(name);
            }
    
            public synchronized void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.printf("%s 优先级为 [%d] -------> %d\n", this.getName(), this.getPriority(), i);
                    // 当 i 为 2 时,调用当前线程的 yield 函数
                    if (i == 2) {
                        Thread.yield();
    
                    }
                }
            }
    
        }
    
    }
    

    main() 方法里创建了两个 YieldTread 线程,控制台输出结果如下:

    thread-1 优先级为 [5] -------> 0
    thread-1 优先级为 [5] -------> 1
    thread-1 优先级为 [5] -------> 2

    thread-2 优先级为 [5] -------> 0
    thread-2 优先级为 [5] -------> 1
    thread-2 优先级为 [5] -------> 2

    thread-1 优先级为 [5] -------> 3
    thread-1 优先级为 [5] -------> 4
    thread-2 优先级为 [5] -------> 3
    thread-2 优先级为 [5] -------> 4

    通常情况下 t1 首先执行,让 t1 的 run() 函数执行到了 i 等于 2 时让出当前线程的执行时间。所以我们看到前三行都是 t1 在执行,让出执行时间后 t2 开始执行。后面逻辑简单思考下就得知了,这里也不做过多诠释。

    因此,调用 yield() 就是让出当前线程的执行权,这样一来让其他线程得到优先执行。

    总结与参考

    本章内容属于线程的基础,本系列会更新到线程池相关。
    这章内容也及其重要,因为它是后面的基础。
    正确理解才能让我们对各种线程问题有方向和思路。

    参考读物

    相关文章

      网友评论

          本文标题:Java 基础 —— 多线程(读书笔记)「一」

          本文链接:https://www.haomeiwen.com/subject/kfdyyttx.html