JAVA基础——多线程

Posted 我永远信仰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础——多线程相关的知识,希望对你有一定的参考价值。

1、线程、进程、多任务

多任务

首先需要了解操作系统中的多任务:在同一时刻运行多个程序的能力。

例如,在编辑或下载邮件的同时可以打印文件。

多线程程序(进程)

多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。

通常,每一个任务称为一个线程,可以同时运行一个以上线程的程序称为多线程程序,也可以称为进程。

线程是进程的一个执行片段。

多进程与多线程的区别

本质的区别在于每个进程拥有自己的一整套资源,而线程是共享资源。这听起来似乎有风险,的确也是这样,后面将解释这个问题。

然而,共享资源使线程之间的通信比进程之间的通信更有效、更容易。此外,在有些操作系统中,与进程相比,线程更“轻量级”,创建、销毁一个线程比启动一个新进程的开销要小得多。

多线程解释

使用单线程只有一条执行路径,在调用其他线程时需要等其执行完,主线程再继续执行,如果调用的线程需要执行一些耗时的操作,那么这种效率明显是极低的,如左图。

使用多线程可以解决上面遇到的问题。调用其它线程的时候,为它开辟一个新的线程,它是一个子线程,与主线程并行执行,效率明显提高。

在实际中,多线程非常有用。例如,一个浏览器可以同时下载几幅图片,可以在下载图片的同时也可以播放视频。浏览网页……

2、线程状态

线程可以有五大状态:

  • new (新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed_Waiting(计时等待)
  • Terminated(被终止)

线程的五大状态

1.新线程创建(new)

​ 当用new操作符创建一个新线程时,如new Thread(),该线程还没开始运行。这意味着它的状态是new,程序还没开始运行线程中的代码。在线程运行之前还有一些基础工作要做。

可运行线程(Runnable)

​ 一旦调用start方法,线程处于Runnable(可运行)状态。一个可运行状态的线程可能正在运行,也可能没有运行,这取决于操作系统给线程提供运行的时间。(一个正在运行中的线仍然处于可运行状态)

​ 一旦一个线程开始运行,它不必始终保持运行,事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程提供一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。选择下一个线程时,操作系统考虑线程的优先级——见后续。

​ 在具有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当然,如果线程的数目多于处理器的数目,调度器依然采用时间片机制。

​ 在任何给定的时刻,一个可运行线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。

三种创建方式:(只关注前两种)

方法1

继承Thread类,重写run方法(线程的执行体),调用start开启线程。

public class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 8; i++) {
            System.out.println("我在学习线程"+i);
        }
    }
	//主线程
    public static void main(String[] args) {
        //调用start方法开启多线程。这里可以看出交替输出,说明两个线程在交替执行。
        new Thread1().start();
        //执行普通的run方法。这里测试发现是顺序输出,只是执行了普通的方法
        //new Thread1().run();
        for (int i = 0; i < 8; i++) {
            System.out.println("我在写代码"+i);
        }
    }
}

注意:线程开启不一定执行,由cpu调度。

一个多线程实例。(下载3张网络图片)

首先需要导入一个包 apache下的commons-io包,百度即可。

然后将这个包添加到库,就可以开始啦。

import org.apache.commons.io.FileUtils;

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

//多线程测试类
public class TestThread2 extends Thread{
    private String url;//图片的url,网络地址。
    private String name;//保存的文件名

    public TestThread2(String url,String name){
       this.url=url;
       this.name=name;
    }

    //下载图片线程的执行体
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为"+name);
    }

    //主线程
    public static void main(String[] args) {
        //创建三个下载器,分别下载三种图片,参数url和文件名。
        TestThread2 t1 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKWBZUWGIPXnSACv153Pjp5wAAL6tQAAAAAAK_X_094.jpg", "1.jpg");
        TestThread2 t2 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKmBZUVmIIIjiACJyjAuzCC4AAL6swNQumsAInKk211.jpg", "2.jpg");
        TestThread2 t3 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0C/09/ChMkKWDMTWaIBEoYAA3JrKqtoqIAAQzRgPfjJsADcnE262.jpg", "3.jpg");

        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}


//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            //用包下的工具类FileUtils,下载网络图片的方法copyURLToFile
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            System.out.println("IO异常,downloader方法出现问题");
        }
    }
}

运行结果。

可见,输出顺序并不是按照代码的顺序来执行的。说明着三个线程同时执行,谁最先下好谁先输出。

方法2

实现Runnable接口,重写run方法(线程的执行体)。用Thread代理执行,将实现了Runnable类的对象丢给Thread代理调用start开启线程。这是与方法1的主要区别

public class TestThread3 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 8; i++) {
            System.out.println("我在学习线程"+i);
        }
    }
    //主线程
    public static void main(String[] args) {
        TestThread3 testThread3 = new TestThread3();
        //代理,将实现了Runnable类的对象丢给Thread,然后调用start方法。
        new Thread(testThread3).start();
        for (int i = 0; i < 8; i++) {
            System.out.println("我在写代码"+i);
        }
    }
}

下载三张图片的例子改成用Runnable实现。需要改动的地方

将extends Thread改成Implements Runnable

线程启动方式改为

//开启线程
t1.start();
t2.start();
t3.start();
//改为
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();

一个实例:模拟抢票(初识并发问题)

三个人抢票。票被抢光程序执行结束

public class TestThread4 implements Runnable{
    private int ticket = 10;//票数

