多线程原理详解01(程序、进程、线程介绍,线程创建的三种方式(Thread、Runnable、Callable)、三种方式各自实现多线程的具体操作、代码解析)

目录

多线程原理详解

01_线程简介

任务、进程、线程、多线程

多任务

就是在一个时间内做很多事情,比如边吃饭变玩手机,但是本质上,在那一秒内,其实只是在玩手机,或者在一秒内只是在吃饭。

看起来像是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情而已

多线程

比如原本一条道路看成一个线程,多辆车都在一个道路上形式,容易出问题。

但是如果给道路划分路线,相当于弄多条线程,让多辆车在各自的线程行驶,出问题的概率就变小。

平常项目中的普通的方法调用,基本是一条线程走到底,即使调用方法,也是在方法执行完后继续往下走,只用到主线程而已。

多线程就是多条线程各自执行,效率更高。

就像一个大超市,只有一个收银员(单线程)和多个收银员(多线程)的区别。

程序、进程、线程

在操作系统中运行的程序就是进程,比如微信、QQ、腾讯视频、游戏这些就是程序 ,也就是进程

一个进程(腾讯视频)可以有多个线程(视频声音、图像、弹幕等)

Process(进程)与 Thread (线程)

程序(静态的)-->程序跑起来变成进程(动态的)-->进程里面包含多个线程,真正执行功能的就是这些线程

总结:

注意:

单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。

多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的

02_线程创建三种方式:

三种创建方式主要是:继承 Thread 类、实现 Runnable 接口,还有一个是 实现 Callable 接口

最好的是实现 Runnable 接口,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度。

这三种方式都能开启多个线程来执行业务逻辑。

1、继承 Thread 类

步骤:

1、自定义一个类,继承 Thread 类;

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

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

解释:

自定义一个类去继承Thread 类,作为一个线程类对象,然后就可以重写Thread类的 run() 方法;

接着在main主线程里面,可以通过线程对象去调用 start() 方法,该方法的作用是让 java 虚拟机去启动一个新的线程去执行 run() 方法里面的代码。

然后主线程继续执行自己的业务代码。

这样主线程和新的线程就都同时执行,这个就是多线程操作。

1-1:通过继承Thread类实现多线程

当我们调用 start() 方法后,start() 方法会通知 Java 虚拟机启动一个新线程(也就是子线程),并在新线程中调用该线程对象(t1)的 run() 方法。

而主线程调用start() 方法后,也会继续执行下面的代码。

这个时候,主线程和新线程就会并发执行,这样就演示出了多线程并发执行的特性。

演示代码
java 复制代码
//创建线程方式一:继承 Thread 类, 重写 run() 方法,调用 start() 方法开启线程
//注意:线程开启不一定立即执行,由 CPU 调度执行
public class TestThread01 extends Thread
{
    //重写 run() 方法
    @Override
    public void run()
    {
        //run 方法线程体
        for (int i = 0; i < 20; i++)
        {
            System.err.println("run方法线程:bbbbbb--->" + i);
        }

    }

    //main 主线程
    public static void main(String[] args)
    {
        //创建一个线程对象
        TestThread01 t1 = new TestThread01();

        //直接调用run方法,只是普通的调用,跟多线程没有关系
        //t1.run();

        //调用start()方法开启线程,主线程和run()方法的线程就会同时进行
        t1.start();

        for (int i = 0; i < 20; i++)
        {
            System.err.println("main主线程:aaaaaaaaaa--->" + i);
        }
    }

}
1-2:演示多线程同步下载图片:

先下载一个 Apache 的 Commons IO 的 jar 包,是一个文件下载的工具类库

下载之后解压,把这个 jar 包拷贝到项目里面去

点击右键,点击【Add library】添加进去即可。

有了这个工具类库,才可以使用比如 FileUtils 这样的工具类

代码实现:

代码中,可以看出如果是单线程下载这3张图片的话,下载顺序应该是1、2、3

可从结果看,下载顺序是3、2、1,说明是多线程在同步下载图片,哪条线程快就哪条先下载完。

如图,图片成功下载下来了,这图片是我其他 博客文章里面的图片

演示代码
java 复制代码
package cn.ljh.threaddemo.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

// 实现多线程同步下载图片
public class TestThread02 extends Thread
{
    private String url; //网络图片地址
    private String name;//保存的文件名

    //构造器
    public TestThread02(String url ,String name){
        this.url = url;
        this.name = name;
    }

