一文搞懂 Java 线程

什么是线程?

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。(来源百度百科)

最简单的理解

最简单的理解就是在Java中,当我们启动main函数时其实就启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称为主线程

线程的创建与运行

在Java中创建线程有三种方式,分别是

  • 实现Runnable接口的run()方法
/**
 * @author Woo_home
 * @create by 2019/10/24
 */
public class RunnableTask implements Runnable{
    @Override
    public void run() {
        System.out.println("Thread running......");
    }

    public static void main(String[] args) throws InterruptedException{
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}
  • 继承Thread类并重写run()方法
/**
 * @author Woo_home
 * @create by 2019/10/24
 */
public class ThreadTest extends Thread{

    @Override
    public void run() {     //继承Thread类并重写run()方法
        System.out.println("Thread running......");
    }

    
    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();//创建线程
        test.start();//启动线程
    }
}

  • 使用FutureTask方式(这里暂时不讲)
/**
 * @author Woo_home
 * @create by 2019/10/24
 */
//类似Runnable
public class CallerTask implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "Thread running......";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> futureTask = new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(futureTask).start();
        try {
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

CallerTask类实现了Callable接口的call()方法,在main函数内首先创建了一个FutureTask对象(构造函数为CallerTask的实例),然后使用创建的FutureTask对象作为任务创建了一个线程并且启动它,然后通过futureTask.get()等待任务执行完毕并返回结果

Runnable接口的引入以及策略模式在Thread中的使用

Runnable的职责

Runnable接口其实非常简单,只是定义了一个无参无返回值的抽象run()方法,具体代码如下:

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

在很多时候JDK中代表线程的就只有Thread这个类,而线程执行单元就是run()方法,可以用过继承Thread类然后重写run()方法来实现自己的业务逻辑,也可以实现Runnable接口实现自己的业务逻辑,如:

private Runnable target;
@Override
    public void run() {
        if (target != null) {
            target.run(); //这个run()就是Runnable接口中的run()方法
        }
    }

总结:使用继承的方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造是函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java并不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runnable则没有这个限制。继承Thread和实现Runnable都没办法拿到任务的返回结果,但是FutureTask方式可以

线程的运行状态

对于多线程的开发而言,编写程序的过程之中总是按照:定义的线程主体类,而后通过 Thread 类进行线程的启动,但是并不意味着你调用了 start() 方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态
在这里插入图片描述

  • 1、任何一个线程的对象都应该使用 Thread 类进行封装,所以线程的启动使用的是 start(),但是启动的时候若干个线程都将进入到一种叫就绪状态,现在并没有执行
  • 2、进入到就绪状态之后就需要等待进行资源的调度,当某个线程调度成功之后则进入到运行状态(run()),但是所有的线程不可能一直持续执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间之后就需要让出资源,而后这个线程就将进入到阻塞状态,随后重新回归到就绪状态
  • 3、当 run() 方法执行完毕之后,实际上该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态
  • 4、start() 只是准备执行,并不是执行,具体执行得等到操作系统的确认
    多线程的主要操作方法都在 Thread 类中定义了

线程的命名和取得

多线程的运行状态是不确定的,那么在程序的开发之中为了可以获取到一些需要使用到线程只能依靠线程的名字来进行操作。所以线程的名字是一个至关重要的概念,这样在 Thread 类之中就提供有线程名称的处理

  • 构造方法:public Thread(Runnable target, String name)
  • 设置名字:public final void setName(String name)
  • 取得名字:public final String getName()

对于线程对象的获得是不可能只是依靠一个 this 来完成的,因为线程的状态是不可控的,但是有一点可以明确的是,所有的线程对象一定要执行 run() 方法,那么这个时候可以考虑获取当前线程,在 Thread 类里面提供了获取当前线程的方法

  • 获取当前线程: public static Thread currentThread()

举例:(观察线程的命名操作)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        new Thread(thread,"线程 A").start();      // 设置线程的名字
        new Thread(thread).start();               // 未设置线程的名字
        new Thread(thread,"线程 B").start();      // 设置线程的名字
    }
}

输出:
在这里插入图片描述
从输出可以发现当开发者为线程设置名字的时候就使用设置的名字,如果没有设置名字,则会自动生成一个不重复的名字,比如我们在上述的代码中再加几个线程

