1 - Java多线程基础篇

  1. 进程和线程基本概念
  2. Java多线程入门类和接口
  3. 线程组和线程优先级
  4. Java线程的状态及主要转化方法
  5. Java线程间的通信

1. 进程和线程基本概念

1.1 进程产生的背景

  1. 最初的计算机,用户input --> 计算机response
    大部分时间计算机都在等待用户输入,效率很低
  2. 批处理操作系统,一系列需要操作的指令 --> 清单 --> 一次性交给计算机。
    然而,批处理操作系统的指令依旧是串行的,内存中始终只有一个程序在运行,所以效率也不高
  3. 提出来进程,不同应用程序(正在运行的程序)在内存中分配不同的空间,各个进程之间互不干扰。
    CPU采用时间片轮转的方式执行进程: CPU为每个进程分配一个时间段,称为它的时间片,如果时间片结束,进程还在运行,则暂停这个进程的运行,并将CPU分配给另一个进程(上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立刻进行切换,不用等待时间片用完。
    当进程暂停时,会保存当前进程的状态(进程的表示,进程使用的资源等),等下一次切换回来时根据之前保存的状态进行切换,不用等待时间片用完。
    进程让操作系统并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。
  4. 提出来线程,一个进程在一段时间可以同时做多个任务,一个进程多个子线程,一个子线程负责一个单独的子任务。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

1.2 进程 VS 线程

多进程也可以实现并发,为什么要使用多线程?

  • 进程间通信比较复杂,线程间通信相对简单,因为线程之间的共享资源的通信比较容易
  • 进程时重量级的,而线程是轻量级的,多线程方式系统开销较小

进程与线程的区别
本质区别在于:是否单独占有内存地址空间及其他系统资源(比如I/O)

  • 进程独占一定的内存地址空间,所以进程间存在存在内存隔离,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程的内存地址和资源,数据共享简单,但是同步复杂。
  • 一个进程出问题不会影响到其他进程,可靠性高;一个线程崩溃可能会影响到整个程序的稳定性,可靠性低
  • 进程的创建和销毁不仅需要保存寄存器和栈消息,还需要资源的分配回收以及分页调度,开销较大;线程只需要保存寄存器和栈消息,开销较小。

进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

1.3上下文切换

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。

上下文切换: 进程切换 or 线程切换
上下文: 某一时间点 CPU 寄存器和程序计数器的内容。寄存器是CPU内部的少量速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运算速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。

eg A --> B

  1. 先挂起A, 将其在cpu中的状态(寄存器中内容中间值一类的)保存在内存中。
  2. 在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
  3. 当B执行完,根据程序计数器指向的位置恢复线程A。

2. Java多线程入门类和接口

2.1 Thread类和Runnable接口

Java中使用多线程,我们需要“线程类”,JDK提供了Thread类和Runnable接口去实现自己的“线程类”

  • 继承Thread类,并重写run方法
  • 实现Runnable接口的run方法
2.1.1 继承Thread类

Thread类的例子:

package Basic.introduction;

public class ThreadClass {

    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("MyThread");
        }

        /**
         * 在调用了start()方法后,该线程才算启动
         *
         * 在程序中调用了start()方法后,虚拟机会先创建一个线程,
         * 等到这个线程第一次获得时间片的时候调用run()方法
         *
         * 不可多次调用start()方法,在第一次调用start()方法后,
         * 再次调用start()方法会抛出异常。
         * @param args
         */
        public static void main(String[] args) {
            Thread myThread = new MyThread();
            myThread.start();
        }
    }

}

2.1.2 实现Runnable接口

Runnable接口的例子:

package Basic.introduction;

public class RunnableInterface {

    public static class MyThread implements Runnable{
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        new Thread(new MyThread()).start();

        //Java 8 函数式编程,可以省略MyThread类
        /**
         * Runnable是一个函数式接口,可以使用Java 8的函数式编程来简化代码
         */
        new Thread(() -> {
            System.out.println("Java 8 匿名内部类");
        }).start();
    }

}

2.1.3 Thread类构造方法

常用的构造方法如下:

