重新学习并发-Java线程

摘要

线程是操作系统调度的最小单元,在多核环境下实现多线程能够显著提升程序性能。本文会先简单的介绍Java线程基础知识,并从启动一个线程到线程间不同的通信方式。

线程

在现代操作系统中运行一个程序时,会为其创建一个进程。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器和局部变量等属性,并且能够访问共享的内存变量,处理器在这些线程上高速切换实现并发。

线程是比进程更轻量级的调度执行单位,各个线程共享着进程资源(内存地址、文件I/O等),也可以进行独立的调度(线程是CPU调度的基本单位)。

实现线程的方式

使用内核线程实现

内核线程直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵线程调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以看成是内核的一个分身,这样处理器就可以同时处理多任务。

操作系统会对内核线程进行一个封装,程序一般通过调用其封装后的接口 — 轻量级进程(Light Weight Process,LWP),轻量级进程和内核线程是1:1的对应关系。轻量级进程的局限就是:线程的创建、同步等都需要进行系统调用,系统调用的代价需要从用户态和内核态中来回切换,代价较高;每个轻量级进程都有对应的内核线程支持,轻量级进程需要消耗一定的内核资源,一个系统所支持的轻量级进程的数量也是有限的。
Alt

使用用户线程

如果一个线程只要不是内核线程就可以认为是用户线程。轻量级进程的实现始终是建立在内核之上,属于用户线程,但是其许多操作都要进行系统调用,效率会受到限制;

用户线程的建立、同步、销毁等都是在用户态中实现,不需要借助内核,这种操作是快速且低消耗的;
用户线程的优势就在于不需要内核线程的支持,所有对于线程的操作都是要考虑的,比如线程的创建、切换与调度,阻塞如何处理等问题,在设计用户线程时都是需要解决的,现在采用用户线程的程序越来越少;

Java线程

目前JDK版本中,操作系统支持怎样的线程模型就很大程度上决定了Java虚拟机的线程是怎样映射的,线程的实现是基于操作系统原生线程模型来实现。线程模型的差异对于并发规模和操作成本产生影响,但是对于Java程序的编码和运行过程来说,这些差异都是透明的;

线程调度

现在操作系统基本都是采用时分的形式调度和运行线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程的时间片用完之后就会发生线程调度,并且等到下次分配。线程分配到时间片的多少也就决定了线程使用处理器资源的多少。线程调度是指系统为线程分配处理器使用权的过程,主要分为协同式线程调度和抢占式线程调度;

采用协同式线程调度,线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,主动通知并切换到另一个线程上,好处就是实现简单,线程要把自己的事情干完之后才会进行线程切换,切换操作自己可知,不会产生线程同步的问题;弊端就是线程执行时间不可控,如果一个线程有问题且不通知系统进行线程切换,将会导致线程一直阻塞,相当不稳定;

采用抢占式线程调度,每个线程由系统分配执行时间,线程的切换不由线程本身来决定。线程的执行时间是系统可控的,不会出现一个线程导致进程阻塞的问题;

Java线程调度是系统自动完成的,但是如何给线程多分配或者少分配一些处理器资源?
在Java线程中可以通过设置一个整型成员变量priority来控制优先级来实现,一共设置10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。但是线程的优先级并不是太靠谱,因为Java线程是通过映射到系统原生线程上来实现的,所以线程的调度最终还是取决于操作系统;许多操作系统提供线程优先级但是并不能与Java线程的优先级一一对应;在不同的JVM以及操作系统上,线程规划有差异,有些操作系统甚至会忽略对于线程优先级的设定。

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

线程状态


状态名称说明
NEW线程创建后尚未启动
RUNNABLE运行状态,Java中将就绪和运行状态统称为“运行中”
BLOCKED阻塞状态,线程阻塞于锁
WAITING等待状态,线程进入等待状态,等待通知或中断
TIME_WAITING超时等待状态
TERMINATED终止状态,表示当前线程已经执行完毕

线程状态转换入如下所示:
在这里插入图片描述

Daemon线程

Daemon线程是一种支持型线程,主要用作程序中后台调度以及支持性工作,当一个Java程序中不存在非Daemon线程时,Java虚拟机会退出,可以通过Thread.setDaemon(true)将线程设置为Daemon线程;