public class Demo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        new Thread(thread,"线程 A").start(); // 设置线程的名字
        new Thread(thread).start();               // 未设置线程的名字
        new Thread(thread).start();
        new Thread(thread).start();
        new Thread(thread,"线程 B").start(); // 设置线程的名字
    }
}

在这里插入图片描述
可以发现,如果没有命名的线程会自动产生一个编号来代替线程的名字,来保证不重复,那么这种编号是如何产生的呢?其实这种属性命名主要是依靠了 static 属性来完成的,在 Thread 类里面定义有如下操作:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

从代码中可以发现,如果是没有命名的线程会执行这个构造方法,这个构造方法的特点就是 nextThreadNum(),我们来看下 nextThreadNum() 这个方法,发现这是一个静态的方法,所以这也就是为什么会产生不重复的编号了

private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

我们再来举个例子

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        new Thread(thread,"线程对象").start(); // 设置线程的名字
        thread.run(); 						  // 对象直接调用 run() 方法
    }
}

在这里插入图片描述
通过此时的代码可以发现当使用了 “thread.run()” 直接在主方法之中调用线程类对象中的 run() 方法所获得的线程对象的名字为 “main” ,所以可以得出一个结论:主方法也是一个线程。那么现在的问题来了,所有的线程都是在进程上的一个划分,那么进程在哪里?每当使用 Java 命令执行程序的时候就表示启动了一个 JVM 的进程,一台电脑上可以同时启动若干个 JVM 进程,所以 JVM 进程都会有各自的线程
在这里插入图片描述
在任何的开发之中,主线程可以创建若干个子线程。然而创建子线程的目的就是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程去处理

举例: (子线程处理)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) {
        System.out.println("1、执行操作任务一");
        new Thread(() -> { // 交由子线程去计算
            int temp = 0;
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                temp += i;
            }
        }).start();
        System.out.println("2、执行操作任务二");
        System.out.println("n、执行操作任务N");
    }
}

在这里插入图片描述
主线程负责处理整体流程,而子线程负责处理耗时操作

线程的休眠

如果说现在希望某一个线程可以暂缓执行,那么就可以使用休眠处理,在 Thread 类中定义的休眠方法如下:

  • 休眠:public static void sleep(long millis) throws InterruptedException
  • 休眠:public static void sleep(long millis, int nanos) throws InterruptedException

在进行休眠的时候有可能会产生中断异常 “InterruptedException”,中断异常属于 Exception 的子类,所以证明该异常必须进行处理

举例:(观察线程休眠处理)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ".i = " + i);
                try {
                    Thread.sleep(1000); // 暂缓执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread1").start();
    }
}

输出:
在这里插入图片描述
休眠的主要特点就是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在你有多个线程对象,那么休眠也是有先后顺序的

举例:(产生多个线程对象进行休眠处理)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) {
        for (int num = 0; num < 5; num++) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ".i = " + i);
                    try {
                        Thread.sleep(1000); // 暂缓执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"Thread1 - " + num).start();
        }
    }
}

此时产生五个线程对象,并且这五个线程对象执行的方法体是相同的。但是此时从程序执行的感觉上来讲好像是若干个线程一起进行了休眠,而后一起进行了自动唤醒
在这里插入图片描述

线程中断

在上面我们讲过线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其它线程完成的。在 Thread 类里面提供有这种中断执行的方法

  • 判断线程是否被中断:public boolean isInterrupted()
  • 中断线程执行:public void interrupt()

举例:(观察线程的中断处理操作)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("线程的启动");
            try {
                Thread.sleep(10000); // 休眠 10 s
                System.out.println("线程的关闭");
            } catch (InterruptedException e) {
                System.out.println("O_O");
            }
        });
        thread.start(); // 开始执行
        Thread.sleep(1000);
        if (!thread.isInterrupted()) { // 判断该线程中断了没
            System.out.println("线程中断了");
            thread.interrupt();        // 线程中断
        }
    }
}

输出:
在这里插入图片描述
所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理

线程的强制执行

所谓的线程强制执行指的是当满足于某些条件之后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束

举例:(先来观察一下没有强制执行的程序)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行.i = " + i);
            }
        },"正常的线程执行操作");
        thread.start();
        for (int i = 0; i < 100; i++) {
            Thread.sleep(100);
            System.out.println("main number = " + i);
        }
    }
}

