15—JAVA(进阶)—多线程

01多线程

Java 是少数的几种支持“多线程”的语言之一。大多数的程序语言只能循序运行单独一个程序块,无法同时运行不同的多个程序块。 Java 的“多线程”恰可弥补这个缺憾,它可以让不同的程序块一起运行,如此一来可让程序运行更为顺畅,同时也可达到多任务处理的目的。
例如:我们的手机可以边聊微信边听歌,就是多线程

1.1进程与线程

  • 进程:是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的 CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好象是在“同时”运
    行一样。
  • 线程是比进程更小的执行单位,线程是进程内部单一的一个顺序控制流。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,形成多条执行线索。一个进程可能包含了多个同时执行的线程。
  • 多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程和进程的主要差别体现在以下两个方面:
    ( 1)、同样作为基本的执行单元,线程是划分得比进程更小的执行单位。
    ( 2)、每个进程都有一段专用的内存区域。与此相反,线程却共享内存单元(包括代码和数据),通过共享的内存单元来实现数据交换、实时通信与必要的同步操作。
  • 多线程的应用范围很广。
    在一般情况下,程序的某些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其它部分的执行,这种情况下,就可以考虑创建一个线程,令它与那个事件或资源关联到一起,并让它独立于主程序运行。通过使用线程,可以避免用户在运行程序和得到结果之间的停顿,还可以让一些任务(如打印任务)在后台运行,而用户则在前台继续完成一些其它的工作。总之,利用多线程技术,可以使编程人员方便地开发出能同时处理多个任务的功能强大的应用程序。

1.2认识线程

  • 在传统的程序语言里,运行的顺序总是必须顺着程序的流程来走,遇到 if-else 语句就加以判断,遇到 forwhile 等循环就会多绕几个圈,最后程序还是按着一定的程序走,且一次只能运行一个程序块。
  • Java 的“多线程”打破了这种传统的束缚。
    所谓的线程( Thread)是指程序的运行流程,“多线程”的机制则是指可以同时运行多个程序块,使程序运行的效率变得更高,也可克服传统程序语言所无法解决的问题。
    例如:有些包含循环的线程可能要使用比较长的一段时间来运算,此时便可让另一个线程来做其它的处理。

1.2.1 通过继承 Thread 类实现多线程

  • Thread 类存放在 java.lang 包下,不用手动导包
    通过继承Thread类实现多线程的步骤:

  • 1,编写一个类继承Thread类

  • 2,重写父类的run()方法,将要执行的语句放入方法体中

  • 3,使用类对象或者匿名对象调用父类的start()方法启动线程

  • 注意:当我们调用start()方法后,java虚拟机会调用我们重写的run()方法,所以run()方法不需要我们自己调用

  • 编写类的格式:

class 类名称 extends Thread // 从 Thread 类扩展出子类
{
    属性
    方法…
    修饰符 run(){ // 复写 Thread 类里的 run()方法
        使用线程处理的程序;
    }
}
  • 演示代码:
//1.编写一个类实现Thread类
class TestThread extends Thread{
    //2.重写父类的run()方法
    public void run(){
        System.out.println("TestThread执行了。。。");
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        //3.使用匿名对象调用父类的start()方法,启动线程
        //线程启动后会执行run()方法中的内容
        new TestThread().start();

        //在主线程中定义一个循环
        for(int i=0;i<100;i++){
            System.out.println("main执行了。。。");
        }
    }
}
  • 通过分析上面代码执行的结果可以发现,在main方法执行的过程中,穿插着执行了TestThread方法中run()方法体中的内容。

1.2.2通过实现 Runnable 接口实现多线程

  • 由于JAVA 程序只允许单一继承,即一个子类只能有一个父类,所以在 Java 中如果一个类继承了某一个类,同时又想采用多线程技术的时,就不能用 Thread 类产生线程,因为 Java 不允许多继承,这时就要用 Runnable接口来创建线程了
  • 注意:Thread类是Runnable接口的子类

