栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【基础篇】一篇文章带你了解多线程的魅力

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【基础篇】一篇文章带你了解多线程的魅力

1、 Java多线程⼊⻔类和接⼝ 1.1Thread类和Runnable接⼝

JDK提供了 Thread 类和 Runnalble 接⼝来让我们 实现⾃⼰的“线程”类。

  • 继承 Thread 类,并重写 run ⽅法;
  • 实现 Runnable 接⼝的 run ⽅法;
1.1.1继承Thread类

⾸先是继承 Thread 类:

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

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

注意要调⽤ start() ⽅法后,该线程才算启动!

  • 我们在程序⾥⾯调⽤了start()⽅法后,虚拟机会先为我们创建⼀个线程,然 后等到这个线程第⼀次得到时间⽚时再调⽤run()⽅法。
  • 注意不可多次调⽤start()⽅法。在第⼀次调⽤start()⽅法后,再次调⽤start() ⽅法会抛出异常。
1.1.2实现Runnable接⼝

接着我们来看⼀下 Runnable 接⼝(JDK 1.8 +):

@FunctionalInterface
public interface Runnable {
 	public abstract void run();
}

可以看到 Runnable 是⼀个函数式接⼝,这意味着我们可以使⽤Java 8的函数式编 程来简化代码。

 public static class MyThread implements Runnable{

        public void run() {
            System.out.println("mythread");
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread).start();
    }
1.1.3Thread类构造⽅法

Thread 类是⼀个 Runnable 接⼝的实现类,我们来看看 Thread 类的源码。

查看 Thread 类的构造⽅法,发现其实是简单调⽤⼀个私有的 init ⽅法来实现初 始化。

// Thread类源码

// ⽚段1 - init⽅法
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals)
    
// ⽚段2 - 构造函数调⽤init⽅法
public Thread(Runnable target) {
 	init(null, target, "Thread-" +nextThreadNum(), 0);
}

// ⽚段3 - 使⽤在init⽅法⾥初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();

// ⽚段4 - 两个对⽤于⽀持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • g:线程组,指定这个线程是在哪个线程组下;
  • target:指定要执⾏的任务;
  • name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,⻅⽚ 段2;
  • acc:⻅⽚段3,⽤于初始化私有变量 inheritedAccessControlContext 。
  • inheritThreadLocals:可继承的 ThreadLocal ,⻅⽚段4, Thread 类⾥⾯有两 个私有属性来⽀持 ThreadLocal 。
1.1.4 Thread类的⼏个常⽤⽅法
  • currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;
  • start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;
  • yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿
    意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield()
    ⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;
  • sleep():静态⽅法,使当前线程睡眠⼀段时间;
  • join():使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是 Object类的wait⽅法实现的;
1.1.5 Thread类与Runnable接⼝的⽐较:
  • 由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。
  • Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。
  • Runnable接⼝出现,降低了线程对象和线程任务的耦合性。
  • 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量。

所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类。

1.2 Callable、Future与FutureTask

我们使⽤ Runnable 和 Thread 来创建⼀个新的线程。但是它们有⼀个弊 端,就是 run ⽅法是没有返回值的。⽽有时候我们希望开启⼀个线程去执⾏⼀个任 务,并且这个任务执⾏完成后有⼀个返回值。

JDK提供了 Callable 接⼝与 Future 类为我们解决这个问题,这也是所谓的“异步” 模型。

1.2.1 Callable接⼝

Callable 与 Runnable 类似,同样是只有⼀个抽象⽅法的函数式接⼝。不同的 是, Callable 提供的⽅法是有返回值的,⽽且⽀持泛型。

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

Callable ⼀般是配合线程池⼯ 具 ExecutorService 来使⽤的。ExecutorService 可以使⽤ submit ⽅法来让⼀个 Callable 接⼝执⾏。它会返回 ⼀个 Future ,我们后续的程序可以通过这个 Future 的 get ⽅法得到结果。

// ⾃定义Callable
class Task implements Callable{
     @Override
     public Integer call() throws Exception {
         // 模拟计算需要⼀秒
         Thread.sleep(1000);
         return 2;
 }
    
 public static void main(String args[]){
 // 使⽤
     ExecutorService executor = Executors.newCachedThreadPool();
     Task task = new Task();
     Future result = executor.submit(task);
     // 注意调⽤get⽅法会阻塞当前线程,直到得到结果。
     // 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。
     System.out.println(result.get());
 }
}