部分运行截图
在这里插入图片描述
这个时候可以发现主线程和子线程都在交替执行者,但是如果说现在你希望主线程独占执行。那么就可以利用 Thread 类中的方法

  • 强制执行:public final void join() throws InterruptedException
package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread(); // 主线程
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                if (i == 3) {
                    try {
                        mainThread.join();  // main 线程要先执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行.i = " + i);
            }
        },"正常的线程执行操作");
        thread.start();
        for (int i = 0; i < 100; i++) {
            Thread.sleep(100);
            System.out.println("main线程 number = " + i);
        }
    }
}

部分运行截图
在这里插入图片描述
在进行线程强制执行的时候一定要获取强制执行线程对象之后才可以执行 join() 调用

线程的礼让

线程的礼让就是指先将资源让出去让别的线程先执行。线程的礼让可以使用 Thread 中提供的方法:

  • 礼让:public static void yield()

举例:(使用线程礼让操作)
为了方便,我们直接把上面的代码拿过来修改吧,如下:

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                if (i % 3 == 0) {
                    Thread.yield(); // 线程礼让
                    System.out.println("线程礼让执行");
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行.i = " + i);
            }
        },"正常的线程执行操作");
        thread.start();
        for (int i = 0; i < 100; i++) {
            Thread.sleep(100);
            System.out.println("main线程 number = " + i);
        }
    }
}

礼让执行的时候每一次调用 yield() 方法都只会礼让一次当前的资源

线程的优先级

从理论上来讲线程的优先级越高越有可能先执行(越有可能先抢占到资源)。在 Thread 类里面针对于优先级提供了两个处理方法

  • 设置优先级:public final void setPriority(int newPriority)
  • 获取优先级:public final int getPriority()

在进行优先级定义的时候都是通过 int 类型的数字来完成的,而对于此数字的选择在 Thread 类里面定义了三个常量

  • 最高优先级:MAX_PRIORITY 默认值为 10
  • 中等优先级:NORM_PRIORITY 默认值为 5
  • 最低优先级:MIN_PRIORITY 默认值为 1

举例:(观察优先级)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行. " + i);
            }
        };
        Thread threadA = new Thread(runnable,"线程对象A");
        Thread threadB = new Thread(runnable,"线程对象B");
        Thread threadC = new Thread(runnable,"线程对象C");
        threadA.setPriority(Thread.MIN_PRIORITY);   // 将 threadA 设置为最低优先级
        threadB.setPriority(Thread.MIN_PRIORITY);   // 将 threadB 设置为最低优先级
        threadC.setPriority(Thread.MAX_PRIORITY);   // 将 threadC 设置为最高优先级
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

输出:
在这里插入图片描述
前面我们说过主方法也是一个线程也叫主线程,那么主线程的优先级呢?

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
public class Demo1 {
    public static void main(String[] args) {
        // 默认的线程
        System.out.println(new Thread().getPriority());
        // 主方法线程
        System.out.println(Thread.currentThread().getPriority());
    }
}

输出结果为 5,前面我们说过中等优先级的默认值也是 5,那么也就是说主线程属于中等优先级,而默认创建的线程也是中等优先级
在这里插入图片描述
总结一下:只是优先级高的有可能先执行

线程的同步

在多线程的处理当中,可以利用 Runnable 描述多个线程操作的资源,而 Thread 描述的是每一个线程对象,于是当多个线程访问统一资源的时候如果处理不当就会产生数据的错误操作


  • 同步问题的引出

