多线程

线程简介

  一个程序执行后,就是一个进程,一个进程可以有多个线程,且至少有一个,否则这个进程就没有存在的意义了。我们自己写的Java程序启动后,通常至少有2个线程,一个是主线程,即main线程,也叫用户线程,我们写的线程都在该线程中;一个是垃圾回收线程,即gc线程,也叫守护线程

 

线程的创建方式

  线程的3种创建方式:

  • Thread  class    --> 继承Thread类(重点)
  • Runnable接口   --> 实现Runnable接口(重点)
  • Callable接口      --> 实现Callable接口(了解)

一、Thread

  使用方法,三步走: 

    1、自定义线程类,并继承Thread类;

  2、重写run()方法,编写线程体执行的代码;

  3、创建线程对象,调用start()方法,启动线程。

  代码示例:

// 1、创建一个线程类,并继承Thread类
public class TestThread1 extends Thread {
    // 2、重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("这是run线程的数据" + i);
        }
    }
    // 这里是用main函数测试的
    public static void main(String[] args) {
        // 3、创建线程对象,并调用start()方法,启动线程
        TestThread1 thread1 = new TestThread1();
        thread1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("这是main线程的数据" + i);
        }
    }
}

  从执行结果可以看出,2个循环是‘同时’执行的,输出的结果是穿插的。

                 

 

 Thread方法的练习:使用多线程,下载网络图片

  准备工作:下载一个FileUtils文件工具类的依赖jar包

  1、到maven仓库中,搜索Commons IO,然后下载对应的jar包(由于我是在Java项目测试的,所以下载的jar包,如果是Springboot项目中,可以下载对应的pom依赖);

  2、添加到项目中:新建package叫lib,将jar包复制到该包中;

  3、添加jar包到项目的Library中:右键lib,选择add as library,点击确定即可。

  至此,准备工作完成。

  多线程测试下载网络图片代码示例:   

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 1、自定义线程类,并继承Thread类
public class TestThread2 extends Thread{
    private String url;
    private String name;
    // 通过有参构造的方式,在new一个线程时,传入相应的参数;
    public TestThread2(String url, String name){
        this.url = url;
        this.name = name;
    }
    // 2、重写run()方法
    @Override
    public void run() {
        // 方法的执行体,去调用图片下载的方法
        WebDownload webDownload = new WebDownload();
        webDownload.download(url,name);
        System.out.println("当前下载了" + name);
    }

    public static void main(String[] args) {
        // 3、创建线程对象,并调用start()方法;
        TestThread2 t1 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F0%2F5850a64def538.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=6be89ef4fb6e5043d23461b1a7452a41","图片一.jpeg");
        TestThread2 t2 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F6%2F577e098f74c2b.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=152af68eccf40a7c6c9fe52f30002c37","图片二.jpeg");
        TestThread2 t3 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20171017%2Fc3da1ba09e2d4f0680e23e3f5099bb98.jpeg&refer=http%3A%2F%2F5b0988e595225.cdn.sohucs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=983abc09a1bf16dafd59f1e25618f869","图片三.jpeg");
        t1.start();
        t2.start();
        t3.start();
    }
}
class WebDownload{
    // 下载方法
    public void download(String url, String name){
        try {
            // 这个工具类,就是前面准备工作下载
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("文件下载的方法抛出异常" + e);
        }
    }
}
### 这里是输出结果:
顺序是3、1、2,且每次执行后,结果的顺序是不一样的。这就说明,线程是由CPU来调度的,并不是启动后,立马就执行的。

