Java多线程入门

Posted woodwhale

tags:

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

Java多线程学习(入门)

前言

目前对于线程的了解仅仅停留在学习python的threading库,很多线程的概念没有真正弄清楚,所以选择来系统性的学习多线程。那么这次选择的是Java的多线程学习,等学完了分析一下Java和python使用多线程和底层实现的区别吧!

跟着【狂神说Java】多线程详解 学习的,笔记和代码跟着敲的,方便自己之后复习。

1、进程与线程

首先,我们做个简单的比喻:进程 = 火车,线程 = 车厢。

所以我们可以得到一个前提:线程是在进程下进行的

然后有以下特点:

  • 一个进程中可以有多线程

  • 不同进程很难共享数据

  • 相同进程的不同线程共享数据非常简单

  • 进程不会相互影响,但是一个线程挂了就导致整个进程挂了

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位

  • 进程要比线程消耗更多的计算机资源

2、线程的创建

1.第一种方式创建线程

  • 用以下三个步骤来创建线程
  1. 继承Thread类
  2. 重写run方法
  3. 调用start开启线程

线程开启不一定立即执行,由cpu调度开启执行。

并且由于我们大部分电脑使用的都是单核的,所以实际上多线程就是多个线程交替执行,而非同时执行

我们看一个例子

// 继承Thread类
// 重写run方法
// 调用start开启线程
public class TestThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("114514"+ "--" + i) ;
        }
    }

    // main线程,主线程
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.start();
        for(int i = 0; i < 20; i++) {
            System.out.println("1919810" + "--" + i);
        }
    }
}

我们使用start方法来开启线程

得到的结果是

1919810–0
114514–0
1919810–1
114514–1
1919810–2
114514–2
1919810–3
1919810–4
1919810–5
114514–3
1919810–6
1919810–7
1919810–8
1919810–9
1919810–10
1919810–11
1919810–12
114514–4
1919810–13
114514–5
1919810–14
114514–6
1919810–15
114514–7
1919810–16
114514–8
1919810–17
114514–9
1919810–18
114514–10
114514–11
114514–12
114514–13
114514–14
114514–15
114514–16
114514–17
114514–18
114514–19
1919810–19

进程已结束,退出代码为 0

可以看到114514和1919810是在交替执行的

那么如果我们把start方法改为我们重写的run方法呢?

testThread.start();改为如下

testThread.run();

结果如下

114514–0
114514–1
114514–2
114514–3
114514–4
114514–5
114514–6
114514–7
114514–8
114514–9
114514–10
114514–11
114514–12
114514–13
114514–14
114514–15
114514–16
114514–17
114514–18
114514–19
1919810–0
1919810–1
1919810–2
1919810–3
1919810–4
1919810–5
1919810–6
1919810–7
1919810–8
1919810–9
1919810–10
1919810–11
1919810–12
1919810–13
1919810–14
1919810–15
1919810–16
1919810–17
1919810–18
1919810–19

进程已结束,退出代码为 0

这个明显就是单线程,将我们重写的run方法跑完再执行下面的for循环。

所以我们必须按照上面的三步走,我们重写完了run方法之后,使用start方法就是执行run方法中的代码,只不过是使用了另一个线程运行而已。

2.多线程图片下载测试

那么在知晓了如何创建线程之后,我们用多个线程来下载网络上的图片来测试线程

package com.woodwhale.demo01;

import org.apache.commons.io.FileUtils;

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

// 练习Thread,实现多线程同步下载图片
public class TestThread2 extends Thread{
    private String url;
    private String name;
    public TestThread2(String url, String name) {
        this.name = name;
        this.url = url;
    }
    // run方法下载
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了"+name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://api.woodwhale.top/random.php","1.jpg");
        TestThread2 t2 = new TestThread2("https://api.woodwhale.top/random.php","2.jpg");
        TestThread2 t3 = new TestThread2("https://api.woodwhale.top/random.php","3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }

}
// 下载器
class WebDownloader{
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常");
        }
    }
}

运行结果如下:

下载了3.jpg
下载了1.jpg
下载了2.jpg

进程已结束,退出代码为 0

再看看我们的目录,多了三张图片

说明我们下载成功了!

而且从下载完的顺序来看,多线程是连续交替的,而非从1到3的连续下载。所以线程并非立刻执行,而是由cpu调度的!

3.第二种方式创建线程

用以下三个步骤来实现线程创建

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法 ,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
package com.woodwhale.demo01;


// 创建线程方式2,runnable接口
// 实现run方法
// 执行线程丢入runnable接口实现类,调用start方法
public class Test1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("114514!" + i);
        }
    }

    public static void main(String[] args) {
        Test1 test = new Test1();

//        Thread thread = new Thread(test);
//        thread.start();

        new Thread(test).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("1919810!" + i);
        }
    }
}

对于多线程的使用,我们推荐使用第二种方法——runnable接口

3、Callable接口

我们用callable接口来写一个多线程实现下载图片

callable接口构建的四个步骤

  1. 创建执行服务
  2. 提交执行
  3. 获取结果
  4. 关闭服务

callable接口和runnable接口的区别就是:

  • 可以定义返回值
  • 可以捕获异常
package com.woodwhale.demo02;