下面编写一个简单的商品抢购程序,将创建若干个线程对象实现抢购的处理操作
举例:(实现商品抢购)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class ProductThreadDemo implements Runnable {

    private int products = 10;  // 总商品数为 10

    @Override
    public void run() {
        while (true) {
            if (this.products > 0) {
                System.out.println(Thread.currentThread().getName() + "抢购.product" + this.products--);
            } else {
                System.out.println("------ 商品已经抢购完咯 ------");
                break;
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        ProductThreadDemo demo = new ProductThreadDemo();
        new Thread(demo,"用户 A").start();
        new Thread(demo,"用户 B").start();
        new Thread(demo,"用户 C").start();
    }
}

此时的线程将创建三个线程对象,并且这三个线程对象将进行 10 个商品的抢购,从输出可以看到没什么问题
在这里插入图片描述
此时的程序在进行抢购的处理的时候并没有任何的问题(假象),下面可以模拟一下抢购中的延迟操作,就是加个 Thread.sleep(100)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class ProductThreadDemo implements Runnable {

    private int products = 10;  // 总商品数为 10

    @Override
    public void run() {
        while (true) {
            if (this.products > 0) {
                try {
                    Thread.sleep(100);      // 模拟网络延迟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "抢购.product" + this.products--);
            } else {
                System.out.println("------ 商品已经抢购完咯 ------");
                break;
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        ProductThreadDemo demo = new ProductThreadDemo();
        new Thread(demo,"用户 A").start();
        new Thread(demo,"用户 B").start();
        new Thread(demo,"用户 C").start();
    }
}

现在出现了一个用户抢到商品 0 和 -1 的情况,也就是说商品明明已经被用户们抢购完了,你现在还来个用户抢够到商品,上哪去找商品发给用户呢?
在这里插入图片描述
这个时候追加了延迟问题就暴露出来了,而实际上这个问题一直都在(自己画的图,不好看勿喷)
在这里插入图片描述

如何实现线程同步呢?

经过上述的分析之后已经可以确认同步问题所产生的主要原因了,那么下面就需要进行同步问题的解决,但是解决同步问题的关键是锁,那么什么是锁呢?指的是当某一个线程执行操作的时候,其它线程外面等待;
在这里插入图片描述
如果想要在程序当中实现这把锁的功能,就可以使用 synchronized 关键字来实现,利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作里面的代码只允许一个线程执行

1、利用同步代码块进行处理

// 用法
synchronized(同步对象) {
	同步代码块操作
}

一般要进行同步对象处理的时候可以采用当前对象 this 进行同步
举例:(利用同步代码块解决数据同步访问问题)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class ProductThreadDemo implements Runnable {

    private int products = 10;  // 总商品数为 10

    @Override
    public void run() {
        while (true) {
            synchronized (this) {                   // 每次只允许一个线程访问
                if (this.products > 0) {
                    try {
                        Thread.sleep(100);      // 模拟网络延迟
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢购.product" + this.products--);
                } else {
                    System.out.println("------ 商品已经抢购完咯 ------");
                    break;
                }
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        ProductThreadDemo demo = new ProductThreadDemo();
        new Thread(demo,"用户 A").start();
        new Thread(demo,"用户 B").start();
        new Thread(demo,"用户 C").start();
    }
}

在这里插入图片描述
此时就不会出现商品抢购为负数的问题了,但是加入同步处理之后,程序的整体性能下降了。同步实际上会造成性能的降低

2、利用同步方法解决

那么如何实现同步方法呢?只需要在方法定义上使用 synchronized 关键字即可

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class ProductThreadDemo implements Runnable {

    private int products = 10;  // 总商品数为 10

    public synchronized boolean sale() {
        if (this.products > 0) {
            try {
                Thread.sleep(100);      // 模拟网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢购.product" + this.products--);
            return true;
        } else {
            System.out.println("------ 商品已经抢购完咯 ------");
            return false;
        }
    }

    @Override
    public void run() {
        while (this.sale()) {
            ;
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        ProductThreadDemo demo = new ProductThreadDemo();
        new Thread(demo,"用户 A").start();
        new Thread(demo,"用户 B").start();
        new Thread(demo,"用户 C").start();
    }
}

输出
在这里插入图片描述

Java 多线程(生产者与消费者)

在多线程的开发过程之中最为著名的案例就是生产者与消费者操作,该操作的主要流程如下:

  • 生产者负责信息内容的生产
  • 每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息
  • 如果生产者没有生产完则消费者需要等待它生产完成,反之如果消费者还没有对信息进行消费, 则生产者应该等待消费处理完成后在继续生产

程序的基本实现

可以将生产者与消费者定义为两个独立的线程类对象,但是对于现在生产的数据,可以使用如下的组成:

  • 数据一:title:Java 、content:生产者与消费者
  • 数据二:title:Python、content:数据分析

既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就需要有一个数据的保存的集中点,那么可以定义一个单独的 Message 类实现数据的保存
在这里插入图片描述
举例:(生产者与消费者的实现)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */
 
// 消息中心
class Message {
    private String title;
    private String content;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

// 生产者
class Producer implements Runnable{

    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                this.message.setTitle("Java");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.message.setContent("生产者与消费者");
            } else {
                this.message.setTitle("Python");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.message.setContent("数据分析");
            }
        }
    }
}

// 消费者
class Consumer implements Runnable {

    private Message message;

    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.message.getTitle() + " - " + this.message.getContent());
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(new Producer(message)).start(); // 启动生产者线程
        new Thread(new Consumer(message)).start(); // 启动消费者线程
    }
}

输出:
在这里插入图片描述
通过整个代码的执行会发现此时有两个主要的问题:

  • 问题一:数据不同步
  • 问题二:生产一个取走一个,但是发现有了重复生产和重复取出的问题

解决生产者与消费者的同步问题

如果要解决问题的话,首先要解决的就是数据同步处理的问题了,如果要想解决数据同步最简单的做法就是使用 synchronized 关键字定义同步代码块或同步方法,于是这个时候对于同步的处理就可以直接在 Message 类中完成

举例:(解决同步操作)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class Message {
    private String title;
    private String content;

    public synchronized void set(String title, String content) {
        this.title = title;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.content = content;
    }

    public synchronized String get() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return this.title + " -- " + this.content;
    }
}

class Producer implements Runnable{

    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                this.message.set("Java","生产者与消费者");
            } else {
                this.message.set("Python","数据分析");
            }
        }
    }
}

class Consumer implements Runnable {

    private Message message;

    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.message.get());
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(new Producer(message)).start(); // 启动生产者线程
        new Thread(new Consumer(message)).start(); // 启动消费者线程
    }
}

