Thread介绍

2021-10-18 09:07:22 浏览数 (1)

简介

说到线程就不得不提与之相关的另一概念:进程,那么什么是进程?与线程有什么关系呢?简单来说一个运行着的应用程序就是一个进程,比如:我启动了自己手机上的酷猫音乐播放器,这就是一个进程,然后我随意点了一首歌曲进行播放,此时酷猫启动了一条线程进行音乐播放,听了一部分,我感觉歌曲还不错,于是我按下了下载按钮,此时酷猫又启动了一条线程进行音乐下载,现在酷猫同时进行着音乐播放和音乐下载,此时就出现了多线程,音乐播放线程与音乐下载线程并行运行,说到并行,你一定想到了并发吧,那并行与并发有什么区别呢?并行强调的是同一时刻,并发强调的是一段时间内。线程是进程的一个执行单元,一个进程中至少有一条线程,进程是资源分配的最小单位,线程是 CPU 调度的最小单位。

程序是静态的,进程是动态的。多进程是操作系统中多个程序同时执行。

如果程序是采用多线程技术编写的,那么运行在单核单线程上运行的话,是并发执行;那么运行在多核多线程上运行的话,是并行执行的。

进程

狭义:进程是正在运行的程序的实例(一个程序)。

广义:进程是一个具有一定独立功能的程序,关于某个集合的一次运行活动。

进程(程序段 数据段 进程控制块PCB 进程标识符PID)

  • 资源分配的基本单位
  • 独立运行的基本单位

目的:改善资源利用率以及提高系统吞吐量

线程

线程是操作系统能够进行运算调试的最小单位,它被包含在进程中,是进程中实际动作单位。一个线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程执行不同的任务。

  • 独立调度的基本单位

目的:减少程序并发执行是所付出的时空开销

线程是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

Java默认有2个线程:main线程、GC线程。

代码语言:javascript复制
并发(单核):多个线程操作同一个资源。
并行(多核):多个线程操作多个资源。

并发编程的本质:充分利用CPU的资源

多线程就是分时利用CPU,宏观上让所有线程一起执行 ,也叫并发。

生命周期

代码语言:javascript复制
public enum State {    
    NEW, //新生
    RUNNABLE, //运行
    BLOCKED, //阻塞
    WAITING, //等待  死死等
    TIMED_WAITING, //超时等
    TERMINATED; //终止
}
  • 使用new创建线程对象后,系统并没有提供运行所需的资源
  • 使用start()方法启动线程后(RUNNING、RUNABLE)系统就分配资源(具备执行的资格),但是具体执行还得听后CPU调配
  • RUNABLE状态的线程只能进入RUNNING状态或者意外终止(不可进入BLOCKED状态)
  • BLOCKED状态可以进入RUNABLE状态(不可以进入RUNNING)
代码语言:javascript复制
public class ThreadTest34 {

    public static void main(String[] args) throws Exception{
        Thread thread = new ThreadUser34();
        System.out.println("线程在Main方法中的状态1:"   thread.getState());
        Thread.sleep(1000);
        thread.start();
        Thread.sleep(1000);
        System.out.println("线程在Main方法中的状态2:"   thread.getState());
    }
}

class  ThreadUser34 extends Thread{

    public ThreadUser34() {
        System.out.println("构造方法的状态:"   Thread.currentThread().getState());
    }

    @Override
    public void run() {
        System.out.println("run方法中的状态:"   Thread.currentThread().getState());
    }
}

执行结果:
构造方法的状态:RUNNABLE
线程在Main方法中的状态1:NEW
run方法中的状态:RUNNABLE
线程在Main方法中的状态2:TERMINATED

创建线程

创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式:

  • 第一种是重新Thread的run方法
  • 第二种是实现Runnable接口的run方法,并将Runnable实例用作构造Thread的参数
  • 第三种使用FutureTask,在创建Thread对象的时候传进去

Thread负责线程本身相关的职责和控制,而Runnable则负责逻辑执行单元的部分。

代码语言:javascript复制
Thread的run源码:

@Override
public void run() {
    //如果构造Thread时传入了Runnable,则会执行Runnable的run方法。
    if (target != null) {
        target.run();
    }
    //负责需要重新run方法
}

备注:线程的执行单元指的是run方法

继承Thread类创建

  • 线程的启动顺序并不是按照start的顺序启动的,而是随机启动的
代码语言:javascript复制
@Slf4j
public class ThreadTest {

    public static void main(String[] args){

        log.info("主线程ID:"   Thread.currentThread().getId()   ", 线程名字:"    Thread.currentThread().getName());

        Thread thread = new MyThread();
        thread.start();
    }
}

/**
 * 通过继承Thread来实现
 */
@Slf4j
class MyThread extends Thread{