Thread(Runnable target)
Thread(Runnable target, String name)
2.1.4 Thread类几个常用方法
  • currentThread():静态方法,返回对当前正在执行的线程对象的引用;
  • start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
  • yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
  • sleep():静态方法,使当前线程睡眠一段时间;
  • join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
2.1.5 Thread类 VS Runnable接口

优先使用“实现Runnable接口”这种方式自定义线程类

  • Java单继承多实现,Runnable接口使用起来比Thread更灵活
  • Runnable接口更符合面向对象,将任务单独进行封装
  • Runnable接口的出现,降低了线程对象和线程任务的耦合性
  • 如果使用线程时不需要使用Thread类的诸多方法,那么使用Runnable接口更加轻量

2.2 Callable、Future与FutureTask

run方法没有返回值,有时候希望开启线程去执行任务时候需要有个返回值,从而Callable接口和Future接口解决了这个问题。--> 异步模型

2.2.1 Callable接口

Callable与Runnable类似,但是其提供的方法是有返回值的。
Callable一般是配合线程池工具ExecutorService来使用的。ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Future的get方法得到结果。

Callable的例子

package Basic.introduction;

import javafx.concurrent.Task;

import java.util.concurrent.*;

//自定义Callable
public class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //模拟计算需要1s的时间
        Thread.sleep(1000);
        return 2;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Callable
        ExecutorService executorService = Executors.newCachedThreadPool();
        CallableTask task = new CallableTask();
        //调用的是submit(Callable<T> task)方法
        Future<Integer> result = executorService.submit(task);

        // 注意调用get方法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使用可以设置超时时间的重载get方法。
        System.out.println(result.get());

        //使用FutureTask类, FutureTask能够在高并发环境下确保任务只执行一次。
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        //调用的submit(Runnable task)方法
        executorService.submit(futureTask);
        System.out.println(futureTask.get());

    }
}

2.2.2 Future接口

Future接口中几个比较简单的方法:

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

cancel是试图取消一个线程的执行,不一定取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数paramBoolean表示是否采用中断的方式取消线程执行。
为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。

2.2.3 FutureTask类

Future接口有一个实现类是FutureTask, FutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口。


FutureTask实现

Future只是一个接口,而它里面的cancel,get,isDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

2.2.4 FutureTask的几种状态
/**
  *
  * state可能的状态转变路径如下:
  * NEW -> COMPLETING -> NORMAL
  * NEW -> COMPLETING -> EXCEPTIONAL
  * NEW -> CANCELLED
  * NEW -> INTERRUPTING -> INTERRUPTED
  */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。

3. 线程组和线程优先级

3.1 线程组(ThreadGroup)

我们可以使用ThreadGroup对线程进行批量处理。

  • Thread必须存在于一个ThreadGroup中,Thread不能独立于threadGroup存在
  • 执行main()方法的线程名为main
  • 若ThreadGroup在new Thread的时候没有显式指定,默认将父线程(当前执行new Thread的线程)的线程组设置为自己的线程组。

示例代码:

package Basic.groupAndPriority;

public class ThreadGroupDemo {
    public static void main(String[] args) {
        /**
         *执行结果:
         *
         * 执行main方法线程名字:main
         * testThread当前线程组名字:main
         * testThread线程名字:Thread-0
         */
        Thread testThread = new Thread(()->{
            System.out.println("testThread当前线程组的名字: "+
                    Thread.currentThread().getThreadGroup().getName());
            System.out.println("testThread当前线程的名字: "+
                    Thread.currentThread().getName());
        });

        testThread.start();
        System.out.println("执行main方法线程的名字: "+Thread.currentThread().getName());

    }
}

ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构,这么设计的原因是防止"上级"线程被"下级"线程引用而无法有效地被GC回收。

3.2 线程优先级

Java可以指定线程的优先级,范围是1-10,默认是5,但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java线程的执行顺序由调度程序决定,优先级在被调用之前设定。
高优先级的线程比低优先级的线程有更高的几率被先执行,使用方法Thread类的setPriority()实例方法来设定线程的优先级。