import org.apache.commons.io.FileUtils;

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

// callable的好处
// 1、可以定义返回值
// 2、可以捕获异常

public class TestCallable implements Callable<Boolean> {

    private String url;
    private String name;

    public TestCallable(String url ,String name) {
        this.name = name;
        this.url = url;
    }
    @Override
    public Boolean call() throws Exception {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载文件"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/NTE5MTI0MzA0ODU1MDY3NTI4OF8xNjI2MDE0OTkzMzIx_4.jpg","1.jpg");
        TestCallable t2 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/556062.jpg","2.jpg");
        TestCallable t3 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/910923.png","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();

        // 关闭服务
        ser.shutdown();

    }

    static class WebDownloader{
        public void downloader(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4、静态代理

静态代理模式:

  • 真实对象和代理对象都要实现同一接口

  • 代理对象要代理真实角色

好处:

  • 代理对象可以做很多真实对象做不了的时期

  • 真实对象仅仅做自己的事情就好了

所以我们的Thread类就是一个静态代理

new Thread(() -> System.out.println(“114”)).start();

Thread就是代理,实现了runnalbe接口,这个输出语句真实对象

如果这个输出语句直接使用runnable接口也能完成自己的多线程任务,但是如果使用了静态代理Thread类,就可以使用代理完成更多事情

5、lambda表达式

  • 避免匿名内部类定义过多
  • 属于函数式编程

使用lambda表达式的前提是函数式接口

函数式接口的定义:只包含唯一一个抽象方法

我们多线程使用的runnable接口就是一个函数式接口

public interface Runnable{
    public abstract void run();
}

我们可以通过5种方法来写下面这段

package com.woodwhale.demo02;

public class TestLamda {
    // 3、静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
        // 实现类
        ILike like = new Like();
        like.lambda();

        // 静态内部类
        like = new Like2();
        like.lambda();

        // 4、局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        // 5、匿名内部类。没有类的名称,必须借助接口或者父类实现
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        // 6、用lambda简化
        like = ()->{
            System.out.println("i like lambda5");
        };
        like.lambda();

    }
}

// 1、定一个函数式接口
interface ILike{
    void lambda();
}

// 2、实现类
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("i like lamda");
    }
}

我们把其中的lambda表达式拿出来看:

// 6、用lambda简化
        like = ()->{
            System.out.println("i like lambda5");
        };
        like.lambda();

上面的就是lambda表达式,因为前面实例化了like对象,所以可以直接写

我们再来看lambda的简化

package com.woodwhale.demo02;

interface ILove{
    void love(int a);
}

public class TestLambda2 {

    public static void main(String[] args) {
        ILove love = (int a)->{
            System.out.println("i love you" + a);
        };
        love.love(520);


    }
}

这个是一个类

ILove love = (int a)->{
            System.out.println("i love you" + a);
        };
        love.love(520);

可以简化为(去掉参数类型 )

ILove love = (a)->{
            System.out.println("i love you" + a);
        };
        love.love(520);

还能简化为(去掉括号)(如果多个参数则不能去掉参数括号)

ILove love = a->{
            System.out.println("i love you" + a);
        };
        love.love(520);

如果只有一行代码,还可以简化为(去掉大括号)

ILove love = a->System.out.println("i love you" + a);
love.love(520);

6、线程状态

这里用狂神说的多线程的课程中的图

1.线程停止

不推荐使用jdk自带的stop();等方法,已经过时并且废弃了

推荐线程自己停下来

建议使用一个标志位进行终止变量:flag = true 那么终止线程

据个例子

package com.woodwhale.start;

import com.woodwhale.demo02.TestCallable;

// 测试停止线程
// 1、建议线程正常停止--->利用次数,不建议死循环
// 2、建议使用flag
// 3、不要使用stop(); jdk不建议使用的方法
public class TestStop implements Runnable{

    // 1、设置flag
    private boolean flag = true;

    // 2、重写run方法
    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run ... Thread" + i++);
        }
    }

    // 3、设置一个公开的方法停止线程
    public void stop() {
        this.flag = false;
    }

    // 4、测试
    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        // 1s之后设置线程停止
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        testStop.stop();
        System.out.println("线程停止!");
    }
}

2.线程休眠

Thread.sleep();方法

  • sleep指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间到之后线程进入就绪状态
  • sleep可以模拟网络延迟、倒计时…
  • 每一个对象都有一个锁,sleep不会释放锁

模拟倒计时

package com.woodwhale.start;

// 模拟倒计时
public class TestSleep {
    public static void tenDown() throws InterruptedException {
        int num = 10;
        do {
            System.out.println(num--);
            Thread.sleep(1000);
        } while (num > 0);
    }

    public static void main(String[] args) throws InterruptedException {
        tenDown();
    }

}

当然在我们程序运行时,我们有的时候需要模拟网络延迟,这个时候我们也可以使用Thread.sleep()方法

3.线程礼让

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

礼让使用yield()方法

举个例子:

package com以上是关于Java多线程入门的主要内容,如果未能解决你的问题,请参考以下文章

JAVA实现多线程入门

java多线程快速入门(十九)

java 多线程 快速入门

Java多线程入门

(Java多线程系列一)快速入门

JAVA多线程入门