多线程基础

本文详细介绍了多线程的基础概念,包括进程与线程的区别和联系,Java中进程和线程的特性,以及并发和并行的概念。此外,文章还阐述了线程的运行原理,如栈与栈帧、执行图解和上下文切换。此外,讲解了Java线程的API,包括构造方法、常用API、守护线程、优先级设定,以及三种创建线程的方式:继承Thread类、实现Runnable接口和Callable接口。
摘要由CSDN通过智能技术生成

多线程基础

一、基本概念

1、程序、进程、线程

1)程序与进程

# 区别
	1. 程序是一组指令的有序集合,是一个静态的实体。	
	2. 进程是程序从创建、运行到消亡的一次执行过程,是一个动态的实体。

# 联系
	1. 程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,
       这种执行的过程称之为进程。	
    2. 进程可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),
       部分只可以运行一个实例进程(例如360安全卫士)    

# 包含关系
    1. 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
       一个程序没有对应的进程 -> 程序没有执行
       一个程序有多个对应进程 -> 程序运行在不同的数据集  
    2. 一个进程有且只有一个与之对应的程序。

2)进程与线程

# 区别
	1. 进程是 操作系统 资源分配和调度的基本单位。
	2. 线程是 处理器 任务调度和执行的基本单位。
	   一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

# 内存分配									
	进程之间的地址空间和资源是相互独立的。								
	同一进程的线程共享本进程的地址空间和资源。								

# 影响关系									
	一个进程崩溃后,在保护模式下不会对其他进程产生影响。								
	一个线程崩溃后,整个进程都会死掉。

# 包含关系									
	一个进程至少包含一个线程,若有多个线程,则执行过程是多条线共同完成的。						
	线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。								

# 执行过程									
	每个独立的进程有程序运行的入口、顺序执行序列和程序出口。								
	但线程不能独立执行,必须依存于应用程序,由应用程序提供多个线程执行控制。

# 资源开销									
	每个进程都有独立的代码和数据空间(程序上下文),程序间的切换开销较大。						
	每个线程都有自己独立的运行栈和程序计数器(PC),线程间的切换开销较小。

3)Java中的进程和线程

  • 进程
    • Java程序的执行就是一个进程的开启,该进程至少包含两个线程:
      1. 主线程/main()方法线程
      2. 垃圾回收机制线程
    • 有独立的内存空间,进程间的数据存放空间是独立的。
  • 线程
    • 堆空间、方法区共享,栈空间独立
    • 线程消耗的资源比进程小的多,因此在开发多任务运行时,通常考虑创建多线程而非多进程。
    • Java中线程的执行取决于JVM的调度,这也造成了多线程的随机性(线程之间存在上下文切换)

在这里插入图片描述

4)查看进程和线程

【Windows】

任务管理器	可以查看进程和线程数,也可以用来杀死进程
tasklist 	查看进程
taskkill 	杀死进程

【Linux】

ps -ef 				查看所有进程
ps -fT -p [pid] 	查看某个进程(pid)的所有线程
kill 				杀死进程
top 				实时显示进程状态
top -H -p [pid] 	查看某个进程(pid)的所有线程

【Java】

jps 				列出正在运行的 Java 进程
jstack [pid] 		生成指定 Java 进程的线程转储信息
jconsole 			用于监视和管理 Java 虚拟机的图形化工具

2、并发 & 并行

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间处理(dealing with)多件事情的能力,
  • 并行(parallel)是同一时间做(doing)多件事情的能力

举个例子:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这就是并发
  • 雇了3个保姆,一个做饭、一个打扫卫生、一个喂奶,互不干扰,这就是并行
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行。

图示说明:

单核 cpu 下,线程实际还是串行执行的。这种多个线程轮流使用 CPU 的做法称为并发(concurrent)

  • 任务调度器将 cpu 的时间片分给不同线程使用,只是由于线程间的切换非常快(时间片很短),给人感觉是同时运行的。

在这里插入图片描述

多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行(parallel)的。

在这里插入图片描述

3、同步 & 异步

  • 同步:需要等待结果返回,才能继续运行
  • 异步:不需要等待结果返回,就能继续运行

多线程可以让方法执行变为异步的,将一些耗时操作异步执行,避免阻塞主线程。

二、线程的运行原理

1、栈与栈帧

Java 虚拟机栈 描述的是 Java方法 执行的内存模型:

  • 每个线程 在创建时 都会创建一个虚拟机栈(线程私有),其内部保存一个个的栈帧(Stack Frame)。
  • 每个方法 被调用时 都会创建一个栈帧(Stack Frame),存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 每个线程 只能有一个活动栈帧/当前栈帧,对应着当前正在执行的那个方法(即当前方法)。

2、执行图解

public class FrameTest {
    public static void main(String[] args) {
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }
}

在这里插入图片描述

方法执行结束,对应的栈帧就会出栈(对应的内存也会被释放掉),并更新程序计数器。

在这里插入图片描述

3、上下文切换 Context Switch

以下原因会导致 CPU 不再执行当前的线程,转而执行另一个线程的代码:

  • 线程的 CPU 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

当上下文切换(Context Switch)发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。

  • JVM 中 程序计数器(线程私有)记录了下一条 jvm 指令的执行地址。

频繁的上下文切换(Context Switch)会影响性能。

三、线程相关API

1、构造方法

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

构造函数功能说明
Thread()创建一个新的线程
Thread(String name)创建一个新的线程,并指定线程名
Thread(Runnable target)创建一个新的线程,带有指定任务
Thread(Runnable target, String name)创建一个新的线程,带有指定任务,并指定线程名

