【并发编程】JAVA多线程与并发编程(一)

1. 进程与线程

1.1 进程的由来

从计算机的发展历程来讲,最初的计算机只能接受一些特定的指令,用户输入什么,计算机就做出相对应的操作,其他时间计算机就处于等待之中,这时的效率非常低下;后来有了批处理操作系统,就是把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机执行,但是批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的任务需要等待上一个任务执行结束之后才能执行。

面对日益增长的性能要求,于是先辈们提出了进程的概念:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。(来自百度百科)。简单点说就是应用程序在内存中分配空间,各个进程之间互不干扰,同时进程保存着程序每一个时刻运行的状态。

从CPU的角度来理解进程就是,CPU为每一个进程分配一个时间段,称作它的时间片,如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

1.2 线程的由来

进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情。于是先辈们又提出了线程的概念:让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。

1.3 线程与进程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

1.4 多进程与多线程

从优缺点方面来看:

  • 多进程优点:

    1. 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
    2. 通过增加CPU,就可以容易扩充性能;
    3. 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
  • 多进程缺点:

    1. 逻辑控制复杂,需要和主程序交互;
    2. 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;
  • 多线程的优点:

    1. 无需跨进程边界;
    2. 程序逻辑和控制方式简单;
    3. 所有线程可以直接共享内存和变量等;
    4. 线程方式消耗的总资源比进程方式好。
  • 多线程缺点:

    1. 每个线程与主程序共用地址空间,受限于进程分配的地址空间;
    2. 线程之间的同步和加锁控制比较麻烦;
    3. 一个线程的崩溃可能影响到整个程序的稳定性;
    4. 到达一定的线程数程度后,即使再增加CPU也无法提高性能;

2. 上下文切换

多线程编程中一般线程的个数都大于cpu核心的个数,而一个cpu核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,cpu采取的策略是为每个线程分配时间片并以轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完cpu时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以在加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换。

引起线程上下文切换的原因:

  1. 当前执行的任务的时间片用完之后,系统cpu正常调度下一个任务;
  2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一个任务;
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一个任务;
  4. 用户代码挂起当前任务,让出cpu时间;
  5. 硬件中断;

3. 线程的创建与生命周期

3.1 线程的创建

3.1.1 继承Thread类

3.1.1.1. Thread类的实现方式:
public class MyThread extends Thread {
    @Override 
    public void run() { 
        System.out.println("---- my thread ----"); 
      } 
    public static void main(String[] args) { 
        MyThread thread = new MyThread(); 
        thread.start(); 
    }
}
3.1.1.2. Thread类的构造方法

Thread类是一个Runnable接口的实现类,我们来看看Thread类的源码。

查看Thread类的构造方法,发现其实是简单调用一个私有的init()方法来实现初始化。init的方法签名:

// Thread类源码 

// 片段1 - init方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals)

// 片段2 - 构造函数调用init方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

// 片段3 - 使用在init方法里初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext = 
    acc != null ? acc : AccessController.getContext();

// 片段4 - 两个对用于支持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

源码init()方法的参数分别是:

  • g:线程组,指的是这个线程是在哪个线程组下
  • target:指定要执行的任务
  • name:线程的名字,多个线程的名字可以使重复的。
  • stackSize:预期堆栈大小,不指定默认为0,0代表忽略这个属性
  • acc:用于初始化私有变量inheritedAccessControlContext
  • inheritThreadLocals:可继承的ThreadLocal

实际上我们大多是直接调用下面两个构造方法:

Thread(Runnable target) 
Thread(Runnable target, String name)

3.1.2 实现Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("---- my runnable ----");
    }
    public static void main(String[] args) {
        new Thread(new MyRunnable()).start();
    }
}

为什么实现Runnable接口后不能直接调用run()?

多线程运行的原理是:有一个cpu,很多个任务都要竞争这个cpu。于是start()就是排队,等cpu选中这个任务时就可以执行run(),当cpu的运行时间片执行完,这个线程就继续排队,等待下一次执行run()。直到run()方法执行结束,此线程终止。所以说start()方法是让程序处于就绪状态,run()方法是一个线程体,或者说是方法体。

3.1.3 Thread类与Runnable接口的比较

实现一个自定义的线程类,可以有继承Thread类或者实现Runnable接口这两种方式,它们之间有什么优劣呢?

  • 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
  • Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
  • Runnable接口出现,降低了线程对象和线程任务的耦合性。
  • 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。