在这里插入图片描述
在进行同步处理的时候肯定需要有一个同步的处理对象,那么此时肯定要将同步操作交由 Message 类处理是最合适的。这个时候返现数据已经可以正常的保持一致了,但是对于重复操作的问题依然存在

线程的等待与唤醒

如果说现在想要解决生产者与消费者的问题,那么最好的解决方案就是使用等待与唤醒机制。而对于等待与唤醒机制的操作主要依靠的是 Object 类中提供的方法处理的:

  • wait() 等待机制
    • 固定等:public final void wait() throws InterruptedException
    • 设置等待时间:public final void wait(long timeout,int nanos) throws InterruptedException
    • 设置等待时间:public final void wait(long timeout) throws InterruptedException
  • 唤醒第一个等待线程:public final void notify()
  • 唤醒全部等待线程:public final void notifyAll()

如果此时有若干个等待线程的话,那么 notify() 表示的是唤醒第一个等待的,而其它的线程继续等待;而 notifyAll() 表示唤醒所以的线程,哪个线程的优先级高就有可能先执行

对于当前的问题主要的解决方法应该是通过 Message 类完成处理

举例:(修改 Message 类)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/20
 */

class Message {
    private String title;
    private String content;

    /**
     * 表示生产或者消费的形式
     * 如果 flag = true 表示允许生产,但是不允许消费
     * 如果 flag = false 表示允许消费,不允许生产
     */
    private boolean flag;

    public synchronized void set(String title, String content) {
        if (!this.flag) { // 无法进行生产,应该等待被消费
            try {
                super.wait(); // 等待
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.title = title;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.content = content;
        this.flag = false;   // 已经生产过了
        super.notify();      // 唤醒等待的线程
    }

    public synchronized String get() {
        if (this.flag) { // 还未生产,需要等待
            try {
                super.wait();
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            return this.title + " -- " + this.content;
        } finally { // 不管如何都要执行
            this.flag = true; // 继续生产
            super.notify();   // 唤醒等待线程
        }
    }
}

class Producer implements Runnable{

    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                this.message.set("Java","生产者与消费者");
            } else {
                this.message.set("Python","数据分析");
            }
        }
    }
}

class Consumer implements Runnable {

    private Message message;

    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.message.get());
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Message message = new Message();
        new Thread(new Producer(message)).start(); // 启动生产者线程
        new Thread(new Consumer(message)).start(); // 启动消费者线程
    }
}

在这里插入图片描述
这种处理形式就是在进行多线程开发过程之中最原始的处理方案,整个的等待、同步、唤醒机制都是由开发者自行通过原生代码实现控制

如何优雅地停止线程?

具体内容

