Java多线程编程核心技术

时间:2022-07-22
本文章向大家介绍Java多线程编程核心技术,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Java 多线程技术

Thread 类的核心方法较多,应该着重掌握如下关键技术点:

  1. 线程的启动
  2. 如何使线程暂停
  3. 如何使线程停止
  4. 线程的优先级
  5. 线程安全相关的问题

使用多线程

一个进程正在运行时至少会有一个线程在运行,这种情况在Java中也是存在的。这些线程在后台默默地执行,比如调用public static void main()方法的线程就是这样的,而且它是由JVM创建的。

public static void main(String[] args) {
    // main
    System.out.println(Thread.currentThread().getName());
}

在控制台中输出的main其实就是一个名称叫作main的线程在执行main()方法中的代码。另外需要说明一下,在控制台输出的main和main方法没有任何的关系,仅仅是名字相同而已。

继承Thread类

在Java的JDK开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式主要有两种:一种是继承Thread类,另一种是实现Runnable接口。

Thread类的结构:

public class Thread implements Runnable

从上面的源代码中可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。

其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单根继承,所以为了支持多线程,完全可以实现Runnable接口的方式,一边实现一边继承。但用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("MyThread");
    }
}
public class ThreadStudy1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println("main 线程运行结束");

        //main 线程运行结束
        //MyThread
    }
}

从运行结果来看,MyThread的run方法执行的时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。

注意:如果多次调用 start() 方法,则会出现异常 Exception in thread "main" java.lang.IllegalThreadStateException

上面介绍了线程的调用的随机性,下面演示线程的随机性

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("run = " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadStudy1 {
    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.setName("myThread");
            thread.start();
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("main = " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在代码中,为了展现出线程具有随机特性,所以使用随机数的形式来使线程得到挂起的效果,从而表现出CPU执行哪个线程具有不确定性。

另外还需要注意,执行 start() 方法的顺序不代表线程启动的顺序。

public class MyThread2 extends Thread {

    private int i;

    public MyThread2(int i) {
        super();
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(i);
    }
}
public class ThreadStudy2 {
    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threadList.add(new MyThread2(i));
        }

        for (Thread thread :
                threadList) {
            thread.start();
        }
    }
}

实现 Runnable 接口

如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这样的情况。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("运行中");
    }
}
public class RunnableStudy {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("运行结束");
    }
}

另外需要说明的是,Thread类也实现了Runnable接口,那也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。

实例变量与线程安全

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。

public class MyThread3 extends Thread {
    private int count = 5;
    public MyThread3(String name) {
        super();
        this.setName(name);
    }
    @Override
    public void run() {
        super.run();
        while (count > 0) {
            count--;
            System.out.println("由 " + this.currentThread().getName() + " 计算,count = " + count);
        }
    }
}
public class ThreadStudy3 {

    public static void main(String[] args) {

        MyThread3 a = new MyThread3("A");
        MyThread3 b = new MyThread3("B");
        MyThread3 c = new MyThread3("C");

        a.start();
        b.start();
        c.start();
    }
}

一共创建了三个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。

共享距的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。

public class MyThread4 extends Thread {
    private int count = 5;
    @Override
    public void run() {
        super.run();
        count--;
        System.out.println("由 " + this.currentThread().getName() + " 计算,count = " + count);
    }
}
public class ThreadStudy4 {

    public static void main(String[] args) {

        MyThread4 myThread = new MyThread4();
        Thread a = new Thread(myThread, "A");
        Thread b = new Thread(myThread, "B");
        Thread c = new Thread(myThread, "C");
        Thread d = new Thread(myThread, "D");
        Thread e = new Thread(myThread, "E");

        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

会出现 A 和 B 同时对 count 进行处理,产生了“非线程安全”问题。

在JVM中,i--的操作要分成如下三步:

  1. 取得原有 i 值
  2. 计算 i-1
  3. 对 i 进行赋值

在这三个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全的问题。

其实这个示例就是典型的销售场景:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减一操作。这时就需要使多个线程之间进行同步,也就是用按顺序的方式进行减一操作。更改代码如下:

public class MyThread4 extends Thread {
    private int count = 5;
    @Override
    //public void run() {  // 原来的
    synchronized public void run() {  // 新增 synchronized 关键字
        super.run();
        count--;
        System.out.println("由 " + this.currentThread().getName() + " 计算,count = " + count);
    }
}

通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。

snychronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。

currentThread() 方法

currentThread() 方法可返回代码段正在被哪个线程调用的信息。

public class MyThread extends Thread {

    public MyThread() {
        System.out.println("构造方法的打印: " + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run 方法的打印: " + Thread.currentThread().getName());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //构造方法的打印: main
        //run 方法的打印: Thread-0
        // 结论:构造函数被main线程调用,run方法被名为Thread-0线程调用,run方法是自动调用的方法
        myThread.start();

        //构造方法的打印: main
        //run 方法的打印: main
        // 结论:构造函数被main线程调用,run方法被名为main线程调用
        //myThread.run();
    }
}

再来一个比较复杂的情况,代码如下:

public class CountOperate extends Thread {

    public CountOperate() {
        System.out.println("CountOperate --- begin");
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
        System.out.println("this.getName() = " + this.getName());
        System.out.println("CountOperate --- end");
    }

    @Override
    public void run() {
        System.out.println("run --- begin");
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
        System.out.println("this.getName() = " + this.getName());
        System.out.println("run --- end");
    }
}
public class Run2 {

    public static void main(String[] args) {
        CountOperate c = new CountOperate();  // A线程
        Thread t1 = new Thread(c);  // B线程调用A线程
        t1.setName("A");  // 设置B线程名称,A线程保存不变
        t1.start();
    }
}
CountOperate --- begin
Thread.currentThread().getName() = main
this.getName() = Thread-0
CountOperate --- end
run --- begin
Thread.currentThread().getName() = A
this.getName() = Thread-0
run --- end

isAlive() 方法

isAlive()方法的功能是判断当前的线程是否处于活动状态。

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("run = " + this.isAlive());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        System.out.println("begin == " + myThread.isAlive());
        myThread.start();
        System.out.println("end == " + myThread.isAlive());  // 此值是不确定的。
    }
}

什么是活动状态呢? 活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。

sleep() 方法

方法 sleep() 的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂时执行)。这个“正在执行的线程”是指 this.currentThread() 返回的线程。

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            System.out.println("run threadName = " + this.currentThread().getName() + " begin");
            Thread.sleep(2000);
            System.out.println("run threadName = " + this.currentThread().getName() + " end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        System.out.println("begin = " + System.currentTimeMillis());

        //begin = 1595575576611
        //run threadName = main begin
        //run threadName = main end
        //end = 1595575578621
        // 结论:只有main线程执行
        //myThread.run();


        //begin = 1595575600657
        //end = 1595575600658
        //run threadName = Thread-0 begin
        //run threadName = Thread-0 end
        // 结论:启动两个线程执行,互不干扰
        myThread.start();

        System.out.println("end = " + System.currentTimeMillis());
    }
}

getId() 方法

getId() 方法的作用是取得线程的唯一标识。

public class Test {
    public static void main(String[] args) {
        // main - 1
        System.out.println(Thread.currentThread().getName() + " - " + Thread.currentThread().getId());
    }
}

停止线程

停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。停止线程在Java语言中并不像break语句那么干脆,需要一些技巧性的处理。

停止一个线程可以使用Thread.stop()方法,但最好不用它。虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全的(unsafe),而且是已被弃用作废的(deprecated),在将来的Java版本中,这个方法将不可用或不被支持。

大多数停止一个线程的操作使用Thread.interrupt()方法,尽管方法的名称是“停止,中止”的意思,但这个方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

停止不了的线程

调用interrupt() 方法来停止线程,但interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 500000; i++) {
            System.out.println("i=" + (i + 1));
        }
    }
}
public class Run {
    public static void main(String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(200);
            myThread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

从运行的结果来看,调用interrupt方法并没有停止线程。如何停止线程呢?

判断线程是否是停止状态

在Java的SDK中,Thread类提供了两种方法

  1. this.interrupted():测试当前线程是否已经中断。
  2. this.isInterrupted():测试现场是否已经中断。

那么这两个方法有什么区别呢?先来看看this.interrupted()方法的解释:测试当前线程是否已经中断,当前线程是指运行this.interrupted()方法的线程。

public class Run {

    public static void main(String[] args) {

        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(100);
            myThread.interrupt();
            System.out.println("是否停止1? = " + myThread.interrupted());
            System.out.println("是否停止2? = " + myThread.interrupted());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

从上述的结果来看,方法interrupted()的确判断出当前线程是否是停止状态。但为什么第二个布尔值是false呢?查看一下官方帮助文档中对interrupted方法的解释:

测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

文档已经解释得很详细,interrupted()方法具有清除状态的功能,所以第二次调用interrupted()方法返回的值是false。

介绍完interrupted()方法后再来看一下isInterrupted()方法

public class Run {
    public static void main(String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(300);
            myThread.interrupt();
            System.out.println("是否停止1? = " + myThread.isInterrupted());
            System.out.println("是否停止2? = " + myThread.isInterrupted());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

从结果中可以看到,方法isInterrupted()并未清除状态标志,所以打印了两个true。

最后,再来看一下这两个方法的解释:

  1. this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能。
  2. this.isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志。

能停止的线程——异常法

有了前面学习过的知识点,就可在线程中用for语句来判断一下线程是否是停止状态,如果是停止状态,则后面的代码不再运行即可。

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 500000; i++) {
            if (this.interrupted()) {
                System.out.println("已经是停止状态了,我要退出了!!");
                break;
            }
            System.out.println("i=" + (i + 1));
        }
    }
}
public class Run {
    public static void main(String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(500);
            myThread.interrupt();
        } catch (InterruptedException e) {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

上面的示例虽然停止了线程,但如果for语句下面还有语句,还是会被继续运行的。

public class MyThread2 extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 500000; i++) {
            if (this.interrupted()) {
                System.out.println("已经是停止状态了,我要退出了!!");
                break;
            }
            System.out.println("i=" + (i + 1));
        }
        System.out.println("我被输出,如果此代码是for又继续运行,线程并未停止");
    }
}

该如何解决语句继续运行的问题呢?

public class MyThread3 extends Thread {

    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 500000; i++) {
                if (this.interrupted()) {
                    System.out.println("已经是停止状态了,我要退出了!!");
                    throw new InterruptedException();
                }
                System.out.println("i=" + (i + 1));
            }
            System.out.println("我在for语句外面");
        } catch (InterruptedException e) {
            System.out.println("进入run方法中的catch了");
            e.printStackTrace();
        }
    }
}

在沉睡中停止

如果在sleep()状态下停止线程,会是什么效果呢?

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        try {
            System.out.println("run begin");
            Thread.sleep(20000);
            System.out.println("run end");
        } catch (InterruptedException e) {
            System.out.println("在沉睡中被停止,进入catch! " + this.isInterrupted());
            e.printStackTrace();
        }
    }
}
public class Run {
    public static void main(String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(2000);
            myThread.interrupt();
        } catch (InterruptedException e) {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

从打印的结果来看,如果在sleep状态下停止某一线程,会进入catch语句,并且清除停止状态值,使之变为false。

使用 return 停止线程

将方法interrupt()return结合使用也能实现停止线程的效果。

public class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            if (this.isInterrupted()) {
                System.out.println("停止了");
                return;
            }
            System.out.println("timer=" + System.currentTimeMillis());
        }
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(2000);
        myThread.interrupt();
    }
}

不过还是建议使用“抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。

yield 方法

yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用 CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

public class MyThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            //Thread.yield(); // 去掉注释符,将CPU让给其他资源
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用时: " + (endTime - beginTime) + " 毫秒");
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread thread = new MyThread();
        thread.start();
    }
}

线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。

设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。

在Java中,线程的优先级分为 1 - 10 这10个登记,如果小于 1 或 大于 10 ,则 JDK 抛出异常 throw new IllegalArgumentException()

线程优先级的继承特性

在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A线程是一样的。

public class MyThread2 extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread2 run priority = " + this.getPriority());
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread run priority = " + this.getPriority());
        MyThread2 thread2 = new MyThread2();
        thread2.start();
    }
}
public class Run {
    public static void main(String[] args) {
        System.out.println("main thread begin priority = " + Thread.currentThread().getPriority());
        // Thread.currentThread().setPriority(6);  // 更改main线程优先级
        System.out.println("main thread end priority = " + Thread.currentThread().getPriority());
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

优先级具有规则性

public class MyThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        long addResult = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 50000; j++) {
                addResult = addResult + j;
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("***** myThread use time = " + (endTime - beginTime));
    }
}
public class MyThread2 extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        long addResult = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 50000; j++) {
                addResult = addResult + j;
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("$$$$$ myThread2 use time = " + (endTime - beginTime));
    }
}
public class Run {

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            MyThread myThread = new MyThread();
            myThread.setPriority(10);
            myThread.start();

            MyThread2 myThread2 = new MyThread2();
            myThread2.setPriority(1);
            myThread2.start();
        }
    }
}

从图片中可以发现。高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。另外,不要以为MyThread 线程先被 main 线程所调用就会先执行完。当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关,说明线程的优先级具有一定的“规则性”,也就是CPU尽量将执行资源让给优先级比较高的线程。

线程的优先级还具有“随机性”,也就是优先级较高的线程不一定每一次都先执行完。它们的关系具有不确定性和随机性。

守护线程

在Java线程中有两种线程,一种是用户线程,另一种是守护线程。

守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

用个比较通俗的比喻来解释一下“守护线程”:任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者。

public class MyThread extends Thread {

    private int i = 0;