输出结果:2
1.2.2 Future接⼝

Future 接⼝只有⼏个⽐较简单的⽅法:

public interface Future {
    boolean cancel(boolean var1);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;
}

cancel ⽅法是试图取消⼀个线程的执⾏。

注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些 其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成 功”的意思。参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。

所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable 。 如果为了可取消性⽽使⽤ Future 但⼜不提供可⽤的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。

1.2.3 FutureTask类

上⾯介绍了 Future 接⼝。这个接⼝有⼀个实现类叫 FutureTask 。 FutureTask 是 实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝:

public class FutureTask implements RunnableFuture{
   ......
}
public interface RunnableFuture extends Runnable, Future {
    
    void run();
}

那 FutureTask 类有什么⽤?为什么要有⼀个 FutureTask 类?前⾯说到 了 Future 只是⼀个接⼝,⽽它⾥⾯的 cancel , get , isDone 等⽅法要⾃⼰实现 起来都是⾮常复杂的。所以JDK提供了⼀个 FutureTask 类来供我们使⽤。

public static class Task implements Callable{

        public Integer call() throws Exception {
            Thread.sleep(1000);
            return 2;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        FutureTask futureTask = new FutureTask(new Task());
        executorService.submit(futureTask);
        System.out.println(futureTask.get());
    }

使⽤上与第⼀个Demo有⼀点⼩的区别 。⾸先,调⽤ submit ⽅法是没有返回值的。 这⾥实际上是调⽤的 submit(Runnable task) ⽅法,⽽上⾯的Demo,调⽤的 是 submit(Callable task) ⽅法。

然后,这⾥是使⽤ FutureTask 直接取 get 取值,⽽上⾯的Demo是通过 submit ⽅ 法返回的 Future 去取值。

在很多⾼并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能 够在⾼并发环境下确保任务只执⾏⼀次。

1.2.4 FutureTask的⼏个状态
    
	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是任 务完成后的瞬时状态。

2、 线程组和线程优先级 2.1 线程组(ThreadGroup)

Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。

ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必 然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组。

public class ThreadGroupTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("testThread当前线程组的名字" + Thread.currentThread().getThreadGroup().getName());
            System.out.println("testThread线程名字" + Thread.currentThread().getName());
        });

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

执⾏main⽅法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0

ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状 结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回 收。

2.2 线程的优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给 操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系 统决定。

Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会 在线程被调⽤之前设定。

通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们 使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。

    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println("我是默认线程优先级:"+thread.getPriority());

        Thread thread1 = new Thread();
        thread1.setPriority(10);
        System.out.println(thread1.getPriority());
    }


我是默认线程优先级:5
我是设置过的线程优先级:10

既然有1-10的级别来设定了线程的优先级,这时候可能有些读者会问,那么我是不 是可以在业务实现的时候,采⽤这种⽅法来指定⼀些线程执⾏的先后顺序?

NO,大肉特肉。

Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给 操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的线程调度算法决定的。

    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) {
        IntStream.range(1,10).forEach(i->{
            Thread thread = new Thread(new T1());
            thread.setPriority(i);
            thread.start();
        });
    }

某次输出:

当前执⾏的线程是:Thread-9,优先级:5
当前执⾏的线程是:Thread-1,优先级:1
当前执⾏的线程是:Thread-13,优先级:7
当前执⾏的线程是:Thread-7,优先级:4
当前执⾏的线程是:Thread-15,优先级:8
当前执⾏的线程是:Thread-17,优先级:9
当前执⾏的线程是:Thread-3,优先级:2
当前执⾏的线程是:Thread-11,优先级:6
当前执⾏的线程是:Thread-5,优先级:3

Java提供⼀个线程调度器来监视和控制处于RUNNABLE状态的线程。**线程的调度策略采⽤抢占式,优先级⾼的线程⽐优先级低的线程会有更⼤的⼏率优先执⾏。**在 优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有⼀个默认的主线程,就是通过JVM启动的第⼀个线程main线程。

还有⼀种线程称为守护线程(Daemon),守护线程默认的优先级⽐较低。 (jvm中的GC回收就是属于守护线程)

  • 如果某线程是守护线程,那如果所有的⾮守护线程结束,这个守护线程也会 ⾃动结束。
  • 应⽤场景是:当所有⾮守护线程结束时,结束其余的⼦线程(守护线程)⾃ 动关闭,就免去了还要继续关闭⼦线程的麻烦。
  • ⼀个线程默认是⾮守护线程,可以通过Thread类的setDaemon(boolean on) 来设置。

