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

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

总结与参考

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

参考读物

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

推荐阅读更多精彩内容