当前下载了图片三.jpeg
当前下载了图片一.jpeg
当前下载了图片二.jpeg


 二、Runnable

  使用方法三步走:

  1、定义线程类,实现Runnable接口;

  2、实现run()方法,编写线程执行体代码;

  3、创建线程对象,调用start()方法,启动线程。

  代码示例:

 1 // 1、创建线程类,实现Runnable接口
 2 public class TestThread3 implements Runnable {
 3     // 2、实现run()方法,编写线程体执行程序
 4     @Override
 5     public void run() {
 6         // 线程执行体
 7         for (int i = 0; i < 2000; i++) {
 8             System.out.println("我是线程体中的数据" + i);
 9         }
10     }
11 
12     public static void main(String[] args) {
13         // 3、创建实现了Runnable接口的类的对象
14         TestThread3 testThread = new TestThread3();
15         Thread t1 = new Thread(testThread);
16         t1.start();
17         // 上面2行可以合并为下面1行
18         // new Thread(testThread).start();
19 
20         for (int i = 0; i < 2000 ; i++) {
21             System.out.println("我是main体中的数据" + i);
22         }
23     }
View Code

   运行结果部分截图:可以看出是穿插输出的。

    

 

 模拟多线程抢票

  直接上代码:

public class TestThread4 implements Runnable {
    private int tickets = 10;
    @Override
    public void run() {
        while(true){
            if(tickets <= 0){
                break;
            }
            System.out.println("第" + tickets-- + "张票被用户" + Thread.currentThread().getName() + "买走了");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();
        new Thread(ticket, "张三").start();
        new Thread(ticket, "李四").start();
        new Thread(ticket, "王五").start();
        new Thread(ticket, "赵六").start();
    }
}
View Code

  运行结果如图:

         

 

   由上图发现问题:第9张票,同时被2个人买走,这在现实中是不合理的,这就涉及到了并发的问题,需要用线程同步来解决。(后续讲解)

 

线程状态

  线程一共有5种状态,它们之间的关系,如下图所示:

   

一、线程停止

  线程停止的方式有多种,但是JDK提供的一些方法如stop()、destroy()等方法都是不建议使用的,且已废弃,所以推荐让线程自己停下来,即设置一个标志位,并在线程中使用这个标志位,当达到某种条件时,通过转换标志位的值,来进行线程的终止。

  示例代码:

public class TestLamda implements Runnable {
    // 1、设置一个标志位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        // 2、线程中的方法使用标志位
        while(flag){
            System.out.println("这是线程中的数据 " + i++);
        }
    }

    // 3、外部定义一个改变标志位的方法
    public void stopThread(){
        this.flag = false;
    }

    public static void main(String[] args) {
        TestLamda tl = new TestLamda();
        new Thread(tl).start();

        for (int i = 0; i < 1000 ; i++) {
            System.out.println("这是main中的数据 " + i);
            if(i == 988){
                // 4、当达到某种条件时,调用3中的方法,转换标志位,来终止线程。
                tl.stopThread();
                System.out.println("线程要停止了。。。");
            }
        }
    }
}

  运行结果:

 

 

   由图可知,当main方法中的i值为988时,停止了线程中的方法,此时,线程中的设置累加到了785,每启动一次,该值是不同的。

 

二、线程休眠

  线程休眠,JDK提供的方法为Thread.sleep();

  • sleep(时间) 指定当前线程阻塞(休眠)的毫秒数。
  • sleep方法存在InterruptedException异常,需要try...catch捕捉或抛出;
  • sleep时间结束后,线程重新进入 就绪状态。
  • sleep可以模拟网络延时、倒计时等等。
  • 每一个对象都有一个锁,sleep不会释放锁

  使用sleep(),每隔2秒,输出一下当前时间;

public static void main(String[] args) {
        while(true){
            System.out.println(new Date());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

输出结果:

Mon Jan 25 22:29:56 CST 2021
Mon Jan 25 22:29:58 CST 2021
Mon Jan 25 22:30:00 CST 2021
Mon Jan 25 22:30:02 CST 2021
Mon Jan 25 22:30:04 CST 2021
Mon Jan 25 22:30:06 CST 2021

 

三、线程礼让

  线程礼让,JDK提供的方法为Thread.yield();

  • 让当前正在执行的线程暂停,但不阻塞。
  • 将线程从 运行状态 转为 就绪状态 。
  • 让CPU重新调度,礼让 不一定成功。  

  代码示例:

public class TestYield implements Runnable {
    public static void main(String[] args) {
        TestYield testLamda = new TestYield();
        new Thread(testLamda,"线程A").start();
        new Thread(testLamda,"线程B").start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始执行");
        Thread.yield();  // 线程礼让
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

  多次运行结果如下图:

     

 

四、线程强制执行 

  线程的强制执行,用的是join方法,需要new一个Thread对象,然后调用该方法;

  该方法的作用是:优先执行join进来的线程中的方法。就好比排队时插队。

   代码示例:

public class TestJoin implements Runnable {
    public static void main(String[] args) {
        TestJoin test = new TestJoin ();
        // 需要创建对象
        Thread th = new Thread(test,"线程A");
        th.start();
        for (int i = 0; i < 100; i++) {
            if(i == 20){ // 当i == 20时,VIP线程插入,强制执行;
                try {
                    th.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        // 这是main线程执行的语句 System.out.println(
"我是普通用户" + i); } } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("我是VIP,我先来" + i); } } }

  执行结果截图:可以看出,VIP进来后,所有的数据跑完,才跑main中的数据。

             

 线程的优先级

  Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。(注:优先级高的不一定100%最先执行)

  线程的优先级使用数字来表示,范围从1-10,其中1、5、10还可以用以下方式表示:

  • Thread.MIN_PRIORITY  => 1; 
  • Thread.MAX_PRIORITY => 10;
  • Thread.NORM_PRIORITY => 5;

  线程优先级的设置/获取方式分别是:

  •  getPriority()    获取优先级;
  •  setPriority(int num)   设置优先级;

  如果要设置线程的优先级,一定要先设置,再调用start()方法。

  代码示例:

public class Test implements Runnable {
    public static void main(String[] args) {
        // main线程的优先级
        System.out.println(Thread.currentThread().getName() + "的优先级为 -->" + Thread.currentThread().getPriority());
        Test test = new Test();
        Thread th1 = new Thread(test);
        Thread th2 = new Thread(test);
        Thread th3 = new Thread(test);
        Thread th4 = new Thread(test);
        Thread th5 = new Thread(test);
        th1.setPriority(Thread.MIN_PRIORITY);
        th2.setPriority(Thread.MAX_PRIORITY);
        th3.setPriority(6);
        th4.setPriority(3);
        th1.start();
        th2.start();
        th3.start();
        th4.start();
        th5.start(); // 没有设置,则是默认的优先级,即5 
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的优先级为 -->" + Thread.currentThread().getPriority());
    }
}

  多次运行,结果如图:优先级高的,并不一定最先执行

     

 线程同步(重点)

  线程同步是用来解决并发(多个线程操作同一资源的现象,即为并发)问题的。

  线程同步本质就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再进入。

  当然,仅仅有队列还是不行的,这样是不安全的,为了安全,需要用到锁机制,队列加锁才能真正意义上实现 线程同步

  当一个线程获得对象的锁,其他线程则必须挂起、等待,这样就会引起性能问题,正所谓:鱼与熊掌不可兼得,性能和安全亦是如此。

 线程不安全示例

  示例一:买火车票:

public class Test implements Runnable {
   // 定义10张火车票
private int ticket = 10;
   // 定义状态位用来终止线程
private boolean flag = true; public static void main(String[] args) { Test te = new Test(); new Thread(te, "张三").start(); new Thread(te, "李四").start(); new Thread(te, "王五").start(); } @Override public void run() { while(flag){ if(ticket <= 0){ flag = false; } try { // 延迟 可以放大问题 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "张票"); } } }

  不安全现象:多人买同一张票、票数出现负数。

 原因:每一个线程都是一个独立执行的路径,每个线程都在自己的工作内存中交互,当看到最后一张票时,各自把最后一张票拿到自己的内存中进行处理,从而导致数据不安全。

  对于线程不安全的处理,可以使用synchronized关键字来处理,它有2种使用方法:

  • synchronized方法:在方法上加synchronized关键字;
  • synchronized块:将对象加锁,具体语法:synchronized(Object){执行体}

  因此买火车票的安全处理方式,是将买票的方法加锁,即可实现安全买票。

 

  方法加锁有2种方式

  一是:synchronized 的方式,它可以隐式的加锁,解锁;

  二是:Lock锁的方式,它是显示的加锁(lock()),解锁(unlock()),详见 ReentrantLock类的源码。

 

 

 

  此外,synchronized还可以锁对象,Lock锁是无法锁对象的。

线程通讯

  线程间通讯需要使用到线程的wait()方法和notify()notifyAll()方法。

 

线程池

  线程池,其实就是一个能容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源;

  创建线程池的方式:通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法,这里以Runnable的方式为例:

  • 创建线程池对象;
  • 创建 Runnable 接口子类的实例对象;
  • 提交 Runnable 接口子类对象;
  • 关闭线程池;
// 创建线程池的方式  10是线程池中线程的个数。
ExecutorService threadService = Executors.newFixedThreadPool(10);
// 创建Runnable的实例对象
MyRunnable myRunnable = new MyRunnable();
// 从线程池中获取线程对象
threadService.submit(myRunnable);


// 关闭连接池
threadService.shutdown();

 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中。

posted on 2021-01-29 00:20  Miracle_Jerry  阅读(175)  评论(0)    收藏  举报