package Basic.groupAndPriority;

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        /**
         * 运行结果:
         * 
         * 我是默认的线程优先级: 5
         * 我是设置过的线程优先级: 10
         */
        Thread a = new Thread();
        System.out.println("我是默认的线程优先级: "+a.getPriority());

        Thread b = new Thread();
        b.setPriority(10);
        System.out.println("我是设置过的线程优先级: "+b.getPriority());
    }
}

不能通过设置优先级的方式指定线程的执行顺序: Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的。

代码验证:

package Basic.groupAndPriority;

import java.util.stream.IntStream;

/**
 * 测试设定的优先级和实际运行的顺序
 */
public class ThreadPriorityTest2 {
    public static class T1 extends Thread{
        @Override
        public void run() {
            super.run();
            System.out.println(String.format("当前执行的线程是:%s,优先级:%d",
                    Thread.currentThread().getName(),
                    Thread.currentThread().getPriority()));
        }
    }

    public static void main(String[] args) {
        /**
         * 执行结果为:
         * 
         * 当前执行的线程是:Thread-17,优先级:9
         * 当前执行的线程是:Thread-1,优先级:1
         * 当前执行的线程是:Thread-3,优先级:2
         * 当前执行的线程是:Thread-7,优先级:4
         * 当前执行的线程是:Thread-5,优先级:3
         * 当前执行的线程是:Thread-15,优先级:8
         * 当前执行的线程是:Thread-11,优先级:6
         * 当前执行的线程是:Thread-13,优先级:7
         * 当前执行的线程是:Thread-9,优先级:5
         */
        IntStream.range(1,10).forEach(i -> {
            Thread thread = new Thread(new T1());
            thread.setPriority(i);
            thread.start();
        });
    }
}

Runnable状态的线程的执行顺序:Java提供一个线程调度器来监视和控制处于RUNNABLE状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。

另一种特殊的线程: 守护线程(Daemon)
守护线程默认的优先级比较低。如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。一个线程默认是非守护线程,可以通过Thread类的setDaemon(boolean on)来设置。

线程组和线程优先级不一致会怎么样:
如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

3.3 线程组常用的方法与数据结构

3.3.1 线程组的常用方法

获取当前的线程组名字

Thread.currentThread().getThreadGroup().getName()

复制线程组

//复制线程组
ThreadGroup threadGroup = new ThreadGroup("threadGroup");
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);

线程组统一异常处理

package Basic.groupAndPriority;

public class ThreadGroupMethodsDemo {

    public static void main(String[] args) {
        //获取当前的线程组名字
        System.out.println(Thread.currentThread().getThreadGroup().getName());

        //复制线程组
        ThreadGroup threadGroup = new ThreadGroup("threadGroup");
        Thread[] threads = new Thread[threadGroup.activeCount()];
        threadGroup.enumerate(threads);

        //线程组统一异常处理
        ThreadGroup threadGroup1 = new ThreadGroup("group1"){
            // 继承ThreadGroup并重新定义以下方法
            // 在线程成员抛出unchecked exception会执行此方法
            @Override
            public void uncaughtException(Thread t, Throwable e){
                System.out.println(t.getName()+": "+e.getMessage());
            }
        };

        //定义一个线程处于threadGroup中的一员
        /**
         * 运行结果:
         * Thread-0: 测试异常
         */
        Thread thread1 = new Thread(threadGroup1, new Runnable() {
            @Override
            public void run() {
                //抛出unchecked异常
                throw new RuntimeException("测试异常");
            }
        });

        thread1.start();


    }
}

3.3.2 线程组的数据结构
  • threadGroup可以包含其他线程组/线程 --> 树状结构
  • threadGroup可以统一控制线程的优先级 & 检查线程组的权限

4. Java线程的状态及主要转化方法

4.1 操作系统中的线程状态转换

现在操作系统中,线程被当作轻量级进程。Therefore, 操作系统线程的状态和操作系统的进程的状态是一致的

操作系统线程状态转化图

主要有以下三个状态:

  • 就绪状态(ready): 线程正在等待使用CPU,经调度程序调用之后可进入running状态。
  • 执行状态(running): 线程正在使用CPU。
  • 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。