还有说 Runnable更容易可以实现多个线程间的资源共享,而Thread不可以! 纯属扯淡。

3.2 Callable、Future与FutureTask

通常来说,使用RunnableThread来创建一个新的线程。但是他们有一个弊端,就是run()方法没有返回值,而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后返回一个值。

JDK提供了Callable接口与Future接口解决这类的问题

3.2.1 Callable接口

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        int random = (int) (Math.random() * 100);
        Thread.sleep(5000);
        return random;
    }

    public static void main(String[] args) {
        System.out.println("---- main start----");
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(new MyCallable());
        try {
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
        System.out.println("---- main stop----");
    }
}

上面的源码展示了如何使用Callable接口的使用,Callable一般配合线程池工具来使用的。

Callable源码:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

3.2.2 Future接口

Future接口只有几个比较简单的方法,其中cancel()方法是试图取消一个线程的执行。
试图取消,并一定能取消成功。因为任务可能已完成、已取消、或者其他因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数paramBoolean表示是否采用中断的方式取消线程执行。所以有时候,为了任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用Future但又不提供可用的结果。则可以声明Future<?>形式类型、并返回null作为底层任务的结果。

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException,ExecutionException,TimeoutException;
}

3.2.3 FutureTask类

FutureTask是实现RunnableFuture接口,而RunnableFuture接口同时继承了Runnable接口和Future接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

有了Future为什么还要有FutureTask类?看源码可以知道Future只是一个接口,而它里面的cancel()get()isDone()等方法要自己实现起来都是非常复杂的。所以提供了一个FutureTask类来使用。

示例代码:

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        int random = (int) (Math.random() * 100);
        Thread.sleep(5000);
        return random;
    }

    public static void main(String[] args) {
        System.out.println("---- main start----");
        ExecutorService executor = Executors.newSingleThreadExecutor();
        FutureTask<Integer> future = new FutureTask<>(new MyCallable());
        executor.submit(future);
        try {
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
        System.out.println("---- main stop----");
    }
}

Future使用不同的是:首先FutureTask调用submit(Runnable task)方法是没有返回值的,而Future示例代码中调用的是submit(Callable<T> task)方法。

然后,这里是使用FutureTask直接取get()返回的值,而Future示例代码中是通过submit()方法返回的Future去取值。

在很多高并发的环境下,有可能CallableFutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。

为什么FutureTask能够确保线程只执行一次?

源码中针对线程状态进行了判断:

  1. state != NEW,表示任务正在被执行或已经完成, 直接return;
  2. 或者尝试CAS,将当前线程设置为执行run()的线程。如果失败,说明已经有其他线程先行一步执行了run(),则当前线程return退出。
public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        //持有Callable的实例,后续会执行该实例的call()方法
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                //执行中抛的异常会放入outcome中保存
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

FutureTask内部定义了以下状态:

private volatile int state;                //对象的状态
private static final int NEW          = 0; //任务新建和执行中
private static final int COMPLETING   = 1; //任务将要执行完毕
private static final int NORMAL       = 2; //任务正常执行结束
private static final int EXCEPTIONAL  = 3; //任务异常
private static final int CANCELLED    = 4; //任务取消
private static final int INTERRUPTING = 5; //任务线程即将被中断
private static final int INTERRUPTED  = 6; //任务线程已中断

FutureTask对象初始化时,在构造器中把state置为为NEW,之后状态的变更依据具体执行情况来定。例如任务执行正常结束前,state会被设置成COMPLETING,代表任务即将完成,接下来很快就会被设置为NORMAL或者EXCEPTIONAL,这取决于调用Callable中的call()方法是否抛出了异常。有异常则就设置为EXCEPTIONAL,反之设置为NORMAL

任务提交后、任务结束前取消任务,那么有可能变成为CANCELLED或者INTERRUPTED。在调用cancel()方法时,如果传入fasle表示不中断线程,state会被设置为CANCELLED,反之state先被变成为INTERRUPTING,后变为INTERRUPTED

总结下,FutureTask的状态流转过程,可以出现以下四种情况:

  1. 任务正常执行并返回: NEW -> COMPLETING -> NORMAL
  2. 执行中出现异常:NEW -> COMPLETING -> EXCEPTIONAL
  3. 任务执行过程中被取消,并且不响应中断:NEW -> CANCELLED
  4. 任务执行过程中被取消,并且响应中断:NEW -> INTERRUPTING -> INTERRUPTED

