Java 多线程开发 01 —— 线程创建

系列文章目录

Java 多线程开发 01 —— 线程创建
Java 多线程开发 02 —— 静态代理模式
Java 多线程开发 03 —— Lambda表达式
Java 多线程开发 04 —— 线程状态控制、优先级、守护线程
Java 多线程开发 05 —— synchronized、Lock、死锁
Java 多线程开发 06 —— 管程法、信号灯法



1. 一些概念

程序、进程、线程三者的区别:

  1. 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

  2. 进程是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位。

  3. 一个进程可能包含若干线程,线程是CPU调度和执行的单位。

     例如英雄联盟是一个程序,运行英雄联盟后我们可以通过Ctrl+Alt+.看到他的进程,在游戏里面的各种操作都是多线程执行的。

注意:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,在一个CPU情况下,同一个时间点CPU只能执行一个代码,但因为切换太快,所以才有同时执行的错觉。

一些核心概念:

  • 线程是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程;
  • main()是主线程,系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关,先后顺序不能人为干预;
  • 对同一份资源操作时会存在资源抢夺问题,需要加入并发控制;
  • 线程会带来额外开销,如CPU调度时间,并发控制开销;
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2. 线程创建

三种方式:

  1. 继承Thread 类(重点)
  2. 实现Runnable接口(重点)
  3. 实现Callable接口(了解)

方法一:继承 Thread 类

package lessen04;

//创先线程方式一:继承Thread类,重写run()方法,调用start()方法
public class TestThread01 extends Thread{
    
    
    @Override
    public void run() {
    
    
        //run方法线程
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("我是run()方法"+i);
        }
    }

    //主线程
    public static void main(String[] args) {
    
    
        TestThread01 thread01 = new TestThread01();
        thread01.start();
        //thread01.run();

        //主线程
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("我是main()方法"+i);
        }
    }
}



分别用start()和run()来运行的结果如下:

可以看到,调用start()启动多线程后,内容是交替的,而调用run()则是运行完run()的循环再执行main()里面的循环。多线程的调用是由CPU决定的,也就是说每次运行的结果可能不相同。

下面通过多线程实现网络图片下载,配合commoms-io包
【链接 commons-io-2.8.0-bin.tar.gz



导入jar包:
导入commoms-io包


代码如下:其中忽略证书验证参考 关于Https验证证书问题!!

package lessen04;

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

//多线程实现URL资源下载
public class TestThread02 extends Thread{
    
    
    private String url;
    private String fileName;

    public TestThread02(String url, String fileName){
    
    
        this.url = url;
        this.fileName = fileName;
    }

    @Override
    public void run() {
    
    
        WebDownloader downloader = new WebDownloader();
        downloader.download(this.url, this.fileName);
        System.out.println("下载的文件名:"+fileName);
    }