2、常用API

public class Thread implements Runnable {
    // 获取当前线程对象
    public static native Thread currentThread();

    // 判断线程是否处于活跃状态(true:正在运行 false:运行完毕)
    public final native boolean isAlive();

    // 启动线程,JVM调用此线程的run方法
    public synchronized void start() {...}

    // 获取当前线程id(id唯一,默认是'Thread-编号',编号从0开始)
    public long getId() {...}

    // 获取当前线程名称
    public final String getName() {...}

    // 设置当前线程名称
    public final synchronized void setName(String name) {...}

    // 获取线程的状态(Thread.State枚举类,6种)
    public State getState() {...}

    // 获取活跃线程数(默认只有 main线程 和 垃圾回收线程)
    public static int activeCount() {...}
}

3、守护线程

// 设置当前线程为守护线程(必须在线程start之前设置)
public final void setDaemon(boolean on) {...}

// 判断当前线程是否为守护线程
public final boolean isDaemon() {...}

Java中的线程分为两种:用户线程守护线程

守护线程:
	程序运行时,在后台提供一种通用服务的线程,比如垃圾回收线程。(不属于程序中不可或缺的部分)
	
进程何时关闭:
	进程中的所有线程都执行完毕 或 进程中只剩下守护线程

注意事项:
	1. 正在运行的常规线程不能设置为守护线程,否则会抛出IllegalThreadStateException异常。
	2. 在Daemon线程中产生的新线程也是Daemon的。
	3. 守护线程不应该访问固有资源,如文件、数据库。因为它会在任何时候甚至在一个操作的中间发生中断。

当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。

public class DaemonThreadTest {
    public static void main(String[] args) {
        // 守护线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(200);
                    System.out.println("t1 输出" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setDaemon(true);
        t1.start();

        // 用户线程
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 输出" + i);
            }
        });
        t2.start();
    }
}

4、线程的优先级

// 设置当前线程的优先级1~10(设置的只是概率,不是一定的)
public final void setPriority(int newPriority) {...}

// 返回当前线程的优先级
public final int getPriority() {...}

线程的调度:

  • 多个线程执行时,线程会被JVM控制以某种方式执行,我们把这种情况称之为线程调度
  • JVM采用的是抢占式调度(竞争CPU时间片),因此多个线程之间会发生上下文切换。

线程的优先级:

  • 线程的优先级从低到高被划分为1-10级,线程提供了3个常量来表示线程的优先级:
    • 最低:Thread.MIN_PRIORITY 1
    • 默认:Thread.NORM_PRIORITY 5
    • 最高:Thread.MAX_PRIORITY 10
  • 线程优先级会提示(hint)调度器优先调度该线程,但它 仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 空闲时,优先级几乎没作用。
public class ThreadPriorityTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                try {
                    System.out.println("thread 输出" + i);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        thread.setPriority(Thread.MIN_PRIORITY);
        thread.start();
    }
}

四、线程的创建(三种方式)

1、继承Thread类

public class InheritCreate {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
    
    // 继承Thread类,重写run方法
    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出打印"+i);
            }
        }
    }
}

2、实现Runnable接口(无返回值)

public class RunnableCreate1 {
    public static void main(String[] args) {   
        MyRunnable task = new MyRunnable();	// 任务对象
        Thread thread = new Thread(task);
        thread.start();
    }

    // 实现Runnable接口,重写run方法(不带返回值的任务)
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出:"+i);
            }
        }
    }
}
public class RunnableCreate2 {
    public static void main(String[] args) {
        // 匿名内部类 - 创建Runnable实例
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("输出"+i);
                }
            }
        });
        t.start(); 
    }
}
public class RunnableCreate3 {
    public static void main(String[] args) {
        // lambda表达式 - 创建Runnable实例
        Thread t = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出"+i);
            }
        });
        t.start();   
    }
}

线程对象调用 start() 方法,最终执行的都是 重写的 run() 方法。(源码比较简单,这里就省略了)

3、实现Callable接口(有返回值)

Callable接口类似于Runnable,区别在于Runable执行后,不能返回结果;而 Callable 如果有结果,可以返回

public class CallableCreate {
    public static void main(String[] args) {
        // FutureTask包装Callable任务,FutureTask可以用于获取执行结果
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        // 创建线程执行Callable任务(结果会被缓存,提高效率)
        new Thread(futureTask, "A").start();
        // 获取线程的执行结果(可能会阻塞,放到后面 或 使用异步处理)
        try {
            Integer num = futureTask.get();
            System.out.println("得到线程处理结果:" + num);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 实现Callable接口,实现call()方法(带返回值的任务)
    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() {
            int num = 0;
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出" + i);
                num += i;
            }
            return num;
        }
    }
}

FutureTaskRunnableFuture的实现类,而RunnableFutureRunnable的子接口

public class FutureTask<V> implements RunnableFuture<V> {
    // FutureTask构造方法可以传入Callable
    public FutureTask(Callable<V> callable) {}
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

4、源码分析

如果创建Thread时指定了Runnable target,则最终执行的都是 target重写的 run() 方法。

在这里插入图片描述

5、创建方式的比较

继承Thread类:多个线程分别完成各自的任务

  • 存在单继承的局限性

实现Runnable/Callable接口:多个线程共同完成一个任务 或 多个线程分别完成不同的任务

  • 避免了Java单继承的局限性。
  • 实现 线程代码(Thread) 和 任务代码模块(Runnable/Callable) 的解耦合,提升了代码的扩展性。
  • 体现数据共享的概念(JMM内存模型图),代码可以被多个线程共享,代码和数据独立
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scj1022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值