在之前,我们有谈到⼀个线程必然存在于⼀个线程组中,那么当线程和线程组的优 先级不⼀致的时候将会怎样呢?我们⽤下⾯的案例来验证⼀下:

    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup("t1");
        threadGroup.setMaxPriority(6);
        Thread thread = new Thread(threadGroup, "thread");
        thread.setPriority(9);
        System.out.println(threadGroup.getMaxPriority());
        System.out.println(thread.getPriority());

    }
我是线程组的优先级6
我是线程的优先级6

所以,如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取⽽代之的是线程组的最⼤优先级。

2.3 线程组的常⽤⽅法及数据结构

线程组还可以包含其他的线程组,不仅仅是线程。

⾸先看看 ThreadGroup 源码中的成员变量

    public class ThreadGroup implements Thread.UncaughtExceptionHandler {
     private final ThreadGroup parent; // ⽗亲ThreadGroup
     String name; // ThreadGroupr 的名称
     int maxPriority; // 线程最⼤优先级
     boolean destroyed; // 是否被销毁
     boolean daemon; // 是否守护线程
     boolean vmAllowSuspension; // 是否可以中断
     int nUnstartedThreads = 0; // 还未启动的线程
     int nthreads; // ThreadGroup中线程数⽬
     Thread threads[]; // ThreadGroup中的线程
     int ngroups; // 线程组数⽬
     ThreadGroup groups[]; // 线程组数组
}

然后看看构造函数:

// 私有构造函数
private ThreadGroup() {
     this.name = "system";
     this.maxPriority = Thread.MAX_PRIORITY;
     this.parent = null;
}

// 默认是以当前ThreadGroup传⼊作为parent ThreadGroup,新线程组的⽗线程组是⽬前正在运⾏线
public ThreadGroup(String name) {
     this(Thread.currentThread().getThreadGroup(), name);
}

// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
 	this(checkParentAccess(parent), parent, name);
}

// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
     this.name = name;
     this.maxPriority = parent.maxPriority;
     this.daemon = parent.daemon;
     this.vmAllowSuspension = parent.vmAllowSuspension;
     this.parent = parent;
     parent.add(this);
}

第三个构造函数⾥调⽤了 checkParentAccess ⽅法,这⾥看看这个⽅法的源码:

// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
     parent.checkAccess();
     return null;
}

// 判断当前运⾏的线程是否具有修改线程组的权限
public final void checkAccess() {
     SecurityManager security = System.getSecurityManager();
     if (security != null) {
     security.checkAccess(this);
     }
}

总结来说,线程组是⼀个树状的结构,每个线程组下⾯可以有多个线程或者线程 组。线程组可以起到统⼀控制线程的优先级和检查线程的权限的作用。

3、 Java线程的状态及主要转化⽅法 3.1操作系统中的线程状态转换

⾸先我们来看看操作系统中的线程状态转换。

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状 态其实和操作系统进程的状态是⼀致的。

操作系统线程主要有以下三个状态:

  • 就绪状态(ready):线程正在等待使⽤CPU,经调度程序调⽤之后可进⼊ running状态。
  • 执⾏状态(running):线程正在使⽤CPU。
  • 等待状态(waiting): 线程经过等待事件的调⽤或者正在等待其他资源(如 I/O)。
3.2 Java线程的6个状态
 NEW,
 RUNNABLE,
 BLOCKED,
 WAITING,
 TIMED_WAITING,
 TERMINATED;
3.2.1 New

处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法。

private void testStateNew() {
     Thread thread = new Thread(() -> {});
     System.out.println(thread.getState()); // 输出 NEW
}

从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。

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

要分析这两个问题,我们先来看看start()的源码:

public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
    
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                
            }
        }
    }

我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。

private native void start0();

我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:

public void testStartMethod() {
        Thread thread = new Thread(() -> {});
        thread.start(); // 第⼀次调⽤
        thread.start(); // 第⼆次调⽤
    }


  • 第⼀次调⽤时threadStatus的值是0。
  • 第⼆次调⽤时threadStatus的值不为0。