通过实现 Runnable 接口实现多线程的步骤

  • 1,编写一个类实现Runnable接口
  • 2,重写父接口中的run()方法,将要执行的语句放入方法体中
  • 3,创建Runnable接口子类的实例化对象
  • 4,通过 Thread 类的 start()方法,启动多线程序(查看JDK文档可以发现,Runnable接口中只有一个run()方法)
  • 通过查找JDK 文档中的 Thread 类发现,在 Thread 类之中,有这样一个构造方法:public Thread(Runnable target),由此构造方法可以发现,可以将一个 Runnable 接口的实例化对象作为参数去实例化 Thread 类对象
  • 编写类的格式:
class 类名称 implements Runnable // 实现 Runnable 接口
{
	属性
	方法…
	修饰符 run(){ // 复写 Thread 类里的 run()方法
	以线程处理的程序;
	}
}
  • 演示代码:
//1.编写一个类实现Runnable接口
class TestThread implements Runnable{
    //2.重写父接口的run()方法
    public void run(){
        System.out.println("TestThread执行了。。。");
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        //3.创建Runnable子类的实例化对象
        TestThread t=new TestThread();
        //4.通过 Thread 类的 start()方法,启动多线程序
        new Thread(t).start();

        //在主线程中定义一个循环
        for(int i=0;i<100;i++){
            System.out.println("main执行了。。。");
        }
    }
}

1.2.3 两种多线程实现机制的比较

  • 不管实现了 Runnable 接口还是继承了 Thread 类其结果都是一样的,那么这两者之间有什么关系呢?通过查看 JDK 文档发现二者之间的联系如下:
    在这里插入图片描述
  • Thread类是Runnable接口的子类
  • 那么两者之间除了这些联系之外还有什么区别呢?
  • 继承Thread类
    1,一个类继承 Thread 类之后,这个类的对象无论调用多少次 start()方法,结果都只有一个线程在运行,而且程序运行还会出现异常
class TestThread extends Thread{
    private int tickit=20;
    public void run(){
        while(true){
            if(tickit>0){
                System.out.println(Thread.currentThread().getName()+"出售票"+tickit--);
            }else { break;}
            }
        }
    }

public class Test {
    public static void main(String[] args) throws Exception {
       TestThread t=new TestThread();
       //创建一个对象对此调用start方法
       t.start();
       t.start();
       t.start();
    }
}

运行结果:

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.base/java.lang.Thread.start(Thread.java:794)
	at Test.main(Test.java:17)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
Thread-0出售票20
Thread-0出售票19
Thread-0出售票18
Thread-0出售票17
Thread-0出售票16
Thread-0出售票15
Thread-0出售票14
Thread-0出售票13
Thread-0出售票12
Thread-0出售票11
Thread-0出售票10
Thread-0出售票9
Thread-0出售票8
Thread-0出售票7
Thread-0出售票6
Thread-0出售票5
Thread-0出售票4
Thread-0出售票3
Thread-0出售票2
Thread-0出售票1

2,通过创建多个线程对象后可以发现,每个对象会各自占有各自的资源,所以可以得出结论,用 Thread 类实际上无法达到资源共享的目的。

class TestThread extends Thread{
    private int tickit=10;
    public void run(){
        while(true){
            if(tickit>0){
                System.out.println(Thread.currentThread().getName()+"出售票"+tickit--);
            }else { break;}
            }
        }
    }

public class Test {
    public static void main(String[] args) throws Exception {
    	//创建多个匿名对象
       new TestThread().start();
       new TestThread().start();
       new TestThread().start();
       new TestThread().start();
    }
}

运行结果:

