Skip to content

Latest commit

 

History

History
520 lines (343 loc) · 17.4 KB

1_基础知识.md

File metadata and controls

520 lines (343 loc) · 17.4 KB

基础知识

一、进程和线程

1. 进程

进程是资源分配的基本单位

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。

下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。

img

2. 线程

线程是 CPU 调度的基本单位

一个进程中可以有多个线程,它们共享进程资源。

QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。

img

3. 进程与线程的区别

  • 资源

    进程是资源调度的基本单位。

    线程不占有任何资源。

  • 调度

    线程是独立调度的基本单位。

    同一进程中,线程的切换不会引起进程切换,但一个进程中线程切换到另一个进程中的线程时,就会引起进程切换了。

  • 系统开销

    进程的创建和撤销所付出的开销比线程的创建和撤销要大很多。

    类似的,进程切换的开销也比线程切换的开销要大很多,因为进程切换时,涉及到当前执行进程的 CPU 环境的保存新调度进程 CPU 环境的设置,而线程切换时只需要设置和保存少量的寄存器内容,开销很小。

  • 通信

    线程可以通过直接读写同一进程中的数据进行通信。

    进程之间的通信需要借助 IPC (InterProcess Communication)。

4. 多进程与多线程

多进程的目的是提高 CPU 的使用率

多线程的目的是提高应用程序的使用率

多线程共享同一进程资源(堆内存和方法区),但是栈内存是独立的,JVM 中一个线程对应一个线程栈。多线程抢夺的是 CPU 资源,一个时间点上只有一个线程运行。

5. 并发与并行

  • 并发:一个处理器同时处理多个任务,是逻辑上的同时发生

  • 并行:多个处理器或者多核处理器同时处理多个不同的任务,是物理上的同时发生

来个比喻:并发是一个人同时吃三馒头,而并行就是三个人同时吃三个馒头。

下图反映了一个包含 8 个操作的任务在一个有两核心的 CPU 中创建四个线程运行的情况。

假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。

单就一个 CPU 而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高, 多 CPU 的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。

二、创建线程的几种方式

1. 三种创建线程的方式

Java 有 3 种创建线程的方式:

  • 继承 Thread 类,重写 run() 方法

    public class MyThread extends Thread{
        private void attack() {
            System.out.println("Fight");
            System.out.println("Current Thread is : " + Thread.currentThread().getName());
        }
    
        @Override
        public void run() { //重写 run() 方法
            attack();
        }
    }
  • 实现 Runnable 接口,重写 run() 方法

  • 实现 Callable 接口,重写 call() 方法

2. 选择

实现接口会更好一些:

  • 避免单继承带来的局限

    Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;

  • 代码能够被多个线程共享

  • 适合多个相同程序的线程处理同一资源的情况

  • 使用接口实现的线程可以对线程进行复用(线程池中创建的线程就是实现 Rubnnable 接口的):

    Runnable 是轻量级的,重复 new 不会耗费太多资源。

    Thread 是重量级的,并且线程执行结束完后就销毁了,无法再次利用。

注意:一个线程只能 start 一次,如果多次 start,会出现 java.lang.IllegalException

3. Runnable 和 Callable 的区别

  • Callable 的实现方法是 call()

    Runnable 的实现方法是 run()

  • Callable 任务执行后有返回值。Callable 与 Future 配合使用,运行 Callable 任务和获取一个 Future 对象 Future 表示异步计算后的结果;

    Runnable 任务执行后是没有返回值的

  • Callable 的 call() 方法可以抛出异常

    Runnable 的 run() 方法不能抛出异常

4. 实现处理线程的返回值

方式一:主线程等待法

如果未获取到值,则主线程等待,一直到获取值为止。

public class CycleWait implements Runnable{
    private String value;

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
    }
    
    public static void main(String[] args) throws InterruptedException {
        CycleWait cycleWait = new CycleWait();
        Thread t = new Thread(cycleWait);
        t.start();

        //TODO:如果未获取到值,则主线程等待,一直到获取值为止。
        while (cycleWait.value == null){ //主线程等待法
            Thread.sleep(1000);
        }
        System.out.println("value:"+cycleWait.value);
        //输出结果:
        //value:we have data now
    }
}

方式二:join()

使用 Thread 的 join() 阻塞当前线程以等待子线程处理完毕

public class CycleWait2 implements Runnable{
    private String value;

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait2 cycleWait = new CycleWait2();
        Thread t = new Thread(cycleWait);
        t.start();

        //TODO:使用 Thread 的 join 方法阻塞当前线程,等待子线程执行完毕
        t.join(); //阻塞当前主线程,直到 t 线程执行完毕
        System.out.println("value:"+cycleWait.value);
        //输出结果:
        //value:we have data now
    }
}

方式三:Callable + FutureTask

实现 Callablee 接口,使用 FutureTask 接收 call() 方法返回的数据。

public class CycleWait3 implements Callable<String>{
    private String value;

    @Override
    public String call() throws Exception {
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
        return value;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CycleWait3 cycleWait = new CycleWait3();
        FutureTask ft = new FutureTask(cycleWait);
        Thread t = new Thread(ft);
        t.start();

        while (!ft.isDone()){
            Thread.sleep(1000);
        }
        // FutureTask 的 get() 方法会一直阻塞,一直会等待任务执行完毕,才返回。
        System.out.println("value:"+ft.get());
    }

方式四:线程池 + Future

线程池执行任务,返回的结果使用 Future 接收。

public class CycleWait4 implements Callable<String>{
    private String value;

