Java多线程-基础篇

一、概述

如果你想一个程序运行得更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发就是用于多处理器编程的工具。如果你有一台多处理器的机器,那么就可以在这些处理器上分布多个任务,从而提高吞吐量。例如web服务器,在Servlet就是为每个单独的请求分配一个线程,从而将大量的请求分布到多个CPU处理器上。

有一个表面上的事实是,在单处理器上运行并发程序的开销要比程序的所有部分顺序执行的开销大,因为其中增加了上下文切换的开销(从一个任务切换到另一个任务),而程序的所有部分按顺序执行则可以免去上下文切换的代价。

但当程序遇到阻塞时,这个情况就不一样了,当程序中的任务因为程序控制范围之外的一些条件(例如I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。此时如果没有用并发,程序将会停止执行,直到这个外部条件发生变化。如果使用并发编写程序,当一个任务阻塞时,另外的任务还可以继续执行,因此程序可以保持继续向前执行。可以说,如果没有阻塞,那么在单处理器上编写并发程序将毫无意义。

二、进程

进程是运行在它自己的地址空间内的“自包容”的程序。多任务操作系统可以通过周期性的将CPU从一个进程切换到另外一个进程,来实现同时运行多个进程(程序)。操作系统会将每个进程互相隔离开,它们彼此不会互相干涉,并且不需要相互通信。与此相反,线程会共享诸如内存、I/O这样的资源。因此编写多线程程序需要控制不同线程驱动的任务之间对这些资源的访问,保证这些资源不会被多个任务同时访问。

三、任务和线程

并发编程使我们可以将程序划分为多个分离的、独立运行的多个任务。在Java中这些任务实现RunnableCallable接口,换句话说在Java中实现了RunnableCallable接口的类叫做线程的任务。每个独立的任务都有一个线程驱动执行。线程是在进程中的单一的顺序控制流。

四、线程运行机制

Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每 个线程都会分配到数量合理的时间去驱动它的任务。在这个过程中CPU将轮流给每个任务分配其占用CPU的时间,每个任务都觉得自己一直占用着CPU,但事实上CPU时间是划分成片段分配给每个任务的(如果程序确实运行在多CPU的系统上那每个任务确实会在整个CPU上执行)。

五、定义任务

要想定义任务只需要实现Runnable接口并实现run()方法,使得任务可以执行你的业务逻辑。

  1. 编写任务类

    /// 编写任务类
    public class TaskClass implements Runnable {
      
      public void run() {
        /// 运行业务逻辑代码
      }
      
    }
    
  2. 创建线程驱动任务执行

    public class ThreadClass {
      public static void main(String[] args) {
        Thread t = new Thread(new TaskClass);  /// 创建线程
        t.start(); /// 启动线程
      }
    }
    

    t.start()方法的执行将会立即返回,由线程执行Runnable的run()方法,主程序将继续向后执行。

  3. Thread对象和垃圾回收

    在使用其他普通对象时,垃圾回收机制会回收不再使用(没有直接引用的对象)的对象,然而遇到Thread类对象时情况就不是如此了。每个Thread都 “注册” 了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。

    public class ThreadClass {
      public static void main(String[] args) {
        new Thread(new TaskClass).start();   // Thread没有任何对这个对象的引用,但是垃圾回收器并不会回收
      }
    }
    

六、使用 Executor

Executor作为一个中介对象在客户端和任务执行之间提供了一个间接层,由这个中介对象执行任务。Executor允许你管理异步任务的执行而无需显式的管理线程的生命周期。这是在Java中启动任务的优先方法。

  1. CachedThreadPool

    CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。

    public class CachedThreadPool {
      public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(); (1)
        for (int i = 0; i < 10; i++) {
          exec.execute(new TaskClass());  (2)
        }
        exec.shutdown();  (3)
      }
    }
    

    (1)使用Executor的静态方法创建ExecutorService(具有服务生命周期的Executor),这能确定ExecutorService的类型。CachedThreadPool将为每个任务创建一个线程。

    (2)ExecutorService会构建正确的上下文执行Runnable对象。

    (3)调用shutdown()方法可以防止新的任务提交给这个Executor。

  2. FixedThreadPool

    FixedThreadPool可以 一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量了。

    public class FixedThreadPool {
      public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(10); (1)
        for (int i = 0; i < 10; i++) {
          exec.execute(new TaskClass());
        }
        exec.shutdown();
      }
    }
    

    (1) 创建线程数量为10的ExecutorService。

  3. SingleThreadExecutor

    SingleThreadExecutor可以创建一个线程的ExecutorService。当向SingleThreadExecutor提交多个任务时只有一个任务得到执行,其他任务将会排队,会在上一个任务结束之后得到执行。所有任务使用相同的线程。

    public class SingleThreadPool {
      public static void main(String[] args) {
        ExecutorService exec = Executors.newSingleThreadExecutor(); (1)
        for (int i = 0; i < 10; i++) {
          exec.execute(new TaskClass());
        }
        exec.shutdown();
      }
    }
    

    (1)将会创建一个线程的ExecutorService。

