并发编程基础

本文内容提要:wait()、notify()、join()、sleep()、yield()、interrupt()、ThreadLocal、InheritThreadLocal、TransmittableThreadLocal。

Thread的生命周期

Thread的生命周期分为初始化,就绪,运行,阻塞,终止,其中只有运行状态的线程拥有CPU资源的时间片。

线程的生命周期

Object-线程的wait()和notify()

​ 线程的等待和通知方法放在Object类里而非Thread类,对于wait()方法来说,必须在调用之前获取对应实例的监视器锁,否则会抛出IllegalMonitorStateException。而通常,锁资源可以是任意对象,把wait()、notify()、notifyAll()方法放在Obejct方法里,符合Java把所有类都会使用的方法定义在Object类的思想。

​ 注意:正如前文所提:调用wait()之前,必须在调用之前获取对应实例的监视器锁。

 private static void interruptTest() throws InterruptedException {
        Integer obj = 1;
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("this is begining");
                try {
                  synchronized (obj) {
                        obj.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("this is ending");
            }
        });
            a.start();
        a.join();
 }

​ 当线程调用wait方法后,会释放锁资源,并进入阻塞状态。等待其它线程调用notify()方法、或者notifyAll()方法唤醒,或者interrupt中断和wait(time)调用后等待超时的虚假唤醒。当调用notify()函数,且对于锁对象obj,存在多个线程处于阻塞状态,会随机选一个进行唤醒。而notifyAll()则会唤醒obj下所有阻塞的对象。注意:唤醒并不代表立刻执行,而是竞争锁,竞争到锁后才会到就绪状态,只有等到竞争到CPU资源也就是时间片后才变成运行状态继续执行。

​ 上述的运行->阻塞->就绪->执行的状态转换涉及到一个细节,就是线程如何知道再次执行时从哪里开始继续往下执行,因此会在阻塞时,或者说进行时间片切换时,记录当前执行地址,这里用到的是线程私有的程序计数器

Thread里的方法

等待线程终止的join()方法

​ 有时候存在这样的需求,主线程开启n个子线程,并希望在所有子线程结束后在进行一些逻辑操作。这时候就需要用到join()方法。

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Thread a = new Thread(() -> {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("subThread is over");
    });

    a.start();
    a.join();
    System.out.println("main is over");
}

输出结果为:

subThread is over
main is over

主线程在调用了a线程后进入阻塞状态,这时可以通过interrupt()方法中断阻塞状态。

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Thread a = new Thread(() -> {
        while (true) {
        }
    });

    Thread b = new Thread(() -> {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mainThread.interrupt();
    });

    b.start();
    a.start();
    try {
        a.join();
    } catch (InterruptedException e) {
        System.out.println("main is interrupted");
    }
    System.out.println("main is over");
}

输出结果为:

main is interrupted
main is over

让线程睡眠的sleep()方法

sleep()方法会让当前线程进入阻塞状态,但不会释放锁资源。

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Integer lock1 = 1, lock2 = 1;
    Thread a = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("a get lock1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("a get lock2");
            }
        }
    });
    Thread b = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("b get lock1");
            synchronized (lock2) {
                System.out.println("b get lock2");
            }
        }
    });
    a.start();
    b.start();
    a.join();b.join();
    System.out.println("main is over");
}

​ 在上面的例子中,由于a线程先获取到锁资源lock1,即使a调用sleep()方法进入阻塞状态,b线程仍然无法获取到锁资源lock1(即lock1在a线程sleep()之后并没有被释放)。注意:sleep()入参不能为负数,会抛出异常。

让出CPU时间片的yield()方法

​ yield()方法调用后会暗示线程调度器希望让出当前线程所占的时间片,但是线程调度器可以无条件忽略这个暗示。如果yield()方法成功让出CPU时间片,就会进入就绪状态,等待重新竞争到时间片继续执行。所以,存在这样的情况线程A在调用yield()方法后,通过竞争在下一轮线程调度中再次获取到了时间片。同样的,yield()方法不会让出锁资源,下面的demo可以证明即使a线程yield(),b线程获取到时间片开始执行,仍然无法获取到lock1资源,所以输出结果仍然是先执行完a线程。

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Integer lock1 = 1, lock2 = 1;
    Thread a = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("a get lock1");
            Thread.yield();
            synchronized (lock2) {
                System.out.println("a get lock2");
            }
        }
    });
    Thread b = new Thread(() -> {
        System.out.println("b get cpu!");
        synchronized (lock1) {
            System.out.println("b get lock1");
            synchronized (lock2) {
                System.out.println("b get lock2");
            }
        }
    });
    a.start();
    b.start();
    a.join();
    b.join();
    System.out.println("main is over");
}

