美文网首页
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() 就是让出当前线程的执行权,这样一来让其他线程得到优先执行。

总结与参考

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

参考读物

相关文章

  • android 多线程 — 线程的面试题和答案

    这里都是我从各个地方找来的资料,鸣谢: Java多线程干货系列—(一)Java多线程基础 JAVA多线程和并发基础...

  • 技术体系

    一,java核心 java基础,jvm,算法,多线程,设计模式 Java基础:java基础相关,全栈java基础 ...

  • Java多线程目录

    Java多线程目录 Java多线程1 线程基础Java多线程2 多个线程之间共享数据Java多线程3 原子性操作类...

  • java多线程相关

    (一) 基础篇 01.Java多线程系列--“基础篇”01之 基本概念 02.Java多线程系列--“基础篇”02...

  • Java基础

    Java基础 集合基础 集合框架 多线程基础 多线程框架 反射 代理 集合基础 ArrayList LinkedL...

  • Java多线程高级特性(JDK8)

    [TOC] 一、Java多线程 1.Java多线程基础知识 Java 给多线程编程提供了内置的支持。一条线程指的是...

  • Java架构师阅读书单

    一、内功心法 Java基础: 《Java核心技术》《Java编程思想》《Effective Java》 多线程...

  • Java多线程系列目录(共43篇)-转

    最近,在研究Java多线程的内容目录,将其内容逐步整理并发布。 (一) 基础篇 Java多线程系列--“基础篇”0...

  • Java多线程学习(三)——synchronized(上)

    在前两节的《Java多线程学习(一)——多线程基础》和《Java多线程学习(二)——Thread类的方法介绍》中我...

  • Android中的多线程

    1. Java多线程基础 Java多线程,线程同步,线程通讯 2. Android常用线程 HandlerThre...

网友评论

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

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