    //重写run方法
    @Override
    public void run()
    {
        //run方法里面调用了文件下载的downloader方法
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.err.println("下载了文件名为:"+ name);
    }

    //主线程
    public static void main(String[] args)
    {
        //创建三个线程对象
        TestThread02 t1 = new TestThread02(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "1.jpg");
        TestThread02 t2 = new TestThread02(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "2.jpg");
        TestThread02 t3 = new TestThread02(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "3.jpg");
        //启动3条线程
        t1.start();
        t2.start();
        t3.start();
    }
}


//图片下载器
class WebDownloader{

    //下载方法
    public void downloader(String url,String  name){
        try
        {
            //把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e)
        {
            e.printStackTrace();
            System.err.println("IO异常,downloader 方法出现问题");
        }
    }

}

2、实现 Runnable 接口 (推荐)

步骤:

1、自定义一个类,实现 Runnable 接口;

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

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

2-1:通过实现Runnable接口来实现多线程

这个和上面继承 Thread 类的操作差别不大。

如图:这个实现了Runnable接口的自定义线程类TestThread03,作为参数传进到 Thread 这个线程对象里面,然后通过 Thread 这个线程对象调用 start() 方法启动新线程。

解释下这个自定义线程类TestThread03,为什么说它是一个代理对象。

详细解释:

代码中:

将 t 视为一个代理对象的原因是它充当了线程对象(thread)和具体任务逻辑之间的中介。

具体来说,t 自定义线程类对象 实现了 Runnable 接口,并在其 run() 方法中定义了线程要执行的任务逻辑。

当我们通过 Thread 类创建一个 thread 对象时,需要将一个实现了 Runnable接口的对象(t)作为参数传入,那么这个对象(t)就是线程对象(thread)的代理,它 (t) 负责管理线程的生命周期和执行任务的逻辑。

在调用 thread.start() 方法后,线程对象(thread)会自动调用代理对象(t)的 run() 方法,从而执行具体的任务逻辑。

t 作为一个代理对象,充当了线程对象(thread)和具体任务逻辑之间的桥梁,使得线程的管理和任务的执行能够分开进行,提高了代码的灵活性和可维护性。

简单来说:

实现了Runnable接口的t对象,它实现了run()方法,作用是在run()方法里面写具体的业务逻辑代码。

而创建一个 Thread 对象,作用是用来调用 start() 方法从而启动新线程。

而启动的新线程要执行的业务逻辑代码在t对象的run()方法里面,所以就把 t 作为代理对象传给 thread 对象。

这样就是通过代理的方式实现了线程的启动(thread.start())和任务(任务在run()方法里面)的执行。

和上面的继承 Thread 类差不多。

再简洁的说法:

继承 Thread 类的自定义线程类对象,是自身调用 start 方法启动新线程。

实现Runnable 接口的自定义线程类对象,是作为创建Thread线程对象的参数,由Thread线程对象来调用start方法启动新线程

具体代码
java 复制代码
package cn.ljh.threaddemo.demo01;

//创建线程方式2:实现 Runnable 接口,重写 run 方法,执行线程需要丢入 runnable 接口实现类,调用 start 方法

public class TestThread03 implements Runnable
{
    //实现 run() 方法
    @Override
    public void run()
    {
        //run 方法线程体
        for (int i = 0; i < 20; i++)
        {
            System.err.println("run方法线程:bbbbbb--->" + i);
        }
    }

    //main 主线程
    public static void main(String[] args)
    {
        //创建 Runnable 接口的实现类对象
        TestThread03 t = new TestThread03();

        //创建线程对象,通过线程对象thread来开启我们的线程---代理对象t
        Thread thread = new Thread(t);

        thread.start();

        for (int i = 0; i < 20; i++)
        {
            System.err.println("main主线程:aaaaaaaaaa--->" + i);
        }
    }
}
2-2:通过实现Runnable接口来实现多线程同步下载图片

小小区别:

具体代码
java 复制代码
// 实现多线程同步下载图片
public class TestThread04 implements Runnable
{
    private String url; //网络图片地址
    private String name;//保存的文件名
    
    //构造器
    public TestThread04(String url ,String name){
        this.url = url;
        this.name = name;
    }
    
    //重写run方法
    @Override
    public void run()
    {
        //run方法里面调用了文件下载的downloader方法
        WebDownloader04 webDownloader = new WebDownloader04();
        webDownloader.downloader(url,name);
        System.err.println("下载了文件名为:"+ name);
    }
    