    public static void main(String[] args) throws Exception {
    
    
        SslUtils.ignoreSsl();//这里是忽略Https证书验证
        TestThread02 t1 = new TestThread02("https://tva1.sinaimg.cn/large/0080xEK2gy1glossaxhr6j30u00u0q5x.jpg", "1.jpg");
        TestThread02 t2 = new TestThread02("https://tva1.sinaimg.cn/large/0080xEK2gy1glosqz6vhtj30u0140jvn.jpg", "2.jpg");
        TestThread02 t3 = new TestThread02("https://tva1.sinaimg.cn/large/0080xEK2gy1glosrfyn1uj30qo0zkk23.jpg", "3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}

//资源下载器
class WebDownloader{
    
    
    public void download(String url, String fileName){
    
    
        try {
    
    
            FileUtils.copyURLToFile(new URL(url), new File(fileName));
        } catch (IOException e) {
    
    
            e.printStackTrace();
            System.out.println("IOException,下载器出错");
        }
    }
}


结果如下:
在这里插入图片描述

方法二:实现 Runnable 接口

和方法一类似,区别是不用继承Thread,而是实现Runnable。同时,启动方式需要将实现Runnable接口的类实例化后放入Thread类中。

下面通过实现Runnable接口的多线程来进行龟兔赛跑模拟,期间兔子会休息一次。

package lessen04;

/**
 * 用多线程实现龟兔赛跑模拟
 */
public class Race implements Runnable{
    
    
    private static String winner;

    @Override
    public void run() {
    
    
        for (int i = 1; i <= 100; i++) {
    
    

            //模拟兔子休息
            if (Thread.currentThread().getName().equals("兔子") && i==60){
    
    
                try {
    
    
                    Thread.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            //判断游戏是否结束
            if (gameOver(i))
                break;
            System.out.println(Thread.currentThread().getName()+"——>跑了"+i+"步");
        }

    }

    private boolean gameOver(int steps){
    
    
        //若已有胜利者则当前线程也无需执行,退出循环。
        if (winner!=null)
            return true;
        //若有人已经跑到100,则成为胜利者
        if(steps >= 100){
    
    
            winner = Thread.currentThread().getName();
            System.out.println("胜利者是"+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
    
    
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();
    }
}

方法三:实现 Callable 接口

使用Callable的好处:

  1. 可以定义返回值。
  2. 可以抛出异常。

其余都没什么区别,一般都是用前面两种方法即可。

具体代码【修改前面通过URL下载网络资源的代码】

package lessen04;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
//可自定义类型Boolean
public class TestCallable04 implements Callable<Boolean> {
    
    

    private String url;
    private String fileName;

    public TestCallable04(String url, String fileName){
    
    
        this.url = url;
        this.fileName = fileName;
    }

    //不是用run(),而是call(),且有返回值,可自定义返回值
    @Override
    public Boolean call() {
    
    
        WebDownloader downloader = new WebDownloader();
        downloader.download(this.url, this.fileName);
        System.out.println("下载的文件名:"+fileName);
        return true;
    }

    public static void main(String[] args) throws Exception {
    
    
        SslUtils.ignoreSsl();
        TestCallable04 t1 = new TestCallable04("https://tva1.sinaimg.cn/large/0080xEK2gy1glossaxhr6j30u00u0q5x.jpg", "1.jpg");
        TestCallable04 t2 = new TestCallable04("https://tva1.sinaimg.cn/large/0080xEK2gy1glosqz6vhtj30u0140jvn.jpg", "2.jpg");
        TestCallable04 t3 = new TestCallable04("https://tva1.sinaimg.cn/large/0080xEK2gy1glosrfyn1uj30qo0zkk23.jpg", "3.jpg");

        //以下是和Thread、Runnable的区别
        //1. 创建执行服务,有三个线程,所以线程池为3
        ExecutorService service = Executors.newFixedThreadPool(3);
        //2. 提交执行,类似start()
        Future<Boolean> r1 = service.submit(t1);
        Future<Boolean> r2 = service.submit(t2);
        Future<Boolean> r3 = service.submit(t3);
        //3. 获取结果,即call()返回值
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();
        System.out.println("rs1: "+rs1);
        System.out.println("rs2: "+rs2);
        System.out.println("rs3: "+rs3);
        //4. 关闭服务,这是另外两种没有的
        service.shutdown();
    }
}

3. 线程池

线程池:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

线程池的好处 :

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不用每次创建)
  • 便于线程管理
    • ocrePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后终止

实现方法:通过接口 ExecutorService 和 工具类 Executors 的 newFixedThreadPool()方法。

具体代码如下:

package lessen04_Thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {
    
    
    public static void main(String[] args) {
    
    
        //1. 创建服务
        ExecutorService service = Executors.newFixedThreadPool(5);

        //2. 执行服务
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //3. 关闭服务
        service.shutdown();
    }
}

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

可以发现,其实前面创建线程的第三种方法 Callable 接口就是通过线程池实现的。区别在于,Callable 接口是用 submit() 启动线程,并且有返回值,而通过execute() 是需要一个 Runnable 。

猜你喜欢

转载自blog.csdn.net/qq_39763246/article/details/112903482