Java多线程初探

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,是静态的代码。

进程

是一个内存中运行的应用程序,每个进程都有一个独立的内存空间 。进程是程序的⼀次执行过程,是动态的。系统运行⼀个程序即是⼀个进程从创建,运行到消亡的过程。也就是说,⼀个进程就是⼀个执行中(运行)的程序,它在计算机中⼀个指令接着⼀个指令地执行,(程序在执行时会被操作系统载入内存中)每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。在 Java 中,当启动 main 函数时其实就是启动了⼀个 JVM 的进程, main 函数所在的线程就 是这个进程中的⼀个线程,也称主线程。⼀个 Java 程序的运行是 main 线程和多个其他线程同时运行。

线程

是进程中的一个执行路径(一个进程在执行的过程中可以产生多个线程),共享一个内存空间和系统资源,线程之间可以自由切换,并发执行. 一个进程最少有一个线程 ,线程是进程划分成的更小的运行单位。一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。所以系统在产生一个线程, 或是在各个线程之间切换时,负担比进程小得多,正因为如此,线程也被称为轻量级进程。进程属于操作系统的范畴,在同⼀段时间内,可以同时执行多个程序,而线程是在同⼀程序内几乎同时执行⼀个以上的程序段。

线程和进程的区别

线程具有许多传统进程所具有的特征,又称为轻型进程(Light—Weight Process)或进程元,传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

资源开销:每个进程都有独立的内存空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一进程中的线程共享内存空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多线程共同完成的; 

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

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都会死亡,因此多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

图解进程和线程的关系

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域

由上图可得,⼀个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空 间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。 线程和进程最大的不同在于基本上各进程是独立的但是同⼀进程中的线程可能会相互影响。线程执行开销小但不利于资源的管理和保护而进程则相反。

为什么程序计数器是私有的?

程序计数器主要有两个作用:

1. 字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制,如:顺序执行、 选择、循环、异常处理。

2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道该线程上次运行到哪。 需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的 是 Java 代码时程序计数器记录的才是下⼀条指令的地址。所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建⼀个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直到执行完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚 拟机中和 Java 虚拟机栈合⼆为⼀。 所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

 堆和方法区是所有线程共享的资源,其中堆是进程中最大的⼀块内存,主要用于存放新创建的对象 (所有对象都在此分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java内存区域详解

https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md

线程有哪些基本状态?

 线程创建之后将处于 NEW(新建) 状态,调用start() 方法后开始运行,线程此时处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运 行) 状态(执行run方法)。(操作系统隐藏 JVM中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态所以 Java 系统⼀般将这两 个状态统称为 RUNNABLE(运行中) 状态 )

当线程执行wait() 方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他 线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态在等待状态的基础上增加 了超时限制,比如通过 sleep(long millis)或 wait(long millis)可以将  线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的 run() 之后将会进入到 TERMINATED(终止) 状态。 

线程NEW状态
new创建一个Thread对象时,并没处于运行状态,因为没有调用start方法启动改线程,那么此时的状态
就是新建状态。

线程RUNNABLE状态
线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看
cpu的调度,这个中间状态叫可执行状态(RUNNNABLE)。可执行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行)状态(执行run方法)
线程的RUNNING状态

当CPU开始调度处于Runnable状态的线程时,此时线程才得以真正执行,即进 入到运行(Running)状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行, 首先必须处于就绪状态中。一旦cpu通过轮询或其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。
线程的BLOCKED状态

处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  2. 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  3. 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状 态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

线程的TERMINATED状态
TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命
周期结束。
以下情况会进入TERMINATED状态:

  1. “线程运行正常结束(run方法执行完毕),结束生命周期
  2. “线程运行出错意外结束
  3. “JVMCrash导致所有的线程都结束

线程调度

分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度(Java使用) 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为抢占式调度。

CPU使用抢占式调度模式在多个线程间进行高速的切换。对于CPU的一个核而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度非常快,看上去就是 在同一时刻运行。

多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

为什么要使用多线程?

  • 从计算机底层来说: 线程是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的开销远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文 切换的开销。
  • 从互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
  • 从计算机底层来说:
  1. 单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只 有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单 地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100% 了。
  2. 多核时代:多核时代多线程主要是为了提高 CPU 利用率。假如要计算一个复杂的任务,只用一个线程的话,CPU 只有一个 CPU 核心被利用,而创建多个线程就可以让多个 CPU 核心被利用,提高了 CPU 的利用率。

什么是上下文切换?

多线程编程中⼀般线程的个数大于 CPU 核心的个数,而⼀个 CPU 核心在任意时刻只能被⼀个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于⼀次上下文切 换。简而言之,当前任务在执行完 CPU 时间片切换到另⼀个任务之前会先保存自己的状态,以便下次 再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下文切换。 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换 中,每次切换都需要纳秒量级的时间,因此上下文切换对系统来说意味着消耗大量的 CPU 时间,(可能是操作系统中时间消耗最大的操作) Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下文切换和模式切换的时间消耗很少。