    @Override
    public String call() throws Exception {
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
        return value;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService service = Executors.newCachedThreadPool();
        CycleWait4 cycleWait = new CycleWait4();
        //将任务提交给线程池处理
        Future<String> future = service.submit(cycleWait);

        while (!future.isDone()){
            Thread.sleep(1000);
        }
        System.out.println("value:"+future.get());

        //关闭线程池
        service.shutdown();
    }
}

三、线程的几种状态

img

新建(New)

创建后尚未启动。

可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片。

包含了操作系统线程状态中的 Running 和 Ready。

调用 start() 方法后开始运行,线程这时候处于 Ready 状态。可运行状态的线程获得了 CPU 时间片后就进入Running 状态。

阻塞(Blocked)

等待获取一个排它锁,如果其他线程释放了锁就会结束此状态。

无限期等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 LockSupport.unpark(Thread)

*限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)

阻塞和等待的区别在于:

阻塞是被动的,它是在等待获取一个排它锁。

等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

死亡(Terminated)

可以是线程完成任务后自己结束,或者产生了异常而结束。

四、基础线程机制

Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作

主要有三种 Executor:

  • CachedThreadPool:一个任务创建一个线程;
  • FixedThreadPool:所有任务只能使用固定大小的线程;
  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}

Daemon

Java 中有两类线程:

  • 用户线程(User Thread):运行在前台的线程,比如主线程。
  • 守护线程(Daemon Thread):程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

一旦所有的用户线程都退出了,虚拟机也就退出了,因此不要在守护线程中执行业务逻辑操作,因为随时都可能中断(甚至无法执行 finnally 中的语句)。

注意:在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

start() 和 run()

  • start()

    启动一个新线程,新线程会执行 run() 方法;

    start 不能被重复调用,如果重复调用,会抛 java.lang.IllegalException

  • run()

    和一个成员方法相同,可以被重复调用;

    单独调用 run() 不会启动新线程

sleep() 和 wait()

sleep() 和 wait() 的区别:

  • 持有者

    sleep() 是 Thread 的静态方法;

    wait() 是 Object 的成员方法,所以任何对象都可以调用 wait()

  • 锁是否会释放

    sleep() 会一直持有同步锁,继续占用 CPU 资源;

    wait() 会释放同步锁,使得其他线程可以使用同步块或者同步方法

  • 使用范围

    wait() 只能在同步代码块或同步方法中使用,否则,会抛 java.lang.IllegalMonitorStateException

    sleep() 可在任何地方使用

yield() 和 wait()

Thread.yield() 声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

yield() 和 wait() 的区别:

  • yield() 是 Thread 的静态方法

    wait() 是 Object 的成员方法

  • yield() 让当前线程由“运行状态”进入“就绪状态”;

    wait() 让当前线程由“运行状态”进入“等待状态”

  • yield() 不会释放同步锁;

    wait() 会释放同步锁,使得其他线程可以使用同步块或者同步方法

notify() 和 notifyAll()

锁池:线程 A 已经拥有某个对象的锁,线程 B、C 想要调用这个对象的同步方法,此时线程 B、C 就会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池。

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

notifyAll() 让所有处于等待池的线程进入锁池去竞争锁

五、终止线程的几种方式

1. 退出标志

使用退出标志,使线程正常退出,也就是 run 方法完成后线程终止。

需要 while() 循环以某种特定的条件下退出,最直接的方法就是设计一个 boolean 类型的标志,并且通过设置这个标志来控制循环是否退出。 一般需要加上 volatile 来保证标志的可见性。

2. Thread.stop()

Thread.stop() 强制终止线程(不建议使用)。

3. interrupt() 中断线程

  • 线程处于阻塞状态,使用 interrupt() 则会抛出 InteruptedException 异常。

    (线程处于阻塞状态)

  • 使用 while(! isInterrupted()) {...} 来判断线程是否被中断,使用 interrupt() 则线程终止。

    (线程处于运行状态)

综合线程处于“阻塞状态”和“运行状态”的终止方式,通用的终止线程的形式如下:

try{
    while(!isInteruppted()){ //isInteruppted() 保证只要中断标记为 true,就终止线程
        //...
    }
}catch(InteruptedException e){
    //保证当发生 InteruptedException 时,线程被终止。
}

举例如下:

public class InterruptExample implements Runnable{
    @Override
    public void run() {
        int i = 0;
        try {
            //isInteruppted() 保证只要中断标记为 true,就终止线程
            while (!Thread.currentThread().isInterrupted()) {
                Thread.sleep(100);
                i++;
                System.out.println(Thread.currentThread().getName() + " (" + 
                        Thread.currentThread().getState() + ") loop " + i);
            }
        } catch (InterruptedException e) {
            // 保证当发生 InteruptedException 时,线程被终止。
            System.out.println(Thread.currentThread().getName() + " (" + 
                    Thread.currentThread().getState() + ") catch InterruptedException.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable interruptTask = new InterruptExample();
        
        Thread t1 = new Thread(interruptTask, "t1");
        System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

        t1.start();                      // 启动“线程t1”
        System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

        // 主线程休眠300ms,然后主线程给t1发“中断”指令。
        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

        // 主线程休眠300ms,然后查看t1的状态。
        Thread.sleep(300);
        System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
    }
}
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.