    //主线程
    public static void main(String[] args)
    {
        //创建三个线程对象
        TestThread04 t1 = new TestThread04(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "1.jpg");
        TestThread04 t2 = new TestThread04(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "2.jpg");
        TestThread04 t3 = new TestThread04(
                "https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
                "3.jpg");
        //通过线程对象启动3条线程
        Thread thread = new Thread(t1);
        thread.start();
        new Thread(t2).start();
        new Thread(t3).start();
    }
}

//图片下载器
class WebDownloader04{
    //下载方法
    public void downloader(String url,String  name){
        try
        {
            //把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e)
        {
            e.printStackTrace();
            System.err.println("IO异常,downloader 方法出现问题");
        }
    }
}
2-3:演示多个线程同时操作同一个对象出现的并发问题
演示买火车票的例子

演示多个线程同时操作同一个对象---以买火车票为例子

多个线程操作同一个资源的情况下,线程不安全,数据紊乱,

这个就是多线程情况下会出现的并发问题。

具体代码
java 复制代码
//演示多个线程同时操作同一个对象---以买火车票为例子
//出现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread05 implements Runnable
{
    //票数
    private int ticketNums = 10;

    @Override
    public void run()
    {
        while (true){
            if (ticketNums<=0){
                //终止当前所在循环
                break;
            }
            //模拟延迟
            try
            {
                Thread.sleep(500);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            System.err.println(Thread.currentThread().getName()+"--->买到了第【 "+ticketNums-- + " 】张票");
        }
    }

    public static void main(String[] args)
    {
        TestThread05 t = new TestThread05();
        //启动新线程  参数1:t 就是代理对象   参数2:name 就是当前线程名字
        new Thread(t,"小黄").start();
        new Thread(t,"小白").start();
        new Thread(t,"小绿").start();
    }
}
通过 synchronized 关键字同步代码块解决并发问题

简单解决下上面买车票的并发问题:

使用 synchronized 保证同一时间只有一个线程能来操作这个代码块。

具体代码
java 复制代码
package cn.ljh.threaddemo.demo01;


//演示多个线程同时操作同一个对象---以买火车票为例子
//出现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread05 implements Runnable
{
    //票数
    private int ticketNums = 10;

    @Override
    public void run()
    {
        while (true)
        {
            synchronized (this)
            {
                if (ticketNums <= 0)
                {
                    //终止当前所在循环
                    break;
                }
                System.err.println(Thread.currentThread().getName()
                        + "--->买到了第【 " + ticketNums-- + " 】张票");

            }
            //模拟延迟
            try
            {
                Thread.sleep(500);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args)
    {
        TestThread05 t = new TestThread05();
        //启动新线程  参数1:t 就是代理对象   参数2:name 就是当前线程名字
        Thread t1 = new Thread(t, "小黄");
        Thread t2 = new Thread(t, "小白");
        Thread t3 = new Thread(t, "小绿");

        t1.start();
        t2.start();
        t3.start();
    }
}
2-4:演示龟兔赛跑案例

自定义一个线程类,实现 Runnable 接口,然后启动两个线程(一个兔子线程和一个乌龟线程),用for循环,看哪个线程先执行到100米。

用多线程的方式模拟龟兔赛跑代码分析
具体代码
java 复制代码
// 模拟龟兔赛跑案例
public class Race implements Runnable
{
    //胜利者
    private static String winner;

    //实现 run() 方法
    @Override
    public void run()
    {
        for (int i = 0; i <= 100; i++)
        {
            //模拟兔子休息--当前线程是"兔子" 并且 每跑10步,就休息一会
            if (Thread.currentThread().getName().equals("兔子") && i%10==0){
                try
                {
                    Thread.sleep(5);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果flag为true,表示比赛结束,直接终止当前所在循环
            if (flag){
                break;
            }
            System.err.println(Thread.currentThread().getName()
                    + " ---> 跑了【 " + i + " 】步");
        }

    }
    
    //主线程
    public static void main(String[] args)
    {

        Race race = new Race();
        //启动名为兔子的新线程
        new Thread(race, "兔子").start();
        //启动名为乌龟的新线程
        new Thread(race, "乌龟").start();

    }
    
    //判断是否完成比赛的方法
    public boolean gameOver(int steps)
    {
        //判断是否有胜利者
        if (winner != null)
        {
            //已经存在胜利者了,返回true
            return true;
        } else if (steps >= 100)
        {
            winner = Thread.currentThread().getName();
            System.err.println("胜利者是:【 " + winner + " 】");
            return true;
        }
        return false;
    }
}

3、实现 Callable 接口

步骤:

演示多线程同时下载图片

演示的依然是这个多线程同时下载图片的功能,不过这里是实现 Callable 接口。

这里的多线程操作,是通过创建一个线程池,然后把实现了 Callable 接口的自定义线程类对象(t1、t2、t3)作为任务参数提交给线程池去执行,并且可以获取线程执行后的结果,最后关闭线程池。

t1、t2、t3各自代表一个任务,它们实现了Callable接口,并且在call()方法中定义了具体的任务逻辑。

提交t1、t2、t3任务给线程池后,线程池会负责安排线程来执行t1、t2、t3中定义的任务逻辑。

通过源码可以看出 ,Callable 接口里面只有一个 call() 方法需要重写

具体代码
java 复制代码
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//线程创建方式三:实现 Callable 接口
/**
 * Callable 的好处
 * 1、可以定义返回值
 * 2、可以抛出异常
 */

// 实现多线程同步下载图片
public class TestCallable implements Callable<Boolean>
{
    private String url; //网络图片地址
    private String name;//保存的文件名

    //构造器
    public TestCallable(String url, String name)
    {
        this.url = url;
        this.name = name;
    }

    //实现 Call 方法
    @Override
    public Boolean call() throws Exception
    {
        //run方法里面调用了文件下载的downloader方法
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.err.println("下载了文件名为:" + name);

        //返回true表示任务执行成功
        return true;
    }

    //主线程
    public static void main(String[] args) throws Exception
    {
        //创建三个线程对象
        TestCallable t1 = new TestCallable(
                "https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
                "1.jpg");
        TestCallable t2 = new TestCallable(
                "https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
                "2.jpg");
        TestCallable t3 = new TestCallable(
                "https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
                "3.jpg");

        //ExecutorService是Java中用于管理线程池的接口,通过ExecutorService,可以创建一个线程池,并使用该线程池执行任务

        // 1、创建执行服务:创建了一个固定大小为3的线程池,这意味着该线程池最多同时运行3个线程    fixed:固定的
        ExecutorService ser = Executors.newFixedThreadPool(3);

        // 2、提交执行:使用ExecutorService的submit()方法提交了三个任务(t1、t2、t3)给线程池执行,并且通过Future对象(r1、r2、r3)接收返回结果
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);

        // 3、获取返回值结果
        Boolean result1 = r1.get();
        Boolean result2 = r2.get();
        Boolean result3 = r3.get();

        System.err.println(result1);
        System.err.println(result2);
        System.err.println(result3);

        // 4、关闭服务,也就是关闭线程池
        ser.shutdown();

    }

    //图片下载器
    class WebDownloader
    {
        //下载方法
        public void downloader(String url, String name)
        {
            try
            {
                //把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e)
            {
                e.printStackTrace();
                System.err.println("IO异常,downloader 方法出现问题");
            }
        }
    }
}

4、总结:三种创建方式的区别:

Thread、Runnable、Callable,都能用来开启多个线程来执行业务逻辑。

继承Thread 类 和 实现 Runnable接口的对比:

OOP 指的是面向对象编程(Object-Oriented Programming)

继承 Thread 类: (重写run()方法)

自定义线程子类A,继承 Thread 类,然后重写 run() 方法,把具体的业务逻辑写在run方法里面。

通过子类自身去调用 start() 方法,让 JVM 去开启一条新的线程去执行 run() 方法。

这样就可以启动多条线程。

实现 Runnable 接口: (实现 run() 方法)

自定义线程类B,实现 Runnable 接口,它启动多个线程,是通过 创建多个Thread 线程对象,然后把线程类B作为参数传给 Thread 线程对象,由Thread线程对象来调用 start 方法的

实现 Callable 接口:(实现 call() 方法)

自定义线程类C,实现 Callable 接口,它启动多个线程,是通过创建一个线程池,然后把线程类对象C作为任务参数提交到线程池,由线程池负责提供新线程来执行线程类对象C中定义的任务逻辑

线程类对象C中定义的任务逻辑,就是指重写 call() 方法里面的业务逻辑。

相关推荐
中草药z4 个月前
【JavaEE】Callable,Semaphore和CountDownLatch
java·开发语言·高并发·多线程·countdownlatch·callable·semaphore
重剑DS6 个月前
Runnable和Callable的使用
java·开发语言·runnable·callable
飞翔的佩奇1 年前
java多线程之Lock锁原理以及案例实现电影院卖票
java·多线程·thread·reentrantlock·lock·同步锁·runnable