Thread-3出售票10
Thread-2出售票10
Thread-1出售票10
Thread-1出售票9
Thread-1出售票8
Thread-0出售票10
Thread-1出售票7
Thread-2出售票9
Thread-3出售票9
Thread-2出售票8
Thread-1出售票6
Thread-0出售票9
Thread-0出售票8
Thread-0出售票7
Thread-0出售票6
Thread-0出售票5
Thread-0出售票4
Thread-0出售票3
Thread-0出售票2
Thread-0出售票1
Thread-1出售票5
Thread-2出售票7
Thread-3出售票8
Thread-2出售票6
Thread-1出售票4
Thread-2出售票5
Thread-3出售票7
Thread-2出售票4
Thread-1出售票3
Thread-1出售票2
Thread-1出售票1
Thread-2出售票3
Thread-2出售票2
Thread-2出售票1
Thread-3出售票6
Thread-3出售票5
Thread-3出售票4
Thread-3出售票3
Thread-3出售票2
Thread-3出售票1
  • 实现Runnable接口
    1,通过创建多个线程对象后可以发现,尽管启动了四个线程对象,但是结果都是操纵了同一个资源,实现了资源共享的目的
class TestThread implements Runnable{
    private int tickit=10;
    public void run(){
        while(true){
            if(tickit>0){
                System.out.println(Thread.currentThread().getName()+"出售票"+tickit--);
            }else { break;}
            }
        }
    }

public class Test {
    public static void main(String[] args) throws Exception {
        TestThread t=new TestThread();
        //启动四个线程对象
       new Thread(t).start();
       new Thread(t).start();
       new Thread(t).start();
       new Thread(t).start();
    }
}

运行结果:

Thread-2出售票8
Thread-2出售票6
Thread-2出售票5
Thread-1出售票9
Thread-2出售票4
Thread-3出售票7
Thread-3出售票1
Thread-0出售票10
Thread-2出售票2
Thread-1出售票3

1.2.4实现Runnable接口的好处

  • 1, 适合多个相同程序代码的线程去处理同一资源的情况,把虚拟 CPU(线程)同程序的代码、数据有效分离,较好地体现了面向对象的设计思想。
  • 2, 可以避免由于 Java 的单继承特性带来的局限。开发中经常碰到这样一种情况,即:当要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,那么就只能采用实现 Runnable 接口的方式了。
  • 3,增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程可以操作相同的数据,与它们的代码无关。当共享访问相同的对象时,即共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable 接口的类的实例。

1.3线程状态

  • 每个 Java 程序都有一个缺省的主线程(所谓缺省:就是自动启动,不用手动创建),对于 Java 应用程序,主线程就是 main()方法执行的线程。
  • 要想实现多线程,必须在主线程中创建新的线程对象。
  • 任何线程一般具有五种状态,即创建、就绪、运行、阻塞、终止
    在这里插入图片描述

1、 新建状态
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。
新建一个线程对象可采用线程构造方法来实现,例如:

 Thread thread=new Thread();

2、 就绪状态
新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件
3、 运行状态
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。 run()方法定义了该线程的操作和功能。
4、 堵塞状态
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出 CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()、 suspend()、 wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。
5、 死亡状态
线程调用 stop()方法时或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

1.4线程操作的一些方法

  • 在 JAVA 实现多线程的程序里,虽然 Thread 类实现了Runnable 接口,通过查看JDK可知Runnable接口中只有一个run()方法,操作线程的主要方法在 Thread类之中。

在下面介绍Thread类中的方法

1.4.1 获取和设置线程的名称

public final void setName(String name):获取线程名称
public final String getName():设置线程名称

  • 线程的名称一般在启动线程前设置,但也允许为已经运行的线程设置名称。
  • 允许两个 Thread 对象有相同的名字,但为了清晰,应该尽量避免这种情况的发生。
  • 另外,如果程序并没有为线程指定名称,则系统会自动的为线程分配一个名称
  • 注意:默认情况下JAVA程序会产生两个线程:一个是 main()方法线程,另外一个就是垃圾回收( GC)线程,无需手动开启。

1.4.2 判断线程是否启动

  • 调用Thread 类的 start()方法之后启动线程
    public final boolean isAlive():判断线程是否处于活动状态
  • 线程已经启动且尚未终止,则为活动状态。
  • 如果该线程处于活动状态,则返回 true;否则返回 false。
  • 演示代码:
class TestThread implements Runnable{
   public void run(){
       System.out.println("TestThread线程启动了。。。");
   }
}

public class Test {
    public static void main(String[] args) throws Exception {
        //创建线程对象
        TestThread t=new TestThread();
        //调用Thread类的构造
        Thread t1=new Thread(t);
        //设置线程名字
        t1.setName("TestThread");
        //获取线程名字
        String threadName=t1.getName();
        System.out.println(threadName);
        System.out.println("还未调用start方法。。。。");
        System.out.println("线程状态:"+t1.isAlive());
        t1.start();
        System.out.println("调用了start方法。。。");
        System.out.println("线程状态:"+t1.isAlive());

    }
}
  • 运行结果:
TestThread
还未调用start方法。。。。
线程状态false
调用了start方法。。。
TestThread线程启动了。。。
线程状态true

1.4.3 后台线程与 setDaemon()方法

  • 对 Java 程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程在运行,这个进程就会结束。
  • 前台线程是相对后台线程而言的,前面所介绍的线程都是前台线程。
  • 那么什么样的线程是后台线程呢?如果某个线程对象在启动(调用 start()方法)之前调用了 setDaemon(true)方法,这个线程就变成了后台线程。
  • public final void setDaemon(boolean on):将该线程标记为守护(后台)线程
  • 该方法必须在启动线程前调用

1.4.4线程的强制运行

  • public final void join()throws InterruptedException:用来强制某一线程运行
  • 使用 join()方法会抛出一个 InterruptedException,所以在程序中需要用 try…catch()捕获
  • 此方法应放在start方法之后
  • 所谓的强制执行其实是,当某一线程调用join方法后,该线程中的代码将会被并入到了 main 线程中,此线程中的代码不执行完, main 线程中的代码就只能一直等待
  • 查看 JDK 文档可以发现,除了有无参数的 join 方法外,还有两个带参数的 join 方法,分别是 join(long millis)join(long millis,int nanos),它们的作用是指定合并时间,前者精确到毫秒,后者精确到纳秒,
    意思是两个线程合并指定的时间后,又开始分离,回到合并前的状态
  • 代码演示
class TestThread implements Runnable{
   public void run(){
       for(int i=0;i<10;i++) {
           System.out.println("TestThread线程启动了。。。");
       }
   }
}

public class Test {
    public static void main(String[] args) throws Exception {
        //创建线程对象
        TestThread t=new TestThread();
        //调用Thread类的构造
        Thread t1=new Thread(t);
        //设置线程名字
        t1.setName("TestThread");
        t1.start();
        //调用join方法强制此线程执行
        // 此线程结束之前不会执行主线程中的内容
        try {
            t1.join();
        }catch (InterruptedException e){
            System.out.println("线程中断了。。。");
        }
        System.out.println("主线程执行了。。。");
    }
}

1.4.5 线程的休眠

  • public static void sleep(long millis) throws InterruptedException:在指定的毫秒数内让当前正在执行的线程休眠
  • 使用 sleep()方法会抛出一个 InterruptedException,所以在程序中需要用 try…catch()捕获

1.4.6 线程的中断

  • public void interrupt():中断线程
    -public boolean isInterrupted():测试线程是否已经中断

1.5多线程的同步

1.5.1 同步问题的引出

  • 在卖票程序中,使用多线程,极有可能碰到一种意外,就是同一张票号被打印两次或多次,也可能出现打印出的票号为 0 或是负数。
  • 分析下面这行代码