多线程可能带来什么问题?

并发编程的是为了能程序提高执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度,而且可能会遇到很多问题如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

创建线程的三种方式的对比

1)实现Runnable、Callable接口创建多线程。

  • 优势: 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。任务与线程本身是分离的,提高了程序的健壮性。
  • 劣势:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

2)使用继承Thread类创建多线程

  • 优势: 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当 前线程。
  • 劣势: 线程类已经继承了Thread类,所以不能再继承其他父类。线程池技术不接收Thread类型的线程

Runnable和Callable的区别

  • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  • Call方法可以抛出异常,run方法不可以。
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的 方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任 务的执行,还可获取执行结果。

Callable仅、在 Java 1.5 中引入,目的就是处理Runnable不支持的用例。如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁。 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。 (Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))Callable获取返回值 Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续执行,如果不调用不会阻塞。

@FunctionalInterface
public interface Runnable {
 /**
 * 被线程执⾏,没有返回值也⽆法抛出异常
 */
 public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
 /**
 * 计算结果,或在⽆法这样做时抛出异常。
 * @return 计算得出的结果
 * @throws 如果⽆法计算结果,则抛出异常
 */
 V call() throws Exception;
}
  •  编写类实现Callable接口 , 实现call方法
  •  创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(callable);
  • 通过Thread,启动线程 new Thread(future).start();  

为什么调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?

  • new 一个 Thread,线程进入了初始(New)状态。调用start() 会执行线程的相应准备工作(就绪Runnable),然后分配到时间片后自动执行 run() 方法(进入运行态)的内容,(调用 start() 方法,会启动一个线程并使线程进入了就绪状态(并没有运行),当分配到时间 片后就可以开始运行(调用run方法)(run方法执行结束,此线程终止,然后CPU再调度其他线程)这是真正的多线程工作。
  • 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程 中执行它,所以这并不是多线程工作。 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通 方法调用,还是在主线程里执行。

判断一个线程是否存活可以使用线程对象.isalive()判断

interrupted :判断当前线程是否已经中断,会清除状态

isInterrupted:判断线程是否已经中断,不会清除状态

jdk1.5后,引入了一个枚举类TimeUnit,对sleep方法进行了封装如要表达X小时X分X秒X毫秒

Thread.sleep(8575899L); 
TimeUnit.HOURS.sleep(6); 
TimeUnit.MINUTES.sleep(23); 
TimeUnit.SECONDS.sleep(25); 
TimeUnit.MILLISECONDS.sleep(999);

yield方法有什么作用?

yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法 而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在 进入到暂停状态后马上又被执行。

yield方法的使用:

https://blog.csdn.net/zhuwei898321/article/details/72844506?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

线程的中断

线程是一个独立的执行路劲,它是否应该结束应该由其自身决定,因为线程在执行任务的过程中,可能会涉及资源的使用和释放,若此时外部掐死该线程可能导致资源无法释放产生内存垃圾(无法回收),而且占用资源导致其他线程无法使用(也是stop方法被废弃的原因)因此采用给线程打标记(属性)的方式,来标记该线程的中断信号,当检测到该标记后会触发异常,在catch块中进行异常的处理。(如何关闭也可以不关闭)

public static void main(String[] args) {
        Thread thread=new Thread(new MyRunnable());
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        thread.interrupt();//主线程结束其任务后给t1线程添加中断标记,从而触发                        
         InterruptedException 异常
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+i);
                try {
                    Thread.sleep(1000);//sleep方法会抛出受检异常InterruptedException
                    //会检查是否携带中断记号,若有,则会new一个InterruptedException异常,进入 catch块
                    //但是这并不会导致线程死亡,只是一个标记,若不做处理则无影响
                } catch (InterruptedException e) {//线程终端异常
//                    e.printStackTrace();
                    System.out.println("发现中断标记但不死亡");
//                    System.out.println("让线程死亡直接return终结该run方法");
                }
            }
        }
    }

守护线程

线程分为用户线程与守护线程

用户线程:当一个进程不包含任何存活的用户线程时,进程结束

守护线程:守护用户线程,当最后一个用户线程死亡,守护线程自动死亡

public static void main(String[] args) {
        Thread thread=new Thread(new MyRunnable());
        thread.setDaemon(true);//表示该线程为守护线程
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
//此时主线程死亡,导致守护线程thread也死亡(不会继续执行)

    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//线程终端异常
                    System.out.println("发现中断标记但不死亡");
                }
            }
        }
    }

同步与异步

  • 同步:排队执行 , 效率低但是安全(线程安全)
  • 异步:同时执行 , 效率高但是数据不安全(线程不安全)

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生(单位时间内不⼀定同时执行)
  • 并行:指两个或多个事件在同一时刻发生(同时发生)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值