JAVA多线程理解

  线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。多线程程序应用能够在同一时间执行多于一个任务,进而提升整体处理性能。本文针对JAVA中线程模块做了简单的解释与实战,介绍了线程的相关概念,锁的概念,线程间通信的方法等,内容比较简单,但是却比较繁琐,概念性东西比较多,有些内容比较细节,但是却在面试或者使用中是不可忽略的东西。


文章目录


前言

  本文将从最基础的部分开始讲解JAVA线程的知识,最后深入了解JAVA多线程的高级应用以及优化。适用于刚接触JAVA多线程的小伙伴。本文中所涉及的项目以及代码点赞或者评论后均可后台私聊获取,欢迎小伙伴深夜骚扰。


一、线程基础

1、线程和进程的概念

  打开一个软件就是一个进程,一个进程分多个线程运行。简单的说,进程是操作系统运行的最小单位,线程是操作系统分配资源的最小单位。举个例子:我们打开QQ.exe,就已经开启了一个进行,我们在QQ上一边视频通话一边打字聊天,这便是多个QQ线程在工作帮我们完成的。
在这里插入图片描述

2、多线程

2.1、线程的创建方式

  继承Thread类,重写run()方法,调用start()方法启动线程。
  实现Runnable接口,重写run()方法,调用start()方法启动线程。
  使用线程工具类创建线程。比如:Executors.newFixedThreadPool()创建线程池,调用eecute()执行。

2.2、线程的几个常用方法

  currentThread()方法:Thread.currenThread()返回一个当前正在运行的线程对像,通过这个返回值可以获取到当前线程的一些信息,比如:线程名,线程ID等信息。
在这里插入图片描述
  sleep()方法:Thread.sleep(100)表示让当前正在运行的线程睡眠(暂停)一段时间,单位毫秒。
在这里插入图片描述
  getId()方法:Thread.currentThread().getId()可获取当前线程的唯一标识。
  isAlive()方法:Thread.currentThread().isAlive()用来判断当前线程是否处于活跃状态。如果线程已经启动但未结束,那么线程就处于活跃状态。
示例:

public class Thread01 {
    public static void main(String[] args) {
        ThreadA ta = new ThreadA();
        new Thread(ta, "A").start();
    }
}
class ThreadA extends Thread {

   public void run() {
    System.out.println("当前线程的名字为:" + Thread.currentThread().getName());
    System.out.println("当前线程的存活状态为:" + Thread.currentThread().isAlive());
    System.out.println("当前线程的ID为:" + Thread.currentThread().getId());
   }
}

运行结果:
在这里插入图片描述

3、暂停线程

  停止线程可以使用stop()、suspend()、resume()三个方法停止线程,但是停止线程可能会造成一些未知的错误,目前JDK中这三个方法已经被标记为已弃用,所以不推荐使用这三个方法停止线程,下边着重介绍interrupt()方法停止线程。

3.1 stop()、suspend()、resume()停止线程

  stop()、suspend()、resume()被标记为已弃用的原因分析:
  stop()方法暴力停止线程会造成线程并释放锁,如果立即停止线程可能会造成一些清理工作突然停止,无法彻底完成。突然释放锁会导致数据不能及时同步,会出现一些数据不同步的问题。
示例:

// 创建Thread01.java
public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        User  us = new User();
        ThreadA1 ta1 = new ThreadA1(us);
        ta1.start();
        Thread.sleep(500);
        ta1.stop();
        System.out.println(us.getName() + " " + us.getPass());
    }
}

// 创建ThreadA1.java
class ThreadA1 extends Thread {
    private User us;

    public ThreadA1(User us) {
        this.us = us;
    }

