并发编程基础

本文内容提要: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等)。

参照

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值