查看当前线程状态的源码:

 public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }



 public static State toThreadState(int var0) {
        if ((var0 & 4) != 0) {
            return State.RUNNABLE;
        } else if ((var0 & 1024) != 0) {
            return State.BLOCKED;
        } else if ((var0 & 16) != 0) {
            return State.WAITING;
        } else if ((var0 & 32) != 0) {
            return State.TIMED_WAITING;
        } else if ((var0 & 2) != 0) {
            return State.TERMINATED;
        } else {
            return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
        }
    }

所以得出答案:

两个问题的答案都是不可⾏,在调⽤⼀次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调⽤start()⽅法会抛出 IllegalThreadStateException异常。

3.2.2 RUNNABLE

表示当前线程正在运⾏中。处于RUNNABLE状态的线程在Java虚拟机中运⾏,也 有可能在等待其他系统资源(⽐如I/O)。

Java中线程的RUNNABLE状态


    /**
    *可运行线程的线程状态。可运行线程中的一个线程
    *状态在Java虚拟机中执行,但它可以
    *等待来自操作系统的其他资源
    *如处理器。
    */

Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和 running两个状态的。

3.2.3 BLOCKED

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

举个栗子:

假如今天你下班后准备去⻝堂吃饭。
你来到⻝堂仅有的⼀个窗⼝,发现前⾯已经有个⼈在窗⼝前了,
此时你必须得等前⾯的⼈从窗⼝离开才⾏。

假设你是线程t2,你前⾯的那个⼈是线程t1。
此时t1占有了锁(⻝堂唯⼀的窗⼝),
t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。
3.2.4 WAITING

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

调⽤如下3个⽅法会使线程进⼊等待状态:

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

我们延续上⾯的例⼦继续解释⼀下WAITING状态:

你等了好⼏分钟现在终于轮到你了,突然你们有⼀个“不懂事”的经理突然来了。
你看到他你就有⼀种不祥的预感,果然,他是来找你的。
他把你拉到⼀旁叫你待会⼉再吃饭,说他下午要去作报告,赶紧来找你了解⼀下项⽬的情况。
你⼼⾥虽然有⼀万个不愿意但是你还是从⻝堂窗⼝⾛开了。

此时,假设你还是线程t2,你的经理是线程t1。
虽然你此时都占有锁(窗⼝)了,“不速之客”来了你还是得释放掉锁。
此时你t2的状态就是WAITING。然后经理t1获得锁,进⼊RUNNABLE状态。
要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能⼀直等待了。

3.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):同上,也是禁⽌线程进⾏调度指定时 间;

我们继续延续上⾯的例⼦来解释⼀下TIMED_WAITING状态:

到了第⼆天中午,⼜到了饭点,你还是到了窗⼝前。
突然间想起你的同事叫你等他⼀起,他说让你等他⼗分钟他改个bug。
好吧,你说那你就等等吧,你就离开了窗⼝。
很快⼗分钟过去了,你⻅他还没来,你想都等了这么久了还不来,
那你还是先去吃饭好了。

这时你还是线程t1,你改bug的同事是线程t2。
t2让t1等待了指定时间,t1先主动释放了锁。
此时t1等待期间就属于TIMED_WATING状态。
t1等待10分钟后,就⾃动唤醒,拥有了去争夺锁的资格。

等待超时就是等待设置了等待时间。

3.2.6 TERMINATED

终⽌状态。此时线程已执⾏完毕。

3.3 线程状态的转换

线程状态转换图:

3.3.1 BLOCKED与RUNNABLE状态的转换