4.2 Java线程的六种状态

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
4.2.1 NEW

处于NEW状态的线程此时还未启动 --> 未调用Thread实例的start()方法

关于start()的两个引申问题:

  1. 反复调用同一个线程的start()方法是否可行?
  2. 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。比如,threadStatus为2代表当前线程状态为TERMINATED。

4.2.2 RUNNABLE

当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。
Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和running两个状态的。

4.2.3 BLOCKED

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。

4.2.4 WAITING

等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。

调用如下3个方法会使线程进入等待状态:

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  • Thread.join():等待另一个线程执行完毕,底层调用的是Object实例的wait方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
4.2.5 TIMED_WAITING

超时等待状态,线程等待一个具体的时间,时间到后被自动唤醒。

调用如下方法会使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
  • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
4.2.6 TERMINATED

终止状态。此时线程已执行完毕。

4.3 线程状态的转换

线程状态转换
4.3.1 BLOCKED与RUNNABLE状态的转换

处于BLOCKED状态的线程是因为在等待锁的释放。假如这里有两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状态。

例子:

package Basic.threadStateTransfer;


import org.junit.jupiter.api.Test;

public class BlockTestDemo {

    @Test
    public void blockedTest() throws InterruptedException {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"a");

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"b");

        /**
         * 在同时执行a.start(); b.start();的时候
         * 输出结果为:
         * a:RUNNABLE
         * b:RUNNABLE
         *
         * 并不是预期的:
         * a: TIME_WAITING
         * b: BLOCKING
         *
         * 原因如下:
         * 1. 在测试方法blockedTest()内还有一个main线程
         * 2. 启动线程后执行run方法还是需要消耗一定时间的
         * 测试方法的main线程只保证了a,b两个线程调用start()方法(转化为RUNNABLE状态),
         * 还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。
         *
         * 如果希望能打印出来希望的结果,处理下测试方法里的main线程就可以了,你让它“休息一会儿”,
         * 打断点或者调用Thread.sleep方法就行。这里需要注意的是main线程休息的时间,要保证在线程争夺锁的时间内,
         * 不要等到前一个线程锁都释放了你再去争夺锁,此时还是得不到BLOCKED状态的。
         */
        a.start();
        Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
        b.start();

        System.out.println(a.getName() + ":" + a.getState());
        System.out.println(b.getName() + ":" + b.getState());
    }


    // 同步方法争夺锁, synchronized的方法是加锁的方法
    private synchronized void testMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.3.2 WAITING状态与RUNNABLE状态的转换

根据转换图我们知道有3个方法可以使线程从RUNNABLE状态转为WAITING状态。我们主要介绍下Object.wait()和Thread.join()。

Object.wait()

调用wait()方法前线程必须持有对象的锁。Object相当于一个锁对象,这个Thread调用,Object.wait()相当于释放了对这个对象锁的拥有权,并且将自己放到wait的状态下。
线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。The java.lang.Object.notifyAll() wakes up all threads that are waiting on this object's monitor. A thread waits on an object's monitor by calling one of the wait methods.
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。

package Basic.threadCommunication;

public class WaitAndNotify {

    private static final Object lock = new Object();