输出结果为:

a get lock1
b get cpu!
a get lock2
b get lock1
b get lock2
main is over

如果将yield()替换为wait(),a线程进入阻塞状态后,释放资源,b线程成功获取到lock1锁资源,输出结果证明他们的差异。

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Integer lock1 = 1, lock2 = 1;
    Thread a = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("a get lock1");
            try {
                lock1.wait();
            } catch (InterruptedException e) {
                System.out.println("a is interrupted");
            }
            synchronized (lock2) {
                System.out.println("a get lock2");
            }
        }
    });
    Thread b = new Thread(() -> {
        System.out.println("b get cpu!");
        synchronized (lock1) {
            System.out.println("b get lock1");
            synchronized (lock2) {
                System.out.println("b get lock2");
            }
            a.interrupt();
        }
    });
    a.start();
    b.start();
    a.join();
    b.join();
    System.out.println("main is over");
}

输出结果:

a get lock1
b get cpu!
b get lock1
b get lock2
a is interrupted
a get lock2
main is over

设置中断标志的interrupt()方法

​ 前文的最佳配角interrupt()方法,并非暴力地直接中断对应的线程,而是对对应的线程设置中断标志。

// 检测当前实例线程是否被中断,中断true,否则false
private native boolean isInterrupted(boolean ClearInterrupted);

// 检测当前线程是否被中断,如果发现线程被中断,会清除中断标志,返回true。否则返回false
private native boolean interrupted(){
        return currentThread().isInterrupted(true);
}

// 设置中断标志位true
public void interrupt();

interrupted()检测的是当前线程

这里要注意的是interrupted()检测的是当前线程,跟句柄无关。如下面的demo:

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Thread a = new Thread(() -> {
        while (true) {

        }
    });
    a.start();
    a.interrupt();
    System.out.println();
    System.out.println("is interrupted :" + a.isInterrupted()); // 1
    System.out.println("is interrupted :" + a.interrupted());  // 2
    System.out.println("is interrupted :" + a.isInterrupted()); // 3
}

输出结果:

true
false
true

2处虽然句柄为a线程,但是正如前文所述,在interrupted()方法中会调用Thread.getCurrentThread()方法获取当前线程,获取到线程为主线程,而主线程并未被中断,所以输出false。

interrupt() 只是设置中断标志,并非直接中断

public static void main(String[] args) throws InterruptedException {

    final Thread mainThread = Thread.currentThread();
    Thread a = new Thread(() -> {
        while (true) {
            System.out.println("a is working");
        }
    });
    a.start();
    a.interrupt();
    a.join();
}

输出结果:

a is working
a is working
a is working
a is working
a is working
...

可以发现如果a在内部没有调用wait、sleep等方法进入阻塞状态,就不会被中断。

ThreadLocal — 你不得不知道的坑

​ ThreadLocal只能在保证当前线程可以获取到对应的变量。

​ 考虑到存在这样的情况,主线程在ThreadLocal中放了参数,并启用了多个子线程进行工作,同时子线程需要用到前面主线程在ThreadLocal中放置的参数。这时候考虑到用InheritThreadLocal,在Thread.init()方法源码中可以看到,当线程初始化时,InheritThreadLocal中存放的参数会被复制到子线程的InheritThreadLocal中。

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

  ...  
  ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

那么是否InheritThreadLocal就已经能解决多线程问题了呢?答案是并不能。因为我们知道在线程池的使用中,为了减少线程初始化和销毁的性能消耗,提出了线程复用的概念。对于核心线程来说,一旦被初始化后,就不会被销毁。对于InheritThreadLocal而言,其变量的传递主要依赖于Thread.init()方法中进行参数复制传递。

​ 所以当使用线程池时,会发现每个线程的InheritThreadLocal中的参数,一旦被赋值后就不会再更新,也就失去了它的正确性,可以理解为是非线程安全的。这时候可以考虑使用TransmittableThreadLocal来解决,具体可见TTL项目的官网说明(如传递链路id等)。

参照

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

推荐阅读更多精彩内容

  • 现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线...
    java架构源阅读 206评论 0 0
  • Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势。线程作...
    八年码农阅读 224评论 0 1
  • 前言:Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有的有事.线程...
    叫我胖虎大人阅读 247评论 0 0
  • 1. 线程简介 1.1 什么是线程 线程是现代操作系统能够进行调度和运算的基本单位 在一个进程中可以创建多个线程,...
    ygxing阅读 322评论 0 0
  • 特别说明:文章内容是《Java并发编程的艺术》读书笔记 Java是一种多线程语言,从诞生开始就内置了对多线程的支持...
    codersm阅读 165评论 0 0