处于BLOCKED状态的线程是因为在等待锁的释放。假如这⾥有 两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状 态。我们先来看⼀个例⼦:

 @Test
    public void blocked () 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();
        System.out.println(a.getName()+":"+a.getState());
        System.out.println(b.getName()+":"+b.getState());
    }


    private  synchronized void testMethod(){
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

初看之下,⼤家可能会觉得线程a会先调⽤同步⽅法,同步⽅法内⼜调⽤了 Thread.sleep()⽅法,必然会输出TIMED_WAITING,⽽线程b因为等待线程a释放 锁所以必然会输出BLOCKED。

其实不然,有两点需要值得⼤家注意,⼀是在测试⽅法blockedTest()内还有⼀个 main线程,⼆是启动线程后执⾏run⽅法还是需要消耗⼀定时间的。不打断点的情 况下,上⾯代码中都应该输出RUNNABLE。

测试⽅法的main线程只保证了a,b两个线程调⽤start()⽅法(转化为 RUNNABLE状态),
还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。 

这时你可能⼜会问了,要是我想要打印出BLOCKED状态我该怎么处理呢?其实就 处理下测试⽅法⾥的main线程就可以了,你让它“休息⼀会⼉”,调⽤ Thread.sleep⽅法就⾏。

public void blockedTest() throws InterruptedException {
 ······
 a.start();
 Thread.sleep(1000L); // 需要注意这⾥main线程休眠了1000毫秒,⽽testMethod()⾥休眠了
 b.start();
 System.out.println(a.getName() + ":" + a.getState()); // 输出?
 System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

在这个例⼦中,由于main线程休眠,所以线程a的run()⽅法跟着执⾏,线程b再接 着执⾏。

在线程a执⾏run()调⽤testMethod()之后,线程a休眠了2000ms(注意这⾥是没有 释放锁的),main线程休眠完毕,接着b线程执⾏的时候是争夺不到锁的,所以这 ⾥输出:

a:TIMED_WAITING
b:BLOCKED
3.3.2 WAITING状态与RUNNABLE状态的转换

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

Object.wait()

  • 调⽤wait()⽅法前线程必须持有对象的锁。
  • 线程调⽤wait()⽅法时,会释放当前的锁,直到有其他线程调⽤ notify()/notifyAll()⽅法唤醒等待锁的线程。
  • 需要注意的是,其他线程调⽤notify()⽅法只会唤醒单个等待锁的线程,如有多个线程都在等待这个锁的话不⼀定会唤醒到之前调⽤wait()⽅法的线程。
  • 同样,调⽤notifyAll()⽅法唤醒所有等待锁的线程之后,也不⼀定会⻢上把时 间⽚分给刚才放弃锁的那个线程,具体要看系统的调度。

Thread.join()

  • 调⽤join()⽅法不会释放锁,会⼀直等待当前线程执⾏完毕(转换为 TERMINATED状态)。
public void blockedTest() {
 	······
     a.start();
     a.join();
     b.start();
     System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
     System.out.println(b.getName() + ":" + b.getState());
}
a:TERMINATED
b:TIMED_WAITING

要是没有调⽤join⽅法,main线程不管a线程是否执⾏完毕都会继续往下⾛。

a线程启动之后⻢上调⽤了join⽅法,这⾥main线程就会等到a线程执⾏完毕,所以 这⾥a线程打印的状态固定是TERMIATED。

⾄于b线程的状态,有可能打印RUNNABLE(尚未进⼊同步⽅法),也有可能打印 TIMED_WAITING(进⼊了同步⽅法)。

3.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状 态。

我们再来改⼀改刚才的示例:

public void blockedTest() {
     ······
     a.start();
     a.join(1000L);
     b.start();
     System.out.println(a.getName() + ":" + a.getState()); // 输出 TIMED_WAITING
     System.out.println(b.getName() + ":" + b.getState());
}

//这⾥调⽤a.join(1000L),因为是指定了具体a线程执⾏的时间的,并且执⾏时间是⼩于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。 

b线程状态仍然不固定(RUNNABLE或BLOCKED)。

3.3.4 线程中断
在某些情况下,我们在线程启动后发现并不需要它继续执⾏下去时,需要中断线程。
⽬前在Java⾥还没有安全直接的⽅法来停⽌线程,
但是Java提供了线程中断机制来处理需要中断线程的情况。
线程中断机制是⼀种协作机制。
需要注意,通过中断操作并不能直接终⽌⼀个线程,⽽是通知需要被中断的线程⾃⾏处理。

  • Thread.interrupt():中断线程。这⾥的中断线程并不会⽴即停⽌线程,⽽是设 置线程的中断状态为true(默认是flase);
  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个⽅法 的影响,意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这 个线程的中断状态重新转为false;
  • Thread.isInterrupted():测试当前线程是否被中断。与上⾯⽅法不同的是调⽤ 这个⽅法并不会影响线程的中断状态。

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

4、Java线程间的通信

合理的使⽤Java多线程可以更好地利⽤服务器资源。⼀般来讲,线程内部有⾃⼰私 有的线程上下⽂,互不⼲扰。但是当我们需要多个线程之间相互协作的时候,就需 要我们掌握Java线程的通信⽅式。本⽂将介绍Java线程之间的⼏种通信原理。

4.1 锁与同步

在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程和锁的 关系,⼀个锁同⼀时间只能被⼀个线程持有。 其他线程如果需要得到这个锁,就得等这个线程释放该锁。

什么是同步呢?

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。

举例:

假如我们现在有2位正在 抄暑假作业答案的同学:线程A和线程B。
当他们正在抄的时候,⽼师突然来修改 了⼀些答案,可能A和B最后写出的暑假作业就不⼀样。
我们为了A,B能写出2本相 同的暑假作业,我们就需要让⽼师先修改答案,然后A,B同学再抄。
或者A,B同学先抄完,⽼师再修改答案。这就是线程A,线程B的线程同步。  

可以以解释为:线程同步是线程之间按照⼀定的顺序执⾏。

为了达到线程同步,我们可以使⽤锁来实现它。

我们先来看看⼀个⽆锁的程序:

public class NoneLock {
    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
    static class ThreadA implements Runnable{

        @Override
        public void run() {
            for(int i=0;i<50;i++){
                System.out.println("ThreadA"+i);
            }
        }
    }

    static class ThreadB implements Runnable{

        @Override
        public void run() {
            for(int i=0;i<50;i++){
                System.out.println("ThreadB"+i);
            }
        }
    }

}

执⾏这个程序,你会在控制台看到,线程A和线程B各⾃独⽴⼯作,输出⾃⼰的打 印值。如下是我的电脑上某⼀次运⾏的结果。每⼀次运⾏结果都会不⼀样。

ThreadB14
ThreadA41
ThreadB15
ThreadB16
ThreadB17
ThreadB18
ThreadA42
ThreadB19
ThreadB20
ThreadB21
ThreadB22

那我现在有⼀个需求,我想等A先执⾏完之后,再由B去执⾏,怎么办呢?最简单 的⽅式就是使⽤⼀个“对象锁”:

public class ObjectLock {
    private static Object lock =new Object();

    static class ThreadA implements Runnable{

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

    static class ThreadB implements Runnable{

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

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

这⾥声明了⼀个名字为 lock 的对象锁。我们在 ThreadA 和 ThreadB 内需要同步的 代码块⾥,都是⽤ synchronized 关键字加上了同⼀个对象锁 lock 。

上⽂我们说到了,根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么 线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。

这⾥在主线程⾥使⽤sleep⽅法睡眠了10毫秒,是为了防⽌线程B先得到锁。
因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运⾏。
这样就会先输出B的内容,然后B执⾏完成之后⾃动释放锁,线程A再执⾏。
4.2 等待/通知机制

上⾯⼀种基于“锁”的⽅式,线程需要不断地去尝试获得锁,如果失败了,再继续尝 试。这可能会耗费服务器资源。

⽽等待/通知机制是另⼀种⽅式。

Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法和 notify() , notifyAll() ⽅法来实现的。

notify()⽅法会随机叫醒⼀个正在等待的线程,⽽notifyAll()会叫醒所有正在等 待的线程。 

前⾯我们讲到,⼀个锁同⼀时刻只能被⼀个线程持有。⽽假如线程A现在持有了⼀ 个锁 lock 并开始执⾏,它可以使⽤ lock.wait() 让⾃⼰进⼊等待状态。这个时 候, lock 这个锁是被释放了的。

这时,线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使 ⽤ lock.notify() ,通知之前持有 lock 锁并进⼊等待状态的线程A,说“线程A你不 ⽤等了,可以往下执⾏了”。

需要注意的是,这个时候线程B并没有释放锁 lock ,
除⾮线程B这个时候使⽤ lock.wait() 释放锁,
或者线程B执⾏结束⾃⾏释放锁,线程A才能得 到 lock 锁。  

我们⽤代码来实现⼀下:

public class WaitAndNotify {
    private static 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("ThreadA:"+i);
                        lock.notify();
                        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("ThreadB:"+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();
    }
}



输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4

线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使 ⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后⾃⼰使⽤ wait() ⽅法陷⼊等待 并释放 lock 锁。

需要注意的是等待/通知机制使⽤的是使⽤同⼀个对象锁,如果你两个线程使⽤的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。

4.3 信号量

JDK提供了⼀个类似于“信号量”功能的类 Semaphore 。但本⽂不是要介绍这个类, ⽽是介绍⼀种基于 volatile 关键字的⾃⼰实现的信号量通信。

后⾯会有专⻔的章节介绍 volatile 关键字,这⾥只是做⼀个简单的介绍。

volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变量,
在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。

⽐如我现在有⼀个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出 2…以此类推。我应该怎样实现呢?

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);
                    synchronized (this){
                        signal++;
                    }
                }
            }
        }
    }

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


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

}


// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4

我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥ 需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操 作,所以我们需要使⽤ synchronized 给它“上锁”。

信号量的应⽤场景:

假如在⼀个停⻋场中,⻋位是我们的公共资源,线程就如同⻋辆,⽽看⻔的管理员 就是起的“信号量”的作⽤。

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

其实JDK中提供的很多多线程通信⼯具类都是基于信号量模型的。后面会介绍⼀些常⽤的通信⼯具类。 敬请期待

4.4 管道

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

这⾥的示例代码使⽤的是基于字符的:

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

public class Pipe {
    static class ReaderThread implements Runnable{

        private PipedReader reader;
        public ReaderThread(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.println((char)receive);
//                    System.out.println(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");
            int receive=0;
            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.connect(reader); //连接才能通信

        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

输出:
this is reader
this is writer
t
e
s
t

我们通过线程的构造函数,传⼊了 PipedWrite 和 PipedReader 对象。可以简单分析 ⼀下这个示例代码的执⾏流程:

  • 线程ReaderThread开始执行
  • 线程ReaderThread使用管道reader.read() 进⼊”阻塞“
  • 线程WriterThread开始执⾏
  • 线程WriterThread⽤writer.write(“test”)往管道写⼊字符串
  • 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕
  • 线程ReaderThread接受到管道输出的字符串并打印
  • 线程ReaderThread执⾏完毕。

管道通信的应⽤场景:

这个很好理解。使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发 送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。

4.5 其它通信相关 4.5.1 join⽅法

join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等 join的这个线程执⾏完成后,再继续执⾏当前线程。

有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主线程往往将早于⼦线程结束之前结束。

如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤ 到join⽅法了。

public class Join {
    static class ThreadA implements Runnable{

        @Override
        public void run() {

            try {
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我已睡一秒");
            } 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⽅法,我会先被打印出来,加了就不⼀样了");
    }
}
注意join()⽅法有两个重载⽅法,⼀个是join(long), ⼀个是join(long, int)。

实际上,通过源码你会发现,join()⽅法及其重载⽅法底层都是利⽤了wait(long)这个⽅法。

对于join(long, int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,⽽是对第⼆个参数做了简单的判断和处理。
4.5.2 sleep⽅法

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

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

sleep⽅法是不会释放当前的锁的,⽽wait⽅法会。这也是最 常⻅的⼀个多线程⾯试题。

它们还有这些区别:

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

ThreadLocal是⼀个本地线程副本变量⼯具类。内部是⼀个弱引⽤的Map来维护。 这⾥不详细介绍它的原理,⽽是只是介绍它的使⽤,后面有单独介绍原理的文章。

有些朋友称ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal 类并不属于多线程间的通信,⽽是**让每个线程有⾃⼰”独⽴“的变量,线程之间互不 影响。**它为每个线程都创建⼀个副本,每个线程可以访问⾃⼰内部的副本变量。

ThreadLocal类最常⽤的就是set⽅法和get⽅法。

public class ThreadLocalDemo {
    static class ThreadA implements Runnable {

        private ThreadLocal threadLocal;
        public ThreadA(ThreadLocal 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 threadLocal;

        public ThreadB(ThreadLocal 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 threadLocal=new ThreadLocal<>();
        new Thread(new ThreadA(threadLocal)).start();
        new Thread(new ThreadB(threadLocal)).start();
    }
}


输出:
ThreadA输出:A
ThreadB输出:B

可以看到,虽然两个线程使⽤的同⼀个ThreadLocal实例(通过构造⽅法传⼊), 但是它们各⾃可以存取⾃⼰当前线程的⼀个值。

那ThreadLocal有什么作⽤呢?如果只是单纯的想要线程隔离,在每个线程中声明 ⼀个私有变量就好了呀,为什么要使⽤ThreadLocal?

如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关 联,则可以考虑使⽤ThreadLocal。

最常⻅的ThreadLocal使⽤场景为⽤来解决数据库连接、Session管理等。数据库连 接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明⼀些 私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/870220.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号