多线程基础知识整理

一、JAVA多线程编程

1-1 进程与线程

并发性和并行性

在单个处理器的多线程进程中,处理器可以在线程之间切换执行资源,从而执行并发。

在共享内存的多处理器环境内的同一个多线程进程中,进程中的每个线程都可以在一个单独的处理器上并发运行,从而执行并行。如果进程中的线程数不超过处理器的数目,则线程的支持系统和操作环境可确保每个线程在不同的处理器上执行。例如,在线程数和处理器数目相同的矩阵乘法中,每个线程和每个处理器都会计算一行结果。

多线程结构一览

每个进程都包含一个线程,因此对多个进程进行编程即是对多个线程进行编程。但是,进程同时也是一个地址空间,因此创建进程会涉及到创建新的地址空间。

创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对于在进程之间切换,在线程之间进行切换所需的时间更少,因为后者不包括地址空间之间的切换。

在进程内部的线程间通信很简单,因为这些线程会共享所有内容,特别是地址空间。所以,一个线程生成的数据可以立即用于其他所有线程。

1-2 Thread类实现多线程

调用start()方法实现异步执行效果:

class MyThread extends Thread {
    private String title;
    public MyThread(String title) {
        this.title = title;
    }
    @Override
    public void run() {
        for(int x = 0; x < 10; x ++){
            System.out.println(this.title + "运行,x = " + x);
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        //每个线程对象只允许启动一次
        new MyThread("线程A").start();
        new MyThread("线程B").start();
        new MyThread("线程C").start();
    }
}

capture_20200916183119137

1-3 Runnable接口实现多线程

由于不再继承Thread父类,那么此时MyThread类中就不再支持有start()方法,不能用Thread.start()方法是无法进行多线程启动的,那么这个时候就要看下Thread类所提供的构造方法

构造方法:public Thread(Runnable target);

class MyThread implements Runnable {
    private String title;
    public MyThread(String title) {
        this.title = title;
    }
    @Override
    public void run() {
        for(int x = 0; x < 10; x ++){
            System.out.println(this.title + "运行,x = " + x);
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        //每个线程对象只允许启动一次
        Thread threadA = new Thread(new MyThread("线程A"));
        Thread threadB = new Thread(new MyThread("线程B"));
        Thread threadC = new Thread(new MyThread("线程C"));
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

这个时候的多线程实现里面可以发现,由于只是实现了Runnable接口对象,所以此时线程主体类上就不再有单继承局限了,这样的设计才是一个标准型的设计。

从JDK1.8开始,Runnable接口使用了函数式接口定义,所以也可以使用lambda表达式进行线程类实现。

class MyThread implements Runnable {
    private String title;
    public MyThread(String title) {
        this.title = title;
    }
    @Override
    public void run() {
        for(int x = 0; x < 10; x ++){
            System.out.println(this.title + "运行,x = " + x);
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        for (int x = 0 ; x < 3 ; x ++) {
            String title = "线程对象-" + x ;
            Runnable run = ()->{
                for (int y = 0 ; y < 10 ; y ++) {
                    System.out.println(this.title + "运行,y = " + y);
                }
            };
            new Thread(run).start();
        }
    }
}

主方法再次优化:

public class ThreadDemo{
    public static void main(String[] args){
        for (int x = 0 ; x < 3 ; x ++) {
            String title = "线程对象-" + x ;
            new Thread(()->{
                for (int y = 0 ; y < 10 ; y ++) {
                    System.out.println(this.title + "运行,y = " + y);
                }
            }).start();
        }
    }
}

在以后的开发中对于多线程的实现,优先考虑的就是Runnable接口实现,并且永恒都是通过Thread类对象启动多线程。

1-4 Thread与Runnable关系

经过一系列的分析之后可以i发现,在多线程的实现过程之中已经有了两种做法:Thread类、Runnable接口如果从代码本身来讲我们肯定使用Runnable是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充。

但是从结构上也需要来观察Thread与Runnable的联系,打开Thread类的定义:

capture_20200917164313892

在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread调用Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候先调用Thread类的run()方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。

多线程开发的本质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述主要通过Runnable完成的。

capture_20200917164814672

范例:利用卖票程序来实现多个线程的资源并发访问。

class MyThread implements Runnable {
    private int ticket = 5;
    @Override
    public void run() {
        for(int x = 0; x < 100; x ++){
            if(this.ticket > 0){
            	System.out.println(this.title + "卖票,ticket = " + this.ticket --);
            }
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        //三个线程访问同一个资源
        MyThread mt = new MyThread();
        new Thread(mt).start();
        new Thread(mt).start();
        new Thread(mt).start();
    }
}

通过内存分析图来分析本程序的执行结构。

capture_20200917185109787

1-5 Callable接口实现多线程

从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点:当线程执行完毕之后我们无法获取一个返回值,所以从JDK1.5之后就提出了一个新的线程实现接口:java.util.concurrent.Callable接口,首先来观察这个接口的定义:

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

可以发现Callable定义的时候可以设置一个泛型,此泛型的类型就是返回数据的类型,这样的好处是可以避免向下转型所带来的安全隐患。

capture_20200917211151042

import java.util.concurrent.Callable;

class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        for(int x = 0; x < 10; x ++) {
            System.out.println("******** 线程执行、x = " + x);
        }
        return "线程执行完毕。";
    }
}
public class ThreadDemo{
    public static void main(String[] args) throws Exception {
        FutureTask<String> task = new FutureTask<>(new MyThread());
        new Thread(task).start();
        System.out.println("【线程返回数据】" + task.get());
    }
} 

面试题:请解释Runnable与Callable的区别?

Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的;

java.lang.Runnable接口之中只提供有一个run()方法,并且没有返回值;

java.util.concurrent.Callable接口提供有call()方法,可以有返回值;

1-6 多线程运行状态

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

capture_20200917230809989

1、任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候若干个线程都将进入到一种就绪状态,现在并没有执行;

2、进入到就绪状态之后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run()方法),但所有的线程不可能一直持续执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间之后就需要让出资源,而后这个线程就将进入到阻塞状态,随后重新回归到就绪状态;

3、当run()方法执行完毕之后,实际上该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态

二、线程常用操作方法

多线程的主要操作方法都在Thread类中定义了。

2-1 线程的命名和取得

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

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

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

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

范例:观察线程的命名操作

class MyThread implements Runnable {
    @Override
    public void run() {
		System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        //三个线程访问同一个资源
        MyThread mt = new MyThread();
        new Thread(mt,"线程A").start();  
        new Thread(mt).start();			
        new Thread(mt,"线程B").start();  

    }
}

当开发者为线程设置名字的时候就使用设置的名字,而如果没有设置名字,则会自动生成一个不重复的名字,这种自动的属性命名主要是依靠了static属性完成的,在Thread类里面定义有如下操作:

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

观察一个程序:

class MyThread implements Runnable {
    @Override
    public void run() {
		System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        //三个线程访问同一个资源
        MyThread mt = new MyThread();
        new Thread(mt,"线程对象").start();  
        mt.run(); 
    }
}

通过此时的代码可以发现当使用了"mt.run()"直接在主方法之中调用线程类对象中的run()方法所获得的线程对象的名字为"main",所以可以得出一个结论:主方法也是一个线程。那么现在的问题来了,所有的线程都是在进程上的划分,那么进程在哪里?每当使用java命令执行程序的时候就表示启动了一个JVM的进程,一台电脑上可以同时启动若干个JVM进程,所以每一个JVM进程都会有各自的线程。

public class Demo {
    public static void main(String args[]) {
        for (int x = 0 ; x < Integer.MAX_VALUE ; x ++) {
            System.out.println();
        }
    }
}

在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程处理 ;

范例:子线程处理

class MyThread implements Runnable {
    @Override
    public void run() {
		System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadDemo{
    public static void main(String[] args){
		System.out.println("1、执行操作任务一。");
        new Thread(()->{	//子线程负责统计
            int temp = 0;
            for (int x = 0 ; x < Integer.MAX_VALUE ; x ++) {
                temp += x;
        	}
        }).start();
        System.out.println("2、执行操作任务二。");
        System.out.println("n、执行操作任务N。");
    }
}

主线程负责处理整体流程,而子线程负责处理耗时操作。

2-2 线程休眠

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

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

在进行休眠的时候有可能产生中断异常"InterruptedException",中断异常属于Exception的子类,该异常必须进行处理。

观察休眠处理:

public class ThreadDemo{
    public static void main(String[] args){
		new Thread(()->{
        	for (int x=0 ; x<10 ; x++){
                System.out.println(Thread.currentThread().getName() + "、x = " + x);
                try{
                    Thread.sleep(1000); //暂缓1秒执行
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"线程对象").start();
    }
}

休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,休眠也是有先手顺序的。

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

public class ThreadDemo{
    public static void main(String[] args){
        Runnable run = ()->{
            for (int x=0 ; x<10 ; x++){
                System.out.println(Thread.currentThread().getName() + "、x = " + x);
                try{
                    Thread.sleep(1000); //暂缓1秒执行
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
        for (int num=0 ; num<5 ; num++){
            new Thread(run,"线程对象 - " + num).start();
        }
    }
}

此时将产生五个线程对象,并且这五个线程对象执行的方法体是相同的。此时从程序执行的感觉上来讲好像是若干个线程一起进行了休眠,而后一起进行了自动唤醒,但是实际上是有差别的。

2-3 线程中断

在之前发现线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其他线程完成的。在Thread类里面提供有这种中断执行的处理方法:

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

范例:观察线程的中断处理操作

public class ThreadDemo{
    public static void main(String[] args){
		Thread thread = new Thread(()->{
			System.out.println("***72个小时的疯狂我需要睡觉补充精力。");
            try{
                Thread.sleep(10000);    //预计准备休眠10秒
                System.out.println("***睡足了");
            }catch(InterruptedException e){
                System.out.println("谁打扰我睡觉");
            }
        });
        thread.start();    //开始睡
        Thread.sleep(1000);    //先睡个1秒
        if(!thread.isInterrupted()){    //该线程中断了吗?
            System.out.println("我偷偷的打扰一下你的睡眠。");
            thread.interrupt();    //中断执行  
        }
    }
}

所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理。

2-4 线程强制运行

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

范例:观察一个没有强制执行的程序

public class ThreadDemo{
    public static void main(String[] args){
        Thread thread = new Thread(()->{
            for (int x=0 ; x<100 ; x++){
                try{
                    Thread.sleep(100); 
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行、x = " + x);
            }
        },"玩耍的线程");
        thread.start();
    }
}

这个时候主线程和子线程都在交替执行着,但是如果说现在你希望主线程独占执行。那么就可以利用Thread类中的方法:

  • 强制执行:public final void join() throws InterruptedException

强制执行的程序:

public class ThreadDemo{
    public static void main(String[] args){
        Thread mainThread = Thread.currentThread();//获得主线程
        Thread thread = new Thread(()->{
            for (int x=0 ; x<100 ; x++){
                if(x == 3){    //现在霸道的线程来了
                    try{
                        mainThread.join();    //霸道的线程要先执行
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
                try{
                    Thread.sleep(100); 
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行、x = " + x);
            }
        },"玩耍的线程");
        thread.start();
        for (int x=0 ; x<100 ; x++){
            Thread.sleep(100); 
            System.out.println("【霸道的main线程】number = " + x);
        }
    }
}

在进行线程强制执行的时候一定要获取强制执行线程对象之后才可以执行join()调用。

2-5 线程礼让

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

  • 礼让:public static void yield();
public class ThreadDemo{
    public static void main(String[] args){
        Thread thread = new Thread(()->{
            for (int x=0 ; x<100 ; x++){
                if(x % 3 == 0){
                    Thread.yield();    //线程礼让
                    System.out.println("###玩耍的线程礼让执行###");
                }
                try{
                    Thread.sleep(100); 
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行、x = " + x);
            }
        },"玩耍的线程");
        thread.start();
        for (int x=0 ; x<100 ; x++){
            Thread.sleep(100); 
            System.out.println("【霸道的main线程】number = " + x);
        }
    }
}

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

2-6 线程优先级

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

  • 设置优先级:public final void setPriority(int newPriority);

  • 获取优先级:public final int getPriority();

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

  • 最高优先级:public static final int MAX_PRIORITY; //10
  • 中等优先级:public static final int NORM_PRIORITY; //5
  • 最低优先级:public static final int MIN_PRIORITY; //1

范例:观察优先级

public class ThreadDemo{
    public static void main(String[] args){
        Runnable run = ()->{
            for(int x=0 ; x<10 ; x++){
                try{
                    Thread.sleep(1000); 
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ "执行。");
            }
        };
        Thread threadA = new Thread(run,"线程对象A");
        Thread threadB = new Thread(run,"线程对象B");
        Thread threadC = new Thread(run,"线程对象C");
        threadA.setPriority(Thread.MIN_PRIORITY);
        threadB.setPriority(Thread.MIN_PRIORITY);
        threadC.setPriority(Thread.MAX_PRIORITY); //优先级高的只是有可能先执行
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

主方法是一个主线程,那么主线程的优先级呢?

public class ThreadDemo{
    public static void main(String[] args){
        System.out.println(new Thread().getPriority());				//默认线程优先级:5
		System.out.println(Thread.currentThread().getPriority());   //主线程优先级:5
    }
}

主线程是属于中等优先级,而默认创建的线程也是中等优先级,所以主线程和子线程实际上是并发执行。

三、线程的同步与死锁

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

3-1 同步问题引出

下面编写一个简单的卖票程序,将创建若干个线程对象实现卖票的处理操作。

范例:实现卖票操作

class MyThread implements Runnable {
    private int ticket = 10;  
    @Override
    public void run(){
        while(true){
            if(this.ticket>0){
                System.out.println(Thread.currentThread().getName()+ "卖票,ticket = " + this.ticket --);
            }else{
                System.out.println("***** 票已经卖光了 *****");
                break;
            }
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args){
		Mythread mt = new Mythread();
        new Mythread(mt,"票贩子A").start();
        new Mythread(mt,"票贩子B").start();
        new Mythread(mt,"票贩子C").start();
    }
}

此时的程序将创建三个线程对象,并且这三个线程对象将进行5张票的出售。此时程序在进行卖票处理的时候并没有任何的问题(假象),下面可以模拟一下卖票中的延迟操作。

class MyThread implements Runnable {
    private int ticket = 10;  
    @Override
    public void run(){
        while(true){
            if(this.ticket>0){
                try{
                    Thread.sleep(1000);   //模拟网络延迟
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ "卖票,ticket = " + this.ticket --);
            }else{
                System.out.println("***** 票已经卖光了 *****");
                break;
            }
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args){
		Mythread mt = new Mythread();
        new Mythread(mt,"票贩子A").start();
        new Mythread(mt,"票贩子B").start();
        new Mythread(mt,"票贩子C").start();
    }
}

这个时候追加了延迟问题就暴露出来了,而实际上这个问题一直都在。

会出现票数为0或-1的情况

capture_20200919011430801

3-2 线程同步处理

经过分析之后已经可以确认同步问题所产生的主要原因了,那么下面就需要进行同步问题的解决,但是解决同步问题的关键是锁,指的是当某一个线程执行操作的时候,其他线程外面等待;

capture_20200920001024090

如果要想在程序之中实现这把锁的功能,就可以使用synchronized关键字来实现,利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作里面的代码只允许一个线程执行。

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

synchronized(同步对象){
    同步代码操作;
}

一般要进行同步对象处理的时候可以采用当前对象this进行同步。

范例:利用同步代码块解决数据同步访问问题

class MyThread implements Runnable {
    private int ticket = 10; 
    @Override
    public void run(){
        synchronized(this){  //每一次只允许一个线程进行访问
            while(true){
                if(this.ticket>0){
                    try{
                        Thread.sleep(1000);   //模拟网络延迟
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+ "卖票,ticket = " + this.ticket --);
                }else{
                    System.out.println("***** 票已经卖光了 *****");
                    break;
                }
            }
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args){
		Mythread mt = new Mythread();
        new Mythread(mt,"票贩子A").start();
        new Mythread(mt,"票贩子B").start();
        new Mythread(mt,"票贩子C").start();
    }
}

加入同步处理之后,程序的整体的性能下降了。同步实际上会造成性能的降低

2、利用同步方法解决:只需要在方法定义上使用synchronized关键字即可。

class MyThread implements Runnable {
    private int ticket = 10;  
    public synchronized boolean sale(){
        if(this.ticket>0){
            try{
                Thread.sleep(100);   //模拟网络延迟
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "卖票,ticket = " + this.ticket --);
            return true;
        }else{
            System.out.println("***** 票已经卖光了 *****");
            return false;
        }
    }
    @Override
    public void run(){
        while(this.sale()){
			;
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args){
		MyThread mt = new MyThread();
        new Thread(mt,"票贩子A").start();
        new Thread(mt,"票贩子B").start();
        new Thread(mt,"票贩子C").start();
    }
}

在日后学习Java类库的时候会发现,系统中许多的类上使用的同步处理采用的都是同步方法。

3-3 线程死锁

死锁是在进行多线程同步的处理之中有可能产生的一种问题,所谓的死锁指的是若干个线程彼此互相等待的状态。

一个简单的代码来观察一下死锁的表现形式,但是对于此代码不作为重点。

范例:死锁的展示

public class DeadLock implements Runnable{
    private ZhangSan zs = new ZhangSan();
    private LiSi ls = new LiSi();
    @Override
    public void run(){
        zs.say(ls);
    }
    public DeadLock(){
        new Thread(this).start();
        ls.say(zs);
    }
    public static void main(String[] args){
        new DeadLock();
    }
}
class ZhangSan{
    public synchronized void say(LiSi ls){
        System.out.println("2、张三:过这条路,得先付10元");
        ls.get();
    }
    public synchronized void get(){
        System.out.println("4、张三:得到了10元,于是让出了路");
    }
}
class LiSi{
    public synchronized void say(ZhangSan zs){
        System.out.println("1、李四:让我先过,我再给钱");
        zs.get();
    }
    public synchronized void get(){
        System.out.println("3、李四:逃过一劫,可以继续出发。");
    }
}

//死锁状态,解除其中一个synchronized即可解锁

现在死锁造成的主要原因是因为彼此都在互相等待着,等待着对方先让出资源。死锁实际上是一种开发中出现的不确定的状态,有的时候代码如果处理不当则会不定期出现死锁,这是属于正常开发中的调试问题。若干个线程访问同一资源时一定要进行同步处理,而过多的同步会造成死锁。

四、综合实战:“生产者-消费者”模型

4-1 生产者与消费者基本程序模型

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

  • 生产者负责信息内容的生产;

  • 每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息;

  • 如果生产者没有生产完则消费者要等待它生产完成,如果消费者还没有对信息进行消费,则生产者应该等待消费处理完成后再继续生产。

程序的基本实现:

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

  • 数据一:title= 张三、content= 弓长;
  • 数据二:title= 李四、content= 木子;

capture_20200921003241613

范例:程序基本结构

public class ThreadDemo{
    public static void main(String[] args) throws Exception {
        Message msg = new Message();
        new Thread(new Producer(msg)).start();
        new Thread(new Consumer(msg)).start();
    }
}
class Producer implements Runnable{
    private Message msg;
    public Producer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            if(x % 2 == 0){
                this.msg.setTitle("张三");
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                this.msg.setContent("弓长");
            }else{
                this.msg.setTitle("李四");
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                this.msg.setContent("木子");
            }
        }
    }
}
class Consumer implements Runnable{
    private Message msg;
    public Consumer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            try{
                Thread.sleep(100);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(this.msg.getTitle() + " - " + this.msg.getContent());
        }
    }
}
class Message{
    private String title;
    private String content;
    public void setContent(String content){
        this.content = content;
    }
    public void setTitle(String title){
        this.title = title;
    }
    public String getContent(){
        return this.content;
    }
    public String getTitle(){
        return this.title;
    }
}

通过整个代码的执行你会发现此时有两个主要问题:

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

4-2 解决生产者-消费者同步问题

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

范例:解决同步操作

class Message{
    private String title;
    private String content;
    public synchronized void set(String title,String content) {
        this.title = title;
        try{
            Thread.sleep(100);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        this.content = content;
    }
    public synchronized String get(){
        try{
            Thread.sleep(100);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        return this.title + " - " + this.content;
    }
}
public class ThreadDemo{
    public static void main(String[] args) throws Exception {
        Message msg = new Message();
        new Thread(new Producer(msg)).start();
        new Thread(new Consumer(msg)).start();
    }
}
class Producer implements Runnable{
    private Message msg;
    public Producer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            if(x % 2 == 0){
                this.msg.set("张三","弓长");
            }else{
                this.msg.set("李四","木子");
            }
        }
    }
}
class Consumer implements Runnable{
    private Message msg;
    public Consumer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            System.out.println(this.msg.get());
        }
    }
}

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

4-3 利用Object类解决重复操作

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

  • 等待机制:

    • 死等:public final void wait() throws InterruptedException;
    • 设置等待时间:public final void wait(long timeout) throws InterruptedException;
    • 设置等待时间:public final void wait(long timeout, int nanos) throws InterruptedException;
  • 唤醒第一个等待线程:public final void notify();

  • 唤醒全部等待线程:public final void notifyAll();

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

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

capture_20200921211123097

范例:修改Message类

class Message{
    private String title;
    private String content;
    private boolean flag = true;	//表示生产或消费的形式
    // flag = true:允许生产,但是不允许消费
    // flag = false:允许消费,不允许生产
    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(10);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        try{
        	return this.title + " - " + this.content;
        }finally{	//不管如何都要执行
            this.flag = true;	//继续生产
            super.notify();		//唤醒等待线程
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args) throws Exception {
        Message msg = new Message();
        new Thread(new Producer(msg)).start();
        new Thread(new Consumer(msg)).start();
    }
}
class Producer implements Runnable{
    private Message msg;
    public Producer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            if(x % 2 == 0){
                this.msg.set("张三","弓长");
            }else{
                this.msg.set("李四","木子");
            }
        }
    }
}
class Consumer implements Runnable{
    private Message msg;
    public Consumer(Message msg){
        this.msg = msg;
    }
    @Override
    public void run(){
        for(int x = 0; x < 100; x ++){
            System.out.println(this.msg.get());
        }
    }
}

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

五、多线程深入话题

5-1 优雅的停止线程

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

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

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

范例:实现线程柔和的停止

public class ThreadDemo{
    public static boolean flag = true;
    public static void main(String[] args) throws Exception{
        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); 	
        flag = false;		//停止线程
    }
}

万一现在有其他的线程去控制这个flag的内容,那么这个时候对于线程的停止也不是说停就立刻停止的,而是会在执行中判断flag的内容来完成。

5-2 后台守护线程

现在假设有一个人并且这个人有一个保镖,那么这个保镖一定是在这个人活着的时候进行守护,如果这个人已经死了,保镖没用了。所以在多线程里可以进行守护线程的定义,也就是说如果现在主线程的程序或者其他的线程还在执行的时候,那么守护线程将一直存在,并且运行在后台状态。

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

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

范例:使用守护线程

public class ThreadDemo{
    public static boolean flag = true;
    
    public static void main(String[] args) throws Exception{
        Thread userThread = new Thread(()->{
            for(int x = 0; x < 10; x ++){
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
            }
        },"用户线程");		//完成核心业务
        Thread daemonThread = new Thread(()->{
            for(int x = 0; x < Integer.MAX_VALUE; x ++){
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
            }
        },"守护线程");		//完成核心业务
        daemonThread.setDaemon(true);//设置为守护线程
        userThread.start();
        daemonThread.start();
    }
}

可以发现所以的守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也就消失了,在整个的JVM里面最大的守护线程就是GC线程。

程序执行中GC线程会一直存在,如果程序执行完毕,GC线程也将消失。

5-3 volatile关键字

在多线程的定义之中,volatile关键字主要是在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。

这样的话在一些书上就将其错误的理解为同步属性了。

在正常进行变量处理的时候往往会经历如下的几个步骤:

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

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

capture_20200922225340691

class MyThread implements Runnable {
    private volatile int ticket = 5; //直接内存操作
    @Override
    public void run() {
        synchronized(this){
            while(this.ticket > 0){
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "买票处理,ticket = " + this.ticket--);
            }
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args){
        MyThread mt = new MyThread();
        new Thread(mt,"票贩子A").start();
        new Thread(mt,"票贩子B").start();
        new Thread(mt,"票贩子C").start();
    }
}

六、多线程综合案例

6-1 数字加减

多线程案例分析一

​ 设计4个线程对象,两个线程执行减操作,两个线程执行加操作。

public class ThreadDemo{
    public static void main(String[] args) throws Exception{
        Resource res = new Resource();
        SubThread st = new SubThread(res);
        AddThread at = new AddThread(res);
        new Thread(at,"加法线程 - A").start();
        new Thread(at,"加法线程 - B").start();
        new Thread(st,"减法线程 - X").start();
        new Thread(st,"减法线程 - Y").start();
    }
}

class Resource{						
    private int num = 0 ; 			
    private boolean flag = true ; 	
    
    public synchronized void add() throws Exception { 		
        while(this.flag == false){			
            super.wait();
        }
        Thread.sleep(100);
        this.num++;
        System.out.println("【加法操作 - " + Thread.currentThread().getName() + "】num = " + this.num);
        this.flag = false;			
        super.notifyAll();			
    }
    public synchronized void sub() throws Exception { 		
        while(this.flag == true){			
            super.wait();
        }
        Thread.sleep(200);
        this.num--;
        System.out.println("【减法操作 - " + Thread.currentThread().getName() + "】num = " + this.num);
        this.flag = true;
        super.notifyAll();
    }
}

class AddThread implements Runnable{
    private Resource resource;
    public AddThread(Resource resource){
        this.resource = resource;
    }
    @Override
    public void run(){
        for(int x = 0 ; x < 50 ; x ++){
        	try{
                this.resource.add();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

class SubThread implements Runnable{
    private Resource resource;
    public SubThread(Resource resource){
        this.resource = resource;
    }
    @Override
    public void run(){
        for(int x = 0 ; x < 50 ; x ++){
        	try{
                this.resource.sub();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

这一题目是一个经典的多线程的开发操作,这一个程序里面一定要考虑的核心本质在于:加一个、减一个,整体的计算结果应该只在0、-1、1之间循环出现。

永远在循环(loop)里调用 wait 和 notify,不是在 If 语句

现在你知道wait应该永远在被synchronized的背景下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在while循环,而不是if语句中调用wait。因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者开始小号数据。所以记住,永远在while循环而不是if语句中使用wait!我会推荐阅读《Effective Java》,这是关于如何正确使用wait和notify的最好的参考资料。

那么我们之前做的while和if...else...语句的操作其实是一样的,就是保护核心逻辑代码不被错误的唤醒执行,碰巧解决了这个问题。

6-2 生产电脑

设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工要等待新电脑产出;如果生产出的电脑没有搬走,则要等待电脑搬走之后再生产,并统计出生产的电脑数量。

在本程序之中实现的就是一个标准的生产者与消费者的处理模型,那么下面实现具体的程序代码。

public class ThreadDemo{
    public static void main(String[] args) throws Exception{
        Resource res = new Resource();
        new Thread(new Producer(res)).start();
        new Thread(new Consumer(res)).start();
    }
}
class Producer implements Runnable{
    private Resource resource;
    public Producer(Resource resource){
        this.resource = resource;
    }
    public void run(){
        for(int x = 0; x < 50; x ++){
            try{
            	this.resource.make();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
class Consumer implements Runnable{
    private Resource resource;
    public Consumer(Resource resource){
        this.resource = resource;
    }
    public void run(){
        for(int x = 0; x < 50; x ++){
            try{
            	this.resource.get();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
class Resource{
    private Computer computer;
    public synchronized void make() throws Exception{
        if(this.computer != null){	
            super.wait();
        }
        Thread.sleep(100);
        this.computer = new Computer("HS牌电脑",1.1);
        System.out.println("【生产电脑】" + this.computer);
        super.notifyAll();
    }
    public synchronized void get() throws Exception{
        if(this.computer == null){	
            super.wait();
        }
        Thread.sleep(10);
        System.out.println("【取走电脑】" + this.computer);
        this.computer = null;	
        super.notifyAll();
    }
}
class Computer{
    private static int count = 0;
    private String name;
    private double price;
    public Computer(String name, double price){
        this.name = name;
        this.price = price;
        count ++;
    }
    public String toString(){
        return "【第" + count + "台电脑】" + "电脑名字:" + this.name + "、价值:" + this.price;
    }
}

6-3 竞争抢答

实现一个竞拍抢答程序:要求设置三个抢答者(三个线程),而后同时发出抢答指令,抢答成功者给出成功提示,未抢答成功者给出失败提示。

对于这一个多线程的操作由于里面需要牵扯到数据的返回问题,那么现在最好使用的Callable是比较方便的一种处理形式。

public class ThreadDemo{
    public static void main(String[] args) throws Exception	{
        MyThread mt = new MyThread();
        FutureTask<String> taskA = new FutureTask<String>(mt);
        FutureTask<String> taskB = new FutureTask<String>(mt);
        FutureTask<String> taskC = new FutureTask<String>(mt);
        new Thread(taskA,"竞赛者A").start();
        new Thread(taskB,"竞赛者B").start();
        new Thread(taskC,"竞赛者C").start();
        System.out.println(taskA.get());
        System.out.println(taskB.get());
        System.out.println(taskC.get());
    }
}
class Mythread implements Callable<String>{
    private boolean flag = false;	
    @Override
    public String call() throws Exception{
        synchronized(this){		
            if(this.flag == false) {	
                this.flag = true;
                return Thread.currentThread().getName() + "抢答成功!";
            }else{
                return Thread.currentThread().getName() + "抢答失败!";
            }
        }
    }
}
posted @ 2020-09-27 22:39  颉颃  阅读(181)  评论(0编辑  收藏  举报