注意,在任何线程池中,现有线程在可能的情况下,都会被自动复用。

七、任务返回值

Runnable执行的是独立任务,不具有返回值。如果你希望任务完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。Callable是一个泛型类,类型参数表示的是call()方法的返回值类型,并且必须使用ExecutorService.submit()调用call()。

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
public class TaskClassWithResult implement Callable<String> {
  
  public String call() {
    return "result";
  }
}
public class CallableDemo {
  ExecutorService exec = Executors.newCachedThreadPool();

  List<Future<String>> futureList = new ArrayList<>();
  for (int i = 0; i < 5; i++) {
    Future<String> future = exec.submit(() -> ""); (1)
    futureList.add(future);
  }
  for (Future<String> stringFuture : futureList) {
    try {
      System.err.println(stringFuture.get()); (2)
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    }
  }
}

(1)调用submit方法将会产生Future对象,它用call方法返回结果的特定类型进行了参数化。

(2)当任务完成时get()方法将会获取到返回结果,在任务完成之前调用get()将会阻塞,直到结果准备就绪。

八、线程休眠

线程的sleep()方法将时任务终止执行给定的时间,这使得线程调度器可以切换到另外一个线程,进而驱动另一个任务。

public class SleepDemo implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(100);
            // TimeUnit.SECONDS.sleep(100);  休眠100秒
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

九、线程优先级

线程的优先级将该线程的重要性传递给调度器,尽管CPU处理线程集的顺序是不正确的,但调度器将会倾向于优先执行优先级高的线程。另外,优先级不会导致死锁,优先级较低的线程仅仅是执行的频率较低。

public class ThreadPriorityDemo implements Runnable {

    private final int priority;
    public ThreadPriorityDemo(int priority) {
        this.priority = priority;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority); (1)
        // 执行业务逻辑代码
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new ThreadPriorityDemo(Thread.MAX_PRIORITY));
        }
        exec.execute(new ThreadPriorityDemo(Thread.MIN_PRIORITY));

        exec.shutdown();
    }
}

(1)设置线程优先级,Thread.currentThread()可以获取驱动当前任务的线程。不能在构造器中设置优先级,因为Executor在此刻还没有开始执行任务。

注意:JDK中有10个优先级,为了在不同系统间有可移植性整优先级别时应该只使用MAX_PRIORITY、NORM_PRIORI TY和 MIN_PRIORITY三种级别。

十、线程让步

如果run()方法中的某部分工作已经完成,就可以给线程调度机制一个暗示,你的工作已经做得差不多了,可以让其他线程使用CPU了。通过yield()方法可以实现这个功能(这只是一个建议,没有任何机制保证它将会被采纳)。

十一、后台进程

所谓后台线程,是指程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止了,同时会杀死进程中所有后台线程。例如执行main的就是一个非后台线程,一旦main()完成其工作,程序就终止了。

可以通过setDaemon()方法将线程设置为后台线程,必须在线程启动前调用setDaemon()。

package com.example.demooffer.thread;

import java.util.concurrent.TimeUnit;

public class SimpleDaeonDemo implements Runnable {

    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(30);
                System.err.println(Thread.currentThread() + ": " + this);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaeonDemo());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.err.println("所有后台线程都已经启动完成");
        TimeUnit.SECONDS.sleep(175);
    }

}

通过自定义ThreadFactory可以自定义由Executor创建的线程的属性(后台、优先级、名称):

public class DaemonThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
}

使用ThreadFactory创建线程:

package com.example.demooffer.thread;

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

public class DaemonFromFactory implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(100);
                System.err.println(Thread.currentThread() + ": " + this);
            } catch (InterruptedException e) {
                System.err.println("InterruptedException");
                // throw new RuntimeException(e);
            }

        }

    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory()); (1)
        for (int i = 0; i < 10; i++) {
            exec.execute(new DaemonFromFactory());

        }
        System.err.println("所有后台线程都已经启动完成");
        TimeUnit.SECONDS.sleep(175);
    }
}

(1)通过传递一个ThreadFactory来实现自定义线程属性的功能。

注意:如果一个线程是后台线程,那么它创建的任何线程都将被自动设置成后台线程。可以通过isDaemon()方法判断线程是否为后台线程。

如果后台线程的run()方法在最后一个非后台线程终止前没有执行完成,后台线程run()方法里的finally子句不会被执行,因为当最后一个非后台线程终止时,所有后台线程会“突然”终止(JVM会立即关闭所有后台线程而不会执行任何后续代码)。

public class ADaemon implements Runnable {
    @Override
    public void run() {

        try {
            System.err.println("starting ADaemon.");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            System.err.println("执行finally");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new ADaemon());
        t.setDaemon(true);
        t.start();
        TimeUnit.MICROSECONDS.sleep(500);
    }
}

当你运行上面的程序时,finally字句将不会执行。但是如果你注释掉t.setDaemon(true),就会看到finally字句执行。

