1 线程的创建与启动

1.1 继承Thread类创建线程类

步骤:

  1. 定义Thread类的子类,重写run方法,该run方法代表了线程需要完成的任务。
  2. 创建子类的实例,并使用start方法启动线程。

例子:

public class FirstThread extends Thread {
    public void run(){
        System.out.println("新的线程:" + getName());
    }
    
    public static void main(String[] agrs){
        System.out.println("当前线程为:" + Thread.currentThread().getName());
        new FirstThread().start();
    }
}

1.2 实现Runnable接口创建线程类

步骤:

  1. 定义Runnable的实现类,重写run方法;
  2. 创建Runnable实现类的实例,将此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

例子:

SecondThread st = new SecondThread();
new Thread(st).start();

也可以在创建Thread对象时,指定一个名字:

new Thread(st, “新线程");

2 线程的生命周期

一个线程有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。

当new一个线程之后,该线程就处于新建状态,Java虚拟机仅仅为其分配了内存,并初始化了其成员变量的值。

当线程对象调用了start()方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。

3 控制线程

join线程

当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join方法加入的线程完成为止。

join方法的三种重载形式:

  • join():等待被 join 的线程执行完成。
  • join(long millis):等待被 join 的线程时间最长为 millis 毫秒。如果在 millis 毫秒内,被 join 的线程还没执行结束则不再等待。
  • join(long millis, int nanos):等待被 join 的线程的时间最长为 millis 毫秒加上 nanos 微妙。

后台线程

后台线程(Daemon Thread),又称”守护线程“,或”精灵线程“。调用Thread对象setDaemon(true)方法即可将该线程设置为后台线程。

当所有前台线程死亡时,后台线程也随之死亡。当整个虚拟机只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。

  • isDaemon()方法:用于判断指定线程是否为后台线程。

线程睡眠

可以调用Thread类的静态sleep方法,让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。

  • static void sleep(long millis)

线程让步

调用静态方法yield()可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。

实际上,当某个线程调用了yield方法暂停后,只有优先级与当前线程相同或者更高的就绪状态的线程才会获得执行的机会。

改变线程的优先级

  • setPriority(int newPriority):设置线程的优先级。参数可以是1~10的整数,也可以是Thread类的三个静态常量:
    • MAX_PRIORITY:其值是10
    • MIN_PRIORITY:其值是1
    • NORM_PRIORITY:其值是5

4 线程的同步

4.1 同步代码块

同步代码块的语法格式如下:

synchronized(obj){
    ...
}

括号中的obj就是同步监视器。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程便会释放对该同步监视器的锁定。

4.2 同步方法

同步方法就是使用synchronized关键字来修饰某个方法。同步方法的同步监视器是this,也就是该对象本身。同步方法被调用后,任何时刻只能有一条线程获得对同步方法所属对象的锁定。

public synchronized void test() {
    System.out.println("synchronized method()... ")
}

4.3 释放同步监视器的锁定

线程会在如下的几种情况释放对同步监视器的锁定:

  • 当前线程的同步方法、同步代码块正常执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
  • 当线程在执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

在下面情况下,线程不会释放同步监视器:

  • 线程执行同步代码块或同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂定当前线程的执行;
  • 线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起。

4.4 同步锁(Lock)

通常每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源的并发访问,如ReadWriteLock(读写锁)。通常使用ReentrantLock(可重入锁)。

使用Lock对象的代码如下:

class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try {
            // 需要保证线程安全的代码
            // ...
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock锁具有重入性,即线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

5 死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁。

6 线程通信

6.1 线程的协调运行

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。
  • notify():唤醒此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒此同步监视器上所有等待的线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

上面的三个方法不属于Thread类,而是属于Object类,且必须由同步监视器对象来调用。

6.2 使用条件变量控制协调

​ 如果使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait()、notify()、notifyAll()方法。但可以使用Condition类来保持协调。

​ Condition类提供了如下三个方法:

  • await()
  • signal()
  • signalAll()

上述三个方法分别与wait()、notify()、notifyAll()类似。

例子:

public class Account{
    //显式定义Lock对象
    private final Lock lock = new ReentrantLock();
    //获得指定的Lock对象对应的条件变量
    private fianl Condition cond = lock.newCondition();
    
    private boolean flag = false;
    // ...
    
    public void method(){
        //加锁
        lock.lock();
        try {
            if (!flag){
                cond.await();
            } else {
                // 执行操作
                // ...
                
                cond.signelAll();
            }
        }
    } 
    
}

6.3 使用管道流

管道流有3种存在形式:

  • PipedInputStream 和 PipedOutputStream:管道字节流
  • PipedReader 和 PipedWriter:管道字符流
  • Pipe.SinkChannel 和 PipedSourceChannel:新IO的管道Channel

使用管道流实现多线程通信可按如下步骤进行:

  1. 使用new操作符分别创建管道输入流和管道输出流;
  2. 使用管道输入流或者管道输出流的connect方法把两个输入流和输出流连接起来;
  3. 将管道输入流、管道输出流分别传入两个线程;
  4. 两个线程可以分别依赖各自的管道输入流、管道输出流进行通信。

例子:

class ReaderThread extends Thread {
    private PipedReader pr;
    //用于包装管道流的BufferReader对象
    private BufferedReader br;

    public ReaderThread(){}
    public ReaderThread(PipedReader pr) {
        this.pr = pr;
        this.br = new BufferedReader(pr);
    }

    public void run(){
        String buf = null;
        try{
            //逐行读取管道输入流中的内容
            while ((buf = br.readLine()) != null) {
                System.out.println(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

class WriterThread extends Thread {
    String[] texts = new String[] {
            "第一行",
            "第二行",
            "第三行"
    };

    private PipedWriter pw;
    public WriterThread() {}
    public WriterThread(PipedWriter pw) {
        this.pw = pw;
    }

    public void run() {
        try {
            // 循环100次,向管道输出流中写入100个字符串
            for (int i = 0; i < 100; i++) {
                pw.write(texts[i % 3] + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (pw != null) {
                    pw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

public class PipedCommunicationTest {
    public static void main(String[] args) {
        PipedWriter pw = null;
        PipedReader pr = null;
        try {
            // 分别创建两个独立的管道输出流、输入流
            pw = new PipedWriter();
            pr = new PipedReader();
            // 连接管道输入流、输出流
            pw.connect(pr);
            //将连好的管道流分别传入两个线程,就可以让两个线程通过管道流进行通信
            new WriterThread(pw).start();
            new ReaderThread(pr).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,它们可以非常方便的共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是使用管道流。

7 线程组和未处理异常

线程组

Java使用ThreadGroup来表示线程组,来对一批线程进行分类管理。

在默认情况下,子线程和创建它的父线程处于同一个线程组内。

一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,运行中途不能改变它所属的线程组。

Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组:

  • Thread(ThreadGroup group, Runnable target):以target的run方法作为线程执行体创建新线程,属于group线程组。
  • Thread(ThreadGroup group, Runnable target, String name):设置线程名为name
  • Thread(ThreadGroup group, String name):创建新线程,线程名为name,属于group线程组

ThreadGroup有如下两个构造器:

  • ThreadGroup(String name):以指定线程组名字来创建新的线程组。
  • ThreadGroup(ThreadGroup parent, String name):以指定名字、指定的父线程组创建一个新线程组。

在ThreadGroup里提供了如下几个常用方法:

  • int activeCount():返回此线程组中活动线程的数目。
  • interrupt():中断此线程组中的所有线程。
  • isDaemon():判断该线程组是否是后台线程组。
  • setDaemon(boolean daemon):把该线程组设置为后台线程组。
  • setMaxPriority(int pri):设置线程组的最高优先级。

异常处理

Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口只有一个方法:void uncaughtException(Thread t, Throwable e),参数t代表出现异常的线程,e代表该线程抛出的异常。

Thread类提供了两个方法来设置异常处理器:

  • staticsetDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器。
  • setUncaughtExceptionHandler(Thread.UncaughtException eh):为指定线程实例设置异常处理器。

ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组会作为默认的异常处理器。

线程处理未抛出异常的流程如下:

  1. 首先,JVM会先查找该异常对应的异常处理器(线程实例调用setUncaughtExceptionHandler方法设置的异常处理器),如果找到了,则调用该异常处理器处理该异常;
  2. 否则,如果该线程组有父线程组,则调用父线程组的uncaughtException方法来处理异常;
  3. 否则,如果该线程实例所属的线程类有默认的异常处理器(由线程类调用setDefaultUncaughtExceptionHandler方法设置的异常处理器),就调用该异常处理器处理该异常;
  4. 否则,将异常调用栈的信息打印到System.err错误输出流,并结束该线程。

例子:

//自定义异常处理器
class MyExHandler implements Thread.UncaughtExceptionHandler {
    //实现uncaughtException方法,该方法将处理线程的未处理异常
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t + " 线程出现了异常:" + e);
    }
}
public class ExHandler {
    public static void main(String[] args) {
        // 设置主线程的异常处理器
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a = 5 / 0;
    }
}

8 Callable和Future

Callable接口提供了一个call()方法作为线程执行体,call()方法可以有返回值,也可以声明异常。

Future接口提供了一个FutureTask实现类,该实现类同时实现了Runnable接口,因此其实例可以作为Thread类的target。

在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

  • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
  • V get():返回Callable任务里的call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束时才会得到返回值。
  • V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,unit为timeout指定的单位。如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
  • boolean isCallable():如果在Callable任务正常完成前被取消,则返回true。
  • boolean isDone():如果Callable任务已完成,则返回true。

Callabel接口有泛型限制,Callable接口里的泛型形参类型与call方法返回值类型相同。

例子:

class RtnThread implements Callable<Integer> {
    //实现call方法,作为线程执行体
    public Integer call(){
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
        }
        //call方法可以有返回值
        return i;
    }
}

public class CallableTest {
    public static void main(String[] args) {
        //创建Callable对象
        RtnThread rt = new RtnThread();
        //使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<>(rt);
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
            if (i == 20) {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
            // 获取线程返回值
            System.out.println("子线程的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

9 线程池

线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run()方法,当run()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()方法。

使用Executors工厂类如下的几个静态工厂方法来创建连接池:

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
  • newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
  • newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法传入的参数为1。
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池中。
  • newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程人任务。

前三个方法返回ExecutorService对象;后两个方法返回ScheduledExecutorService对象,它是ExecutorService的子类。

ExecutorService的三个方法:

  • Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池。其中Future对象代表Runnable任务的返回值,但由于run方法没有返回值,所以Future对象将在run方法执行结束后返回null。但可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。
  • <T> Future<T> submit(Runnable task, T result):result显式指定线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result。
  • <T> Future<T> submit(Callable<T> task):Future代表task里call方法的返回值。

ScheduledExecutorService的四个方法:

  • ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):指定callable任务将在delay延迟后执行。
  • ScheduleFuture<?> schedule(Runnable command, long delay, TimeUnit unit):指定command任务将在delay延迟后执行。
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定频率重复执行。也就是说,在initialDelay后开始执行,依次在initialDelay + period、initialDelay + 2 * period ... 处重复执行,以此类推。
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任何一次执行时遇到异常,就会取消后续执行。否则,只能通过程序显式取消或终止来终止该任务。

​ 当用完了一个线程池后,应该调用该线程的shutdown()方法关闭线程池。调用了shutdown()方法后的线程池不在接受新任务,但会将已经提交的任务执行完成。

​ 若调用shutdownNow(),则会试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

例子:

class TestThread implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "的i值为:" + i);
        }
    }
}

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(6);
        pool.submit(new TestThread());
        pool.submit(new TestThread());
        pool.shutdown();
    }
}

10 ThreadLocal类

ThreadLocal类的作用,就是为每一个使用该变量的线程都提供一个变量值的副本,每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本发生冲突。从线程的角度看,就好像每一个线程的ThreadLocal实例都是私有的。

ThreadLocal类的三个public方法:

  • T get():返回此线程局部变量中当前线程副本中的值。
  • void remove():删除此线程局部变量中当前线程副本中的值。
  • void set(T value):设置此线程局部变量中当前线程副本中的值。

例子:

class Account {
    // 该变量是一个线程局部变量,每个线程都会保留该变量的一个副本
    private ThreadLocal<String> name = new ThreadLocal<>();

    public Account(String str){
        this.name.set(str);
        System.out.println("---------" + this.name.get());
    }

    public String getName(){
        return name.get();
    }
    public void setName(String str) {
        this.name.set(str);
    }
}

class MyTest extends Thread {
    private Account account;
    public MyTest(Account account, String name) {
        super(name);
        this.account = account;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            if (i == 3) {
                // 将账户名替换成当前线程名
                account.setName(getName());
            }
            System.out.println(account.getName() + " 账户的i值:" + i);
        }
    }
}

public class ThreadLocalTest {
    public static void main(String[] args) {
        Account account = new Account("初始名");
        new MyTest(account, "线程甲").start();
        new MyTest(account, "线程乙").start();
    }
}

运行结果:

16_9_1

ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上避免了多个线程之间共享资源(变量),也就不需要对多个线程进行同步了。

如果需要进行多个线程之间共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal。

11 包装线程不安全的集合

ArrayList、LinkedList、HashSet、TreeSet、HashMap等集合都是线程不安全的。

可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。几个静态方法如下:

  • <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定collection对应的线程安全的collection。
  • static <T> List<T> synchronizedList(List<T> list):返回指定List对应的线程安全的List对象。
  • static <K,V> Map<K,V> synchronizedMap(Map<K,V> map)
  • static <T> Set<T> synchronizedSet(Set<T> set)
  • static <K,V> SortedMap<K,V> synchronizedSortedMap(SortMap<K,V> m)
  • static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

例子:

HashMap m = Collections.synchronizedMap(new HashMap());

12 线程安全的集合类

在 java.util.concurrent 包下提供了 ConcurrentHashMap 、ConcurrentLinkedQueue 两个支持并发访问的集合,分别代表了支持并发访问的 HashMap 和 Queue 。