    static class ThreadA implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                for (int i=0;i<5;i++){
                    try {
                        //先打印自己需要的内容
                        System.out.println("Thread A: "+i);
                        //使用notify()方法叫醒另一个在等待的线程
                        lock.notify();
                        //自己使用wait()方法陷入等待,释放lock锁
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                for (int i=0;i<5;i++){
                    try {
                        System.out.println("Thread B: "+i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }

}

Thread.join()

调用join()方法不会释放锁,会一直等待当前线程执行完毕(转换为TERMINATED状态)。join()方法的内部是用wait实现的,相当于Thread.wait()方法。

package Basic.threadStateTransfer;

import org.junit.jupiter.api.Test;

public class JoinTestDemo {
    @Test
    public void JoinTest() throws InterruptedException {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"a");

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"b");

        a.start();
        /**
         * 要是没有调用join方法,main线程不管a线程是否执行完毕都会继续往下走。
         *
         * join方法的调用会使得main线程进入到waiting的状态,a线程执行完之后再返回到Runnable的状态
         * a线程启动之后马上调用了join方法,这里main线程就会等到a线程执行完毕,所以这里a线程打印的状态固定是TERMIATED。
         *
         * 至于b线程的状态,有可能打印RUNNABLE(尚未进入同步方法),也有可能打印TIMED_WAITING(进入了同步方法)。
         */
        a.join();
        b.start();

        System.out.println(a.getName() + ":" + a.getState());
        System.out.println(b.getName() + ":" + b.getState());
    }

    // 同步方法争夺锁, synchronized的方法是加锁的方法
    private synchronized void testMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4.3.3 TIMED_WAITING与RUNNABLE状态转换

TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。

Thread.sleep(long)

使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入RUNNABLE状态。

Object.wait(long)

wait(long)方法使线程进入TIMED_WAITING状态。这里的wait(long)方法与无参方法wait()相同的地方是,都可以通过其他线程调用notify()或notifyAll()方法来唤醒。
不同的地方是,有参方法wait(long)就算其他线程不来唤醒它,经过指定时间long之后它会自动唤醒,拥有去争夺锁的资格。

Thread.join(long)

join(long)使当前线程执行指定时间,并且主线程进入TIMED_WAITING状态。

package Basic.threadStateTransfer;

import org.junit.jupiter.api.Test;

public class TimeJoinTestDemo {
    @Test
    public void TimeJoinTest() throws InterruptedException {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"a");

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        },"b");

        a.start();
        /**
         * 这里调用a.join(1000L),因为是指定了具体a线程执行的时间的,
         * 并且执行时间是小于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。
         *
         * b线程状态仍然不固定(RUNNABLE或BLOCKED)。
         *
         * 这里相当于main线程进入了timed_waiting的状态
         */
        a.join(1000L);
        b.start();

        System.out.println(a.getName() + ":" + a.getState());
        System.out.println(b.getName() + ":" + b.getState());
    }


    // 同步方法争夺锁, synchronized的方法是加锁的方法
    private synchronized void testMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.3.4 线程中断

在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在Java里还没有安全直接的方法来停止线程,但是Java提供了线程中断机制来处理需要中断线程的情况。
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。

Thread类中提供的关于线程中断的几种方法:

  • Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase);
  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false;
  • Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。

在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在合适的实际处理中断请求,也可以完全不处理继续执行下去。

5. Java线程间的通信

多线程可以更好的利用服务器资源。一般来说,线程内有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握Java线程的通信方式。

5.1 锁与同步

Java中,锁属于对象,so又称对象锁。一个锁在同一时间只能被同一个线程持有。
线程之间有一个同步的概念,线程同步就是线程之间按一定顺序执行。
线程同步可以通过锁来实现。例如两个多线程希望A执行完后B再执行。

package Basic.threadCommunication;

/**
 * 使用对象锁,等一个Thread执行完之后再去执行下一个
 */
public class ObjectLock {

    private static final Object lock = new Object();

    static class ThreadA implements Runnable{

        @Override
        public void run() {
            //使用sychronized关键字加上了同一个对象锁lock
            synchronized (lock){
                for (int i=0;i<100;i++){
                    System.out.println("Thread A "+i);
                }
            }
        }
    }

    static class ThreadB implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                for (int i=0;i<100;i++){
                    System.out.println("Thread B "+i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        /**
         * 这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。
         * 因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。
         * 这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。
         */
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }

}

这里声明了一个名字为lock的对象锁。在ThreadA和ThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock。
根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock,线程B才能获得锁lock。
缺点:基于"锁",线程需要不断的尝试去获取锁,失败后会重试,很耗费服务器资源。

5.2 等待/通知机制

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。(notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程)

一个锁再同一个时刻只能被一个线程持有,假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。
这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。


等待通知机制

代码实现:

package Basic.threadCommunication;

public class WaitAndNotify {

    private static final Object lock = new Object();