if(tickets>0){
	System.out.println(Thread.currentThread().getName()+" 出 售 票
"+tickets--);
}

分析:

  • 假设 tickets 的值为 1 的时候,线程 1 刚执行完 if(tickets>0)这行代码,正准备执行下面的代码,就在这时,操作系统将 CPU 切换到了线程 2 上执行,此时 tickets 的值仍为 1,线程 2 执行完上面两行代码, tickets 的值变为 0 后, CPU 又切回到了线程 1上执行,线程 1 不会再执行 if(tickets>0)这行代码,因为先前已经比较过了,并且比较的结果为真,线程 1 将直接往下执行这行代码:System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);但此刻 tickets 的值已变为 0,屏幕打印出的将是 0。
  • 要想立即见到这种意外,可用在程序中调用 Thread.sleep()静态方法来刻意造成线程间的这种切换,Thread.sleep()方法迫使线程执行到该处后暂停执行,让出 CPU 给别的线程,在指定的时间(这里是毫秒)后, CPU 回到刚才暂停的线程上执行。
  • 修改完的 TestThread 代码如下
class TestThread implements Runnable{
    //预设2张票
   private int ticket=2;
   public void run(){
       while(true) {
           if (ticket > 0) {
               //捕获异常
               try {
                   //休眠线程
                   Thread.sleep(100);
               }catch (Exception e){
               }
               System.out.println(Thread.currentThread().getName() + "出售票" + ticket--);
           }else {
               break;
           }
       }
   }
}