在多线程操作之中如果要启动多线程肯定使用的是 Thread 类中的 start() 方法,而如果对于多线程需要进行停止处理,Thread 类原本提供有 stop() 方法,但是对于这个方法从 JDK1.2 版本开始就已经被弃用了,而且一直到现在也不建议出现在你的代码之中,而且除了 stop() 方法之外还有几个方法也被弃用了:

  • 停止多线程:public final void stop()
  • 销毁多线程:public void destroy()
  • 挂起线程: public final void suspend()、暂停执行
  • 恢复挂起的线程执行:public final void resume()

之所以弃用这些方法是因为这些方法有可能导致线程的死锁,所以从 JDK1.2 开始就都不建议使用了,如果这个时候要想实现线程的停止需要通过一种柔和的方式进行

举例:(实现线程柔和的停止)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/22
 */
public class ThreadTest {

    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            long num = 0;
            while (flag) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在运行.num = " + num++);
            }
        },"执行线程").start();
        Thread.sleep(200); // 休眠 200 ms
        flag = false;      // 停止线程
    }
}

输出:
在这里插入图片描述
万一现在有其它线程去控制这个 flag 的内容,那么这个时候对于线程的停止也不是说停就停止的,而是会在执行中会判断 flag 的内容来完成的

线程的守护

在多线程里面可以进行守护线程的定义,也就是说如果线程主线程的程序或者其它的线程还在执行的时候守护线程将一直存在,并且运行在后台状态

在 Thread 类里面提供有如下守护线程的操作方法:

  • 设置为守护线程:public final void setDaemon(boolean on)
  • 判断是否为守护线程:public final boolean isDaemon()

举例:(线程守护的例子)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/22
 */
public class ThreadTest {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            for (int num = 0; num < 100; num++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在运行.num = " + num++);
            }
        },"用户线程");      // 完成核心的业务
        Thread daemonThread = new Thread(() -> {
            for (int num = 0; num < Integer.MAX_VALUE; num++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在运行.num = " + num++);
            }
        },"守护线程");      // 完成核心的业务
        daemonThread.setDaemon(true); // 设置为守护线程
        userThread.start();
        daemonThread.start();
    }
}

部分运行截图:
在这里插入图片描述
可以发现所有的守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也就消失了,在整个的 JVM 里面最大的守护线程就是 GC 线程

程序执行中 GC 线程会一直存在,如果程序执行完毕,GC 线程也将消失(最大的守护线程)

volatile 关键字

具体内容

在多线程的定义之中,volatile 关键字主要是在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。这样的话在一些书上就将其错误的理解为同步属性了
在这里插入图片描述
在正常进行变量处理的时候往往会经历如下几个步骤:

  • 获取变量原有的数据内容
  • 为变量进行数学计算
  • 将计算后的变量,保存到原始空间中

如果一个属性上追加了 volatile 关键字,表示的就是不使用副本,而是直接操作原始变量,相当于节约了:拷贝副本、重新保存的步骤

举例:(使用 volatile 关键字)

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/22
 */

class Product implements Runnable {

    private volatile int product = 10;   // 直接内存操作

    @Override
    public void run() {
        while (this.product > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢购.product = " + this.product--);
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        Product product = new Product();
        new Thread(product,"用户 A").start();
        new Thread(product,"用户 B").start();
        new Thread(product,"用户 C").start();
    }
}

输出:
在这里插入图片描述
从输出可以看到 volatile 并不解决同步的问题,只是解决了拷贝副本、重新保存的步骤
为此我们还需修改一下程序,如下:

package com.java.springtest.test;

/**
 * @author Woo_home
 * @create by 2020/1/22
 */

class Product implements Runnable {

    private volatile int product = 10; // 直接内存操作

    @Override
    public void run() {
        synchronized (this) {
            while (this.product > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "抢购.product = " + this.product--);
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        Product product = new Product();
        new Thread(product,"用户 A ").start();
        new Thread(product,"用户 B ").start();
        new Thread(product,"用户 C ").start();
    }
}

输出:
在这里插入图片描述
更多关于 volatile 与 synchronized 关键字的介绍请看这里 volatile 与 synchronized 详解

面试题

请解释 volatile 与 synchronized 的区别?

  • volatile 主要在属性上使用,而 synchronized 是在代码块与方法上使用的
  • volatile 无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,而 synchronized 是实现同步的
发布了187 篇原创文章 · 获赞 874 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/Woo_home/article/details/104053600