    @Override
    public void run() {
        try {
            us.setPassAndName("bb", "bb");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 创建实体类User.java
class User {
    private String name = "aa";
    private String pass = "aa";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPass() {
        return pass;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }

    public synchronized void setPassAndName (String name, String pass) throws InterruptedException {
        this.name = name;
        Thread.sleep(10000);
        this.pass = pass;
    }
}

运行结果:
在这里插入图片描述
  suspend()暂停线程,暂停线程之后,可以用resume()恢复被暂停的线程。暂停线程会造成线程数据不同步的问题,suspend()、resume()的使用还可能会造成公共资源被独占,从而导致其他线程无法访问公共资源。
示例:

// 创建Thread01.java
public class Thread02 {
    public static void main(String[] args) throws InterruptedException {
        User02  us = new User02();
        new Thread(() -> {
            System.out.println("线程A启动了");
            Thread.currentThread().setName("A");
            us.setPassAndName();
            System.out.println("线程A结束了");
        }).start();
        new Thread(() -> {
            System.out.println("线程B启动了");
            Thread.currentThread().setName("B");
            us.setPassAndName();
            System.out.println("线程B结束了");
        }).start();
    }
}

// 创建实体类User02.java
class User02 {
    public synchronized void setPassAndName () {
        System.out.println("进入同步方法");
        if (Thread.currentThread().getName().equals("A")) {
            System.out.println("A 线程被 suspend");
            Thread.currentThread().suspend();
        }
        System.out.println("退出同步方法");
    }
}

运行结果:
在这里插入图片描述
  线程A进入同步方法后被suspend了,导致对象被独占,所以线程B只打印了开始,而无法进入同步方法。

3.2 interrupt()停止线程

  使用interrupt()停止线程,并不会立即停止线程,以下代码示例将会证明这点:
示例:

// 创建Thread03.java
public class Thread03 {
    public static void main(String[] args) throws InterruptedException {
        User03  us = new User03();
        new Thread(() -> {
            System.out.println("线程A启动了");
            Thread.currentThread().setName("A");
            us.setPassAndName();
            System.out.println("线程A结束了");
        }).start();
    }
}

// 创建实体类User03.java
class User03 {
    public synchronized void setPassAndName () {
        System.out.println("进入同步方法");
        System.out.println("当前线程被interrupt了");
        Thread.currentThread().interrupt();
        System.out.println("退出同步方法");
    }
}

运行结果:
在这里插入图片描述
  如上结果可以看出,同步方法被interrupt了,但是后边的代码还是被执行打印出来了,这说明interrupt并不是正真的停止线程。

3.3 使用interrupted()和isInterrupted()判断线程状态

示例:更改Thread03.java中的main方法如下

public static void main(String[] args) throws InterruptedException {
    User03  us = new User03();
    new Thread(() -> {
        System.out.println("线程A启动了");
        Thread.currentThread().setName("A");
        us.setPassAndName();
        System.out.println("当前线程的状态为" + Thread.currentThread().isInterrupted());
        System.out.println("当前线程的状态为" + Thread.currentThread().interrupted());
        System.out.println("线程A结束了");
    }).start();
}

运行结果:
在这里插入图片描述
  由此可以看出,线程被interrupt了之后,线程的确是停止了,因为线程线程状态为interrrupt了,但是后边的语句还是被执行了。
示例:更改Thread03.java中的main方法如下(调换两个输出当前线程状态的语句),结果如下:

// 创建Thread01.java
public class Thread03 {
    public static void main(String[] args) throws InterruptedException {
        User03  us = new User03();
        new Thread(() -> {
            System.out.println("线程A启动了");
            Thread.currentThread().setName("A");
            us.setPassAndName();
            System.out.println("当前线程的状态为" + Thread.currentThread().interrupted());
            System.out.println("当前线程的状态为" + Thread.currentThread().isInterrupted());
            System.out.println("线程A结束了");
        }).start();
    }
}

运行结果:
在这里插入图片描述
  调换两个打印线程状态的位置后,发现打印出来的线程状态一个false,一个为ture,这是因为interrupted()方法有线程状态清除的功能,先调用interrupted()方法判断线程状态的确是被interrupt了,所以为true,紧接着interrupted()方法将该该线程的interrupt为true的状态清除掉(如下图中源码中标出的注释),所以后边调用isInterrupted()方法来判断线程状态的时候就为false。
在这里插入图片描述

3.4 使用interrupt()方法和抛出异常法停止线程(推荐)

示例:

// 创建Thread04.java
public class Thread04 {
    public static void main(String[] args) throws InterruptedException {
        User04  us = new User04();
        new Thread(() -> {
            System.out.println("线程A启动了");
            Thread.currentThread().setName("A");
            try {
                us.setPassAndName();
                System.out.println("线程A结束了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

// 创建实体类User04.java
class User04 {
    public synchronized void setPassAndName () throws InterruptedException {
        System.out.println("进入同步方法");
        System.out.println("当前线程被interrupt了");
        Thread.currentThread().interrupt();
        throw new InterruptedException();
    }
}

运行结果:
在这里插入图片描述
  使用异常法停止线程,可以看到线程是正真被停止了,“线程A结束了”没有打印出来,说明线程并没有继续执行。

4、线程的优先级

  JAVA中的线程具有优先级,优先级越高的线程执行级别就越高,抢占资源的能力就越强。但是优先级高的线程不一定最先执行完,也就是说,线程是否先执行结束与优先级没有关系。JAVA提供setPriority()方法设置线程优先级,范围为1~10。
示例:

/**
 * 线程优先级测试类
 */
// 创建Thread05.java
public class Thread05 {
    public static void main(String[] args) {
        ThreadA05 threadA05 = new ThreadA05();
        threadA05.setName("A");
        threadA05.setPriority(1);
        ThreadB05 threadB05 = new ThreadB05();
        threadB05.setName("B");
        threadB05.setPriority(10);
        threadA05.start();
        threadB05.start();
    }
}

// 创建ThreadA05.java
class ThreadA05 extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

// 创建ThreadB05.java
class ThreadB05 extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

运行结果:
在这里插入图片描述
  线程B的优先级被设置为10,线程A的优先级被设置为1,总是线程B先开始执行,但是却是最后一个执行结束的,所以证实线程是否先执行完与优先级没有关系。再次实验发现,当两个线程的优先级差别不是很大的时候,就不会存在优先级大的线程先执行的问题,所以说线程的执行在大多数情况下是随机的。

4.1 线程的优先级具有继承特点

  如果设置A线程的优先级为6,再用A线程调用B线程,那么B线程的优先级也是6。
示例:创建如下代码

/**
 * 线程优先级继承特性测试类
 */
// 创建Thread06.java
public class Thread06 {
    public static void main(String[] args) {
        Thread.currentThread().setPriority(6);
        ThreadD06 threadD06 = new ThreadD06();
        threadD06.setName("D");
threadD06.start();
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
    }
}

// 创建ThreadC06.java
class ThreadC06 extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

// 创建ThreadD06.java
class ThreadD06 extends ThreadC06 {
    @Override
    public void run() {
        ThreadC06 threadC06 = new ThreadC06();
        threadC06.setName("C");
        threadC06.start();
        System.out.println(Thread.currentThread().getName() + "执行开始");
        System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

运行结果:
在这里插入图片描述
  设置main线程的优先级为6,main调用D线程,D线程调用C线程,发现优先级具有继承性,mian、D、C三个线程的优先级全部被修改为6。

5、守护线程

  守护线程是一直在后台运行的线程,直到所有的非守护线程都不存在了。JVM中一般有两种线程,一种是用户线程,一种是守护线程,最典型的守护线程是垃圾回收线程(GC线程)。守护线程会在最后一个非守护线程结束后而自动销毁。JAVA中提供setDaemon(true/false)来标记线程为守护线程或者是用户线程。源码如下:
在这里插入图片描述

二、线程间通信

1、等待/通知(wait()/notify())机制

  等待/通知机制就是,某个线程在执行的过程中等待另外一个线程通知它,它获取到通知后就会继续执行。最典型的就是就餐问题,送餐员必须等待厨师通知他可以取餐的时候,他才可以去取餐,否则他只能一直在等待。
wait()/notify()必须在synchronized修饰的方法或者代码中调用,也就是说,必须得有锁对象才能正常使用,否则就会报IllegalMonitorStateException。
示例:

/**
 * 等待通知代码测试类
 */
// 创建Thread09.java
public class Thread07 {
    public static void main(String[] args) {
        Object obj = new Object();
        Thread T1 = new Thread(() -> {
            System.out.println("T1 start");
            synchronized (obj) {
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("T1 end");
        });
        T1.start();

        Thread T2 = new Thread(() -> {
            System.out.println("T2 start");
            synchronized (obj) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(i);
                }
                obj.notify();
            }
            System.out.println("T2 end");
        });
        T2.start();
    }
}

运行结果:
在这里插入图片描述
  T1线程启动后打印了“T1 start”后调用了wait()方法,进入等待队列,T2线程启动后,调用了notify()方法唤醒了T1线程,使得T1线程继续执行,打印了“T1 end”这句话。这就是wait()/notify()最基础的用法。

1.2、wait()执行后会立即释放锁,notify()执行后不立即释放锁,必须等待同步方法或者同步代码块里面的代码执行完才会释放锁。

示例:

/**
 * wait()/notify()执行后所释放时间验证类
 */
public class Thread08 {
    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA10 threadA10 = new ThreadA10();
        new Thread(() -> {
            synchronized (lock) {
                Thread.currentThread().setName("T1");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                threadA10.printString();
            }
        }).start();

        new Thread(() -> {
            synchronized (lock) {
                Thread.currentThread().setName("T2");
                threadA10.printString();
                lock.notify();
                for (int i = 0; i < 10; i++) {
                    System.out.println(i);
                }
            }
        }).start();
    }
}

class ThreadA10 {
    public void printString () {
        System.out.println(Thread.currentThread().getName());
    }
}

运行结果:
在这里插入图片描述
  T1线程和T2线程都是用了同一把lock锁,T1线程启动后调用了wait()方法,进入等待队列,T2线程启动后,立马打印了“T2”,说明T1线程立马释放了锁被T2线程获得了lock锁。T2调用了notify()方法唤醒了T1线程,但是T1线程并没有立马,而是等到T2线程执行完后边的代码后,T1线程才继续执行,打印了“T1”这句话。这就说明wait()执行后会立即释放锁,notify()执行后不立即释放锁,必须等待同步方法或者同步代码块里面的代码执行完才会释放锁。

1.3、等待通知的经典案例-生产者/消费者模式

  生产者/消费者模式指的是,生产者生产好数据后通知消费者消费数据,而消费者消费完数据后,又通知生产者生产数据,这是一个往返循环的过程。生产者/消费者模式在处理过程中也是使用的wait()/notify()机制,但是这中机制可能会造成一种假死的现象。下边模拟这中情况。
示例:

/**
 * 生产者/消费者模式(wait()/notify())执行后出现假死现象验证类
 */
public class Thread09 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        P p = new P(lock);
        C c = new C(lock);
        for (int i = 0; i < 2; i++) {
            ThreadA09 threadA09 = new ThreadA09(p);
            ThreadB09 threadB09 = new ThreadB09(c);
            threadA09.setName("P" + i);
            threadB09.setName("C" + i);
            threadA09.start();
            threadB09.start();
        }
        Thread.sleep(500);
        Thread[] threads = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threads);
        for (int i = 0; i < threads.length; i++) {
            System.out.println(threads[i].getName() + " " + threads[i].getState());
        }
    }
}

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

// 生产者
class P {
    private Object lock;

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

    public void product() {
        synchronized (lock) {
            while (!"".equals(ValueObject.value)) {
                try {
                    System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING了");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ValueObject.value = "" + System.currentTimeMillis();
            System.out.println("生产者 " + Thread.currentThread().getName() + " RUNNAVBLE了");
            lock.notify();
        }
    }
}

// 消费者
class C {
    private Object lock;

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

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

class ThreadA09 extends Thread {
    private P p;

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

    @Override
    public void run() {
        while (true) {
            p.product();
        }
    }
}

class ThreadB09 extends Thread {
    private C c;

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

    @Override
    public void run() {
        while (true) {
            c.consume();
        }
    }
}

运行结果:
在这里插入图片描述
  从结果可以看出,所有的生产者/消费者线程都成WAITING状态了,而程序还是运行状态,线程出现假死状态,出现假死状态的可能原因是:notify()是随机唤醒一个等待的线程,有可能是“生态者”唤醒“生态者”或者是“消费者”唤醒“消费者”,不停的这样操作,导致最后所有的线程都成WAITING状态,出现假死装状态。
  从上边的分析可以看出,假死的原因就是因为因为不断的唤醒了同类,解决的办法就是将lock.notify()换成lock.notifyAll()。每次唤醒的线程的时候,将所有的等待的线程全部唤醒。将lock.notify()换成lock.notifyAll()后,会发现程序将会不停的执行下去。

1.4、 等待/通知通信模式实战

  使用等待/通知模式实现交叉备份数据的需求。要求:起10个线程,5个线程备份数据到数据库A,5个线程备份数据到数据库B,并且线程之间交叉执行备份动作。
示例:

/**
 * 等待/通知m模式实战:交叉备份数据
 * 要求:起10个线程,5个线程备份数据到数据库A,5个线程备份数据到数据库B
 *      并且要求线程交叉执行
 */
public class Thread11 {
    public static void main(String[] args) {
        BckDada bckDada = new BckDada();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    bckDada.BckDadaA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();

            new Thread(() -> {
                try {
                    bckDada.BckDadaB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

class BckDada {
    private volatile boolean isStartBckA = false;

    public synchronized void BckDadaA () throws InterruptedException {
        while (isStartBckA) {
            this.wait();
        }
        System.out.println("######");
        isStartBckA = true;
        this.notifyAll();
    }

    public synchronized void BckDadaB () throws InterruptedException {
        while (!isStartBckA) {
            this.wait();
        }
        System.out.println("&&&&&&");
        isStartBckA = false;
        this.notifyAll();
    }
}

运行结果:
在这里插入图片描述
  注意:实现的关键在于private volatile boolean isStartBckA = false这个变量。

2、使用管道进行通信

  管道(pipeStream)是是一种特殊的输入/输出流,用于不同线程间传输数据,一个线程发送数据到输出管道中,另外一个线程从输入管道中获取数据。
  JAVA提供了四个类来实现管道通信:
  字符流管道:PipedWriter、PipedRead
  字节流管道:PipedOutputStream、PipedInputStream
示例:

/**
 * 线程间管道通信测试类
 * 1、字节流方式
 * 2、字符流方式
 */
public class Thread10 {
    public static void main(String[] args) throws InterruptedException {
        // 管道:字节流方式
        PipedInputStream inputStream = new PipedInputStream();
        PipedOutputStream outputStream = new PipedOutputStream();

        // 管道:字符流方式
        PipedWriter pipedWriter = new PipedWriter();
        PipedReader pipedReader = new PipedReader();

        try {
            pipedWriter.connect(pipedReader);
            // pipedReader.connect(pipedWriter);

            inputStream.connect(outputStream);
            // outputStream.connect(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        PipelineReadData pipelineReadData = new PipelineReadData();
        new Thread(() -> pipelineReadData.readData(inputStream)).start();
        new Thread(() -> pipelineReadData.readData(pipedReader)).start();
        Thread.sleep(500);
        PipelineWriteData pipelineWriteData = new PipelineWriteData();
        new Thread(() -> pipelineWriteData.writeData(outputStream)).start();
        new Thread(() -> pipelineWriteData.writeData(pipedWriter)).start();
    }
}

class PipelineReadData {
    public void readData (PipedInputStream inputData) {
        try {
            byte[] bytes = new byte[1024];
            int dataLength = inputData.read(bytes);
            System.out.println("readData read (字节流方式):");
            while (-1 != dataLength) {
                System.out.println(new String(bytes, 0, dataLength));
                dataLength = inputData.read(bytes);
            }
            inputData.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readData (PipedReader inputData) {
        try {
            char[] chars = new char[1024];
            int dataLength = inputData.read(chars);
            System.out.println("readData read (字符流方式):");
            while (-1 != dataLength) {
                System.out.println(new String(chars, 0, dataLength));
                dataLength = inputData.read(chars);
            }
            inputData.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class PipelineWriteData {
    public void writeData (PipedOutputStream outputData) {
        try {
            System.out.println("writeData write (字节流方式):");
            System.out.println("hello word!");
            outputData.write("hello word!".getBytes());
            outputData.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void writeData (PipedWriter outputData) {
        try {
            System.out.println("writeData write (字符流方式):");
            System.out.println("hello word!");
            outputData.write("hello word!");
            outputData.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述

3、使用join()方法

  如果一个线程需要等待另外一个线程运行完毕后采取执行,那么就得使用join()方法,join()方法的意思就是把A线程加入到B线程的内部,相当于将A线程变成了B线程的子线程,那么此时B线程就必须等待A线程执行完才能执行后边的代码,此时B线程是就可以使用A线程执行完的数据。总之:
  join()方法有使线程排队的作用,类似于synchronized达到同步执行的效果,只是join()底层是wait()实现,synchronized底层是使用监视器实现。
  join()方法可以完成线程间数据的可见性。
  join(long)方法底层使用wait(long)方法实现,具有是线程等待一段时间的作用,sleep(long)也能达到这个效果,只是join(long)执行后会释放锁,而sleep(long)不会释放锁。
  join()方法执行完后,具有释放锁的特点。
示例:

/**
 * Thread.join()方法测试类
 */
public class Thread12 {
    public static void main(String[] args) {

        Thread t1 = new Thread(() ->{
            System.out.println("T1 start");
        }, "T1");

        Thread t2 = new Thread(() ->{
            try {
                t1.join(1000); // 这段代码可以保证先打印T1 start后打印T2 start
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T2 start");
        }, "T2");

        t2.start();
        t1.start();
    }
}

运行结果:
在这里插入图片描述
注意:使用join()方法可能会导致join()之后的代码先运行(出现意外的情况下)。

三、ThreadLocal的使用

1、ThreadLocal具有线程隔离性

  如果多个线程都往同一个public static的ThreadLocal的变量中存储数据,那么对应的线程的数据是和线程本身挂钩的,也就是在A线程中不会获取到B线程的数据,在B线程中不会获取到A线程的数据。如下验证:
示例:

/**
 * ThreadLocal的数据隔离性验证
 */
public class Thread13 {
    public static void main(String[] args) {
        CommData.local.set("main set value");
        Thread t1 = new Thread(() -> {
            CommData.local.set("T1 set value");
            System.out.println("T1 get value" + CommData.local.get());
        }, "T1");
        Thread t2 = new Thread(() -> {
            CommData.local.set("T2 set value");
            System.out.println("T2 get value" + CommData.local.get());
        }, "T2");
        t1.start();
        t2.start();
        System.out.println("main get value" + CommData.local.get());
    }
}

class CommData {
    public static ThreadLocal<String> local = new ThreadLocal<>();
}

运行结果:
在这里插入图片描述
  从结果中可以看出,在当前线程中使用CommData.local.get()只能获取当前线程存储的内存,其他线程存储的线程对于当前线程是不可见的。

2、InheritableThreadLocal的数据继承性验证

   InheritableThreadLocal可以调用initialValue()设置初始值,就算没有set,也不会获取到null。
   InheritableThreadLocal在父线程设置的值,可以继承到子线程中使用,但是在子线程设置的值,不会传递到父线程,父线程依旧取得值是旧值。
示例:

/**
 * InheritableThreadLocal的数据继承性验证
 * 1、InheritableThreadLocal可以调用initialValue()设置初始值,就算没有set,也不会获取到null
 * 2、InheritableThreadLocal在父线程设置的值,可以继承到子线程中使用,但是在子线程设置的值,不会传递到父线程,
 *   父线程依旧取得值是旧值
 */
public class Thread14 {
    public static void main(String[] args) {
        CommData14 commData14 = new CommData14();
        // commData14.set("main set value");
        Thread t1 = new Thread(() -> {
            commData14.set("T1 set value");
            System.out.println("T1 get value " + commData14.get());
        }, "T1");
        Thread t2 = new Thread(() -> {
            System.out.println("T2 get value " + commData14.get());
        }, "T2");
        t1.start();
        t2.start();
        System.out.println("main get value " + commData14.get());
    }
}

class CommData14 extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }

    @Override
    protected Object childValue(Object parentValue) {
        return parentValue + " child set value……";
    }
}

运行结果:
在这里插入图片描述
打开main()方法中的第二行注释后,运行结果如下:
在这里插入图片描述
  这说明在InheritableThreadLocal中,子线程中设置的值,不会流传到其他线程中(父线程和其他线程依旧获取的是初始值),而父线程设置的值可以继承至其他所有子线程中(所有的线程都获取到了父线程中设置的值)。

四、锁的使用

1、ReentrantLock锁(可重入锁)

  ReentrantLock锁可以达到和synchronized同样的效果,甚至能比synchronized更加灵活。ReentrantLock锁使用lock()和unlock()解锁和释放锁。

1.1、PeentrantLock锁中的公平锁和非公平锁

  PeentrantLock锁分公平锁和非公平锁,提供两个构造函数:new PeentrantLock()的时候默认是非公平锁,new PeentrantLock(boolean fair)的时候,接受一个boolean值来指定。
示例:

/**
 * ReentrantLock(可重入锁)加锁保证线程之间顺序执行
 *
 *                 ReentrantLock内部类抽象类Sync
 *                          -
 *                          -
 *  ReentrantLock      Sync继承于AQS     Sync的实现类NonfairSync
 *    lock() -----调用----> lock() --------->  lock()
 */
public class Thread15 {
    //private static final ReentrantLock lock = new ReentrantLock();
    private static final ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(Thread15::printString, "A" + i).start();
        }
    }

    public static void printString() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "被执行了");
        } finally {
            lock.unlock();
        }
    }
}

运行结果:
在这里插入图片描述
  非公平锁:创建非公平锁new ReentrantLock()后,加上lock.lock()和lock.unlock()方法后依旧是乱序打印,这说明非公平锁并不是按照执行启动顺序来获取锁的,先启动的并不一定会先获得锁。
  公平锁:创建公平锁new ReentrantLock(true)后,加上lock.lock()和lock.unlock()方法后基本上呈现有序打印,这说明公平锁在一定程度上可以保证,先到的定会先获得锁。
  设置公平锁的时候,打印顺序基本上是按照启动顺序执行的。当然相反的设置非公平锁的时候,一定程度上,后边启动的线程会比它前边启动的线程先执行。

2、PeentrantLock中的Condition

  使用Condition可以实现等待/通知模式。利用Condition可以实现顺序打印。
示例:

/**
 * Condition实现等待/通知模式
 * 1、使用Condition实现将业务排序规划
 */
public class Thread18 {
    public static volatile int nextPrint = 1;
    public static void main(String[] args) throws InterruptedException {
        ThreadA18 threadA18 = new ThreadA18();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> threadA18.aPrintString(nextPrint), "A").start();
            new Thread(() -> threadA18.bPrintString(nextPrint), "B").start();
            new Thread(() -> threadA18.cPrintString(nextPrint), "C").start();
        }
    }
}

class ThreadA18 {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition conditionA = lock.newCondition();
    private static final Condition conditionB = lock.newCondition();
    private static final Condition conditionC = lock.newCondition();
    public void aPrintString(int nextPrint) {
        try {
            lock.lock();
            if (nextPrint != 1) {
                conditionA.await();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            Thread18.nextPrint = 2;
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void bPrintString(int nextPrint) {
        try {
            lock.lock();
            if (nextPrint != 2) {
                conditionB.await();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            Thread18.nextPrint = 3;
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cPrintString(int nextPrint) {
        try {
            lock.lock();
            if (nextPrint != 3) {
                conditionC.await();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            Thread18.nextPrint = 1;
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果:
在这里插入图片描述
  使用Condition实现了线程顺序执行,保证了线程按照我们的规划执行响应的功能。Condition可以灵活的设置不同的线程锁,可以实现针对性对线程控制的能力,在使用上更加灵活。

3、ReentrantReadWriteLock锁(读写锁)

  ReentrantLock在加锁的过程中,是完全排他的效果,即同一个时间,只能允许一个线程执行lock()方法后边的代码,有时候在不需要操作变量,而只是读取的情况下,ReentrantLock效率就比较低,JAVA中提供了一种读写锁(ReentrantReadWriteLock),读锁与写锁是两种锁,可以做到,在只有读操作时候,可以保证多个线程同步执行,在写操作的过程只能一步执行。
示例:

/**
 * ReentrantReadWriteLock读写锁测试类
 * 1、读读共享
 * 2、读写/写读互斥
 * 3、写写互斥
 */
public class Thread19 {
    public static void main(String[] args) {
        ThreadA19 threadA19 = new ThreadA19();
        new Thread(threadA19::read, "A").start();
        new Thread(threadA19::read, "B").start();
        new Thread(threadA19::write, "C").start();
        new Thread(threadA19::write, "D").start();
    }
}

class ThreadA19 {
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    public void read () {
        readWriteLock.readLock().lock();
        System.out.println(Thread.currentThread().getName() + " read " + System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        readWriteLock.readLock().unlock();
    }

    public void write () {
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName() + " write " + System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        readWriteLock.writeLock().unlock();
    }
}

运行结果:
在这里插入图片描述
  从结果可以看出,A和B两个线程几乎是同时执行read()方法中的代码的。因为他们打印的时间几乎是相同的,而C和D两个线程是异步执行的,因为他们打印的时间相差了1000毫秒左右,这是因为D线程执行的过程中必须等待C线程执行完释放速锁之后才能执行。

五 定时器Timer

  JAVA中的定时器Timer是一个负责执行计划任务的功能,即在某个时间开始执行某一个任务。Timer最主要的是负责计划任务,具体的任务的实现需要放在TimerTask的子类中,TimerTask是一个实现了Runnable接口的抽象类,所以需要重写它的run()方法。

1 schedule()方法

  schedule()方法在Timer类中,几个重载的方法。

1.1 schedule(TimerTask task, long delay)

  该方法以启动的时间为主,延迟指定的毫秒数后在执行相关的代码。
示例:

/**
 * Timer定时器测试验证类
 */
public class Thread20 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("***************************");
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task run time" + new Date());
            }
        };
        System.out.println("task start time" + new Date());
        // 该任务启动后,延迟2秒后再执行
        timer.schedule(task, 2000);
    }
}

运行结果:
在这里插入图片描述
  可以看到,任务是在启动后,延迟了2秒后才去执行的。

1.2 schedule(TimerTask task, long delay, long period)

  该任务启动后,延迟指定的毫秒数后执行一次,之后以1000毫秒为间隔,无限重复执行
示例:

/**
 * Timer定时器测试验证类
 */
public class Thread19 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("***************************");
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task2 run time" + new Date());
            }
        };
        System.out.println("task2 start time" + new Date());
        // 该任务启动后,延迟指定的时毫秒数后执行一次,之后以1000毫秒为间隔,无限重复执行
        timer.schedule(task2, 2000, 1000);
    }
}

运行结果:
在这里插入图片描述

1.3 schedule(TimerTask task, Date time)

  该方法以启动的时间为主,在指定的时间执行相关的代码,如果时间在过去,则计划立即执行任务。
示例:

/**
 * Timer定时器测试验证类
 */
public class Thread20 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("***************************");
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task run time" + new Date());
            }
        };
        System.out.println("task start time" + new Date());
        // 该任务启动后,再指定的时间执行相关的任务,如果指定的时间为过去,那么会立即执行。
        // timer.schedule(task, new Date(System.currentTimeMillis() + 2000));
        timer.schedule(task, new Date(System.currentTimeMillis() - 2000));
      }
}

运行结果:
在这里插入图片描述

1.4 schedule(TimerTask task, Date time, long period)

  该任务启动后,在指定的时间(如果是过去,则立马执行)执行一次后,之后以1000毫秒为间隔,无限重复执行
示例:

/**
 * Timer定时器测试验证类
 */
public class Thread21 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("***************************");
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task1 run time" + new Date());
            }
        };
        System.out.println("task1 start time" + new Date());
        // 该任务启动后,在指定的时间(如果晚于当前时间,则立马执行,否则等到指定时间点时才执行)执行一次后,之后以1000毫秒为间隔,无限重复执行
        timer.schedule(task1, new Date(System.currentTimeMillis() + 5000), 1000);
        // timer.schedule(task1, new Date(System.currentTimeMillis() + 5000), 1000);
    }
}

运行结果:
在这里插入图片描述

2 scheduleAtFixedRate()方法

  用法参考schedule()方法。schedule()方法和scheduleAtFixedRate()方法不需要考虑线程安全问题,因为任务都是按照顺序放到队列中,也会按照顺序执行。

3 cancel()方法

  Timer类中cancel()方法,会将所有的任务队列中的任务全部清除掉(但有时也会出现无法清除的现象,导致执行cancel()后,任务依旧会执行),然后自动停止程序。   TimerTask类中的cancle()方法只是清除自身的任务,而不会影响其他任务,程序不会退出。
示例:

/**
 * Timers类中cancel()方法测试验证类
 */
public class Thread22 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        System.out.println("***************************");
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task run time" + new Date());
            }
        };
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task1 run time" + new Date());
            }
        };
        // 该任务启动后
        timer.schedule(task, 2000);
        timer.schedule(task1, 2000);
        // task.cancel();
        timer.cancel();
    }
}

运行结果(放开timer.cancel());
在这里插入图片描述
运行结果(放开task.cancel());
在这里插入图片描述

六 单例模式与多线程

1 饿汉模式单例

  饿汉模式在使用类的时候就已经new出来一个对象的实例,不管有没有用到。这种情况下,对象一直存在内存中,有点占用资源。
实例:

/**
 * 单例模式(饿汉式)
 */
public class Thread23 {
    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            new Thread(() -> System.out.println(
                    Thread.currentThread().getName() + " get: " + SingleModel23.getSingleModel23().hashCode()),
                    "Thread" + i).start();
        }
    }
}

class SingleModel23 {
    private static final SingleModel23 singleModel23 = new SingleModel23();

    private SingleModel23 () {}

    public static SingleModel23 getSingleModel23 () {
        return singleModel23;
    }
}

运行结果:
在这里插入图片描述  从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。但是由于getSingleModel22()方法没有同步,也有可能会出现线程不安全问题。

2 懒汉模式单例(DCL版,推荐)

  懒汉模式单例在使用类的时候,不会立即new出来对象的实例,而是在用到的时候,才开始new,这样可以避免浪费内存的问题。
   需要对对象实例需要增加volatile关键字。
   增加双重检查锁实现,避免线程不安全问题
示例:

/**
 * DCL单例模式(懒汉式)
 */
public class Thread24 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> System.out.println(
                    Thread.currentThread().getName() + " get: " + SingleModel.getSingleModelInstance().hashCode()),
                    "Thread" + i).start();
        }
    }
}