3.3 线程的基本方法

  • wait():调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait()方法一般用在同步方法或同步代码块中

  • yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;

  • sleep():静态方法,使当前线程睡眠一段时间;与 wait()方法不同的是 sleep()不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

  • interrupt():中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
    2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
    3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills)方法),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterrupted()方法将会返回false
    4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程Thread的时候,可以调用 Thread.interrupt()方法,在线程的 run() 方法内部可以根据 Thread.isInterrupted()的值来优雅的终止线程。
  • join():等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

  • notify():唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。

  • notifyAll() :再次唤醒监视器上等待的所有线程。

3.4 线程的生命周期

线程的生命周期包含5个阶段:新建、就绪、运行、阻塞、销毁(死亡)。

  • 新建(NEW):当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

  • 就绪(RUNNABLE):当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

  • 运行(RUNNING):如果处于就绪状态的线程获得了 CPU资源,开始执行 run()方法的线程执行体,则该线程处于运行状态。

  • 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    1. 等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
    2. 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
    3. 其他阻塞(sleep/join):运行(running)的线程执行 Thread.sleep(long ms)t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
  • 销毁(DEAD):线程会以下面三种方式结束,结束后就是死亡状态。

  1. 正常结束:run()或 call()方法执行完成,线程正常结束。
  2. 异常结束:线程抛出一个未捕获的ExceptionError
  3. 调用stop:直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

完整的线程生命周期如下图:
请添加图片描述

3.5 线程的状态

  • NEW:初始状态,线程被构建,但是没有调用start()方法
  • RUNNBALE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
  • BLOCKED:阻塞状态,表示线程阻塞于锁
  • WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
  • TERMINATED:终止状态,表示当前线程已经执行完毕

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。java线程状态变迁如下图:

请添加图片描述

由上图可知:线程创建之后它将处于NEW(新建)状态,调用start()方法后开始运行,线程这时候处于READY(可运行)状态。可运行状态的线程获得了CPU时间片后就处于RUNNING(运行)状态。当线程wait()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而TIME_WAITING(超时等待状态)状态相当于在等待基础上增加了超时限制,比如通过sleep()方法或wait()方法可以将Java线程置于TIME_WAITING状态,当超时时间到达后Java线程将会返回到RUNNABLE状态。线程在执行Runnable的run()方法之后将会进入到TERMINATED状态。

4.线程组与线程的优先级

4.1 ThreadGroup

Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制

ThreadGroupThread的关系就如同他们的字面意思一样简单粗暴,每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在newThread时没有显式指定,那么默认将父线程(就是当前执行new Thread的线程)线程组设置为自己的线程组

代码示例:

public class MyThread  {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{
            System.out.println("当前线程组名称:"
                    + Thread.currentThread().getThreadGroup().getName());
            System.out.println("线程名称:"
                    + Thread.currentThread().getName());
        });

        thread.start();

        System.out.println("main所在的线程的线程组名称:"
                + Thread.currentThread().getThreadGroup().getName());
        System.out.println("main方法线程名称:"+ Thread.currentThread().getName());
    }

执行结果:

main所在的线程的线程组名称:main 
当前线程组名称:main 
main方法线程名称:main 
线程名称:Thread-0

ThreadGroup管理着它下面的ThreadThreadGroup是一个标准的向下引用的树状结构,这样设计的原因是防止“上级”线程被“下级”线程引用而无法有效地被GC回收。

4.2 线程的优先级

java中的线程优先级可以指定为1~10.但是并不是所有的操作系统都支持10级优先级的划分(有些操作系统只支持3级划分:低、中、高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是操作系统决定。

Java默认的线程优先级为5,线程的执行顺序由调度程序决定,线程的优先级会在线程被调用之前设定。

通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。

通过设置优先级的代码如下:

public class MyThread {
    public static void main(String[] args) {
        Thread thread1 = new Thread();
        System.out.println("默认优先级:"+thread1.getPriority());

        Thread thread2 = new Thread();
        thread2.setPriority(10);
        System.out.println("设置后的优先级:"+thread2.getPriority());
    }
}

输出结果:

默认优先级:5 
设置后的优先级:10

是否可以在业务实现的时候,采用这种方法来指定一些线程执行的先后顺序呢?答案是不可以!

Java线程中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定采纳,而真正的调用顺序,是操作系统的线程调度算法决定的。

Java提供一个线程调度器来监视和控制处于RUNNABEL状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则,每个Java程序都有一个默认的主线程,就是通过jvm启动的第一个线程main线程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值