线程相关的基础问题

本文主要讲述了进程和线程发展简史,对于JVM来讲的进程和线程又是什么,Thread的start()方法的原生调用发生了什么,从而理解start()方法和run()方法有什么不同,另外,还介绍了三种处理线程执行完成后的返回值的方法,其实FutureTask和线程池获取线程执行结束的返回值更加常用。另外,介绍了线程的六种状态,还有sleep和wait的区别,notify和notifyAll的区别,yield函数的作用,以及如何优雅的中断线程等问题。

关于进程和线程

  • 串行:初期的计算机只能串行执行任务,并且需要长时间等待用户输入
  • 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行
  • 进程:进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,为并发处理任务提供了可能
  • 线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行

进程是资源分配的最小单位,线程是CPU调度的最小单位 所有与进程相关的资源,都被记录在PCB中,进程是抢占处理机的调度单位;线程属于某个进程,共享其资源。线程只由堆栈寄存器、程序计数器和TCB组成。

线程不能看做独立应用,而进程可看做独立应用;进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径;线程没有独立的地址空间,多进程的程序比多线程程序健壮。进程的切换比线程的切换开销大。

Java进程和线程的关系

Java对操作系统提供的功能进行封装,包括进程和线程。运行一个程序会产生一个进程,进程包含至少一个线程。每个进程对应一个JVM实例,多个线程共享JVM里的堆,Java采用单线程编程模型,程序会自动创建主线程,主线程可以创建子线程,原则上要后于子线程完成执行。

Thread中的start和run方法的区别

调用start()方法会创建一个新的子线程并启动,run()方法只是Thread的一个普通方法的调用。

public class ThreadTest {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()); //main
        }).run(); 

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()); //Thread-1
        }).start();
    }
}

start()点进去其实调用了start0(),而start0()是一个native方法,通过查看源码我们得知:

其实调用start0(),在底层就是创建了一个线程并且让新的线程执行这个run方法

处理线程返回值

1、主线程等待法

这个方法比较简单,但是需要自己实现循环等待的逻辑:

package thread_study;

import java.util.concurrent.TimeUnit;

public class CycleWait implements Runnable {
    private String value;
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.value = "Hello Thread";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cycleWait = new CycleWait();
        Thread thread = new Thread(cycleWait);
        thread.start();
        while(cycleWait.value == null){
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(cycleWait.value);
    }
}

2、使用Thread类的join()阻塞当前线程以等待子线程处理完毕,能够实现比我们自己循环等到更为精细的控制

package thread_study;

import java.util.concurrent.TimeUnit;

public class JoinWait implements Runnable {
    private String value;
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.value = "Hello Thread";
    }

    public static void main(String[] args) throws InterruptedException {
        JoinWait cycleWait = new JoinWait();
        Thread thread = new Thread(cycleWait);
        thread.start();
        thread.join();
        System.out.println(cycleWait.value);
    }
}

3、通过Callable接口实现:通过FutureTask Or 线程池获取 在JDK5之前,线程是没有返回值的,通常为了能够获取线程的返回值而颇费周折

1、通过FutureTask来完成

MyCallable.java

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        String value = "test";
        System.out.println("Ready to work");
        Thread.sleep(5000);
        System.out.println("Task done");
        return value;
    }
}

FutureTaskDemo.java

package thread_study;

import java.util.concurrent.FutureTask;

public class FutureTaskDemo {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        if(!futureTask.isDone()){
            System.out.println("Task is not finished, please wait!");
        }
        System.out.println("task return: " + futureTask.get());
    }
}

2、通过线程池来完成

还是依旧沿用MyCallable.java的代码,下面是ThreePoolDemo.java

package thread_study;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreePoolDemo {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        Future<String> futureTask = cachedThreadPool.submit(new MyCallable());
        if(!futureTask.isDone()){
            System.out.println("Task is not finished, please wait!");
        }
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            cachedThreadPool.shutdown();
        }
    }
}

线程的六种状态

  • 新建(New):创建后尚未启动的线程的状态。
  • 运行(Runnable):包含Running和Ready。
  • 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
    • 没有设置Timeout参数的0bject.wait()方法。
    • 没有设置Timeout参数的Thread.join()方法。
    • LockSupport.park()方法。
  • 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
    • Thread.sleep()方法。
    • 设置了Timeout参数的0bject.wait()方法。
    • 设置了Timeout参数的Thread.join()方法。
    • LockSupport.parkNanos()方法。
    • LockSupport.parkUntil()方法。
  • 阻塞(Blocked):等待获取排它锁
  • 结束(Terminated):已终止线程的状态,线程已经结束执行

那么等待和阻塞式什么关系呢?不都是停下来吗?其实两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外,而进入 waiting 状态是在同步代码之内(然后马上退出同步)。

sleep与wait

sleep和wait最主要的本质区别:Thread.sleep只会让出CPU,不会导致锁行为的改变;Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。这也就是wait必须写在synchronized里面的原因,因为我只有获取到锁了,我才可能释放锁嘛。

notify与notifyAll

锁池EntryList:

假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池

等待池WaitSet:

假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程,不会去竞争该对象的锁。

notify和notifyAll的区别:

  • notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
  • notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。

yield的使用

public class YieldDemo {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            for (int i = 0; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + i);
                if(i == 5){
                    Thread.yield();
                }
            }
        };
        new Thread(runnable, "A").start();
        new Thread(runnable, "B").start();
    }
}

从运行结果来看,yield会让当前线程放弃CPU执行权,让给其他的线程,但是这也是不可控的,调用此方法可能没效果,比如下面这样:

需要注意的是yield不会使当前线程放弃持有的锁

优雅地中断线程 interrupt

已经被弃用的方法:stop(),这个方法不仅暴力,而且不安全, 可能使一些清理性的工作得不到完成。还可能对锁定的内容进行解锁,容易造成数据不同步的问题。

使用interrupt方法中断线程:调用interrupt(),即是通知线程应该中断了 ①如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。 ②如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

需要被调用的线程配合中断 ①在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。 ②如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

package thread_study;

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = ()->{
            int i = 0;
            try{
                while (!Thread.currentThread().isInterrupted()){
                    Thread.sleep(100);
                    i++;
                    System.out.println(Thread.currentThread().getName() + " (" +
                            Thread.currentThread().getState()+") loop " + i);
                }
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName() + " (" +
                        Thread.currentThread().getState()+") catch InterruptedException");
            }
        };

        Thread t1 = new Thread(task, "T1");
        System.out.println(t1.getName() + " (" + t1.getState() + ") is new.");

        t1.start();
        System.out.println(t1.getName() + " (" + t1.getState() + ") is started.");

        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName() + " (" + t1.getState() + ") is interrupted.");

        Thread.sleep(300);
        System.out.println(t1.getName() + " (" + t1.getState() + ") is interrupted now.");
    }
}