    static class ThreadA implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                for (int i=0;i<5;i++){
                    try {
                        //先打印自己需要的内容
                        System.out.println("Thread A: "+i);
                        //使用notify()方法叫醒另一个在等待的线程
                        lock.notify();
                        //自己使用wait()方法陷入等待,释放lock锁
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                for (int i=0;i<5;i++){
                    try {
                        System.out.println("Thread B: "+i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }

}

等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

5.3 信号量

基于volatile关键字自己实现的信号量通信。volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

代码实现:线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。

package Basic.threadCommunication;

public class Signal {

    private static volatile int signal = 0;

    static class ThreadA implements Runnable{

        @Override
        public void run() {
            while (signal<5){
                if (signal % 2==0){
                    System.out.println("ThreadA: "+signal);
                    signal++;
                }
            }
        }
    }

    static class ThreadB implements Runnable{

        @Override
        public void run() {
            while (signal<5){
                if ((signal % 2) == 1){
                    System.out.println("ThreadB: "+signal);
                    signal = signal+1;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(100);
        new Thread(new ThreadB()).start();

    }
}

使用了一个volatile变量signal来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作。
需要注意的是,signal++并不是一个原子操作,所以我们在实际开发中,会根据需要使用synchronized给它“上锁”,或者是使用AtomicInteger等原子类。

信号量应用场景:
多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。

5.4 管道

管道是基于“管道流”的通信方式。JDK提供了PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

package Basic.threadCommunication;

import sun.awt.windows.ThemeReader;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
 * 管道是基于管道流的通信方式
 *
 *
 */
public class Pipe {

    static class ReadThread implements Runnable{

        private PipedReader reader;

        public ReadThread(PipedReader reader){
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;

            try {
                while (((receive = reader.read()) != -1)){
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    static class WriterThread implements Runnable{

        private PipedWriter writer;

        public WriterThread(PipedWriter writer){
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println("this is writer");
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        //writer要和reader连接上才能通信
        writer.connect(reader);

        /**
         * 线程ReaderThread开始执行,
         * 线程ReaderThread使用管道reader.read()进入”阻塞“,
         * 线程WriterThread开始执行,
         * 线程WriterThread用writer.write("test")往管道写入字符串,
         * 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
         * 线程ReaderThread接受到管道输出的字符串并打印,
         * 线程ReaderThread执行完毕。
         */
        new Thread(new ReadThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }

}

应用场景:
使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

5.5 其他通信

5.5.1 join方法

它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。
示例代码:

package Basic.threadCommunication;

public class Join {
    static class ThreadA implements Runnable{

        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡1s");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了1s");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("如果不加join方法,我会被先打出来,加了就不一样啦");
    }
}

5.5.2 sleep方法

sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法:

  • Thread.sleep(long)
  • Thread.sleep(long, int)

sleep方法是不会释放当前的锁的,而wait方法会。

sleep方法 vs. wait方法

  • wait可以指定时间,也可以不指定;而sleep必须指定时间。
  • wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
  • wait必须放在同步块或同步方法中,而sleep可以在任意位置。
5.5.3 ThreadLocal类

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。
ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响(类似线程隔离)。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。
示例代码:

package Basic.threadCommunication;

public class ThreadLocalDemo {

    static class ThreadA implements Runnable{

        private ThreadLocal<String> threadLocal;

        public ThreadA(ThreadLocal<String> threadLocal){
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("A");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadA输出:" + threadLocal.get());
        }

    }

    static class ThreadB implements Runnable {
        private ThreadLocal<String> threadLocal;

        public ThreadB(ThreadLocal<String> threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("B");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadB输出:" + threadLocal.get());
        }
    }

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        /**
         * 可以看到,虽然两个线程使用的同一个ThreadLocal实例(通过构造方法传入),
         * 但是它们各自可以存取自己当前线程的一个值。
         */
        new Thread(new ThreadA(threadLocal)).start();
        new Thread(new ThreadB(threadLocal)).start();
    }

}

开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。
最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

5.5.4 InheritableThreadLocal

InheritableThreadLocal类与ThreadLocal类稍有不同。它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。

Reference:http://concurrent.redspider.group/article/01/5.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。