public class Test {
    public static void main(String[] args) throws Exception {
        //创建线程对象
        TestThread t=new TestThread();
        //启动了四个线程,实现了资源共享的目的
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
  • 运行结果:
Thread-2出售票0
Thread-1出售票1
Thread-3出售票-1
Thread-0出售票2
  • 造成这种意外的根本原因就是因为资源数据访问不同步引起的。那么该如何去解决这个问题呢?解决这种问题的关键是下面要引入的同步的概念

1.5.2 同步代码块

if(tickets>0){
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
  • 当一个线程运行到 if(tickets>0)后,CPU 不去执行其它线程,必须等到下一句执行完后才能去执行其它线程中的有关代码块。这段代码就好比一座独木桥,任何时刻,都只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。
  • 同步代码块定义语法:
synchronized(对象){
	需要同步的代码 ;
}
  • 修改5.1中的代码:
class TestThread implements Runnable{
    //预设2张票
   private int ticket=2;
   public void run(){
       while(true) {
           //同步代码块
           synchronized (this) {
               if (ticket > 0) {
                   //捕获异常
                   try {
                       //休眠线程
                       Thread.sleep(100);
                   } catch (Exception e) {
                   }
                   System.out.println(Thread.currentThread().getName() + "出售票" + ticket--);
               } else {
                   break;
               }
           }
       }
   }
}

public class Test {
    public static void main(String[] args) throws Exception {
        //创建线程对象
        TestThread t=new TestThread();
        //启动了四个线程,实现了资源共享的目的
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
  • 运行结果:
Thread-0出售票2
Thread-0出售票1

1.5.3 对方法进行同步

  • 除了可以对代码块进行同步外,也可以对方法实现同步,只要在需要同步的方法定义前加上 synchronized 关键字即可
  • 同步方法定义语法
访问控制符 synchronized 返回值类型 方法名称(参数)
{. ;
}
  • 在同一类中,使用 synchronized 关键字定义的若干方法,可以在多个线程之间同步,当有一个线程进入了有 synchronized 修饰的方法时,其它线程就不能进入同一个对象使用 synchronized 来修饰的所有方法,直到第一个线程执行完它所进入的synchronized 修饰的方法为止。

1.5.4 死锁

  • 一旦有多个进程,且它们都要争用对多个锁的独占访问,那么就有可能发生死锁。
  • 如果有一组进程或线程,其中每个线程都在等待其它进程或线程才可以执行操作,那么就称它们被死锁了。
  • 最常见的死锁形式是
    当线程 1 持有对象 A 上的锁,而且正在等待对象 B 上的锁;
    而线程 2 持有对象 B 上的锁,却正在等待对象 A 上的锁。
    这两个线程永远都不会获得第二个锁,或是释放第一个锁,所以它们只会永远等待下去。
  • 要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。
  • 强制出现死锁的方法:
    创建了两个类 Class_A 和 Class_B,它们分别具有方法 funA()和 funB(),在调用对方的方法前, funA()和 funB()都睡眠一会儿。在主类 Test 中创建 Class_A 和Class_B实例,然后,产生第二个线程以构成死锁条件。 funA()和 funB()使用 sleep()方法来强制死锁条件出现。而在真实程序中死锁是较难发现的:
//定义Class_A类
class Class_A{
    synchronized void funA(Class_B b){
        String ThreadName=Thread.currentThread().getName();
        try {
            //在调用Class_B中的方法前先睡一会
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(ThreadName+"调用Class_B中的last方法");
        b.last();
    }
    synchronized void last(){
        System.out.println("Class_A中的last方法");
    }
}
//定义Class_B类
class Class_B{
    synchronized void funB(Class_A a){
        String ThreadNmae=Thread.currentThread().getName();
        try {
            //在调用Class_A中的方法前先睡一会
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(ThreadNmae+"调用Class_A中last方法");
        a.last();
    }
    synchronized void last(){
        System.out.println("Class_B中的last方法");
    }
}

//定义测试类并实现Runnable接口(主类)
public class Test implements Runnable{
    Class_A a=new Class_A();
    Class_B b=new Class_B();
    //无参构造
    Test(){
        //设置当前线程的名字
        Thread.currentThread().setName("Main——>Thread");
        //调用此方法会默认调用该类的run方法
        new Thread(this).start();
        a.funA(b);
        System.out.println("mian线程运行结束");
    }
    @Override
    public void run() {
        Thread.currentThread().setName("Test——>Thread");
        b.funB(a);
        System.out.println("其他线程执行完毕");
    }

    public static void main(String[] args) {
        //创建一个类的实例会自动调用这个类的空参构造
       new Test();
    }
}
  • 分析:
    从运行结果可以发现, Test -->> Thread 进入了 b 的监视器,然后又在等待 a 的监视器。同时 Main -->> Thread 进入了 a 的监视器并等待 b 的监视器。这个程序永远不会完成,构成死锁。

1.6线程间的通讯

1.6.1 问题的引出

下面通过这样的一个应用来讲解线程间的通信。

  • 把一个数据存储空间划分为两部分:
    一部分用于存储人的姓名
    另一部分用于存储人的性别。
  • 这里的应用包含两个线程:
    一个线程向数据存储空间添加数据
    另一个线程从数据存储空间中取出数据
  • 这个程序有两种意外需要读者考虑?
    第一种意外:假设添加数据的线程刚向数据存储空间中添加了一个人的姓名,还没有加入这个人的性别, CPU 就切换到了取出数据线程,取出数据的线程就会把这个人的姓名和上一个人的性别联系到了一起。
    假设我们把存入数据线程命名为生产者线程,取出数据的线程命名为消费者线程,这种意外可以用下图表示:
    在这里插入图片描述
    第二种意外:存入数据的线程放了若干次数据,取出数据的线程才开始取数据,或者是取出数据的线程取完一个数据后,还没等到存入数据的线程放入新的数据,又重复取出已取过的数据

1.6.2 问题如何解决

1.6.2.1问题的演示

  • 先看一下刚才假设的情况的代码演示
  • 第一步:定义一个Person类
//定义Person类
class Person{
    String name;
    String sex;
}
  • 第二步:定义生产者线程Producer
//定义生产者线程
class Producer implements Runnable{
    //类做成员变量
    Person p=null;

    //有参构造
    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i=0;
        while(true) {
            //如果线程不停,就交替赋值
            if(i==0) {
                p.name="张三";
                p.sex="男";
            } else {
                p.name="李四";
                p.sex="女";
            }
            i=(i+1)%2;
        }
    }
}
  • 第三步:定义消费者线程
//定义消费者线程
class Consumer implements Runnable{
    //类做成员变量
    Person p=null;
    //Consumer的有参构造
    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        while(true){
            System.out.println(p.name+"<---->"+p.sex);
        }
    }
}
  • 第四步:编写测试类
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        new Thread(new Producer(p)).start();
        new Thread(new Consumer(p)).start();
    }
}
  • 运行结果(部分)
张三<---->女
张三<---->男
李四<---->女
张三<---->女
李四<---->男
张三<---->男
张三<---->女
张三<---->女
张三<---->男
张三<---->
  • 运行结果分析:
    从输出结果可以发现,原本李四是女、张三是男,现在却打印出了张三是女的奇怪现象,这是什么原因呢?从程序可以发现, Producer 类和 Consumer 类都是操纵了同一个 Person 类,有可能 Producer 类还未操纵完 Person 类, Consumer 类就已经将 Person 类中的内容取走了,这就是资源不同步的与原因

1.6.2.2问题的解决

  • Person 类中增加两个同步方法: set()和 get()
  • 第一步:定义Person类
//定义Person类
class Person{
    private String name;
    private String sex;
    //定义set方法,将两个属性一起赋值
    public synchronized void set(String name,String sex){
        this.name=name;
        this.sex=sex;
    }
    public synchronized void get(){
        System.out.println(this.name+"<---->"+this.sex);
    }
}
  • 第二步:定义Producer线程
//定义生产者线程
class Producer implements Runnable{
    //类做成员变量
    Person p=null;

    //有参构造
    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i=0;
        while(true) {
            //如果线程不停,就交替赋值
            if(i==0) {
               p.set("张三","男");
            } else {
               p.set("李四","女");
            }
            i=(i+1)%2;
        }
    }
}
  • 第三步:定义Consumer线程
//定义消费者线程
class Consumer implements Runnable{
    //类做成员变量
    Person p=null;
    //Consumer的有参构造
    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        while(true){
            p.get();
        }
    }
}
  • 第四步:编写测试类:
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        new Thread(new Producer(p)).start();
        new Thread(new Consumer(p)).start();
    }
}
  • 运行结果:
张三<---->男
张三<---->男
张三<---->男
张三<---->男
李四<---->女
李四<---->女
李四<---->女
李四<---->
  • 结果分析:
    可以发现程序的输出结果是正确的,但是这里又有一个新的问题产生了,从程序的执行结果来看, Consumer 线程对 Producer 线程放入的一次数据连续读取了多次,并不符合实际的要求。实际要求的结果是, Producer 放一次数据, Consumer 就取一次;反之, Producer 也必须等到 Consumer 取完后才能放入新的数据,而这一问题的解决就需要使用下面所要讲到的线程间的通信

1.6.2.3问题的进一步解决

  • Java 是通过 Object 类的 wait、 notify、 notifyAll,这几个方法来实现线程间的通信的,又因为所有的类都是从 Object 继承的,所以任何类都可以直接使用这些方法。下面是这三个方法的简要说明:
    wait:调用此方法的线程进入睡眠状态,直到其它线程调用该线程的 notify 方法才能被唤醒。
    notify:唤醒同一对象监听器中调用 wait 的第一个线程。类似排队买票,一个人买完之后,后面的人可以继续买。
    notifyAll:唤醒同一对象监听器中调用 wait 的所有线程,具有最高优先级的线程首先被唤醒并执行。

  • 如果想让上面的程序符合预先的设计需求,必须在类 Person中定义一个新的成员变量status 来表示数据存储空间的状态,当Consumer 线程取走数据后,status 值为 false,当Producer 线程放入数据后, status 值为 true。
    也就是status 为 true 时, Consumer 线程才能取数据,否则就必须等待 Producer 线程放入新的数据后的通知
    反之,只有 status为 false, Producer 线程才能放入新的数据,否则就必须等待 Consumer 线程取走数据后的通知

  • 第一步:定义Person类