在后台默默地完成一些系统性的服务,比如垃圾回收线程等,与之对应的就是用户线程,用户线程就是系统的工作线程,它会先完成这个程序应该要完成的业务操作,如果用户线程全部结束,也意味着这个程序实际上无事可做,设置守护线程必须在start()之前设置;

public final void setDaemon(boolean on){
	checkAccess();
	if(isAlive()){
		throw new IllegalThreadStateException();
	}
	daemon = on;
}

启动和终止线程

线程的创建

线程的创建共有三种方法:

  • 实现Runnable接口;
  • 实现Callable接口;
  • 继承Thread类

实现Runnable接口

需要实现run方法,通过Thread调用start()方法来启动线程,start()方法会新建一个线程并让这个线程执行run()方法:

public class MyRunnable implements Runnable{
	public void run(){
		System.out.println("Hello World!");
	}
	public static void main(String[] args){
		MyRunnable myRunnable = new MyRunnable();
		Thread thread = new Thread(myRunnable);
		thead.start();
	}
}

继承Thread类

需要实现run()方法,因为Thread类也实现了Runnable接口;
当调用start()方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法

public class MyThread extends Thread{
	public static void run(){
		Systm.out.println("Hello World!");
	}
	public static void main(String[] args){
		Thread thread = new MyThread();
		thread.start();
	}
}

实现Callable接口

与Runnable接口相比,Callable可以有返回值,返回值通过FutureTask进行封装;

public class MyCallable implements Callable<Integer> {

    public Integer call(){
        return 123;
    }

    public static void main(String[] args) throws ExecutionException,InterruptedException{
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

实现接口VS继承Thread类

  • Java不支持多继承,因此继承了Thread类就无法继承其他类,但是可以实现多个接口;
  • 类可能只要求可执行即可,继承整个Thread类开销过大;

启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程,线程start()方法的含义是:当前线程同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

启动一个线程前最好为该线程设置线程名称,以便在使用jstack分析程序或者进行问题排查时,方便处理bug;

Thread类解析

与线程运行状态有关的方法

1、start()
start()方法用来启动一个线程,当调用该方法时,相应的线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用;

2、run()
run()方法不需要用户来调用,当通过start()方法启动一个线程之后,一旦线程获得了CPU的执行时间,便进入run()方法体中去执行具体的任务;

3.sleep()
在指定的毫秒数内让线程睡眠,并且交出CPU去执行其他的任务,当线程睡眠结束之后,不一定会立即执行线程,因为此时的CPU可能在执行其他的任务,调用sleep()方法相当于让线程进入阻塞状态;

  • sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象;
  • 使用sleep方法,阻塞的线程被中断时抛出InterruptedException异常
  • public static native void sleep(long millis) throws InterruptedException;

4、yield()
调用该方法只会让当前线程交出CPU资源,让CPU去执行其他的线程,但是yield不能控制具体的交出CPU的时间;

  • yield只会让具有相同优先级的线程具有获取CPU执行时间的机会;
  • 调用yield()方法不会让线程进入阻塞状态,而是让线程重回就绪状态,只需要等待重新得到CPU的执行;
  • 不会释放锁;

5、join()
在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间;

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);等待时间为0,意味着永远等待,直到线程被唤醒;
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join方法会调用wait方法让宿主线程进入阻塞状态,并且释放线程占有的锁,并交出CPU执行权限,结合join方法的声明,有以下三条:

  • join方法会让线程交出cpu执行权限;
  • join方法会让线程释放对一个对象持有的锁;
  • 如果调用join方法,必须捕获InterruptedException异常或者将该异常向上层抛出;

6、interrupt()
中断可以理解为线程的一个标识位属性,表示运行中的线程能否被其他线程内进行中断操作。可以把中断表示其他线程对该线程打了一个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作;

interrupt即中断的意思,单独调用interrupt方法可以使得处于阻塞的线程抛出一个异常,可以用来中断一个处于阻塞状态的线程;线程中断只是给线程发送一个通知,告知目标线程有人希望你退出;

线程通过检查自身是否被中断来进行响应,线程可以通过方法isInterrupted()方法来判断是否被中断,也可以调用静态方法Thread.interrupted()方法对当前线程的中断标识位进行复位;

public void Thread.interrupt() //通知目标线程中断,设置中断标志位
public boolean Thread.isInterrupted()//判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

直接调用interrupt方法不能中断正在运行中的程序;一般会在MyThread类中增加一个volatile属性isStop来标识是否结束while循环,然后在while循环中判断isStop的值

[JAVA工程师必会知识点之并发编程] 1、现在几乎100%的公司面试都必须面试并发编程,尤其是互联网公司,对于并发编程的要求更高,并发编程能力已经成为职场敲门砖。 2、现在已经是移动互联和大数据时代,对于应用程序的性能、处理能力、处理时效性要求更高了,传统的串行化编程无法充分利用现有的服务器性能。 3、并发编程是几乎所有框架的底层基础,掌握好并发编程更有利于我们学习各种框架。想要让自己的程序执行、接口响应、批处理效率更高,必须使用并发编程。 4、并发编程是中高级程序员的标配,是拿高薪的必备条件。 【优惠说明】 1、120余节视频课,原价299元,今日报名立减100,仅需199元 2、现在购课,就送价值800元的编程大礼包! 备注:请添加微信:itxy41,按提示获取讲师答疑服务。 【主讲讲师】 尹洪亮Kevin: 现任职某互联网公司首席架构师,负责系统架构、项目群管理、产品研发工作。 10余年软件行业经验,具有数百个线上项目实战经验。 擅长JAVA技术栈、高并发高可用伸缩式微服务架构、DevOps。 主导研发的蜂巢微服务架构已经成功支撑数百个微服务稳定运行 【推荐你学习这门课的理由:知识体系完整+丰富学习资料】 1、 本课程总计122课时,由五大体系组成,目的是让你一次性搞定并发编程。分别是并发编程基础、进阶、精通篇、Disruptor高并发框架、RateLimiter高并发访问限流吗,BAT员工也在学。 2、课程附带附带3个项目源码,几百个课程示例,5个高清PDF课件。 3、本课程0基础入门,从进程、线程、JVM开始讲起,每一个章节只专注于一个知识点,每个章节均有代码实例。 【课程分为基础篇、进阶篇、高级篇】 一、基础篇 基础篇从进程与线程、内存、CPU时间片轮训讲起,包含线程的3种创建方法、可视化观察线程、join、sleep、yield、interrupt,Synchronized、重入锁、对象锁、类锁、wait、notify、线程上下文切换、守护线程、阻塞式安全队列等内容。 二、进阶篇 进阶篇课程涵盖volatied关键字、Actomic类、可见性、原子性、ThreadLocal、Unsafe底层、同步类容器、并发类容器、5种并发队列、COW容器、InheritableThreadLocal源码解析等内容。 三、精通篇 精通篇课程涵盖JUC下的核心工具类,CountDownLath、CyclicBarrier、Phaser、Semaphore、Exchanger、ReentrantLock、ReentrantReadWriteLock、StampedLock、LockSupport、AQS底层、悲观锁、乐观锁、自旋锁、公平锁、非公平锁、排它锁、共享锁、重入锁、线程池、CachedThreadPool、FixedThreadPool、ScheduledThreadPool、SingleThreadExecutor、自定义线程池、ThreadFactory、线程池切面编程、线程池动态管理等内容,高并发设计模式,Future模式、Master Worker模式、CompletionService、ForkJoin等 课程中还包含 Disruptor高并发无锁框架讲解:Disruptor支持每秒600万订单处理的恐怖能力。深入到底层原理和开发模式,让你又懂又会用。 高并发访问限流讲解:涵盖木桶算法、令牌桶算法、Google RateLimiter限流开发、Apache JMeter压力测试实战。 【学完后我将达到什么水平?】 1、 吊打一切并发编程相关的笔试题、面试题。 2、 重构自己并发编程的体系知识,不再谈并发色变。 3、 精准掌握JAVA各种并发工具类、方法、关键字的原理和使用。 4、 轻松上手写出更高效、更优雅的并发程序,在工作中能够提出更多的解决方案。 【面向人群】 1、 总感觉并发编程很难、很复杂、不敢学习的人群。 2、 准备跳槽、找工作、拿高薪的程序员。 3、 希望提高自己的编程能力,开发出更高效、性能更强劲系统的人群。 4、 想要快速、系统化、精准掌握并发编程的人群。 【课程知识体系图】
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页