    @Override
    public void run() {
        try {
            while (true) {
                i++;
                System.out.println("i=" + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(5000);
        System.out.println("我离开 thread 对象也不再打印了,也就是停止了!");
    }
}

对象及变量的并发访问

着重掌握如下技术点:

  • synchronized对象监视器为Object时的使用
  • synchronized对象监视器为Class时的使用
  • 非线程安全是如何出现的
  • 关键字volatile的主要作用
  • 关键字volatile与synchronized的区别及使用情况

synchronized 同步方法

“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更过的。

“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。

方法内的变量为线程安全

“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的。

public class HasSelfPrivateNum {

    public void addI(String username) {
        try {
            int num = 0;
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num = " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("a");
    }
}
public class ThreadB extends Thread {

    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("b");
    }
}
public class Run {

    public static void main(String[] args) {

        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

可见,方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量时私有的特性造成的。

实例变量非线程安全

如果多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题。

public class HasSelfPrivateNum {

    private int num = 0;  // 共同访问对象中的实例变量

    public void addI(String username) {
        try {
            //int num = 0;
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num = " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

本实验是两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现“非线程安全”问题。要解决这个问题,只需要在 public void addI(String username) 方法前加关键字 synchronized 即可。

public class HasSelfPrivateNum {

    private int num = 0;

    //public void addI(String username) {
    synchronized public void addI(String username) {  // 添加 synchronized 关键字
        try {
            //int num = 0;
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num = " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。

多个对象多个锁

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef1);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef2);
        threadB.start();
    }
}

上面示例是两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。

虽然在HasSelfPrivateNum.java中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?

关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以上面的示例中,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。 但如果多个线程访问多个对象,则JVM会创建多个锁。上面示例创建了两个HasSelfPrivateNum.java类的对象,所以就会产生出两把锁。 同步的单词为 synchronized ,异步的单词为 asynchronized

synchronized 方法与锁对象

为了证明前面讲述线程锁的是对象

public class MyObject {

    public void methodA() {
    //synchronized public void methodA() {  // 加入 synchronized 关键字进行同步处理
        try {
            System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private MyObject object;

    public ThreadA(MyObject object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.methodA();
    }
}
public class ThreadB extends Thread {

    private MyObject object;

    public ThreadB(MyObject object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.methodA();
    }
}
public class Run {
    public static void main(String[] args) {
        MyObject object = new MyObject();
        ThreadA a = new ThreadA(object);
        a.setName("a");
        ThreadB b = new ThreadB(object);
        b.setName("b");
        a.start();
        b.start();
    }
}

通过上面的实验得到结论:调用关键字 synchronized 声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。

那么其他的方法在被调用时会是什么效果呢?如何查看到Lock锁对象的效果呢?

public class MyObject {

    //public void methodA() {
    synchronized public void methodA() {
        try {
            System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
            System.out.println("end time = " + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("methodA end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodB() {
    //synchronized public void methodB() {
        try {
            System.out.println("begin methodB threadName = " + Thread.currentThread().getName());
            System.out.println("begin time = " + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("methodB end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {

    private MyObject object;

    public ThreadB(MyObject object) {
        this.object = object;
    }

    @Override
    public void run() {
        //object.methodA();
        object.methodB();
    }
}

通过上面可以得知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronized类型的方法,如果在methodB()方法前加上synchronized关键字,那就是两个线程访问同一个对象的两个同步的方法。

此实验的结论是:

  1. A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
  2. A线程先持有object对象的Lock锁,B线程如果在这时候调用object对象中的synchronized类型的方法则需要等待,也就是同步。

脏读

上面已经实现多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。

虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

public class PublicVar {

    public String username = "A";
    public String password = "AA";

    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;
            System.out.println("setValue method thread name = " + Thread.currentThread().getName()
                    + " username = " + username + " password = " + password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getValue() {
        System.out.println("getValue method thread name = " + Thread.currentThread().getName()
                    + " username = " + username + " password = " + password);
    }
}
public class ThreadA extends Thread {

    private PublicVar publicVar;

    public ThreadA(PublicVar publicVar) {
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        publicVar.setValue("B", "BB");
    }
}
public class Test {

    public static void main(String[] args) throws InterruptedException {

        PublicVar publicVar = new PublicVar();
        ThreadA threadA = new ThreadA(publicVar);
        threadA.start();
        Thread.sleep(200); // 打印结果受此值大小影响
        publicVar.getValue();
    }
}

出现脏读是因为 public void getValue() 方法并不是同步的,所以可以在任意时候进行调用。解决办法当然就是加上synchronized关键字:

//public void getValue() {
synchronized public void getValue() {
    System.out.println("getValue method thread name = " + Thread.currentThread().getName()
                + " username = " + username + " password = " + password);
}

可见,方法被依次执行。通过这个案例不仅要知道脏读是通过synchronized关键字解决的,还要知道如下内容:

当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法。 当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说username和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。 脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。

synchronized 锁重入

关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁。这也证明在一个 synchronized 方法/块的内部调用本类的其他 synchronized 方法/块时,是永远可以得到锁的。

public class Service {

    synchronized public void service1() {
        System.out.println("service1");
        service2();
    }

    synchronized public void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized public void service3() {
        System.out.println("service3");
    }
}
public class MyThread extends Thread {

    @Override
    public void run() {
        Service test1 = new Service();
        test1.service1();
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

可重入锁也支持在父子类继承的环境中。

public class Main {

    public int i = 10;

    synchronized public void operateIMainMethod() {
        try {
            i--;
            System.out.println("main print i = " + i);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Sub extends Main {

    synchronized public void operateISubMethod() {
        try {
            while (i > 0) {
                i--;
                System.out.println("sub print i = " + i);
                Thread.sleep(100);
                this.operateIMainMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MyThread extends Thread {
    @Override
    public void run() {
        Sub sub = new Sub();
        sub.operateISubMethod();
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。

出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

public class Service {

    synchronized public void testMethod() {
        if (Thread.currentThread().getName().equals("a")) {
            System.out.println("ThreadName=" + Thread.currentThread().getName() + " run beginTime=" + System.currentTimeMillis());
            while (true) {
                if (("" + Math.random()).substring(0, 8).equals("0.123456")) {
                    System.out.println("ThreadName=" + Thread.currentThread().getName() + " run exceptionTime=" + System.currentTimeMillis());
                    Integer.parseInt("a");  // 这里肯定会出现异常
                }
            }
        } else {
            System.out.println("Thread B run Time=" + System.currentTimeMillis());
        }
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.testMethod();
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.testMethod();
    }
}
public class Test {
    public static void main(String[] args) {
        try {
            Service test1 = new Service();
            ThreadA a = new ThreadA(test1);
            a.setName("a");
            a.start();

            Thread.sleep(500);

            ThreadB b = new ThreadB(test1);
            b.setName("b");
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程A出现异常并释放锁,线程B进入方法正常打印,结论就是出现异常的锁被自动释放了。

同步不具有继承性

同步不可以继承

public class Main {

    synchronized public void serviceMethod() {
        try {
            System.out.println("int main 下一步 sleep begin threadName = " + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("int main 下一步 sleep end threadName = " + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Sub extends Main {

    @Override
    //public void serviceMethod() {
    synchronized public void serviceMethod() {
        try {
            System.out.println("int sub 下一步 sleep begin threadName = " + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("int sub 下一步 sleep end threadName = " + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            super.serviceMethod();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Sub sub;

    public ThreadA(Sub sub) {
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }
}
public class ThreadB extends Thread {

    private Sub sub;

    public ThreadB(Sub sub) {
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }
}
public class Test {
    public static void main(String[] args) {
        Sub subRef = new Sub();
        ThreadA a = new ThreadA(subRef);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(subRef);
        b.setName("b");
        b.start();
    }
}

同步不能继承,所以要在子类中添加synchronized关键字

synchronized 同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长时间。在这样的情况下可以使用synchronized同步语句块来解决。

synchronized 方法的弊端

public class Task {

    private String getData1;
    private String getData2;

    public synchronized void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            getData1 = "长时间处理任务1 threadName = " + Thread.currentThread().getName();
            getData2 = "长时间处理任务2 threadName = " + Thread.currentThread().getName();
            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class CommonUtils {
    public static long beginTime1;
    public static long endTime1;
    public static long beginTime2;
    public static long endTime2;
}
public class ThreadA extends Thread {

    private Task task;

    public ThreadA(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}
public class ThreadB extends Thread {

    private Task task;

    public ThreadB(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}
public class Run {

    public static void main(String[] args) {

        Task task = new Task();
        ThreadA a = new ThreadA(task);
        a.start();
        ThreadB b = new ThreadB(task);
        b.start();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long beginTime = CommonUtils.beginTime1;
        if (CommonUtils.beginTime2 < CommonUtils.beginTime1) {
            beginTime = CommonUtils.beginTime2;
        }
        long endTime = CommonUtils.endTime1;
        if (CommonUtils.endTime2 > CommonUtils.endTime1) {
            endTime = CommonUtils.endTime2;
        }
        System.out.println("耗时:" + ((endTime - beginTime) / 1000));
    }
}

在使用 synchronized 关键字来声明方法 public synchronized void doLongTimeTask() 时从运行的时间上来看,弊端很明显,解决这样的问题可以使用 synchronized 同步块。

synchronized 同步代码块的使用

当两个并发线程访问同一个对象 object 中的 synchronized(this) 同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行这个代码块以后才能执行该代码块。

public class ObjectService {

    public void serviceMethod() {
        try {
            synchronized (this) {
                System.out.println("begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end time = " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private ObjectService test1;

    public ThreadA(ObjectService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.serviceMethod();
    }
}
public class ThreadB extends Thread {

    private ObjectService test1;

    public ThreadB(ObjectService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.serviceMethod();
    }
}
public class Run {
    public static void main(String[] args) {
        ObjectService test1 = new ObjectService();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
    }
}

上面虽然使用了 synchronized 同步代码块,但执行的效率还是没有提高,执行的效果还是同步运行的。

如何用 synchronized 同步代码块解决程序执行效率低的问题呢?

用同步代码块解决同步方法的弊端

public class Task {

    private String getData1;
    private String getData2;

    public void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);

            String privateGetData1 = "长时间处理任务1 threadName = " + Thread.currentThread().getName();
            String privateGetData2 = "长时间处理任务2 threadName = " + Thread.currentThread().getName();

            synchronized (this) {
                getData1 = privateGetData1;
                getData2 = privateGetData2;
            }

            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当一个线程访问 object 的一个 synchronized 同步代码块时,另一个线程仍然可以访问该 object 对象中的非 synchronized(this) 同步代码块。

上面,虽然时间缩短,运行效率加快,但同步 synchronized 代码块真的是同步吗?真的持有当前调用对象的锁吗?

答案:是

一半异步,一半同步

不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行。

public class Task {

    public void doLongTimeTask() {
        // 非同步时交叉打印
        for (int i = 0; i < 100; i++) {
            System.out.println("nosynchronized threadName = " + Thread.currentThread().getName() + " i = " + (i + 1));
        }
        System.out.println("");
        // 排队执行
        synchronized (this) {
            for (int i = 0; i < 100; i++) {
                System.out.println("synchronized threadName = " + Thread.currentThread().getName() + " i = " + (i + 1));
            }
        }
    }
}
public class ThreadA extends Thread{

    private Task task;

    public ThreadA(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        task.doLongTimeTask();
    }
}
public class ThreadB extends Thread{

    private Task task;

    public ThreadB(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        task.doLongTimeTask();
    }
}
public class Run {
    public static void main(String[] args) {
        Task task = new Task();
        ThreadA a = new ThreadA(task);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(task);
        b.setName("b");
        b.start();
    }
}

结论:非同步时交叉打印,同步时排队执行。

synchronized 代码块间的同步性

在使用同步 synchronized(this) 代码块时需要注意的是,当一个线程访问 object 的一个 synchronized(this) 同步代码块时,其他线程对同一个 object 中其他 synchronized(this) 同步代码块的访问将被阻塞,这说明该 synchronized 使用的是“对象监视器”

public class ObjectService {

    public void serviceMethodA() {
        try {
            synchronized (this) {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void serviceMethodB() {
        synchronized (this) {
            System.out.println("B begin time = " + System.currentTimeMillis());
            System.out.println("B end time = " + System.currentTimeMillis());
        }
    }
}
public class ThreadA extends Thread {

    private ObjectService test1;

    public ThreadA(ObjectService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.serviceMethodA();
    }
}
public class ThreadB extends Thread {

    private ObjectService test1;

    public ThreadB(ObjectService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.serviceMethodB();
    }
}
public class Run {
    public static void main(String[] args) {
        ObjectService test1 = new ObjectService();
        ThreadA a = new ThreadA(test1);
        a.start();
        ThreadB b = new ThreadB(test1);
        b.start();
    }
}

结果:两个同步代码块按顺序执行。

验证同步 synchronized(this) 代码块是锁定当前对象的

和 synchronized 方法一样,synchronized(this) 代码块也是锁定当前对象的。

public class Task {

    // 异步
    //public void otherMethod() {
    // 同步
    synchronized public void otherMethod() {
        System.out.println("---------------- run otherMethod ----------------");
    }

    public void doLongTimeTask() {
        synchronized (this) {
            for (int i = 0; i < 100; i++) {
                System.out.println("synchronized threadName = " + Thread.currentThread().getName() + " i = " + (i + 1));
            }
        }
    }
}
public class ThreadA extends Thread {

    private Task task;

    public ThreadA(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        task.doLongTimeTask();
    }
}
public class ThreadB extends Thread {

    private Task task;

    public ThreadB(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        task.otherMethod();
    }
}
public class Run {
    public static void main(String[] args) {
        Task task = new Task();
        ThreadA a = new ThreadA(task);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(task);
        b.setName("b");
        b.start();
    }
}

将任意对象作为对象监视器

多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步,阻塞的。

这说明 synchronized 同步方法或 synchronized(this) 同步代码块分别有两种作用。

  1. synchronized 同步方法
    • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    • 同一时间只有一个线程可以执行 synchronized 同步方法中的代码。
  2. synchronized(this) 同步代码块
    • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
    • 同一时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码。

其实Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数。

根据前面对 synchronized(this) 同步代码块的作用总结可知, synchronized(非this对象)格式的作用只有一种:synchronized(非this对象)

  1. 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码
  2. 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码
public class Service {

    private String usernameParam;
    private String passwordParam;
    private String anyString = new String();

    public void setUsernamePassword(String username, String password) {
        try {
            synchronized (anyString) {
                System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.setUsernamePassword("a", "aa");
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.setUsernamePassword("b", "bb");
    }
}
public class Run {

    public static void main(String[] args) {
        Service test1 = new Service();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
    }
}

锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则大大提高运行效率。

更改代码如下:

public class Service {

    private String usernameParam;
    private String passwordParam;

    public void setUsernamePassword(String username, String password) {
        try {
            String anyString = new String(); // 方法里面有一个锁
            synchronized (anyString) {
                System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可见,使用 synchronized(非this对象x)同步代码块格式进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。

下面验证一下使用“synchronized(非this对象x)同步代码块”格式时,持有不同的对象监视器是异步的效果。

public class Service {

    private String anyString = new String();

    public void a() {
        try {
            // synchronized(非this)
            synchronized (anyString) {
                System.out.println("a begin");
                Thread.sleep(3000);
                System.out.println("a end");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // synchronized(this)
    synchronized public void b() {
        System.out.println("b begin");
        System.out.println("b end");
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.a();
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.b();
    }
}
public class Run {
    public static void main(String[] args) {
        Service test1 = new Service();
        ThreadA a = new ThreadA(test1);
        a.start();
        ThreadB b = new ThreadB(test1);
        b.start();
    }
}

由于对象监视器不同,所以运行结果就是异步的。

同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样极易出现“脏读”问题。

public class MyList {

    private List list = new ArrayList<>();

    synchronized public void add(String username) {
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 执行了 add 方法");
        list.add(username);
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 退出了 add 方法");
    }

    synchronized public int getSize() {
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 执行了 getSize 方法");
        int sizeValue = list.size();
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 退出了 getSize 方法");
        return sizeValue;
    }
}
public class ThreadA extends Thread {

    private MyList list;

    public ThreadA(MyList list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add("threadA" + (i + 1));
        }
    }
}
public class ThreadB extends Thread {

    private MyList list;

    public ThreadB(MyList list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add("threadB" + (i + 1));
        }
    }
}
public class Test {

    public static void main(String[] args) {

        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(list);
        b.setName("b");
        b.start();
    }
}

同步块中的代码是同步打印的,当前线程的“执行”和“退出”是成对出现的。但线程A和线程B的执行却是异步的,这就有可能出现脏读的环境。由于线程执行方法的顺序不确定,所有当A和B两个线程执行带有分支判断的方法时,就会出现逻辑上的错误,有可能出现脏读。

细化验证3个结论

synchronized(非this对象x)格式的写法是将x对象本身作为“对象监视器”,这样就可以得出以下3个结论:

  1. 当多个线程同时执行 synchronized(x){}同步块代码时呈同步效果。
  2. 当其他线程执行x对象中synchronized同步方法时呈同步效果。
  3. 当其他线程执行x对象方法里面的synchronized(this)代码块时也呈同步效果。

但需要注意:如果其他线程调用不加synchronized关键字的方法时,还是异步调用。

静态同步synchronized方法与synchronized(class)代码块

关键字synchronized还可以应用在static静态方法上,如果这样写,那么对当前的*.java文件对应的Class类进行持锁。

public class Service {

    synchronized public static void printA() {
        try {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printA");
            Thread.sleep(3000);
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public static void printB() {
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
    }
}
public class ThreadA extends Thread {

    @Override
    public void run() {
        Service.printA();
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        Service.printB();
    }
}
public class Run {

    public static void main(String[] args) {

        ThreadA a = new ThreadA();
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB();
        b.setName("b");
        b.start();
    }
}

从运行结果来看,是同步效果,和将synchronized关键字加到非static方法上使用的效果是一样的。

其实还是有本质上的不同的,synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁

public class Service {

    // Class锁
    synchronized public static void printA() {
        try {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printA");
            Thread.sleep(3000);
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // Class锁
    synchronized public static void printB() {
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
    }

    // 对象锁
    synchronized public void printC() {
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printC");
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printC");
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.printA();
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.printB();
    }
}
public class ThreadC extends Thread {

    private Service test1;

    public ThreadC(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.printC();
    }
}
public class Run {

    public static void main(String[] args) {

        Service test1 = new Service();

        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();

        ThreadC c = new ThreadC(test1);
        c.setName("c");
        c.start();
    }
}

异步的原因是持有不同的锁,一个是对象锁,另一个是Class锁,而Class锁可以对类的所有对象实例起作用。

public class Service {

    // Class锁
    synchronized public static void printA() {
        try {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printA");
            Thread.sleep(3000);
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // Class锁
    synchronized public static void printB() {
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.printA();
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.printB();
    }
}
public class Run {

    public static void main(String[] args) {

        Service serviceA = new Service();
        Service serviceB = new Service();

        ThreadA a = new ThreadA(serviceA);
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB(serviceB);
        b.setName("b");
        b.start();

    }
}

同步 synchronized(class) 代码块的作用其实和 synchronized static 方法的作用一样。

public class Service {

    // Class锁
    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printA");
                Thread.sleep(3000);
                System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printA");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // Class锁
    public static void printB() {
        synchronized (Service.class) {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
        }
    }
}

数据类型String的常量池特性

在 JVM 中具有String常量池缓存的功能,下面结果是true。

public class Test {
    public static void main(String[] args) {
        String a = "a";
        String b = "a";
        System.out.println(a == b);
    }
}

将 synchronized(string) 同步块与String联合使用时,要注意常量池带来的一些例外。

public class Service {

    public static void print(String stringParam) {
        try {
            synchronized (stringParam) {
                while (true) {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.print("AA");
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.print("AA");
    }
}
public class Run {
    public static void main(String[] args) {
        Service test1 = new Service();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
    }
}

结果一直输出“a”,出现这样的情况就是因为String的两个值都是AA,两个线程持有相同的锁,所以造成线程b不能执行。这就是String常量池所带来的问题。因此在大多数的情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如 new Object()实例化一个Object对象,但它并不放入缓存中。

同步 synchronized 方法无限等待与解决

同步方法容易造成死循环

public class Service {

    synchronized public void methodA() {
        System.out.println("methodA begin");
        boolean isContinueRun = true;
        while (isContinueRun) {

        }
        System.out.println("methodA end");
    }

    synchronized public void methodB() {
        System.out.println("methodB begin");
        System.out.println("methodB end");
    }
}
public class ThreadA extends Thread {

    private Service test1;

    public ThreadA(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.methodA();
    }
}
public class ThreadB extends Thread {

    private Service test1;

    public ThreadB(Service test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.methodB();
    }
}
public class Run {
    public static void main(String[] args) {
        Service test1 = new Service();
        ThreadA a = new ThreadA(test1);
        a.start();
        ThreadB b = new ThreadB(test1);
        b.start();
    }
}

线程B永远得不到运行的机会,死锁了。

这时就可以使用同步块来解决这样的问题,代码如下:

public class Service2 {

    Object o1 = new Object();

    public void methodA() {
        synchronized (o1) {
            System.out.println("methodA begin");
            boolean isContinueRun = true;
            while (isContinueRun) {

            }
            System.out.println("methodA end");
        }
    }

    Object o2 = new Object();

    public void methodB() {
        synchronized (o2) {
            System.out.println("methodB begin");
            System.out.println("methodB end");
        }
    }
}

不再出现同步等待的情况。

多线程的死锁

Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”。

死锁是程序设计的Bug,在设计程序时就要避免双方互相持有对方的锁的情况。

锁对象的改变

在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得锁对象,这些线程之间就是异步的。

public class MyService {

    private String lock = "123";

    public void testMethod() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
                lock = "456";
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.testMethod();
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.testMethod();
    }
}
public class Run {

    public static void main(String[] args) throws InterruptedException {
        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        //Thread.sleep(50); // 如果不加50毫秒,结果可能是同步或异步;如果加50毫秒,结果一定是异步的
        b.start();
    }
}

还需要提示一下,只要对象不变,即使对象的属性被改变,运行的结果还是同步的。

volatile 关键字

关键字 volatile 的主要作用是使变量在多个线程间可见。

关键字 volatile 与死循环

如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有什么太大的区别。如果一旦出现“多继承”的情况,则用实现Runnable接口的方式来处理多线程的问题就很有必要的。

通过使用volatile关键字,强制的从公共内存中读取变量的值,内存结构如下:

使用volatile关键字增加了实例变量在多个线程之间的可见性。但volatile关键字最致命的缺点是不支持原子性。

下面将关键字synchronized和volatile进行一下比较

  1. 关键字volatile是现场同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法、代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
  2. 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  3. volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
  4. 再次重申一下,关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

volatile 非原子的特征

关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。

public class MyThread extends Thread {

    volatile public static int count;

    //private static void addCount() {
    // 注意一定要加上static关键字
    // 这样 synchronized 与 static 锁的内容就是MyThread.class类
    // 也就达到同步的效果了
    synchronized private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println("count = " + count);
    }

    @Override
    public void run() {
        addCount();
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread[] myThreadArray = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            myThreadArray[i] = new MyThread();
        }
        for (int i = 0; i < 100; i++) {
            myThreadArray[i].start();
        }
    }
}

如果在方法 private static void addCount() 前加入 synchronized 同步关键字,也就没有必要再使用 volatile 关键字来声明 count 变量了。

关键字 volatile 主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用。

关键字 volatile 提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如 i++,也就是 i = i + 1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。

变量在内存中工作的过程如下:

  1. read和load阶段:从主存复制变量到当前线程工作内存;
  2. use和assign阶段:执行代码,改变共享变量值;
  3. store和write阶段:用工作内存数据刷新主存对应变量的值。

在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的编号,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。

对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

使用原子类进行i++操作

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。

原子操作是不能分割的整体,没有其他线程能够终端或者检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。

public class AddCountThread extends Thread {

    private AtomicInteger count = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(count.incrementAndGet());
        }
    }
}
public class Run {

    public static void main(String[] args) {

        AddCountThread countThread = new AddCountThread();

        new Thread(countThread).start();
        new Thread(countThread).start();
        new Thread(countThread).start();
        new Thread(countThread).start();
        new Thread(countThread).start();

        // 结果累加到 50000
    }
}

原子类也并不完全安全

原子类在具有有逻辑性的情况下输出结果也具有随机性。

public class MyService {

    public static AtomicLong aiRef = new AtomicLong();

    //public void addNum() {
    synchronized public void addNum() {
        System.out.println(Thread.currentThread().getName() + " 加了100之后的值是:" + aiRef.addAndGet(100));
        aiRef.addAndGet(1);
    }
}
public class MyThread extends Thread {

    private MyService myService;

    public MyThread(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.addNum();
    }
}
public class Run {

    public static void main(String[] args) {

        try {
            MyService test1 = new MyService();
            MyThread[] array = new MyThread[5];
            for (int i = 0; i < array.length; i++) {
                array[i] = new MyThread(test1);
            }
            for (int i = 0; i < array.length; i++) {
                array[i].start();
            }
            Thread.sleep(1000);
            System.out.println(test1.aiRef.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

从运行结果可以看到,是每次加100再加1,这就是我们想要得到的过程,结果是505的同时还保证在过程中累加的顺序也是正确的。

线程间通信

线程间的通信就是称为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。

等待/通知机制

不使用等待/通知机制实现线程间通信

使用 sleep() 结合while(true)死循环来实现多个线程间通信。

public class MyList {

    private volatile List list = new ArrayList<>();

    public void add(){
        list.add("Ray");
    }

    public int size() {
        return list.size();
    }
}
public class ThreadA extends Thread {

    private MyList list;

    public ThreadA(MyList list) {
        this.list = list;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                list.add();
                System.out.println("添加了 " + (i + 1) + " 个元素");
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {

    private MyList list;

    public ThreadB(MyList list) {
        this.list = list;
    }

    @Override
    public void run() {
        try {
            while (true) {
                if (list.size() == 5) {
                    System.out.println(" == 5了,线程b要退出了");
                    throw new InterruptedException();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyList test1 = new MyList();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
    }
}

虽然两个线程间实现了通信,但有一个弊端就是,线程ThreadB不停地通过while语句轮询机制来检测某一个条件,这样会浪费CPU资源。

什么是等待/通知机制

比如在就餐时就会出现:

厨师和服务器之间的交互要在“菜品传递台”上,在这期间会有几个问题:

  1. 厨师做完一道菜的时间不确定,所以厨师将菜品放到“菜品传递台”上的时间也不确定。
  2. 服务员取到菜的世界取决于厨师,所以服务员就有“等待”(wait)的状态。
  3. 服务员如何能取到菜呢?这又得取决于厨师,厨师将菜放在“菜品传递台”上,其实就相当于是一种“通知”(notify),这时服务员才可以拿到菜并交给就餐者。
  4. 在这个过程中出现了“等待/通知”机制。

等待/通知机制的实现

方法wait()的作用是使当前的执行代码的线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,知道接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程重新竞争获取锁。如果调用wait()时没有持有适当的锁,则抛出java.lang.IllegalMonitorStateException异常。

方法notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适合的锁,也会抛出java.lang.IllegalMonitorStateException异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。

1) wait

public class Test {
    public static void main(String[] args) {
        try {
            String newString = new String();
            newString.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 结果:java.lang.IllegalMonitorStateException

出现异常的原因是没有“对象监视器”,也就是没有同步加锁。

2) wait + 同步加锁

public class Test2 {
    public static void main(String[] args) {
        try {
            String lock = new String();
            System.out.println("syn上面");
            synchronized (lock) {
                System.out.println("syn第一句");
                lock.wait();
                System.out.println("wait下面");
            }
            System.out.println("syn下面");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 结果:
// syn上面
// syn第一句

但线程不能永远等待下去,那样程序就停滞不前,不继续往下执行了。如果使呈等待wait状态的线程继续运行呢?答案就是使用notify()方法。

3) notify

public class MyThread1 extends Thread {

    private Object lock;

    public MyThread1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                System.out.println("开始 wait time: " + System.currentTimeMillis());
                lock.wait();
                System.out.println("结束 wait time: " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MyThread2 extends Thread {

    private Object lock;

    public MyThread2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("开始 notify time: " + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束 notify time: " + System.currentTimeMillis());
        }
    }
}
public class Test3 {
    public static void main(String[] args) {
        try {
            Object lock = new Object();
            MyThread1 t1 = new MyThread1(lock);
            t1.start();
            Thread.sleep(3000);
            MyThread2 t2 = new MyThread2(lock);
            t2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

从结果来看,3秒后线程被notify通知唤醒。

4) 运用前面size=5的例子

public class MyList {

    private static List list = new ArrayList<>();

    public static void add() {
        list.add("Ray");
    }

    public static int size() {
        return list.size();
    }
}
public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("wait begin " + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("wait end " + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {

    private Object lock;

    public ThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    MyList.add();
                    if (MyList.size() == 5) {
                        lock.notify();
                        System.out.println("已经发出 notify 通知!!");
                    }
                    System.out.println("添加了 " + (i+1) + " 个元素!!");
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {

        try {
            Object lock = new Object();
            ThreadA a = new ThreadA(lock);
            a.start();
            Thread.sleep(500);
            ThreadB b = new ThreadB(lock);
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

日志信息 wait end 在最后输出,这也说明 notify() 方法执行后并不立即释放锁。

关键字 synchronized 可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了wait()和notify()方法,它们必须用在被synchronized同步的Object的临界区内。通过调用wait()方法可以使处于临界区的线程进入等待状态, 同时释放被同步对象的锁。而notify操作可以唤醒一个因调用了wait操作而处于阻塞状态中的线程,使其进入就绪状态。被重新唤醒的线程会试图重新获得临界区的控制权,也就是锁,并继续执行临界区内wait之后的代码。如果发出notify操作时没有处于阻塞状态中的线程,那么该命令会被忽略。

  1. 新创建一个新的线程对象后,再调用它的start()方法,系统会为此线程分配CPU资源,使其处于Runnable(可运行)状态,这是一个准备运行的阶段。如果线程抢占到CPU资源,此线程就处于Running(运行)状态。
  2. Runnable状态和Running状态可相互切换,因为有可能线程运行一段时间后,有其他高优先级的线程抢占了CPU资源,这时此线程就从Running状态变成Runnable状态。
  3. Blocked是阻塞的意思,例如遇到了一个IO操作,此时CPU处于空闲状态,可能会转而把CPU时间片分配给其他线程,这时也可以称为“暂停”状态。Blocked状态结束后,进入Runnable状态,等待系统重新分配资源。
  4. run()方法运行结束后进入销毁阶段,整个线程执行完毕。

方法 wait() 锁释放

当方法 wait() 被执行后,锁被自动释放。

public class Service {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin wait");
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " end wait");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        Service test1 = new Service();
        test1.testMethod(lock);
    }
}
public class ThreadB extends Thread {

    private Object lock;

    public ThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        Service test1 = new Service();
        test1.testMethod(lock);
    }
}
public class Test {
    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA a = new ThreadA(lock);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(lock);
        b.setName("b");
        b.start();

        // 结果:
        //b begin wait
        //a begin wait
    }
}

方法 notify() 锁不释放

当方法 notify() 被执行后,锁不自动释放。

public class Service {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin wait ThreadName = " + Thread.currentThread().getName());
                lock.wait();
                System.out.println("end wait ThreadName = " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void synNotifyMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin notify ThreadName = " + Thread.currentThread().getName());
                lock.notify();
                Thread.sleep(5000);
                System.out.println("end notify ThreadName = " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        Service test1 = new Service();
        test1.testMethod(lock);
    }
}
public class ThreadB extends Thread {

    private Object lock;

    public ThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        Service test1 = new Service();
        test1.synNotifyMethod(lock);
    }
}
public class Test {
    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA a = new ThreadA(lock);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(lock);
        b.setName("b");
        b.start();

        // 结果:
        //begin wait ThreadName = a
        //begin notify ThreadName = b
        //end notify ThreadName = b
        //end wait ThreadName = a
    }
}

必须执行完notify()方法所在的同步synchronized代码块后才释放锁。

当 interrupt 方法遇到 wait 方法

当线程呈wait()状态时,调用线程对象的interrupt()方法会出现java.lang.InterruptedException异常。

总结:

  1. 执行完同步代码块就会释放对象的锁。
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
  3. 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,而此线程对象会进入线程等待池中,等待被唤醒。

方法 wait(long) 的使用

带一个参数的wait(long) 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。

public class MyRunnable {

    static private Object lock = new Object();
    static private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (lock) {
                    System.out.println("wait begin timer = " + System.currentTimeMillis());
                    lock.wait(5000);
                    System.out.println("wait end timer = " + System.currentTimeMillis());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public static void main(String[] args) {
        Thread t = new Thread(runnable);
        t.start();
    }
}

生产者/消费者模式实现

一生产与一消费:操作值

public class ValueObject {
    public static String value = "";
}
public class C {

    private String lock;

    public C(String lock) {
        this.lock = lock;
    }

    public void getValue() {
        try {
            synchronized (lock) {
                if (ValueObject.value.equals("")) {
                    lock.wait();
                }
                System.out.println("get 的值是: " + ValueObject.value);
                ValueObject.value = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class P {

    private String lock;

    public P(String lock) {
        this.lock = lock;
    }

    public void setValue() {
        try {
            synchronized (lock) {
                if (!ValueObject.value.equals("")) {
                    lock.wait();
                }
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                System.out.println("set 的值是: " + value);
                ValueObject.value = value;
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadC extends Thread {

    private C c;

    public ThreadC(C c) {
        this.c = c;
    }

    @Override
    public void run() {
        while (true) {
            c.getValue();
        }
    }
}
public class ThreadP extends Thread {

    private P p;

    public ThreadP(P p) {
        this.p = p;
    }

    @Override
    public void run() {
        while (true) {
            p.setValue();
        }
    }
}
public class Run {

    public static void main(String[] args) {

         String lock = new String("");
         P p = new P(lock);
         C c = new C(lock);
         ThreadP pThread = new ThreadP(p);
         ThreadC cThread = new ThreadC(c);
         pThread.start();
         cThread.start();
    }
}

本示例是一个生产者和一个消费者进行数据的交互,打印的日志get和set是交替运行的。

如果在此实验的基础上,设计出多个生产者和多个消费者,那么在运行的过程中极有可能出现“假死”的情况,也就是所有的线程都呈WAITING等待状态。

多生产与多消费:操作值-假死

“假死”的现象其实就是线程进入WAITING等待状态。如果全部线程都进入WAITING状态,则程序就不再执行任何业务功能了,整个项目呈停止状态。

public class ValueObject {
    public static String value = "";
}
public class C {

    private String lock;

    public C(String lock) {
        this.lock = lock;
    }

    public void getValue() {
        try {
            synchronized (lock) {
                while (ValueObject.value.equals("")) {
                    System.out.println("消费者 " + Thread.currentThread().getName() +" WAITING 了");
                    lock.wait();
                }
                System.out.println("消费者 " + Thread.currentThread().getName() +" RUNNABLE 了");
                ValueObject.value = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class P {

    private String lock;

    public P(String lock) {
        this.lock = lock;
    }

    public void setValue() {
        try {
            synchronized (lock) {
                while (!ValueObject.value.equals("")) {
                    System.out.println("生产者 " + Thread.currentThread().getName() +" WAITING 了");
                    lock.wait();
                }
                System.out.println("生产者 " + Thread.currentThread().getName() +" RUNNABLE 了");
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                ValueObject.value = value;
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadC extends Thread {

    private C c;

    public ThreadC(C c) {
        this.c = c;
    }

    @Override
    public void run() {
        while (true) {
            c.getValue();
        }
    }
}
public class ThreadP extends Thread {

    private P p;

    public ThreadP(P p) {
        this.p = p;
    }

    @Override
    public void run() {
        while (true) {
            p.setValue();
        }
    }
}
public class Run {
    public static void main(String[] args) {
        String lock = new String("");
        P p = new P(lock);
        C c = new C(lock);
        ThreadP[] pThreads = new ThreadP[2];
        ThreadC[] cThreads = new ThreadC[2];
        for (int i = 0; i < 2; i++) {
            pThreads[i] = new ThreadP(p);
            pThreads[i].setName("生产者" + (i + 1));
            pThreads[i].start();
            cThreads[i] = new ThreadC(c);
            cThreads[i].setName("消费者" + (i + 1));
            cThreads[i].start();
        }
    }
}

在代码中确实已经通过 wait/notify 进行通信了,但不保证 notify 唤醒的是异类,也许是同类,比如 “生产者” 唤醒 “生产者”,或 “消费者” 唤醒 “消费者” 这样的情况。如果按这种情况运行的比率积少成多,就会导致所有的线程都不能继续运行下去,大家都在等待,都呈 WAITING 状态,程序最后也就呈 “假死” 状态,不能继续运行下去了。

多生产与多消费:操作值

解决“假死”的情况很简单,将 notify() 改成 notifyAll() 方法即可,它的原理就是不光通知同类线程,也包括异类。这样就不至于出现假死的状态了,程序会一直运行下去。

public class C {

    private String lock;

    public C(String lock) {
        this.lock = lock;
    }

    public void getValue() {
        try {
            synchronized (lock) {
                while (ValueObject.value.equals("")) {
                    System.out.println("消费者 " + Thread.currentThread().getName() +" WAITING 了");
                    lock.wait();
                }
                System.out.println("消费者 " + Thread.currentThread().getName() +" RUNNABLE 了");
                ValueObject.value = "";
                lock.notifyAll();  // 注意
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class P {

    private String lock;

    public P(String lock) {
        this.lock = lock;
    }

    public void setValue() {
        try {
            synchronized (lock) {
                while (!ValueObject.value.equals("")) {
                    System.out.println("生产者 " + Thread.currentThread().getName() +" WAITING 了");
                    lock.wait();
                }
                System.out.println("生产者 " + Thread.currentThread().getName() +" RUNNABLE 了");
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                ValueObject.value = value;
                lock.notifyAll();  // 注意
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

一生产与一消费:操作栈

本示例是使生产者向堆栈List对象中放入数据,使消费者从List堆栈中取出数据。List最大容量是1,实验环境只有一个生产者与一个消费者。

public class MyStack {

    private List list = new ArrayList<>();

    synchronized public void push() {
        try {
            if (list.size() == 1) {
                this.wait();
            }
            list.add("anyString=" + Math.random());
            this.notify();
            System.out.println("push: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String pop() {
        String returnValue = "";
        try {
            if (list.size() == 0) {
                System.out.println("pop操作中的: " + Thread.currentThread().getName() + " 线程呈wait状态");
                this.wait();
            }
            returnValue = "" + list.get(0);
            list.remove(0);
            this.notify();
            System.out.println("pop: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return returnValue;
    }
}
public class C {

    private MyStack myStack;

    public C(MyStack myStack) {
        this.myStack = myStack;
    }

    public void popService() {
        System.out.println("pop = " + myStack.pop());
    }
}
public class P {

    private MyStack myStack;

    public P(MyStack myStack) {
        this.myStack = myStack;
    }

    public void pushService() {
        myStack.push();
    }
}
public class ThreadC extends Thread {

    private C c;

    public ThreadC(C c) {
        this.c = c;
    }

    @Override
    public void run() {
        while (true) {
            c.popService();
        }
    }
}
public class ThreadP extends Thread {

    private P p;

    public ThreadP(P p) {
        this.p = p;
    }

    @Override
    public void run() {
        while (true) {
            p.pushService();
        }
    }
}
public class Run {
    public static void main(String[] args) {
         MyStack myStack = new MyStack();
         P p = new P(myStack);
         C c = new C(myStack);
         ThreadP pThread = new ThreadP(p);
         ThreadC cThread = new ThreadC(c);
         pThread.start();
         cThread.start();
    }
}

程序运行结果是size()不会大于1。

通过使用生产者/消费者模式,容器size()的值不会大于1,这也是本示例想要实现的效果,值在0和1之间进行交替,也就是生产和消费这两个过程在交替执行。

一生产与多消费:操作栈:解决 wait 条件改变与假死

本示例是使用一个生产者向堆栈List对象中放入数据,而多个消费者从List堆栈中取出数据。List最大容量还是1。

public class Run2 {
    public static void main(String[] args) {
         MyStack myStack = new MyStack();
         P p = new P(myStack);
         C c1 = new C(myStack);
         C c2 = new C(myStack);
         C c3 = new C(myStack);
         C c4 = new C(myStack);
         C c5 = new C(myStack);
         ThreadP pThread = new ThreadP(p);
         ThreadC cThread1 = new ThreadC(c1);
         ThreadC cThread2 = new ThreadC(c2);
         ThreadC cThread3 = new ThreadC(c3);
         ThreadC cThread4 = new ThreadC(c4);
         ThreadC cThread5 = new ThreadC(c5);
         pThread.start();
         cThread1.start();
         cThread2.start();
         cThread3.start();
         cThread4.start();
         cThread5.start();
    }
}

此问题的出现就是因为在MyStack类中使用了if语句作为条件判断。

因为条件发生改变时并没有得到及时的相应,所以多个呈wait状态的线程被唤醒,继而执行remove操作,才出现异常,将 if 改为 while 语句即可

public class MyStack {

    private List list = new ArrayList<>();

    synchronized public void push() {
        try {
            //if (list.size() == 1) {
            // 因为条件发生改变时并没有得到及时的相应,所以多个呈wait状态的线程被唤醒,继而执行remove操作,才出现异常,将 if 改为 while 语句即可
            // 到此,没有出现执行异常,却出现了“假死”情况
            while (list.size() == 1) {
                this.wait();
            }
            list.add("anyString=" + Math.random());
            this.notify();
            System.out.println("push: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String pop() {
        String returnValue = "";
        try {
            //if (list.size() == 0) {
            while (list.size() == 0) {
                System.out.println("pop操作中的: " + Thread.currentThread().getName() + " 线程呈wait状态");
                this.wait();
            }
            returnValue = "" + list.get(0);
            list.remove(0);
            this.notify();
            System.out.println("pop: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return returnValue;
    }
}

运行项目没有出现执行异常,却出现了“假死”情况,解决的办法当然还是用notifyAll()方法。

public class MyStack {

    private List list = new ArrayList<>();

    synchronized public void push() {
        try {
            while (list.size() == 1) {
                this.wait();
            }
            list.add("anyString=" + Math.random());
            //this.notify();
            // 解决假死情况
            this.notifyAll();
            System.out.println("push: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String pop() {
        String returnValue = "";
        try {
            while (list.size() == 0) {
                System.out.println("pop操作中的: " + Thread.currentThread().getName() + " 线程呈wait状态");
                this.wait();
            }
            returnValue = "" + list.get(0);
            list.remove(0);
            //this.notify();
            // 解决假死情况
            this.notifyAll();
            System.out.println("pop: " + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return returnValue;
    }
}

项目终于可以正常地运行下去了。

多生产与一消费:操作栈

本示例是使用多生产者向堆栈List对象中放入数据,使用消费者从List堆栈中取出数据。List最大容量还是1,实验环境是多个生产者与一个消费者。

public class Run3 {

    public static void main(String[] args) {
         MyStack myStack = new MyStack();
         P p1 = new P(myStack);
         P p2 = new P(myStack);
         P p3 = new P(myStack);
         P p4 = new P(myStack);
         P p5 = new P(myStack);
         C c = new C(myStack);
         ThreadP pThread1 = new ThreadP(p1);
         ThreadP pThread2 = new ThreadP(p2);
         ThreadP pThread3 = new ThreadP(p3);
         ThreadP pThread4 = new ThreadP(p4);
         ThreadP pThread5 = new ThreadP(p5);
         ThreadC cThread = new ThreadC(c);
         pThread1.start();
         pThread2.start();
         pThread3.start();
         pThread4.start();
         pThread5.start();
         cThread.start();
    }
}

多生产与多消费:操作栈

本示例是使用生产者向堆栈List对象中放入数据,使用消费者从堆栈List中取出数据。List最大容量是1,实验环境是多个生产者与多个消费者。

public class Run4 {
    public static void main(String[] args) {
         MyStack myStack = new MyStack();
        
         P p1 = new P(myStack);
         P p2 = new P(myStack);
         P p3 = new P(myStack);
         P p4 = new P(myStack);
         P p5 = new P(myStack);
        
         C c1 = new C(myStack);
         C c2 = new C(myStack);
         C c3 = new C(myStack);
         C c4 = new C(myStack);
         C c5 = new C(myStack);
        
         ThreadP pThread1 = new ThreadP(p1);
         ThreadP pThread2 = new ThreadP(p2);
         ThreadP pThread3 = new ThreadP(p3);
         ThreadP pThread4 = new ThreadP(p4);
         ThreadP pThread5 = new ThreadP(p5);
        
         ThreadC cThread1 = new ThreadC(c1);
         ThreadC cThread2 = new ThreadC(c2);
         ThreadC cThread3 = new ThreadC(c3);
         ThreadC cThread4 = new ThreadC(c4);
         ThreadC cThread5 = new ThreadC(c5);
        
         cThread1.start();
         cThread2.start();
         cThread3.start();
         cThread4.start();
         cThread5.start();
        
         pThread1.start();
         pThread2.start();
         pThread3.start();
         pThread4.start();
         pThread5.start();
    }
}

实战:等待/通知之交叉备份

创建20个线程,其中10个线程是将数据备份到A数据库中,另外10个线程将数据备份到B数据库中,并且备份A数据库和B数据库是交叉进行的。

首先创建出20个线程,通过一些手段将这20个线程的运行效果变成有序的。

public class DBTools {

    volatile private boolean prevIsA = false;

    synchronized public void backupA() {
        try {
            while (prevIsA) {
                wait();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println("★★★★★");
            }
            prevIsA = true;
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public void backupB() {
        try {
            while (!prevIsA) {
                wait();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println("☆☆☆☆☆");
            }
            prevIsA = false;
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class BackupA extends Thread {

    private DBTools dbTools;

    public BackupA(DBTools dbTools) {
        this.dbTools = dbTools;
    }

    @Override
    public void run() {
        dbTools.backupA();
    }
}
public class BackupB extends Thread {

    private DBTools dbTools;

    public BackupB(DBTools dbTools) {
        this.dbTools = dbTools;
    }

    @Override
    public void run() {
        dbTools.backupB();
    }
}
public class Run {

    public static void main(String[] args) {

        DBTools dbTools = new DBTools();
        for (int i = 0; i < 20; i++) {
            BackupB output = new BackupB(dbTools);
            output.start();
            BackupA input = new BackupA(dbTools);
            input.start();
        }
    }
}

交替打印的原理就是使用如下代码作为标记:

volatile private boolean prevIsA = false;

实现了A和B线程交替备份的效果。

方法 join 的使用

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法了。

方法 join() 的作用是等待线程对象销毁。

使用 join

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            int secondValue = (int)(Math.random() * 10000);
            System.out.println(secondValue);
            Thread.sleep(secondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.start();
            thread.join();
            System.out.println("thread执行完毕后再执行这句话");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进入无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。

方法join具有使线程排队运行的作用,有些类似同步的效果。

join与synchronized的区别是:join在内部使用wait方法等待,而synchronized关键字使用的是“对象监视器”原理作为同步

方法 join 与异常

在join过程中,如果当前线程对象被中断,则当前线程出现异常。

public class ThreadA extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String newString = new String();
            Math.random();
        }
        System.out.println("线程A在run end处打印了");
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        try {
            ThreadA a = new ThreadA();
            a.start();
            a.join();
            System.out.println("线程B在run end处打印了");
        } catch (InterruptedException e) {
            System.out.println("线程B在catch处打印了");
            e.printStackTrace();
        }
    }
}
public class ThreadC extends Thread {

    private ThreadB threadB;

    public ThreadC(ThreadB threadB) {
        this.threadB = threadB;
    }

    @Override
    public void run() {
        threadB.interrupt();
    }
}
public class Run {

    public static void main(String[] args) {

        try {
            ThreadB b = new ThreadB();
            b.start();
            Thread.sleep(500);
            ThreadC c = new ThreadC(b);
            c.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

说明:方法join与方法interrupt如果彼此遇到,则会出现异常。但进程按钮没有停止,原因是线程A还在运行,线程A并未出现异常,是正常执行的状态。

方法 join(long) 的使用

方法 join(long) 中的参数是设定等待的时间。

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            System.out.println("begin timer = " + System.currentTimeMillis());
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {

    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.start();
            // 结果等待了2秒输出
            thread.join(2000);
            //Thread.sleep(2000);
            System.out.println("end timer = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

join(2000)sleep(2000) 区别主要还是来自于这两个方法对同步的处理上。

方法 join(long) 与 sleep(long) 的区别

方法 join(long) 的功能在内部是使用 wait(long) 方法来实现的,所以 join(long) 方法具有释放锁的特点。

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

从源代码中可以了解到,当执行wait(long) 方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。

Thread.sleep(long) 方法却不释放锁。

类 ThreadLocal 的使用

变量值的共享可以使用 public static 变量的形式,所有的线程都使用同一个 public static 变量。如果想实现每一个线程都有自己的共享变量该如何解决呢?

类ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。

方法 get() 与 null

public class Run {
    public static ThreadLocal t1 = new ThreadLocal();
    public static void main(String[] args) {
        if (t1.get() == null) {
            System.out.println("从未放过值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());

        //从未放过值
        //我的值
        //我的值
    }
}

第一次调用t1对象的get()方法时返回的值是null,通过调用set()方法赋值后顺利取出值并打印到控制台上。类ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值,不同线程中的值是可以放入ThreadLocal类中进行保存的。

验证线程变量的隔离性

public class Tools {
    public static ThreadLocal t1 = new ThreadLocal();
}
public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                Tools.t1.set("ThreadA" + (i + 1));
                System.out.println("ThreadA get value = " + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                Tools.t1.set("ThreadA" + (i + 1));
                System.out.println("ThreadB get value = " + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {

        try {
            ThreadA a = new ThreadA();
            ThreadB b = new ThreadB();
            a.start();
            b.start();
            for (int i = 0; i < 100; i++) {
                Tools.t1.set("Main" + (i + 1));
                System.out.println("Main get value = " + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

虽然三个线程都向t1对象中set()数据值,但每个线程还是能取出自己的数据。

解决get()返回null问题

public class ThreadLocalExt extends ThreadLocal {
    @Override
    protected Object initialValue() {
        return "我是默认值,第一次get不再为null";
    }
}
public class Run {
    public static ThreadLocalExt t1 = new ThreadLocalExt();
    public static void main(String[] args) {
        if (t1.get() == null) {
            System.out.println("从未放过值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
}

此案例仅仅证明main线程有自己的值,那其他线程是否会有自己的初始值呢?

再次验证线程变量的隔离性

public class Tools {
    public static ThreadLocalExt t1 = new ThreadLocalExt();
}
public class ThreadLocalExt extends ThreadLocal {
    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }
}
public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("在ThreadA线程中取值=" + Tools.t1.get());
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("在Main线程中取值=" + Tools.t1.get());
                Thread.sleep(100);
            }
            ThreadA a = new ThreadA();
            a.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

说明:子线程与父线程各有各的所拥有的值。

类 InheritableThreadLocal 的使用

使用类 InheritableThreadLocal 可以在子线程中取得父线程继承下来的值。

值继承

public class InheritableThreadLocalExt extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }
}
public class Tools {
    public static InheritableThreadLocalExt t1 = new InheritableThreadLocalExt();
}
public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("在ThreadA线程中取值=" + Tools.t1.get());
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {
    public static void main(String[] args) {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("在Main线程中取值=" + Tools.t1.get());
                Thread.sleep(100);
            }
            Thread.sleep(5000);
            ThreadA a = new ThreadA();
            a.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

值继承再修改

如果在继承的同时还可以对值进行进一步的处理那就更好了。

public class InheritableThreadLocalExt extends InheritableThreadLocal {

    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }

    @Override
    protected Object childValue(Object parentValue) {
        return parentValue + " 我在子线程加的~";
    }
}

但是使用 InheritableThreadLocal 类需要注意一点的是,如果子线程在取得值的同时,主线程将 InheritableThreadLocal 中的值进行更改,那么子线程取到的值还是旧值。

Lock 的使用

使用 Java5 中的 Lock 对象实现同步的效果。

使用 ReentrantLock 类

在 Java 多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在 JDK1.5 中新增了 ReentrantLock 类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。

使用 ReentrantLock 实现同步: 测试1

既然 ReentrantLock 类再功能上相比 synchronized 更多,那么就以一个初步的程序示例介绍使用。

public class MyService {

    private Lock lock = new ReentrantLock();

    public void testMethod() {
        // 获取锁
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println("ThreadName = " + Thread.currentThread().getName() + " " + (i + 1));
        }
        // 释放锁
        lock.unlock();
    }
}
public class MyThread extends Thread {

    private MyService test1;

    public MyThread(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.testMethod();
    }
}
public class Run {
    public static void main(String[] args) {
        MyService test1 = new MyService();
        MyThread a1 = new MyThread(test1);
        MyThread a2 = new MyThread(test1);
        MyThread a3 = new MyThread(test1);
        MyThread a4 = new MyThread(test1);
        MyThread a5 = new MyThread(test1);
        a1.start();
        a2.start();
        a3.start();
        a4.start();
        a5.start();
    }
}

可以看到,当前线程打印完毕之后将锁进行释放,其他线程才可以继续打印。线程打印的数据是分组打印,因为当前线程已经持有锁,但线程之间打印的顺序是随机的。

使用 ReentrantLock 实现同步:测试2

public class MyService {

    private Lock lock = new ReentrantLock();

    public void methodA() {
        try {
            lock.lock();
            System.out.println("methodA begin ThreadName = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("methodA end ThreadName = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        try {
            lock.lock();
            System.out.println("methodB begin ThreadName = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("methodB end ThreadName = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.methodA();
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.methodB();
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService test1 = new MyService();

        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadAA aa = new ThreadAA(test1); // 自行添加
        aa.setName("aa");
        aa.start();
        Thread.sleep(100);
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
        ThreadBB bb = new ThreadBB(test1); // 自行添加
        bb.setName("bb");
        bb.start();
    }
}

此实验说明,调用 lock.lock() 代码的线程就持有了“对象监视器”,其他线程只有等待锁被释放时再次争抢。效果和使用 synchronized 关键字是一样的,线程之间还是顺序执行的。

使用 Condition 实现等待/通知:错误用法与解决

关键字 synchronized 与 wait() 和 notify() / notifyAll() 方法相结合可以实现等待/通知模式,类 ReentrantLock 也可以实现同样的功能,但需要借助于 Condition 对象。 Condition 类是在 JDK5 中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个 Lock 对象里面可以创建多个 Condition(即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

在使用 notify() / notifyAll() 方法进行通知时,被通知的线程却是由 JVM 随机选择的。但使用 ReentrantLock 结合 Condition 类是可以实现前面介绍过的“选择性通知”,这个功能是非常重要的,而且在 Condition 类中是默认提供的。

而synchronized就相当于整个Lock队长中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权,会出现相当大的效率问题。

public class MyService {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void await() {
        try {
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.await();
    }
}
public class Run {

    public static void main(String[] args) {

        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        a.start();
        
        // java.lang.IllegalMonitorStateException
        // 无监视器对象
    }
}

报错的异常信息是监视器错误,解决的办法是必须在 condition.await() 方法调用之前调用 lock.lock() 代码获得同步监视器。修改代码如下:

public class MyService {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            lock.lock();
            System.out.println("condition.await() 前面");
            condition.await();
            System.out.println("condition.await() 后面");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("锁释放了~");
        }
    }
}

结果输出:condition.await() 前面,原因是调用了 Condition 对象的 await() 方法,使当前执行任务的线程进入了等待 WAITING 状态

正确使用 Condition 实现等待/通知

public class MyService {

    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void await() {
        try {
            lock.lock();
            System.out.println("await 时间: " + System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        try {
            lock.lock();
            System.out.println("signal 时间: " + System.currentTimeMillis());
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.await();
    }
}
public class Run {

    public static void main(String[] args) throws InterruptedException {

        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        a.start();
        TimeUnit.SECONDS.sleep(3);
        test1.signal();

        //await 时间: 1596100061468
        //signal 时间: 1596100064479
    }
}

成功实现等待/通知模式。

Object类中的wait()方法相当于Condition类中的await()方法。

Object类中的wait(long timeout)方法相当于Condition类中的await(long time, TimeUnit unit)方法。

Object类中的notify()方法相当于Condition类中的signal()方法。

Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

使用多个Condition实现通知部分线程:错误用法

前面使用一个 Condition 对象来实现等待/通知模式,其实 Condition 对象也可以创建多个。那个一个 Condition 对象和多个 Condition 对象在使用上有什么区别呢?

public class MyService {

    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void awaitA() {
        try {
            lock.lock();
            System.out.println("begin awaitA 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            condition.await();
            System.out.println("end awaitA 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void awaitB() {
        try {
            lock.lock();
            System.out.println("begin awaitB 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            condition.await();
            System.out.println("end awaitB 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll() {
        try {
            lock.lock();
            System.out.println("signalAll 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.awaitA();
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.awaitB();
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        ThreadB b = new ThreadB(test1);
        a.setName("a");
        b.setName("b");
        a.start();
        b.start();
        TimeUnit.SECONDS.sleep(3);
        test1.signalAll();
    }
}

如果想单独唤醒部分线程该怎么处理呢?这时就有必要使用多个 Condition 对象了,也就是 Condition 对象可以唤醒部分指定线程,有助于提升程序运行的效率。可以先对线程进行分组,然后再唤醒指定组中的线程。

使用多个Condition实现通知部分线程:正确用法

public class MyService {

    private Lock lock = new ReentrantLock();
    public Condition conditionA = lock.newCondition();
    public Condition conditionB = lock.newCondition();

    public void awaitA() {
        try {
            lock.lock();
            System.out.println("begin awaitA 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            conditionA.await();
            System.out.println("end awaitA 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void awaitB() {
        try {
            lock.lock();
            System.out.println("begin awaitB 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            conditionB.await();
            System.out.println("end awaitB 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_A() {
        try {
            lock.lock();
            System.out.println("signalAll_A 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            conditionA.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_B() {
        try {
            lock.lock();
            System.out.println("signalAll_B 时间为 = " + System.currentTimeMillis() + " ThreadName = " + Thread.currentThread().getName());
            conditionB.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.awaitA();
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        test1.awaitB();
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(test1);
        b.setName("b");
        b.start();
        TimeUnit.SECONDS.sleep(3);
        test1.signalAll_A();
    }
}

程序运行后,只有线程A被唤醒了。

通过此实验可以得知,使用 ReentrantLock 对象可以唤醒指定种类的线程,这是控制部分线程行为的方便方式。

实现生产者/消费者模式:一对一交替打印

public class MyService {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private boolean hasValue = false;

    public void set() {
        try {
            lock.lock();
            while (hasValue) {
                condition.await();
            }
            System.out.println("打印 ★");
            hasValue = true;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void get() {
        try {
            lock.lock();
            while (!hasValue) {
                condition.await();
            }
            System.out.println("打印 ☆");
            hasValue = false;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            test1.set();
        }
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            test1.get();
        }
    }
}
public class Run {
    public static void main(String[] args) {
        MyService test1 = new MyService();
        ThreadA a = new ThreadA(test1);
        ThreadB b = new ThreadB(test1);
        a.start();
        b.start();
    }
}

通过使用 Condition 对象,成功实现交替打印的效果。

实现生产者/消费者模式:多对多交替打印

public class MyService {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private boolean hasValue = false;

    public void set() {
        try {
            lock.lock();
            while (hasValue) {
                System.out.println("可能会连续打印 ★★");
                condition.await();
            }
            System.out.println("打印 ★");
            hasValue = true;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void get() {
        try {
            lock.lock();
            while (!hasValue) {
                System.out.println("可能会连续打印 ☆☆");
                condition.await();
            }
            System.out.println("打印 ☆");
            hasValue = false;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadA extends Thread {

    private MyService test1;

    public ThreadA(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            test1.set();
        }
    }
}
public class ThreadB extends Thread {

    private MyService test1;

    public ThreadB(MyService test1) {
        this.test1 = test1;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            test1.get();
        }
    }
}
public class Run {
    public static void main(String[] args) {
        MyService test1 = new MyService();
        ThreadA[] threadA = new ThreadA[10];
        ThreadB[] threadB = new ThreadB[10];
        for (int i = 0; i < 10; i++) {
            threadA[i] = new ThreadA(test1);
            threadB[i] = new ThreadB(test1);
            threadA[i].start();
            threadB[i].start();
        }
    }
}

程序运行后又出现“假死”。

可以使用signalAll()方法来解决。

public class MyService {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private boolean hasValue = false;

    public void set() {
        try {
            lock.lock();
            while (hasValue) {
                System.out.println("可能会连续打印 ★★");
                condition.await();
            }
            System.out.println("打印 ★");
            hasValue = true;
            //condition.signal();
            // 解决假死
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void get() {
        try {
            lock.lock();
            while (!hasValue) {
                System.out.println("可能会连续打印 ☆☆");
                condition.await();
            }
            System.out.println("打印 ☆");
            hasValue = false;
            //condition.signal();
            // 解决假死
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

控制台中“打印 ☆”和“打印 ★”是交替输出的,但是“可能会连续打印 ☆☆”和“可能会连续打印 ★★”却不是交替输出的,有时候出现连续打印的情况。原因是程序中使用了一个 Condition 对象,再结合 signalAll() 方法来唤醒所有的线程,那么唤醒的线程就有可能是同类,所以就出现连续打印“可能会连续打印 ☆☆”和“可能会连续打印 ★★”的情况了。

公平锁和非公平锁

公平与非公平锁:锁Lock分为“公平锁”和“非公平锁”,公平锁标识线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出的顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

public class Service {

    private ReentrantLock lock;

    public Service(boolean isFair) {
        lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            lock.lock();
            System.out.println("ThreadName = " + Thread.currentThread().getName() + " 获取锁");
        } finally {
            lock.unlock();
        }
    }
}

公平锁示例:

public class RunFair {

    public static void main(String[] args) {
        final Service test1 = new Service(true);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("★线程 " + Thread.currentThread().getName() + " 运行了");
                test1.serviceMethod();
            }
        };

        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
    }
}

打印的结果基本是呈有序的状态,这就是公平锁的特点。

非公平锁示例:

public class RunNotFair {

    public static void main(String[] args) {
        // 非公平锁
        final Service test1 = new Service(false);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("★线程 " + Thread.currentThread().getName() + " 运行了");
                test1.serviceMethod();
            }
        };

        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
    }
}

非公平锁的运行结果基本上是乱序的,说明先 start() 启动的线程不代表先获得锁。

部分方法示例

getHoldCount()

方法 int getHoldCount() 的作用是查询当前线程保持此锁的个数,也就是调用 lock() 方法的次数。

public class Test1 {

    private ReentrantLock lock = new ReentrantLock();

    public void serviceMethod1() {
        try {
            lock.lock();
            System.out.println("serviceMethod1 getHoldCount = " + lock.getHoldCount());
            serviceMethod2();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void serviceMethod2() {
        try {
            lock.lock();
            System.out.println("serviceMethod2 getHoldCount = " + lock.getHoldCount());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        Test1 test1 = new Test1();
        test1.serviceMethod1();

        //serviceMethod1 getHoldCount = 1
        //serviceMethod2 getHoldCount = 2
    }
}

getQueueLength()

方法 int getQueueLength() 的作用是返回正等待获取此锁定的线程估计数,比如有10个线程,1个线程首先执行await() 方法,那么在调用 getQueueLength() 方法后返回值是9,说明有9个线程同时在等待 lock 的释放。

public class Test2 {

    private ReentrantLock lock = new ReentrantLock();

    public void serviceMethod1() {
        try {
            lock.lock();
            System.out.println("ThreadName = " + Thread.currentThread().getName() + " 进入方法!");
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Test2 test2 = new Test2();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                test2.serviceMethod1();
            }
        };
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
        TimeUnit.SECONDS.sleep(2);
        System.out.println("有 " + test2.lock.getQueueLength() + " 个线程在等待获取锁!");

        //ThreadName = Thread-0 进入方法!
        //有 9 个线程在等待获取锁!
    }
}

getWaitQueueLength(Condition condition)

方法 int getWaitQueueLength(Condition condition) 的作用是返回等待与此锁相关的给定条件 Condition 的线程估计数,如果有5个线程,每个线程都执行同一个 condition对象的 await() 方法,则调用 getWaitQueueLength 方法时返回的int 值就是5。

public class Test3 {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            lock.lock();
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void notifyMethod() {
        try {
            lock.lock();
            System.out.println("有 " + lock.getWaitQueueLength(condition) + " 个线程正在等待 condition");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        final Test3 test3 = new Test3();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                test3.waitMethod();
            }
        };
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
        TimeUnit.SECONDS.sleep(2);

        for (int i = 0; i < 10; i++) {
            test3.notifyMethod();
        }

        //有 10 个线程正在等待 condition
        //有 9 个线程正在等待 condition
        //有 8 个线程正在等待 condition
        //有 7 个线程正在等待 condition
        //有 6 个线程正在等待 condition
        //有 5 个线程正在等待 condition
        //有 4 个线程正在等待 condition
        //有 3 个线程正在等待 condition
        //有 2 个线程正在等待 condition
        //有 1 个线程正在等待 condition
    }
}

hasQueuedThread() 和 hasQueuedThreads()

方法 boolean hasQueuedThread(Thread thread) 的作用是查询指定的线程是否正在等待获取此锁。

方法 boolean hasQueuedThreads() 的作用是查询是否有线程正在等待获取此锁。

public class Test4 {

    public ReentrantLock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            lock.lock();
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Test4 test4 = new Test4();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                test4.waitMethod();
            }
        };
        Thread threadA = new Thread(runnable);
        threadA.start();
        Thread.sleep(500);
        Thread threadB = new Thread(runnable);
        threadB.start();
        Thread.sleep(500);
        System.out.println("test4.lock.hasQueuedThread(threadA) = " + test4.lock.hasQueuedThread(threadA));
        System.out.println("test4.lock.hasQueuedThread(threadB) = " + test4.lock.hasQueuedThread(threadB));
        System.out.println("test4.lock.hasQueuedThreads() = " + test4.lock.hasQueuedThreads());

        //test4.lock.hasQueuedThread(threadA) = false
        //test4.lock.hasQueuedThread(threadB) = true
        //test4.lock.hasQueuedThreads() = true
    }
}

hasWaiters(Condition condition)

方法 boolean hasWaiters(Condition condition)的作用是查询是否有线程正在等待与此锁有关的 condition 条件。

public class Test5 {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            lock.lock();
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void notifyMethod() {
        try {
            lock.lock();
            System.out.println("有没有线程正在等待 condition ? ");
            System.out.println(lock.hasWaiters(condition) ? "有线程在等待" : "没有线程在等待");
            System.out.println("线程数是多少 ?");
            System.out.println(lock.getWaitQueueLength(condition));
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Test5 test5 = new Test5();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                test5.waitMethod();
            }
        };
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
        Thread.sleep(2000);
        test5.notifyMethod();

        //有没有线程正在等待 condition ?
        //有线程在等待
        //线程数是多少 ?
        //10
    }
}

isFair()

方法 boolean isFair() 的作用是判断是不是公平锁

public class Test6 {

    private ReentrantLock lock;

    public Test6(boolean isFair) {
        this.lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            lock.lock();
            System.out.println("公平锁情况: " + lock.isFair());
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test6 test6 = new Test6(true);
        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                test6.serviceMethod();
            }
        };
        Thread thread = new Thread(runnable1);
        thread.start();

        final Test6 test66 = new Test6(false);
        Runnable runnable2 = () -> test66.serviceMethod();
        thread = new Thread(runnable2);
        thread.start();

        //公平锁情况: true
        //公平锁情况: false
    }
}

在默认的情况下, ReentrantLock 类使用的是非公平锁。

isHeldByCurrentThread()

方法 boolean isHeldByCurrentThread() 的作用是查询当前线程是否保持此锁。

public class Test7 {

    private ReentrantLock lock;

    public Test7(boolean isFair) {
        lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            System.out.println("lock.isHeldByCurrentThread() = " + lock.isHeldByCurrentThread());
            lock.lock();
            System.out.println("lock.isHeldByCurrentThread() = " + lock.isHeldByCurrentThread());
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test7 test7 = new Test7(true);
        Runnable runnable = () -> test7.serviceMethod();
        Thread thread = new Thread(runnable);
        thread.start();

        //lock.isHeldByCurrentThread() = false
        //lock.isHeldByCurrentThread() = true
    }
}

isLocked()

方法 boolean isLocked() 的作用是查询此锁是否由任意线程保持。

public class Test8 {

    private ReentrantLock lock;

    public Test8(boolean isFair) {
        lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            System.out.println("lock.isLocked() = " + lock.isLocked());
            lock.lock();
            System.out.println("lock.isLocked() = " + lock.isLocked());
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test8 test8 = new Test8(true);
        Runnable runnable = () -> test8.serviceMethod();
        Thread thread = new Thread(runnable);
        thread.start();

        //lock.isLocked() = false
        //lock.isLocked() = true
    }
}

lockInterruptibly()

方法 void lockInterruptibly() 的作用是:如果当前线程未被中断,则获取锁,如果已经被中断则出现异常。

public class Test9 {

    public ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            // 被中断后不出现异常,正常执行
            //lock.lock();
            // 被中断后异常
            lock.lockInterruptibly();
            System.out.println("lock begin " + Thread.currentThread().getName());
            for (int i = 0; i < Integer.MAX_VALUE / 10; i++) {
                String newString = new String();
                Math.random();
            }
            System.out.println("lock end " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Test9 test9 = new Test9();
        Runnable runnable = () -> test9.waitMethod();
        Thread threadA = new Thread(runnable);
        threadA.setName("A");
        threadA.start();
        Thread.sleep(500);
        Thread threadB = new Thread(runnable);
        threadB.setName("b");
        threadB.start();
        threadB.interrupt(); // 打断
        System.out.println("main end!");
    }
}

使用 Lock() 方法,说明线程B被interrupt中断了,那么执行lock()则不出现异常,正常执行。

如果使用 lockInterruptibly() 方法则报异常。

tryLock()

方法 boolean tryLock() 的作用是,仅在调用时锁未被另一个线程保持的情况下,才获取该锁。

public class Test10 {

    public ReentrantLock lock = new ReentrantLock();

    public void waitMethod() {
        if (lock.tryLock()) {
            System.out.println(Thread.currentThread().getName() + " 获得锁");
        } else {
            System.out.println(Thread.currentThread().getName() + " 没有获得锁");
        }
    }

    public static void main(String[] args) {
        final Test10 test10 = new Test10();
        Runnable runnable = () -> test10.waitMethod();
        Thread a = new Thread(runnable);
        a.setName("a");
        a.start();
        Thread b = new Thread(runnable);
        b.setName("b");
        b.start();

        //a 没有获得锁
        //b 获得锁
    }
}

tryLock(long timeout, TimeUnit unit)

方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。

public class Test11 {

    public ReentrantLock lock = new ReentrantLock();

    public void waitMethod() {
        try {
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + " 获取锁的时间: " + System.currentTimeMillis());
                Thread.sleep(10000);
                //Thread.sleep(2000);
            } else {
                System.out.println(Thread.currentThread().getName() + " 没有获得锁");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        final Test11 test11 = new Test11();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " 调用 waitMethod 时间: " + System.currentTimeMillis());
            test11.waitMethod();
        };
        Thread a = new Thread(runnable);
        a.setName("a");
        a.start();
        Thread b = new Thread(runnable);
        b.setName("b");
        b.start();

        //a 调用 waitMethod 时间: 1596177305800
        //b 调用 waitMethod 时间: 1596177305800
        //a 获取锁的时间: 1596177305801
        //b 没有获得锁
    }
}

awaitUninterruptibly()

  • 线程在调用condition.await()后处于await状态,此时调用thread.interrupt()会报错
  • 但是使用condition.awaitUninterruptibly()后,调用thread.interrupt()则不会报错
public class Test12 {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void testMethod() {
        try {
            lock.lock();
            System.out.println("wait begin");
            // 发生异常
            //condition.await();
            // 没有发生异常
            condition.awaitUninterruptibly();
            System.out.println("wait end");
        } finally {
            lock.unlock();
        }
    }

    public static class MyThread extends Thread {

        private Test12 test12;

        public MyThread(Test12 test12) {
            this.test12 = test12;
        }

        @Override
        public void run() {
            test12.testMethod();
        }
    }

    public static void main(String[] args) {
        try {
            Test12 test12 = new Test12();
            MyThread myThread = new MyThread(test12);
            myThread.start();
            Thread.sleep(3000);
            myThread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

awaitUntil()

在等待时间之内可以被其它线程唤醒,等待时间一过该线程会自动唤醒,和别的线程争抢锁资源

public class Test13 {

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod() {
        try {
            Calendar calendarRef = Calendar.getInstance();
            calendarRef.add(Calendar.SECOND, 10);
            lock.lock();
            System.out.println("wait begin timer = " + System.currentTimeMillis());
            condition.awaitUntil(calendarRef.getTime());
            System.out.println("wait end timer = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void notifyMethod() {
        try {
            Calendar calendarRef = Calendar.getInstance();
            calendarRef.add(Calendar.SECOND, 10);
            lock.lock();
            System.out.println("notify begin timer = " + System.currentTimeMillis());
            condition.signalAll();
            System.out.println("notify end timer = " + System.currentTimeMillis());
        } finally {
            lock.unlock();
        }
    }

    public static class MyThreadA extends Thread {

        private Test13 test13;

        public MyThreadA(Test13 test13) {
            this.test13 = test13;
        }

        @Override
        public void run() {
            test13.waitMethod();
        }
    }

    public static class MyThreadB extends Thread {

        private Test13 test13;

        public MyThreadB(Test13 test13) {
            this.test13 = test13;
        }

        @Override
        public void run() {
            test13.notifyMethod();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 10秒后自动唤醒自己
        Test13 test13 = new Test13();
        MyThreadA myThreadA = new MyThreadA(test13);
        myThreadA.start();

        //wait begin timer = 1596178251932
        //wait end timer = 1596178261902


        // 2秒后被其他线程所唤醒
        /*Thread.sleep(2000);
        MyThreadB myThreadB = new MyThreadB(test13);
        myThreadB.start();
*/
        //wait begin timer = 1596178348390
        //notify begin timer = 1596178350352
        //notify end timer = 1596178350352
        //wait end timer = 1596178350352
    }
}

使用 Condition 实现顺序执行

使用 Condition 对象可以对线程执行的业务进行排序规划。

public class Run {

    volatile private static int nextPrintWho = 1;

    private static ReentrantLock lock = new ReentrantLock();

    final private static Condition conditionA = lock.newCondition();
    final private static Condition conditionB = lock.newCondition();
    final private static Condition conditionC = lock.newCondition();

    public static void main(String[] args) {
        Thread threadA = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 1) {
                        conditionA.await();
                    }
                    for (int i = 0; i < 3; i++) {
                        System.out.println("ThreadA " + (i + 1));
                    }
                    nextPrintWho = 2;
                    conditionB.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread threadB = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 2) {
                        conditionB.await();
                    }
                    for (int i = 0; i < 3; i++) {
                        System.out.println("ThreadB " + (i + 1));
                    }
                    nextPrintWho = 3;
                    conditionC.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread threadC = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 3) {
                        conditionC.await();
                    }
                    for (int i = 0; i < 3; i++) {
                        System.out.println("ThreadC " + (i + 1));
                    }
                    nextPrintWho = 1;
                    conditionA.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread[] aArray = new Thread[5];
        Thread[] bArray = new Thread[5];
        Thread[] cArray = new Thread[5];

        for (int i = 0; i < 5; i++) {
            aArray[i] = new Thread(threadA);
            bArray[i] = new Thread(threadB);
            cArray[i] = new Thread(threadC);
            aArray[i].start();
            bArray[i].start();
            cArray[i].start();
        }
    }
}

使用 ReentrantReadWriteLock 类

类 ReentrantLock 具有完全互斥排他的效果,即同一时间只有一个线程在执行 ReentrantLock.lock() 方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReentrantReadWriteLock 来提升该方法的代码运行速度。

读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读写与写锁互斥,写锁与写锁互斥。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进入写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

类 ReentrantReadWriteLock 的使用:读读共享

public class Service {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.read();
    }
}
public class ThreadB extends Thread {

    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.read();
    }
}
public class Run {

    public static void main(String[] args) {

        Service service = new Service();

        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();

        //获得读锁 a 1596182558582
        //获得读锁 b 1596182558582
    }
}

两个线程几乎同时进入lock()方法后面的代码。说明在此使用了 lock.readLock() 读锁可以提高程序运行效率,允许多个线程同时执行 lock() 后面的代码。

类 ReentrantReadWriteLock 的使用:写写互斥

public class Service {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获取写锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.write();
    }
}
public class ThreadB extends Thread {

    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.write();
    }
}
public class Run {

    public static void main(String[] args) {

        Service service = new Service();

        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();

        //获取写锁 b 1596183904057
        // .... 等10秒
        //获取写锁 a 1596183914067
    }
}

使用写锁代码 lock.writeLock() 的效果就是同一时间只允许一个线程执行 lock() 方法后面的代码。

类 ReentrantReadWriteLock 的使用:读写互斥

public class Service {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获取写锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {

    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.read();
    }
}
public class ThreadB extends Thread {

    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.write();
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();
        Thread.sleep(1000);
        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();

        //获得读锁 a 1596185543808
        //获取写锁 b 1596185553822
    }
}

此实验说明“读写”操作是互斥的,而且下一个示例说明“写读”操作也是互斥的。

即只要出现“写操作”的过程,就是互斥的。

类 ReentrantReadWriteLock 的使用:写读互斥

public class Run2 {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();
        Thread.sleep(1000);
        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();

        //获取写锁 b 1596185710027
        //获得读锁 a 1596185720031
    }
}

定时器 Timer

定时计划任务功能在 Java 中主要使用的就是 Timer 对象,它在内部使用多线程的方式进行处理,所以它和线程技术还是有非常大的关系的。

定时器 Timer 的使用

在 JDK 库中 Timer 类主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务。

Timer 类的主要作用就是设置计划任务,但封装任务的类却是 TimerTask 类。

执行计划任务的代码要放入 TimerTask 的子类中,因为 TimerTask 是一个抽象类。

方法 schedule(TimerTask task, Date time)

该方法的作用是在指定的日期执行一次某一任务

执行任务的世界晚于当前时间:在未来执行的效果

public class Run1 {

    private static Timer timer = new Timer();

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了~ 时间为: " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTask task = new MyTask();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-1 22:09:00"; // 这个是未来时间
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task, dateRef);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        //字符串时间: 2020-8-1 22:09:00 当前时间: 2020-8-1 22:08:15
        //运行了~ 时间为: Sat Aug 01 22:09:00 CST 2020
    }
}

任务虽然执行完了,但进程还未销毁,为什么会出现这样的情况呢?

在创建 Timer 对象时,JDK 源代码如下:

public Timer() {
        this("Timer-" + serialNumber());
}

此构造方法调用的是如下构造方法:

public Timer(String name) {
    thread.setName(name);
    thread.start();
}

查看构造方法可以得知,创建一个 Timer 就是启动一个新的线程,这个新启动的线程并不是守护线程,它一直在运行。

下一步将创建的 Timer 改成守护线程。

public class Run1TimerIsDaemon {

    private static Timer timer = new Timer(true);

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了~ 时间为: " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTask task = new MyTask();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 02:43:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task, dateRef);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

程序运行后迅速结束当前的进程,并且 TimerTask 中的任务不再被运行,因为进程已经结束了。

计划时间早于当前时间:提前运行的效果

如果执行任务的时间早于当前时间,则立刻执行 task 任务。

多个 TimerTask 任务及延时的测试

public class Run2 {

    private static Timer timer = new Timer();

    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了 MyTask1 时间: " + new Date());
        }
    }

    static public class MyTask2 extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了 MyTask2 时间: " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            MyTask2 task2 = new MyTask2();
            SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString1 = "2020-8-2 02:47:00";
            String dateString2 = "2020-8-2 02:48:00";
            Date dateRef1 = sdf1.parse(dateString1);
            Date dateRef2 = sdf2.parse(dateString2);
            System.out.println("字符串 1 时间: " + dateRef1.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            System.out.println("字符串 2 时间: " + dateRef2.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task1, dateRef1);
            timer.schedule(task2, dateRef2);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        //字符串 1 时间: 2020-8-2 2:47:00 当前时间: 2020-8-2 2:46:17
        //字符串 2 时间: 2020-8-2 2:48:00 当前时间: 2020-8-2 2:46:17
        //运行了 MyTask1 时间: Sun Aug 02 02:47:00 CST 2020
        //运行了 MyTask2 时间: Sun Aug 02 02:48:00 CST 2020
    }
}

TimerTask 是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务由可能消耗的时间较长,则后面的任务运行的时间也会被延迟。

public class Run2Later {

    private static Timer timer = new Timer();

    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask1 begin 时间 = " + new Date());
                Thread.sleep(10000);
                System.out.println("MyTask1 end 时间 = " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static public class MyTask2 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask2 begin 时间 = " + new Date());
                Thread.sleep(1000);
                System.out.println("MyTask2 end 时间 = " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            MyTask2 task2 = new MyTask2();
            SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString1 = "2020-8-2 10:20:00";
            String dateString2 = "2020-8-2 10:20:10";
            Date dateRef1 = sdf1.parse(dateString1);
            Date dateRef2 = sdf2.parse(dateString2);
            System.out.println("字符串1时间: " + dateRef1.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            System.out.println("字符串2时间: " + dateRef2.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task1, dateRef1);
            timer.schedule(task2, dateRef2);
        } catch (ParseException e) {
            e.printStackTrace();
        }


        //字符串1时间: 2020-8-2 10:20:00 当前时间: 2020-8-2 10:20:04
        //字符串2时间: 2020-8-2 10:20:10 当前时间: 2020-8-2 10:20:04
        //MyTask1 begin 时间 = Sun Aug 02 10:20:04 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 10:20:14 CST 2020
        //MyTask2 begin 时间 = Sun Aug 02 10:20:14 CST 2020
        //MyTask2 end 时间 = Sun Aug 02 10:20:15 CST 2020
    }
}

由于 task1 需要用时 10秒执行完任务,task1 开始的开始是 10:20:04,结束时间是 10:20:14,那么将要影响 task2 的计划任务执行的时间,task2 以此结束时间为基准,向后延迟了10秒,task2 在 10:20:14 执行任务。因为 Task 是被放入队列中一个一个按顺序执行。

方法 schedule(TimerTask task, Date firstTime, long period)

该方法的作用是在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。

计划时间晚于当前时间:在未来执行的效果

public class Run {

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了~ 时间为 = " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTask task = new MyTask();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 10:31:00";
            Timer timer = new Timer();
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task, dateRef, 4000);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        //字符串时间: 2020-8-2 10:31:00 当前时间: 2020-8-2 10:30:26
        //运行了~ 时间为 = Sun Aug 02 10:31:00 CST 2020
        //运行了~ 时间为 = Sun Aug 02 10:31:04 CST 2020
        //运行了~ 时间为 = Sun Aug 02 10:31:08 CST 2020
        //运行了~ 时间为 = Sun Aug 02 10:31:12 CST 2020
    }
}

每隔4秒运行一个 TimerTask 任务,并且是无限期地重复执行。

计划时间早于当前时间:提前运行的效果

如果计划时间早于当前时间,则立刻执行task任务。

任务执行时间被延时

public class Run1 {

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            try {
            System.out.println("开始运行了~ 时间为 = " + new Date());
            Thread.sleep(5000);
            System.out.println("结束运行了~ 时间为 = " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask task = new MyTask();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 10:34:30";
            Timer timer = new Timer();
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task, dateRef, 4000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串时间: 2020-8-2 10:34:30 当前时间: 2020-8-2 10:34:13
        //开始运行了~ 时间为 = Sun Aug 02 10:34:30 CST 2020
        //结束运行了~ 时间为 = Sun Aug 02 10:34:35 CST 2020
        //开始运行了~ 时间为 = Sun Aug 02 10:34:35 CST 2020
        //结束运行了~ 时间为 = Sun Aug 02 10:34:40 CST 2020
        //开始运行了~ 时间为 = Sun Aug 02 10:34:40 CST 2020
        //结束运行了~ 时间为 = Sun Aug 02 10:34:45 CST 2020
        //开始运行了~ 时间为 = Sun Aug 02 10:34:45 CST 2020
    }
}

按延时的时间结束为准,并且是无限期地重复执行。

TimerTask 类中的 cancel() 方法

TimerTask 类中的 cancel() 方法的作用是将自身从任务队列中清除。

public class Run2 {

    static public class MyTaskA extends TimerTask {
        @Override
        public void run() {
            System.out.println("A 运行了 时间 = " + new Date());
            if (this.cancel()) {
                System.out.println("A 使用了 cancel 方法");
            }
        }
    }

    static public class MyTaskB extends TimerTask {
        @Override
        public void run() {
            System.out.println("B 运行了 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTaskA taskA = new MyTaskA();
            MyTaskB taskB = new MyTaskB();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 10:40:00";
            Timer timer = new Timer();
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(taskA, dateRef, 4000);
            timer.schedule(taskB, dateRef, 4000);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        //字符串时间: 2020-8-2 10:40:00 当前时间: 2020-8-2 10:39:57
        //A 运行了 时间 = Sun Aug 02 10:40:00 CST 2020
        //A 使用了 cancel 方法
        //B 运行了 时间 = Sun Aug 02 10:40:00 CST 2020
        //B 运行了 时间 = Sun Aug 02 10:40:04 CST 2020
        //B 运行了 时间 = Sun Aug 02 10:40:08 CST 2020
    }
}

TimerTask 类的 cancel() 方法是将自身从任务队列中被移除,其他任务不受影响。

Timer 类的 cancel() 方法

与 TimerTask 类中的 cancel() 方法清除自身不同, Timer类中的 cancel() 方法的作用是将任务队列中的全部任务清空。

public class Run3 {

    private static Timer timer = new Timer();

    static public class MyTaskA extends TimerTask {
        @Override
        public void run() {
            System.out.println("A 运行了 时间 = " + new Date());
            timer.cancel();
        }
    }

    static public class MyTaskB extends TimerTask {
        @Override
        public void run() {
            System.out.println("B 运行了 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {
        try {
            MyTaskA taskA = new MyTaskA();
            MyTaskB taskB = new MyTaskB();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 11:14:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(taskA, dateRef, 4000);
            timer.schedule(taskB, dateRef, 4000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串时间: 2020-8-2 11:14:00 当前时间: 2020-8-2 11:13:55
        //A 运行了 时间 = Sun Aug 02 11:14:00 CST 2020
    }
}

全部任务都被清除,并且进程被销毁。

Timer 的 cancel() 方法注意事项

Timer 类的 cancel() 方法有时并不一定会停止执行计划任务,而是正常执行。

public class Run4 {

    static int i = 0;

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("正常执行了 " + i);
        }
    }

    public static void main(String[] args) {
        while (true) {
            try {
                i++;
                Timer timer = new Timer();
                MyTask task = new MyTask();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String dateString = "2020-8-2 11:17:00";
                Date dateRef = sdf.parse(dateString);
                timer.schedule(task, dateRef);
                timer.cancel();
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }

        //正常执行了 125
        //正常执行了 512
        //正常执行了 74135
        //正常执行了 74142
        //正常执行了 74155
    }
}

这是因为 Timer 类中的 cancel() 方法有时并没有争抢到 queue 锁,所以 TimerTask 类中的任务继续正常运行。

方法 schedule(TimerTask task, long delay)

该方法的作用是以执行 schedule(TimerTask task, long delay) 方法当前的时间为参考时间,在此时间基础上延迟指定的毫秒数后执行一次 TimerTask 任务。

public class Run {

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了~ 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {
        MyTask task = new MyTask();
        Timer timer = new Timer();
        System.out.println("当前时间 = " + new Date().toLocaleString());
        timer.schedule(task, 4000);

        //当前时间 = 2020-8-2 11:23:49
        //运行了~ 时间 = Sun Aug 02 11:23:53 CST 2020
    }
}

方法 schedule(TimerTask task, long delay, long period)

该方法的作用是以执行 schedule(TimerTask task, long delay, long period) 方法当前的时间为参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一任务。

public class Run {

    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("运行了~ 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {
        MyTask task = new MyTask();
        Timer timer = new Timer();
        System.out.println("当前时间: " + new Date().toLocaleString());
        timer.schedule(task, 3000, 5000);

        //当前时间: 2020-8-2 11:27:27
        //运行了~ 时间 = Sun Aug 02 11:27:30 CST 2020
        //运行了~ 时间 = Sun Aug 02 11:27:35 CST 2020
        //运行了~ 时间 = Sun Aug 02 11:27:40 CST 2020
    }
}

凡是使用方法中带有 period 参数的,都是无限循环执行的 TimerTask 中的任务。

方法 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

方法 schedule 和 方法 scheduleAtFixedRate 都会按顺序执行,所以不用考虑非线程安全的情况。

方法 schedule 和 scheduleAtFixedRate 主要的区别只在于不延时的情况。

使用 schedule 方法:如果执行任务的世界没有被延时,那么下一次任务的执行时间参考的是上一次任务的“开始”时的时间来计算。

使用 scheduleAtFixedRate 方法:如果执行任务的世界没有被延时,那么下一次任务的执行时间参考的是上一次任务的“结束”时的时间来计算。

schedule 方法任务不延时

public class Run1 {

    private static Timer timer = new Timer();
    private static int runCount;
    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask1 begin 时间 = " + new Date());
                Thread.sleep(1000);
                System.out.println("MyTask1 end 时间 = " + new Date());
                runCount++;
                if (runCount == 5) {
                    timer.cancel();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 11:40:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task1, dateRef, 3000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 11:40:00 当前时间: 2020-8-2 11:39:27
        //MyTask1 begin 时间 = Sun Aug 02 11:40:00 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:40:01 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:40:03 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:40:04 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:40:06 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:40:07 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:40:09 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:40:10 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:40:12 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:40:13 CST 2020
    }
}

在不延时的情况下,如果执行任务的时间没有被延时,则下一次执行任务的时间是上一次任务的开始时间加上 delay 时间。

schedule 方法任务延时

public class Run2 {

    private static Timer timer = new Timer();
    private static int runCount;
    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask1 begin 时间 = " + new Date());
                Thread.sleep(5000);
                System.out.println("MyTask1 end 时间 = " + new Date());
                runCount++;
                if (runCount == 5) {
                    timer.cancel();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 11:43:30";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task1, dateRef, 3000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 11:43:30 当前时间: 2020-8-2 11:43:11
        //MyTask1 begin 时间 = Sun Aug 02 11:43:30 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:43:35 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:43:35 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:43:40 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:43:40 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:43:45 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:43:45 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:43:50 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:43:50 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:43:55 CST 2020
    }
}

如果执行任务的时间被延时,那么下一次任务的执行时间以上一次任务“结束”时的时间为参考来计算。

scheduleAtFixedRate 方法任务不延时

public class Run3 {

    private static Timer timer = new Timer();
    private static int runCount;
    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask1 begin 时间 = " + new Date());
                Thread.sleep(2000);
                System.out.println("MyTask1 end 时间 = " + new Date());
                runCount++;
                if (runCount == 5) {
                    timer.cancel();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 11:54:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.scheduleAtFixedRate(task1, dateRef, 3000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 11:54:00 当前时间: 2020-8-2 11:53:56
        //MyTask1 begin 时间 = Sun Aug 02 11:54:00 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:54:02 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:54:03 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:54:05 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:54:06 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:54:08 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:54:09 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:54:11 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:54:12 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:54:14 CST 2020
    }
}

如果执行任务的时间没有被延时,则下一次执行任务的时间是上一次任务开始的时间加上 delay 时间。

scheduleAtFixedRate 方法任务延时

public class Run4 {

    private static Timer timer = new Timer();
    private static int runCount;
    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            try {
                System.out.println("MyTask1 begin 时间 = " + new Date());
                Thread.sleep(5000);
                System.out.println("MyTask1 end 时间 = " + new Date());
                runCount++;
                if (runCount == 5) {
                    timer.cancel();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 11:57:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.scheduleAtFixedRate(task1, dateRef, 3000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 11:57:00 当前时间: 2020-8-2 11:56:09
        //MyTask1 begin 时间 = Sun Aug 02 11:57:00 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:57:05 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:57:05 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:57:10 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:57:10 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:57:15 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:57:15 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:57:20 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 11:57:20 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 11:57:25 CST 2020
    }
}

如果执行任务的时间被延时,那么下一次任务的执行时间以上一次任务“结束”时的时间为参考来计算。

验证 schedule 方法不具有追赶执行性

public class Run5 {

    private static Timer timer = new Timer();

    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            System.out.println("MyTask1 begin 时间 = " + new Date());
            System.out.println("MyTask1 end 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {

        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 12:08:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.schedule(task1, dateRef, 5000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 12:08:00 当前时间: 2020-8-2 12:10:08
        //MyTask1 begin 时间 = Sun Aug 02 12:10:08 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:10:08 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:10:13 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:10:13 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:10:18 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:10:18 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:10:23 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:10:23 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:10:28 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:10:28 CST 2020
    }
}

时间 2020-8-2 12:08:00 到 2020-8-2 12:10:08 之间的时间所对应的 Task 任务被取消了,不执行了。这就是 Task 任务不追赶的情况。

验证 scheduleAtFixedRate 方法具有追赶执行性

public class Run6 {

    private static Timer timer = new Timer();

    static public class MyTask1 extends TimerTask {
        @Override
        public void run() {
            System.out.println("MyTask1 begin 时间 = " + new Date());
            System.out.println("MyTask1 end 时间 = " + new Date());
        }
    }

    public static void main(String[] args) {

        try {
            MyTask1 task1 = new MyTask1();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2020-8-2 12:08:00";
            Date dateRef = sdf.parse(dateString);
            System.out.println("字符串1时间: " + dateRef.toLocaleString() + " 当前时间: " + new Date().toLocaleString());
            timer.scheduleAtFixedRate(task1, dateRef, 5000);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //字符串1时间: 2020-8-2 12:08:00 当前时间: 2020-8-2 12:12:37
        //MyTask1 begin 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 end 时间 = Sun Aug 02 12:12:37 CST 2020
        //MyTask1 begin 时间 = Sun Aug 02 12:12:37 CST 2020
    }
}

两个时间段内所对应的 Task 任务被“补充性”执行了,这就是 Task 任务追赶执行的特性。

单例模式与多线程

在学习本章时只需要考虑一件事情,那就是:

如何使单例模式遇到多线程是安全的、正确的

立即加载 / “饿汉模式”

什么是立即加载?

立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。

立即加载也称为“饿汉模式”。

public class MyObject {

    // 立即加载方式 == 饿汉模式
    private static MyObject myObject = new MyObject();

    private MyObject() {

    }

    public static MyObject getInstance() {
        // 此代码版本为立即加载
        // 此版本代码的缺点是不能有其他实例变量
        // 因为 getInstance() 方法没有同步
        // 所以有可能出现非线程安全问题
        return myObject;
    }
}
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //337436377
        //337436377
        //337436377
    }
}

打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

延迟加载 / “懒汉模式”

什么是延迟加载?

延迟加载就是在调用 get() 方法时实例才被创建,常见的实现办法就是在 get() 方法中进行 new 实例化。

延迟加载也称为“懒汉模式”。

延迟加载 / “懒汉模式” 解析

延迟加载 / “懒汉模式” 是在调用方法时实例才被创建。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        // 延迟加载
        if (myObject == null) {
            myObject = new MyObject();
        }
        return myObject;
    }
}
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {

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

此实验虽然取得一个对象的实例,但如果是在多线程的环境中,就会出现取出多个实例的情况,与单例模式的初衷是相背离的。

延迟加载 / “懒汉模式” 的缺点

前面两个实验虽然使用 “立即加载” 和 “延迟加载” 实现了单例设计模式,但在多线程的环境中,前面 “延迟加载” 示例中的代码完全就是错误的,根本不能实现保持单例的状态。来看一下如何在多线程环境中结合 “错误的单例模式” 创建出 “多例”。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                // 模式在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //1931467724
        //1931467724
        //1172709796
    }
}

打印出了2种hashCode,说明创建出了2个对象,并不是单例的,这就是“错误的单例模式”,如何解决这个问题呢?

延迟加载 / “懒汉模式” 的解决方案

声明 synchronized 关键字

既然多个线程可以同时进入 getInstrance() 方法,那么只需要对 getInstance() 方法声明 synchronized 关键字即可。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    // 设置同步方法效率太低了
    // 整个方法被上锁
    synchronized public static MyObject getInstance() {
        try {
            if (myObject == null) {
                // 模式在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //486938195
        //486938195
        //486938195
    }
}

此方法加入同步 synchronized 关键字得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行。

尝试同步代码块

同步方法是对方法的整体进行持锁,这对运行效率来讲是不利的。改成同步代码块能解决吗?

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        try {
            // 此种写法等同于:
            // synchronized public static MyObject getInstance()
            // 的写法,效率一样很低,全部代码被上锁
            synchronized (MyObject.class) {
                if (myObject == null) {
                    // 模式在创建对象之前做一些准备性的工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}

此方法加入同步 synchronized 语句块得到相同实例的对象,但这种方法的运行效率也是非常低的,和 synchronized 同步方法一样是同步 运行的。继续更改代码尝试解决这个缺点。

针对某些重要的代码进行单独的同步

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        try {

            if (myObject == null) {
                // 模式在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                // 使用 synchronized (MyObject.class)
                // 虽然部分代码被上锁
                // 但还是有非线程安全问题
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //2096868475
        //834303525
        //486938195
    }
}

此方法使同步 synchronized 语句块,只对实例化对象的关键代码进行同步,从语句的结构上来讲,运行的效率的确得到了提升。但如果是遇到多线程的情况下还是无法解决得到同一个实例对象的结果。到底如何解决“懒汉模式”遇到多线程的情况呢?

使用 DCL 双重检查锁机制

在最后的步骤中,使用的是 DCL(double check lock) 双重检查锁来实现多线程环境中的延迟加载单例设计模式。

public class MyObject {

    private volatile static MyObject myObject;

    private MyObject() {

    }

    // 使用双重检查机制来解决问题,既保证了不需要同步代码的异步执行性
    // 又保证了单例的效果
    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //337436377
        //337436377
        //337436377
    }
}

使用双重检查锁功能,成功解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。

使用静态内置类实现单例模式

DCL 可以解决多线程单例模式的非线程安全问题。当然,使用其他的办法也能达到同样的效果。

public class MyObject {

    // 内部类方式
    private static class MyObjectHandler {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {

    }

    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }
}

使用静态内部类能保证线程安全的原因:

  1. 由于内部静态类只会被加载一次,故该实现方式是线程安全的
  2. 类加载的初始化阶段是单线程的

序列化与反序列化的单例模式实现

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

public class MyObject implements Serializable {

    private static final long serialVersionUID = 888L;

    // 内部类方式
    private static class MyObjectHandler {
        private static final MyObject MY_OBJECT = new MyObject();
    }

    private MyObject() {

    }

    public static MyObject getInstance() {
        return MyObjectHandler.MY_OBJECT;
    }

    // 反序列化时使用,如果注释这段代码,结果不是同一个对象
    /*protected Object readResolve() {
        System.out.println("调用了 readResolve 方法");
        return MyObjectHandler.MY_OBJECT;
    }*/
}
public class SaveAndRead {

    public static void main(String[] args) {
        try {
            MyObject myObject = MyObject.getInstance();
            FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));
            ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
            oosRef.writeObject(myObject);
            oosRef.close();
            fosRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));
            ObjectInputStream iosRef = new ObjectInputStream(fisRef);
            MyObject myObject = (MyObject) iosRef.readObject();
            iosRef.close();
            fisRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


        // 结果一:不是同一个对象
        //2133927002
        //1828972342

        // 结果二:是同一个对象
        //2133927002
        //调用了 readResolve 方法
        //2133927002
    }
}

使用 static 代码块实现单例模式

静态代码块中的代码再使用类的时候就已经执行了,所以可以应用静态代码块这个特性来实现单例设计模式。

public class MyObject {

    private static MyObject instance = null;

    private MyObject() {

    }

    // 静态代码块中的代码在使用类的时候就已经执行了
    static {
        instance = new MyObject();
    }

    public static MyObject getInstance() {
        return instance;
    }
}
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
}
public class Run {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();

        //331082196
        //331082196
        //331082196
        //331082196
        //331082196
        //331082196
    }
}

使用 enum 枚举数据类型实现单例模式

枚举 enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。

public class MyObject {

    private MyObject() {

    }

    /** 枚举类型是线程安全的,并且只会装载一次 */
    private enum Singleton {

        INSTANCE;

        private final MyObject instance;

        Singleton() {
            instance = new MyObject();
        }

        private MyObject getInstance() {
            return instance;
        }
    }

    public static MyObject getInstance() {
        return Singleton.INSTANCE.getInstance();
    }
}

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

拾遗增补

本章中将对前面几章遗漏的知识点进行补充,丰富多线程案例的完整性。

线程的状态

线程对象在不同的运行时期有不同的状态,状态信息就存在于 State 枚举类中

调用与线程有关的方法是造成线程状态改变的主要原因,其因果关系如下:

在调用与线程有关的方法后, 会进入不同的线程状态,这些状态之间某些是可双向切换的,比如 WAITING 和 RUNNING 状态之间可以循环地进行切换;而有些是单向切换的,比如线程销毁后并不能自动进入 RUNNING 状态。

验证 NEW、RUNNABLE 和 TERMINATED

NEW 状态是现场实例化后还从未执行 start() 方法时的状态,而 RUNNABLE 状态是现场进入运行的状态, TERMINATED 是线程被销毁时的状态。

public class MyThread extends Thread {

    public MyThread() {
        System.out.println("构造方法中的状态: " + Thread.currentThread().getState());
    }

    @Override
    public void run() {
        System.out.println("run方法中的状态: " + Thread.currentThread().getState());
    }
}
public class Run {

    //NEW
    //RUNNABLE
    //TERMINATED
    //BLOCKED
    //WAITING
    //TIMED_WAITING

    public static void main(String[] args) {
        try {
            MyThread t = new MyThread();
            System.out.println("main方法中的状态1: " + t.getState());
            Thread.sleep(1000);
            t.start();
            System.out.println("main方法中的状态2: " + t.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //构造方法中的状态: RUNNABLE
        //main方法中的状态1: NEW
        //main方法中的状态2: RUNNABLE
        //run方法中的状态: RUNNABLE
    }
}

注意:构造方法中打印出来的日志其实是显示 main 主线程的状态为 RUNNABLE

验证 TIMED_WAITING

线程状态 TIMED_WAITING 代表线程执行了 Thread.sleep() 方法,呈等待状态,等待时间到达,继续向下运行。

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            System.out.println("begin sleep");
            Thread.sleep(10000);
            System.out.println("end sleep");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 */
public class Run {

    //NEW
    //RUNNABLE
    //TERMINATED
    //BLOCKED
    //WAITING
    //TIMED_WAITING

    public static void main(String[] args) {
        try {
            MyThread t = new MyThread();
            System.out.println("main方法中的状态1: " + t.getState());
            t.start();
            System.out.println("main方法中的状态2: " + t.getState());
            Thread.sleep(1000);
            System.out.println("main方法中的状态3: " + t.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //main方法中的状态1: NEW
        //main方法中的状态2: RUNNABLE
        //begin sleep
        //main方法中的状态3: TIMED_WAITING
        //end sleep
    }
}

执行 sleep 方法后线程的状态枚举值就是 TIMED_WAITING。

验证 TIMED_WAITING

TIMED_WAITING 状态出现在一个线程在一个特定的等待时间内等待另一个线程完成一个动作。

public class MyService {

    synchronized static public void serviceMethod() {
        try {
            System.out.println(Thread.currentThread().getName() + " 进入了业务方法");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MyThread1 extends Thread {

    @Override
    public void run() {
        MyService.serviceMethod();
    }
}
public class MyThread2 extends Thread {

    @Override
    public void run() {
        MyService.serviceMethod();
    }
}
public class Run {

    //NEW
    //RUNNABLE
    //TERMINATED
    //BLOCKED
    //WAITING
    //TIMED_WAITING

    public static void main(String[] args) throws InterruptedException {

        MyThread1 t1 = new MyThread1();
        t1.setName("a");
        t1.start();
        System.out.println("main方法中的t1状态: " + t1.getState());
        MyThread2 t2 = new MyThread2();
        t2.setName("b");
        t2.start();
        Thread.sleep(1000);
        System.out.println("main方法中的t2状态: " + t1.getState());

        //main方法中的t1状态: RUNNABLE
        //a 进入了业务方法
        //main方法中的t2状态: TIMED_WAITING
        //b 进入了业务方法
    }
}

验证 WAITING

状态 WAITING 是线程执行了 Object.wait() 方法后所处的状态。

public class Lock {
    public static final Byte lock = new Byte("0");
}
public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            synchronized (Lock.lock) {
                Lock.lock.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    //NEW
    //RUNNABLE
    //TERMINATED
    //BLOCKED
    //WAITING
    //TIMED_WAITING

    public static void main(String[] args) {
        try {
            MyThread t = new MyThread();
            t.start();
            Thread.sleep(1000);
            System.out.println("main方法中t的状态: " + t.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //main方法中t的状态: WAITING
    }
}

执行 wait 方法后线程的状态枚举值就是 WAITING。

线程组

可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程。这样的组织结构有些类似于树的形式。

线程组的作用是:可以批量的管理线程或线程组对象,有效地对线程或线程组对象进行组织。

线程对象关联线程组: 1级关联

所谓的1级关联就是父对象中有子对象,但并不创建子孙对象。这种情况经常出现在开发中,比如创建一些线程时,为了有效地对这些线程进行组织管理,通常的情况下是创建一个线程组,然后再将部分线程归属到该组中。这里的处理可以对零散的线程对象进行有效的组织和规划。

public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("ThreadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("ThreadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {

        ThreadA aRunnable = new ThreadA();
        ThreadB bRunnable = new ThreadB();

        ThreadGroup group = new ThreadGroup("Ray 的线程组");
        Thread aThread = new Thread(group, aRunnable);
        Thread bThread = new Thread(group, bRunnable);

        aThread.start();
        bThread.start();
        System.out.println("活动的线程数: " + group.activeCount());
        System.out.println("线程组的名称: " + group.getName());

        //活动的线程数: 2
        //ThreadName = Thread-2
        //ThreadName = Thread-3
        //线程组的名称: Ray 的线程组
        //ThreadName = Thread-2
        //ThreadName = Thread-3
        //ThreadName = Thread-3
    }
}

打印的信息表上线程组中有两个线程,并且打印出了线程组的名称。另外,两个线程一直无限地并且每隔1秒打印日志。

线程对象关联线程组:多级关联

所谓的多级关联就是父对象中有子对象,子对象中再创建子对象,也就是出现子孙对象的效果了。但是这种写法在开发中不太常见,如果线程树结构设计得非常复杂反而不利于线程对象的管理,但JDK却提供了支持多级关联的线程树结果。

public class Run {

    public static void main(String[] args) {

        // 在 main 组中添加一个线程组A,然后在这个A组中添加线程对象Z
        // 方法 activeGroupCount 和 activeCount 的值不是固定的
        // 是系统中环境的一个快照
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        ThreadGroup group = new ThreadGroup(mainGroup, "A");

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("runMethod");
                    // 线程必须在运行状态才可以受组管理(管控)
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread newThread = new Thread(group, runnable);
        newThread.setName("Z");
        // 线程必须启动后才能归到组A中
        newThread.start();

        ThreadGroup[] listGroup = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        Thread.currentThread().getThreadGroup().enumerate(listGroup);
        System.out.println("main线程中有多少个子线程组? " + listGroup.length + " 名字为: " + listGroup[0].getName());

        Thread[] listThread = new Thread[listGroup[0].activeCount()];
        listGroup[0].enumerate(listThread);
        System.out.println(listThread[0].getName());

        //main线程中有多少个子线程组? 1 名字为: A
        //runMethod
        //Z
    }
}

本程序代码的结构就是 main 组创建一个新租,然后又在该新租中添加了线程。

线程组自动归属特性

自动归属就是自动归到当前线程组中。

public class Run {

    public static void main(String[] args) {

        // 方法 activeGroupCount 取得当前线程组对象中的子线程组数量
        // 方法 enumerate 的作用是将线程组中的子线程组以复制的形式
        // 拷贝到 ThreadGroup[] 数组对象中
        System.out.println("A处线程: " + Thread.currentThread().getName()
                + " 所属的线程组名: "
                + Thread.currentThread().getThreadGroup().getName()
                + " 中有线程组数量: "
                + Thread.currentThread().getThreadGroup().activeGroupCount());

        // 自动加到 main 组中
        ThreadGroup group = new ThreadGroup("新的组");

        System.out.println("A处线程: " + Thread.currentThread().getName()
                + " 所属的线程组名: "
                + Thread.currentThread().getThreadGroup().getName()
                + " 中有线程组数量: "
                + Thread.currentThread().getThreadGroup().activeGroupCount());

        ThreadGroup[] threadGroups = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        Thread.currentThread().getThreadGroup().enumerate(threadGroups);
        for (int i = 0; i < threadGroups.length; i++) {
            System.out.println("遍历线程组名称" + i + ": " + threadGroups[i].getName());
        }

        //A处线程: main 所属的线程组名: main 中有线程组数量: 0
        //A处线程: main 所属的线程组名: main 中有线程组数量: 1
        //遍历线程组名称0: 新的组
    }
}

本实验要证明的是,在实例化一个 ThreadGroup 线程组 x 时如果不指定所属的线程组,则 x 线程组自动归到当前线程对象所属的线程组中,也就是隐式地在一个线程组中添加了一个子线程组,所以在打印的线程组数量值由0变成1。

获取根线程组

public class Run {

    public static void main(String[] args) {

        System.out.println("线程: " + Thread.currentThread().getName()
                + " 所在的线程组名: " + Thread.currentThread().getThreadGroup().getName());

        System.out.println("main 线程所在的线程组的父线程组的名称: "
                + Thread.currentThread().getThreadGroup().getParent().getName());

        System.out.println("main 线程所在的线程组的父线程组的父线程组的名称: "
                + Thread.currentThread().getThreadGroup().getParent().getParent().getName());

        //线程: main 所在的线程组名: main
        //main 线程所在的线程组的父线程组的名称: system
        //Exception in thread "main" java.lang.NullPointerException
    }
}

运行结果说明 JVM 的根线程组就是 system,再取其父类线程组则出现空异常。

线程组里加线程组

public class Run {

    public static void main(String[] args) {

        System.out.println("线程组名: " + Thread.currentThread().getThreadGroup().getName());
        System.out.println("线程组中活动的线程数量: " + Thread.currentThread().getThreadGroup().activeCount());

        System.out.println("线程组中线程组的数量: " + Thread.currentThread().getThreadGroup().activeGroupCount());
        ThreadGroup newGroup = new ThreadGroup(Thread.currentThread().getThreadGroup(), "newGroup");
        System.out.println("线程组中线程组的数量: " + Thread.currentThread().getThreadGroup().activeGroupCount());

        System.out.println("父线程组名: " + Thread.currentThread().getThreadGroup().getParent().getName());

        //线程组名: main
        //线程组中活动的线程数量: 2
        //线程组中线程组的数量: 0
        //线程组中线程组的数量: 1
        //父线程组名: system
    }
}

本实验用显式的方式在一个线程组中添加了一个子线程组。

组内的线程批量停止

public class MyThread extends Thread {

    public MyThread(ThreadGroup group, String name) {
        super(group, name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始死循环");
        while (!this.isInterrupted()) {

        }
        System.out.println(Thread.currentThread().getName() + "结束死循环");
    }
}
public class Run {

    public static void main(String[] args) {

        try {
            ThreadGroup group = new ThreadGroup("我的线程组");
            for (int i = 0; i < 2; i++) {
                MyThread thread = new MyThread(group, "线程" + (i+1));
                thread.start();
            }
            Thread.sleep(5000);
            group.interrupt();
            System.out.println("调用了 interrupt 方法");
        } catch (InterruptedException e) {
            System.out.println("停了停了");
            e.printStackTrace();
        }

        //线程1开始死循环
        //线程2开始死循环
        //调用了 interrupt 方法
        //线程1结束死循环
        //线程2结束死循环
    }
}

通过将线程归属到线程组中,当调用线程组 ThreadGroup 的 interrupt 方法时,可以将该组中的所有正在运行的线程批量停止。

递归与非递归取得组内对象

public class Run {

    public static void main(String[] args) {

        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        ThreadGroup groupA = new ThreadGroup(mainGroup, "A");
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("runMethod!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        ThreadGroup groupB = new ThreadGroup(groupA, "B");
        // 分配空间,但不一定全部用完
        ThreadGroup[] listGroup1 = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        // 传入 true 是递归取得子组和子孙组
        Thread.currentThread().getThreadGroup().enumerate(listGroup1, true);
        for (int i = 0; i < listGroup1.length; i++) {
            if (listGroup1[i] != null) {
                System.out.println(listGroup1[i].getName());
            }
        }

        System.out.println("-------------------------------------");

        ThreadGroup[] listGroup2 = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        Thread.currentThread().getThreadGroup().enumerate(listGroup2, false);
        for (int i = 0; i < listGroup2.length; i++) {
            if (listGroup2[i] != null) {
                System.out.println(listGroup2[i].getName());
            }
        }

        //A
        //B
        //-------------------------------------
        //A
    }
}

使线程具有有序性

正常的情况下,线程在运行时多个线程之间执行任务的时机是无序的。可以通过改造代码的方式使它们运行具有有序性。

public class MyThread extends Thread {

    private Object lock;
    private String showChar;
    private int showNumPosition;
    private int printCount; // 统计打印了几个字母
    volatile private static int addNumber = 1;

    public MyThread(Object lock, String showChar, int showNumPosition) {
        this.lock = lock;
        this.showChar = showChar;
        this.showNumPosition = showNumPosition;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                while (true) {
                    //第一次进来
                    //%表示求余运算符,运算结果为左操作数除以右操作数所得的余数。
                    //1%3表示将1除以3,结果为0,余数为1,所以1%3等于1
                    if (addNumber % 3 == showNumPosition) {
                        System.out.println("ThreadName = " + Thread.currentThread().getName()
                                + " runCount = " + addNumber + " " + showChar);
                        lock.notifyAll();
                        addNumber++;
                        printCount++;
                        if (printCount == 3) {
                            break;
                        }
                    } else {
                        lock.wait();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Run {

    public static void main(String[] args) {

        Object lock = new Object();
        MyThread a = new MyThread(lock, "A", 1);
        MyThread b = new MyThread(lock, "B", 2);
        MyThread c = new MyThread(lock, "C", 0);
        a.start();
        b.start();
        c.start();

        //ThreadName = Thread-0 runCount = 1 A
        //ThreadName = Thread-1 runCount = 2 B
        //ThreadName = Thread-2 runCount = 3 C
        //ThreadName = Thread-0 runCount = 4 A
        //ThreadName = Thread-1 runCount = 5 B
        //ThreadName = Thread-2 runCount = 6 C
        //ThreadName = Thread-0 runCount = 7 A
        //ThreadName = Thread-1 runCount = 8 B
        //ThreadName = Thread-2 runCount = 9 C
    }
}

SimpleDateFormat 非线程安全

类 SimpleDateFormat 主要负责日期的转换与格式化,但在多线程的环境中,使用此类容易造成数据转换及处理的不准确,因为 SimpleDateFormat 类不是线程安全的。

出现异常

实现使用类 SimpleDateFormat 在多线程环境下处理日期但得出的结果却是错误的情况,这也是在多线程环境开发中容易遇到的问题。

public class MyThread extends Thread {

    private SimpleDateFormat sdf;
    private String dateString;

    public MyThread(SimpleDateFormat sdf, String dateString) {
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = sdf.parse(dateString);
            String newDateString = sdf.format(dateRef).toString();
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName = " + this.getName()
                    + " 报错了 日期字符串: " + dateString + " 转换成的日期为: " + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}
public class Test {

    public static void main(String[] args) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] dateStringArray = new String[] {"2020-01-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01"};
        MyThread[] threads = new MyThread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new MyThread(sdf, dateStringArray[i]);
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }

        //ThreadName = Thread-8 报错了 日期字符串: 2020-04-01 转换成的日期为: 2020-03-01
        //ThreadName = Thread-3 报错了 日期字符串: 2020-05-01 转换成的日期为: 2029-03-01
        //ThreadName = Thread-0 报错了 日期字符串: 2020-01-01 转换成的日期为: 2029-03-01
        //ThreadName = Thread-6 报错了 日期字符串: 2020-05-01 转换成的日期为: 2020-03-01
    }
}

使用单例的 SimpleDateFormat 类在多线程的环境中处理日期,极易出现日期转换错误的情况。

解决异常方法1

public class DateTools {

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return new SimpleDateFormat(formatPattern).parse(dateString);
    }

    public static String format(String formatPattern, Date date) {
        return new SimpleDateFormat(formatPattern).format(date).toString();
    }
}
public class MyThread extends Thread {

    private SimpleDateFormat sdf;
    private String dateString;

    public MyThread(SimpleDateFormat sdf, String dateString) {
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = DateTools.parse("yyyy-MM-dd", dateString);
            String newDateString = DateTools.format("yyyy-MM-dd", dateRef);
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName = " + this.getName()
                        + " 报错了 日期字符串: " + dateString + " 转换成的日期为: " + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}
public class Test {

    public static void main(String[] args) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] dateStringArray = new String[] {"2020-01-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01"};
        MyThread[] threads = new MyThread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new MyThread(sdf, dateStringArray[i]);
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
    }
}

没有输出任何异常,解决处理错误的原理其实就是创建了多个 SimpleDateFormat 类的实例。

解决异常方法2

前面介绍过,ThreadLocal 类能使线程绑定到指定的对象。使用该类也可以解决多线程环境下 SimpleDateFormat 类处理错误的情况。

public class DateTools {

    private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<>();

    public static SimpleDateFormat getSimpleDateFormat(String datePattern) {
        SimpleDateFormat sdf = null;
        sdf = t1.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat(datePattern);
            t1.set(sdf);
        }
        return sdf;
    }
}
public class MyThread extends Thread {

    private SimpleDateFormat sdf;
    private String dateString;

    public MyThread(SimpleDateFormat sdf, String dateString) {
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = DateTools.getSimpleDateFormat("yyyy-MM-dd").parse(dateString);
            String newDateString = DateTools.getSimpleDateFormat("yyyy-MM-dd").format(dateRef).toString();
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName = " + this.getName()
                        + " 报错了 日期字符串: " + dateString + " 转换成的日期为: " + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}
public class Test {

    public static void main(String[] args) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] dateStringArray = new String[] {"2020-01-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01",
                "2020-03-01", "2020-04-01", "2020-05-01"};
        MyThread[] threads = new MyThread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new MyThread(sdf, dateStringArray[i]);
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
    }
}

运行结果和上面一样的。

线程中出现异常的处理

public class MyThread extends Thread {
    @Override
    public void run() {
        String username = null;
        System.out.println(username.hashCode());
    }
}
public class Main1 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();

        //Exception in thread "Thread-0" java.lang.NullPointerException
    }
}

程序运行后再控制台输出空指针异常。在 Java 的多线程技术中,可以对多线程中的异常进行“捕捉”,使用的是 UncaughtExceptionHandler 类,从而可以对发生的异常进行有效的处理。

public class Main2 {

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        t1.setName("t1");
        t1.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("线程: " + t.getName() + " 出现了异常: ");
                e.printStackTrace();
            }
        });
        t1.start();

        //线程: t1 出现了异常:
        //java.lang.NullPointerException

        MyThread t2 = new MyThread();
        t2.setName("t2");
        t2.start();
        //Exception in thread "t2" java.lang.NullPointerException
    }
}

方法 setUncaughtExceptionHandler() 的作用是对指定的线程对象设置默认的异常处理器。在 Thread 类中还可以使用 setDefaultUncaughtExceptionHandler() 方法对所有线程对象设置异常处理器。

public class Main3 {

    public static void main(String[] args) {
        MyThread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("线程: " + t.getName() + " 出现了异常: ");
                e.printStackTrace();
            }
        });

        MyThread t1 = new MyThread();
        t1.setName("t1");
        t1.start();

        MyThread t2 = new MyThread();
        t2.setName("t2");
        t2.start();

        //线程: t1 出现了异常:
        //线程: t2 出现了异常:
        //java.lang.NullPointerException
        //java.lang.NullPointerException
    }
}

方法 setDefaultUncaughtExceptionHandler() 的作用是为指定线程类的所有线程对象设置默认的异常处理器。

线程组内处理异常

public class MyThread extends Thread {

    private String num;

    public MyThread(ThreadGroup group, String name, String num) {
        super(group, name);
        this.num = num;
    }

    @Override
    public void run() {
        int numInt = Integer.parseInt(num);
        while (true) {
            //System.out.println("numInt = " + numInt);
            System.out.println("死循环中: " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000); // 为了方便看到结果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Run {

    public static void main(String[] args) {

        ThreadGroup group = new ThreadGroup("我的线程组");

        MyThread newT = new MyThread(group, "报错线程", "a");
        newT.start();

        MyThread[] myThreads = new MyThread[5];
        for (int i = 0; i < myThreads.length; i++) {
            myThreads[i] = new MyThread(group, "线程" + (i + 1), "1");
            myThreads[i].start();
        }
    }
}

程序运行后,其中一个线程出现异常,但其他线程却一直以死循环的方式持续打印结果。

在默认的情况下,线程组中的一个线程出现异常不会影响其他线程的运行。

如果想要实现线程组内一个线程出现异常后全部线程都停止运行该如何实现呢?

public class MyThreadGroup extends ThreadGroup {

    public MyThreadGroup(String name) {
        super(name);
    }

    // t 参数是出现异常的线程对象
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        super.uncaughtException(t, e);
        this.interrupt();
    }
}
public class MyThread extends Thread {

    private String num;

    public MyThread(ThreadGroup group, String name, String num) {
        super(group, name);
        this.num = num;
    }

    @Override
    public void run() {
        int numInt = Integer.parseInt(num);
        while (this.isInterrupted() == false) {
            //System.out.println("numInt = " + numInt);
            System.out.println("死循环中: " + Thread.currentThread().getName());
        }
    }
}
public class Run {

    public static void main(String[] args) {

        MyThreadGroup group = new MyThreadGroup("我的线程组");

        MyThread newT = new MyThread(group, "报错线程", "a");
        newT.start();

        MyThread[] myThreads = new MyThread[5];
        for (int i = 0; i < myThreads.length; i++) {
            myThreads[i] = new MyThread(group, "线程" + (i + 1), "1");
            myThreads[i].start();
        }
    }
}

程序运行后,其中一个线程出现异常,其他线程全部停止了。

线程异常处理的传递

前面介绍了若干个线程异常处理的方式,那么这些处理的方式如果放在一起运行,会出现什么样的运行结果呢?

public class MyThread extends Thread {

    private String num = "a";

    public MyThread() {
        super();
    }

    public MyThread(ThreadGroup group, String name) {
        super(group, name);
    }

    @Override
    public void run() {
        int numInt = Integer.parseInt(num);
        System.out.println("在线程中打印: " + (numInt + 1));
    }
}
public class MyThreadGroup extends ThreadGroup {

    public MyThreadGroup(String name) {
        super(name);
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        super.uncaughtException(t, e);
        System.out.println("线程组的异常处理");
        e.printStackTrace();
    }
}
public class ObjectUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("对象的异常处理");
        e.printStackTrace();
    }
}
public class StateUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("静态的异常处理");
        e.printStackTrace();
    }
}
public class Run1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 对象
        myThread.setUncaughtExceptionHandler(new ObjectUncaughtExceptionHandler());
        // 类
        MyThread.setDefaultUncaughtExceptionHandler(new StateUncaughtExceptionHandler());
        myThread.start();

        //对象的异常处理
        //java.lang.NumberFormatException: For input string: "a"
    }
}

更改 Run1 部分代码

public class Run1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 对象
        //myThread.setUncaughtExceptionHandler(new ObjectUncaughtExceptionHandler());
        // 类
        MyThread.setDefaultUncaughtExceptionHandler(new StateUncaughtExceptionHandler());
        myThread.start();

        //静态的异常处理
        //java.lang.NumberFormatException: For input string: "a"
    }
}

继续实验

public class Run2 {
    public static void main(String[] args) {
        MyThreadGroup group = new MyThreadGroup("我的线程组");
        MyThread myThread = new MyThread(group, "我的线程");
        // 对象
        myThread.setUncaughtExceptionHandler(new ObjectUncaughtExceptionHandler());
        // 类
        MyThread.setDefaultUncaughtExceptionHandler(new StateUncaughtExceptionHandler());
        myThread.start();

        //对象的异常处理
        //java.lang.NumberFormatException: For input string: "a"
    }
}

更改 Run2 部分代码

public class Run2 {
    public static void main(String[] args) {
        MyThreadGroup group = new MyThreadGroup("我的线程组");
        MyThread myThread = new MyThread(group, "我的线程");
        // 对象
        //myThread.setUncaughtExceptionHandler(new ObjectUncaughtExceptionHandler());
        // 类
        MyThread.setDefaultUncaughtExceptionHandler(new StateUncaughtExceptionHandler());
        myThread.start();
        
        //静态的异常处理
        //线程组的异常处理
        //java.lang.NumberFormatException: For input string: "a"
        //java.lang.NumberFormatException: For input string: "a"
    }
}

本示例想要打印 “静态的异常处理” 的信息,则必须在 public void uncaughtException(Thread t, Throwable e) 方法中加上 super.uncaughtException(t, e); 代码。

继续更改 Run2 部分代码

public class Run2 {
    public static void main(String[] args) {
        MyThreadGroup group = new MyThreadGroup("我的线程组");
        MyThread myThread = new MyThread(group, "我的线程");
        // 对象
        //myThread.setUncaughtExceptionHandler(new ObjectUncaughtExceptionHandler());
        // 类
        //MyThread.setDefaultUncaughtExceptionHandler(new StateUncaughtExceptionHandler());
        myThread.start();

        //Exception in thread "我的线程" java.lang.NumberFormatException: For input string: "a"
        //线程组的异常处理
    }
}

本书完 2020年8月3日