十二、继承Thread

在前面的例子中所有的任务类都实现了Runnable或Callable,在非常简单的情况下,你可以使用直接从Thread类继承的方式实现任务类。

public class SimpleThread extends Thread {

    public SimpleThread() {
        super();
    }

    @Override
    public void run() {
        super.run();
        start();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new SimpleThread();
        }

    }
}

十三、线程join()

在一个线程上调用另外一个线程t的join()方法,其效果是等待一段时间(被挂起),直到线程t执行结束才能继续执行。在join()上可以携带一个超时参数,这样如果目标线程t在指定时间内没有执行结束的话,join方法总能返回。

调用t.join()方法后,t线程可以被中断,做法是调用t.interrupt()方法。

package com.example.demooffer.thread;

public class Sleeper extends Thread {

    private int duration;

    public Sleeper(String name, int sleepTime) {
        super(name);
        duration = sleepTime;
        start();
    }

    @Override
    public void run() {
        try {
            sleep(duration);
        } catch (InterruptedException e) {
            // throw new RuntimeException(e);
            System.err.println(getName() + "was interrupted." + "isInterrupted: " + isInterrupted()); (1)
            return;
        }
        System.err.println(getName() + "has awakened");
    }
}
package com.example.demooffer.thread;

public class Joiner extends Thread {

    private Sleeper sleeper;

    public Joiner(String name, Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }

    @Override
    public void run() {
        try {
            sleeper.join();
        } catch (InterruptedException e) {
            // throw new RuntimeException(e);
            System.err.println("Interrupted");
        }
        System.err.println(getName() + " join completed.");
    }
}

package com.example.demooffer.thread;

public class Joining {
    public static void main(String[] args) {
        Sleeper sleepy = new Sleeper("Sleepy", 1500);
        Sleeper grumpy = new Sleeper("Grumpy", 1500);


        Joiner dopey = new Joiner("Dopey", sleepy);
        Joiner doc = new Joiner("Doc", grumpy);

        grumpy.interrupt();
    }
}

上述代码的运行结果是:

Grumpywas interrupted.isInterrupted: false
Doc join completed.
Sleepyhas awakened
Dopey join completed.

(1)当在线程上调用interrupt()方法时,将给该线程设置一个标志,表明该线程已经被中断。然而在捕获异常时将清理这个标志,所以在catch子句中isInterrupted()获取标志时总为false。

十四、捕获异常

由于线程的本质特性,你不能捕获从线程中抛出的异常。一旦异常从run()方法中抛出,它就会向外传播到控制台,除非你采取特殊的步骤捕获这些错误的异常。

public class ExceptionThread implements Runnable {

    @Override
    public void run() {
        throw new RuntimeException("");
    }

    public static void main(String[] args) {
        try {
            ExecutorService exec = Executors.newCachedThreadPool();
            exec.execute(new ExceptionThread());
        } catch (RuntimeException e) {
            System.err.println("处理异常");
        }

    }
}

当你运行这个程序时输出为:

Task :ExceptionThread.main()
Exception in thread "pool-1-thread-1" java.lang.RuntimeException:
at com.example.demooffer.thread.ExceptionThread.run(ExceptionThread.java:10)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)

可以看到虽然用try-catch包裹了main方法的主体,但从子线程中抛出的异常并不会被catch子句处理。

可以通过Thread. UncaughtExceptionHandler解决这个问题,Thread. UncaughtExceptionHandler为线程异常处理器,它会在线程因为捕获异常而临近死亡的时候被调用。

任务类:

public class ExceptionThread2 implements Runnable {
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.err.println("run by " + t);
        System.err.println("eh = " + t.getUncaughtExceptionHandler());
    }
}

自定义异常处理器

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.err.println("caught: " + e);
    }
}

通过自定义ThreadFactory定义线程的异常处理器

public class HandlerThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);

        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

        return t;
    }
}

运行线程

public class CaptureUncaughtException {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
        exec.execute(new ExceptionThread2());
    }
}

运行结果如下:

Task :CaptureUncaughtException.main()
run by Thread[Thread-0,5,main]
eh = com.example.demooffer.thread.MyUncaughtExceptionHandler@654e6c88

可以看到异常被成功处理了。

除了在线程上设置异常处理器,也可以通过Thread.setDefaultUncaughtExceptionHandler()方法设置全局异常处理器。在单个线程没有设置异常处理器时,将会使用这个这个异常处理器。

public class SettingDefaultHandler {
  public static void main(String() args) {
    Thread.setDefaultUncaughtExceptionHandler(
        new MyUncaughtExceptionHandler());
    ExecutorService exec = Executors.newCachedThreadPool();
    exec:execute (new ExceptionThrèad());
  }
)

继续错误的代价由别人来承担,而承认错误的代价由自己承担。-----《Java编程思想》

posted @ 2023-04-05 21:55  我爱这世间美貌女子  阅读(18)  评论(0编辑  收藏  举报