    @Override
    public void run() {
        while(true){
            if (ticket<=0){//当票被抢光
                break;
            }
            System.out.println(Thread.currentThread().getName()+"抢到了第"+ticket--+"张票");
        }

    }

	
    public static void main(String[] args) {
        //创建一个对象。里面有十张票。
        TestThread4 testThread4 = new TestThread4();
		//开启三个线程抢着同一个对象的十张票。
        new Thread(testThread4,"小明").start();
        new Thread(testThread4,"小红").start();
        new Thread(testThread4,"小芳").start();
    }
}

运行结果:

这里发现,第10张票被抢了两次,同一张票只应该被抢一次,说明程序是有漏洞的。这涉及了并发的问题,后续会讲到。

总结

  • 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用,继承只可以单继承,有局限性。
  • 实现Runnable类
    • 实现接口Runnable类具备多线程能力
    • 启动线程:Thread对象(传入目标对象).start
    • 建议使用:接口可以多实现,灵活方便,同一个对象可以被多个线程使用。避免了单继承的局限性

方法三(了解即可)

执行步骤

  • 创建执行服务
  • 提交执行
  • 获取结果
  • 关闭服务

改造下载三张图片的案例

import com.thread.demo01.WebDownloader;
import java.util.concurrent.*;

public class TestCallable implements Callable<Boolean> {

    private String url;
    private String name;//保存的文件名

    public TestCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }

    //下载图片线程的知执行体
    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为" + name);
        return false;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKWBZUWGIPXnSACv153Pjp5wAAL6tQAAAAAAK_X_094.jpg", "1.jpg");
        TestCallable t2 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKmBZUVmIIIjiACJyjAuzCC4AAL6swNQumsAInKk211.jpg", "2.jpg");
        TestCallable t3 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0C/09/ChMkKWDMTWaIBEoYAA3JrKqtoqIAAQzRgPfjJsADcnE262.jpg", "3.jpg");

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);

        //获取结果
        Boolean rs1 = r1.get();
        Boolean rs2 = r2.get();
        Boolean rs3 = r3.get();

        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);
        //关闭服务
        ser.shutdownNow();
    }
}

总结:

  • 可以定义返回值
  • 可以抛出异常

2.线程停止

线程只能启动一次,线程中断或者结束,一旦进入死亡状态,就不能再次启动。

线程因如下两个原因之一而被终止:

  • 因为run方法正常退出而自然死亡
  • 因为一个没有捕获的异常终止了run方法而意外死亡

注意:

  • 不推荐使用JDK提供的stop()、destory()方法【已废弃】
  • 推荐线程自己停下来
  • 建议使用一个标志位来控制线程终止,当flag=false,终止线程运行。

实例:

在主线程中开启一个子线程,当循环达到900次时,设置标志位为false,终止子线程

public class ThreadStop implements Runnable {
    //设置标志位,控制线程停止
    private boolean flag = true;

    @Override
    public void run() {
        int i = 1;
        while (flag) {
            System.out.println("线程run...在执行" + i++);
        }
    }

    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        ThreadStop threadStop = new ThreadStop();
        new Thread(threadStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main执行" + i + "次");
            if (i == 900) {
                threadStop.stop();
                System.out.println("线程停止了");
            }
        }
    }
}

3.线程休眠

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。

  • sleep(时间) 指定当前线程阻塞的毫秒数
  • sleep存在异常Interrupt Exception;
  • sleep时间到达后线程进入就绪状态
    • sleep进入阻塞
  • sleep可以模拟网络延时,倒计时等
    • 模拟网络延时:放大问题的发生性
  • 每个对象都有一把锁,sleep不会释放锁

实例:

模拟倒计时:

public class ThreadSleep{

    //模拟倒计时,10秒
    public void tenDown() throws InterruptedException {
        int num=10;
        while (true) {
            if (num<=0){
                break;
            }
            System.out.println(num);
            //休眠1秒
            Thread.sleep(1000);
            num--;
        }
    }

    public static void main(String[] args) {
        ThreadSleep threadSleep = new ThreadSleep();
        try {
            threadSleep.tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.线程让步

  • 线程让步,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,让步不一定成功。看CPU的调度情况
    • 比如说有a、b两个线程,a进入了cpu,然后a调用了yield()让步方法,a就从cpu里出来了,a和b重新竞争cpu。cpu重新调度,有可能调用a线程,也有可能调用b。
  • 注意,yield是一个静态方法

实例:

两个线程的礼让。

package com.thread.demo01;

public class ThreadYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }
}
class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程结束执行");
    }
}
/*
运行结果:成功案例
a线程开始执行
b线程开始执行
b线程结束执行
a线程结束执行
*/

5.线程强制执行(插队)

  • 线程插队后,等到该线程执行完,才执行其他线程,其他线程处于阻塞状态

实例:

一开始两个线程共抢资源,当主线程执行到200的时候,vip线程执行join方法(插队),之后等到vip线程执行完后主线程才执行。

package com.thread.demo01;

/**
 * @Author cyh
 * @Date 2021/8/5 21:50
 */
public class ThreadJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("线程VIP"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadJoin threadJoin = new ThreadJoin();
        Thread thread = new Thread(threadJoin);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            if (i == 200) {
                thread.join();
            }
            System.out.println("main"+i);
        }
    }
}

6.观测线程状态

  • 观测线程的状态可以让我们适时的对他做出一些改变。比如发现该线程一致处于等待状态,我们可以让他销毁或者插队。

例子:

观察线程的状态

package com.thread.demo01;

/**
 * @Author cyh
 * @Date 2021/8/5 22:20
 */
public class ThreadState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }以上是关于JAVA基础——多线程的主要内容,如果未能解决你的问题,请参考以下文章

java基础入门-多线程同步浅析-以银行转账为样例

java多线程基础

Java基础之多线程

Java多线程基础

多线程编程学习一(Java多线程的基础)

Java 多线程基础多线程的实现方式