class SingleModel {
    private volatile static SingleModel singleModel;

    private SingleModel() {
    }

    public static SingleModel getSingleModelInstance() {
        if (singleModel == null) {
            synchronized (SingleModel.class) {
                if (singleModel == null) {
                    singleModel = new SingleModel();
                }
            }
        }
        return singleModel;
    }
}

运行结果:
在这里插入图片描述
  从打印结果来看,所有的hashcode都是同一个值,可以确认实现了懒汉式单例模式。由于getSingleModelInstance ()方法DCL机制,所以基本上避免了现线程不安全问题。DCL版本的懒汉式单例模式也是最常用的设计模式。

3 静态内部类实现单例模式

  在JAVA中,静态代码,静态域只会在使用类的时候执行一次,所以一定是线程安全的。所以可以使用静态内部类来实现单例模式。
示例:

/**
 * 静态内部类实现单例模式
 */
public class Thread25 {
    public static class InnerThread25 {
        private static final Thread25 thread25 = new Thread25();
    }

    public static Thread25 getInstance() {
        return InnerThread25.thread25;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            new Thread(() -> System.out.println(
                    Thread.currentThread().getName() + " get: " + Thread25.getInstance().hashCode()),
                    "Thread" + i).start();
        }
    }
}

运行结果:
在这里插入图片描述
  从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。由于静态内部类只会执行一次,所以基本上避免了现线程不安全问题。

4 使用static代码块实现单例模式

  和静态内部类一样,静态代码块在类使用的时候,静态代码块只会执行一次,所以,静态代码块也可以实现单例模式,并且还不用担心线程不安全的问题。
示例:

/**
 * 使用静态代码块实现单例模式
 */
public class Thread26 {
    private static Thread26 thread26;
    static {
        thread26 = new Thread26();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> System.out.println(
                    Thread.currentThread().getName() + " get: " + thread26.hashCode()),
                    "Thread" + i).start();
        }
    }
}

运行结果:
在这里插入图片描述
  从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。由于静态代码块只会执行一次,所以基本上避免了现线程不安全问题。

七 线程状态

  线程状态在Thread.State枚举类中定义,线程在不同的运行时期,便会进入不同的状态。大多数状态之间都是双向的。线程状态转化如下。
在这里插入图片描述

总结

本文针对JAVA中线程模块做了简单的解释与实战,介绍了线程的相关概念,锁的概念,线程间通信的方法等,内容比较简单,但是却比较繁琐,概念性东西比较多,有些内容比较细节,但是却在面试或者使用中是不可忽略的东西。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜间沐水人

文章编写不易,一分钱也是爱。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值