    @Override
    public void run() {
        log.info("子线程ID: "   Thread.currentThread().getId()   ", 子线程名字: "    Thread.currentThread().getName());
    }
}

执行结果:
20:06:38.901 [main] INFO com.java.master.线程.ThreadTest - 主线程ID:1, 线程名字:main
20:06:38.904 [Thread-0] INFO com.java.master.线程.MyThread - 子线程ID: 11, 子线程名字: Thread-0

实现Runnable接口创建

代码语言:javascript复制
@Slf4j
public class ThreadTest2 {

    public static void main(String[] args){

        log.info("主线程ID:"   Thread.currentThread().getId()   ", 线程名字:"    Thread.currentThread().getName());

        Thread thread = new Thread(new MyThread2());
        thread.start();
    }
}

@Slf4j
class MyThread2 implements Runnable{

    @Override
    public void run() {
        log.info("子线程ID: "   Thread.currentThread().getId()   ", 子线程名字: "    Thread.currentThread().getName());
    }
}


执行结果:
20:13:48.236 [main] INFO com.java.master.线程.ThreadTest2 - 主线程ID:1, 线程名字:main
20:13:48.240 [Thread-0] INFO com.java.master.线程.MyThread2 - 子线程ID: 11, 子线程名字: Thread-0

注意:这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务

在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。

事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。

FutureTask方式

代码语言:javascript复制
public class CallableTest {

    public static void main(String[] args) throws Exception{
        Callable<Integer> callable = new CallableService();
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println("线程返回值: "   task.get());
    }
}

class CallableService implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()   "调用了callable方法");
        return (int)(Math.random() * 10);
    }
}

匿名内部类创建

代码语言:javascript复制
public class ThreadTest3 {

    public static void main(String[] args) {

        //匿名内部类方式创建Thread
        new Thread() {
            @Override
            public void run() {
                enjoyMusic();
            }
        }.start();

        //Lambda方式1,等价于上边的匿名内部类创建方式
        new Thread(ThreadTest3::enjoyMusic).start();

        //Lambda方式2,等价于上边的匿名内部类创建方式
        new Thread(() -> {
            enjoyMusic();
        }).start();
    }

    public static void enjoyMusic() {

        for (; ;) {
            System.out.println("music music");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:
music music
music music
music music

成员变量与线程安全

1. 不共享数据

每个线程都有各自的count变量,自己减少自己的count变量的值,这种情况就是不共享。

代码语言:javascript复制
public class ThreadTest4 {

    public static void main(String[] args) {
        Thread thread1 = new MyThread4();
        Thread thread2 = new MyThread4();
        Thread thread3 = new MyThread4();

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread4 extends Thread {

    private int count = 5;

    @Override
    public void run() {
        while (count > 0) {
            count --;
            System.out.println(Thread.currentThread().getName()   " - "  count);
        }
    }
}

执行结果:
Thread-1 - 4
Thread-1 - 3
Thread-0 - 4
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-2 - 4
Thread-0 - 1
Thread-0 - 0
Thread-1 - 1
Thread-1 - 0
Thread-2 - 3
Thread-2 - 2
Thread-2 - 1
Thread-2 - 0

2. 共享变量

共享变量有概率出现不同线程使用相同的count的值,产生了“非线程安全”问题。

代码语言:javascript复制
public class ThreadTest5 {

    public static void main(String[] args) {

        Thread thread = new MyThread5();

        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread5 extends Thread {

    private int count = 5;

    @Override
    public void run() {
        count --;
        System.out.println(Thread.currentThread().getName()   " - "  count);
    }
}

执行结果:
Thread-2 - 3
Thread-1 - 3
Thread-3 - 2

线程分类

线程分为两种,用户线程和守护线程。其实守护线程和用户线程区别不大,可以理解为特殊的用户线程。特殊就特殊在如果程序中所有的用户线程都退出了,那么所有的守护线程就都会被杀死,很好理解,没有被守护的对象了,也不需要守护线程了。

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆。

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

用户线程

默认创建出来的线程都是用户线程。

守护线程

线程是否为守护线程和他的父类有很大关系,如果父类是正常线程,则子类线程也是正常线程,反之亦然。

代码语言:javascript复制
thread.setDaemon(true);

备注:必须在线程启动start()方法之前设置。
代码语言:javascript复制
代码分析:

public static void main(String[] args){

    System.out.println("start");
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    };

//        thread.setDaemon(true);
    thread.start();
    System.out.println("finish");
}

这段代码里边存在两个线程(main,thread),当main线程即使生命周期结束,JVM进程还是不能退出,因为还有一个非守护线程thread在执行。
如果将thread.setDaemon(true)注释放开,则当main线程执行完毕之后,JVM也随之退出,当然thread也就退出了。

备注:守护线程具备自动结束生命周期得特性,而非守护线程则不具备这样的特点。

0 人点赞