//定义Person类
class Person{
    private String name;
    private String sex;
    private boolean status=false;
    //定义set方法,将两个属性一起赋值
    public synchronized void set(String name,String sex){
        if (status){
            try {
                // 睡眠线程,如果没有其他线程调用此线程的notify方法,此线程将一直睡眠
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name=name;
        try {
            //将此线程睡眠一会
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.sex=sex;
        status=true;
        //唤醒最先被睡眠的的此类的线程
        notify();
    }
    public synchronized void get(){
        if(!status){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.name+"<---->"+this.sex);
        status=false;
        notify();
    }
}
  • 第二步:定义Producer线程
//定义生产者线程
class Producer implements Runnable{
    //类做成员变量
    Person p=null;

    //有参构造
    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i=0;
        while(true) {
            //如果线程不停,就交替赋值
            if(i==0) {
               p.set("张三","男");
            } else {
               p.set("李四","女");
            }
            i=(i+1)%2;
        }
    }
}
  • 第三步:定义Consumer线程
//定义消费者线程
class Consumer implements Runnable{
    //类做成员变量
    Person p=null;
    //Consumer的有参构造
    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        while(true){
            p.get();
        }
    }
}
  • 第四步:定义测试类
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        new Thread(new Producer(p)).start();
        new Thread(new Consumer(p)).start();
    }
}
  • 运行结果
张三<---->男
李四<---->女
张三<---->男
李四<---->女
张三<---->男
李四<---->女
张三<---->
  • 运行结果分析
    上面的程序满足了设计的需求,解决了线程间通信的问题,即一个线程自己调用wait()方法进入睡眠,让其他线程来唤醒。
  • wait、 notify、 notifyAll 这三个方法只能在 synchronized 方法中调用,即无论线程调用一个对象的 wait 还是 notify 方法,该线程必须先得到该对象的锁标记,这样, notify只能唤醒同一对象监视器中调用 wait 的线程,使用多个对象监视器,就可以分别有多个 wait、 notify 的情况,同组里的 wait 只能被同组的 notify 唤醒。
  • 一个线程的等待和唤醒过程
    在这里插入图片描述

1.7线程生命周期

  • 任何事务都有一个生命周期,线程也不例外。那么在一个程序中,怎样控制一个线程的生命并让它更有效地工作呢?要想控制线程的生命,先得了解线程产生和消亡的整个过程。
  • 任何线程一般具有五种状态,即创建、就绪、运行、阻塞、终止
  • Java 语言中线程共有六种状态,分别是:
    NEW(初始化状态)
    RUNNABLE(可运行 / 运行状态)
    BLOCKED(阻塞状态)
    WAITING(无时限等待)
    TIMED_WAITING(有时限等待)
    TERMINATED(终止状态)
    其中:BLOCKED,WAITING,TIMED_WAITING 这三种状态属于Not Runnable状态。
  • 线程的声明周期图
    在这里插入图片描述
  • 从上图可知通过调用不同的方法可以把线程装换为不同的状态

02 线程池

2.1 为什么要使用线程池?

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
  • 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
  • 在Java中可以通过线程池来达到这样的效果

2.2 线程池的概念

  • 线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  • 合理利用线程池能够带来三个好处:
  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止消耗过多的内存(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

2.3 线程池的简单使用

  • JDK1.5之后才有

  • Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

  • java.util.concurrent.ExecutorServicejava.util.concurrent.Executor接口的子接口

  • 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("创建一个线程");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("创建的线程: " + Thread.currentThread().getName());
        System.out.println("线程执行结束,将线程返回给线程池");
    }
}

线程池测试类:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyThread t = new MyThread();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyThread中的run()

        // 从线程池中获取线程对象,然后调用MyThread中的run()
        service.submit(t);
        // 再获取个线程对象,调用MyThread的run()
        service.submit(t);
        service.submit(t);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池(一般不做)
        //service.shutdown();
    }
}
  • 运行结果:
创建一个线程
创建一个线程
创建的线程: pool-1-thread-1
创建的线程: pool-1-thread-2
线程执行结束,将线程返回给线程池
线程执行结束,将线程返回给线程池
创建一个线程
创建的线程: pool-1-thread-2
线程执行结束,将线程返回给线程池

猜你喜欢

转载自blog.csdn.net/weixin_45583303/article/details/105673564