乐趣区

关于java:思维导图整理Java并发基础知识

话不多说,先上图。

1、基本概念

欲说线程,必先说过程。

  • 过程:过程是代码在数据汇合上的一次运行流动,是零碎进行资源分配和调度的根本单位。
  • 线程:线程是过程的一个执行门路,一个过程中至多有一个线程,过程中的多个线程共享过程的资源。

操作系统在分配资源时是把资源分配给过程的,然而 CPU 资源比拟非凡,它是被调配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 调配的根本单位

在 Java 中,当咱们启动 main 函数其实就启动了一个 JVM 过程,而 main 函数在的线程就是这个过程中的一个线程,也称主线程。

示意图如下:

一个过程中有多个线程,多个线程共用过程的堆和办法区资源,然而每个线程有本人的程序计数器和栈。

2、线程创立和运行

Java 中创立线程有三种形式,别离为继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。

  • 继承 Thread 类,重写 run()办法,调用 start()办法启动线程
public class ThreadTest {

    /**
     * 继承 Thread 类
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {System.out.println("This is child thread");
        }
    }

    public static void main(String[] args) {MyThread thread = new MyThread();
        thread.start();}
}
  • 实现 Runnable 接口 run()办法
public class RunnableTask implements Runnable {public void run() {System.out.println("Runnable!");
    }

    public static void main(String[] args) {RunnableTask task = new RunnableTask();
        new Thread(task).start();}
}

下面两种都没有返回值。

  • 实现 Callable 接口 call()办法,这种形式能够通过 FutureTask 获取工作执行的返回值
public class CallerTask implements Callable<String> {public String call() throws Exception {return "Hello,i am running!";}

    public static void main(String[] args) {
        // 创立异步工作
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        // 启动线程
        new Thread(task).start();
        try {
            // 期待执行实现,并获取返回后果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {e.printStackTrace();
        } catch (ExecutionException e) {e.printStackTrace();
        }
    }
}

3、罕用办法

3.1、线程期待与告诉

在 Object 类中有一些函数能够用于线程的期待与告诉。

  • wait():当一个线程调用一个共享变量的 wait()办法时,该调用线程会被阻塞挂起,到产生上面几件事件之一才返回:(1)线程调用了该共享对象 notify()或者 notifyAll()办法;(2)其余线程调用了该线程 interrupt() 办法,该线程抛出 InterruptedException 异样返回。
  • wait(long timeout):该办法相 wait() 办法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该办法挂起后,没有在指定的 timeout ms 工夫内被其它线程调用该共享变量的 notify()或者 notifyAll() 办法唤醒,那么该函数还是会因为超时而返回。
  • wait(long timeout, int nanos),其外部调用的是 wait(long timout)函数。

下面是线程期待的办法,而唤醒线程次要是上面两个办法:

  • notify() : 一个线程调用共享对象的 notify() 办法后,会唤醒一个在该共享变量上调用 wait 系列办法后被挂起的线程。一个共享变量上可能会有多个线程在期待,具体唤醒哪个期待的线程是随机的。
  • notifyAll():不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()办法则会唤醒所有在该共享变量上因为调用 wait 系列办法而被挂起的线程。

如果有这样的场景,须要期待某几件事件实现后能力持续往下执行,比方多个线程加载资源,须要期待多个线程全副加载结束再汇总解决。Thread 类中有一个 join 办法可实现。

3.2、线程休眠

Thread 类中有一个动态态的 sleep 办法,当一个个执行中的线程调用了 Thread 的 sleep 办法后,调用线程会临时让出指定工夫的执行权,也就是在这期间不参加 CPU 的调度,然而该线程所领有的监视器资源,比方锁还是持有不让出的。指定的睡眠工夫到了后该函数会失常返回,线程就处于就绪状态,而后参加 CPU 的调度,获取到 CPU 资源后就能够持续运行。

3.3、让出优先权

Thread 有一个动态 yield 办法,当一个线程调用 yield 办法时,理论就是在暗示线程调度器以后线程申请让出本人的 CPU 应用,然而线程调度器能够无条件疏忽这个暗示。

当一个线程调用 yield 办法时,以后线程会让出 CPU 使用权,而后处于就绪状态,线程调度器会从线程就绪队列外面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 行权。

3.4、线程中断

Java 中的线程中断是一种线程间的合作模式,通过设置线程的中断标记并不能间接终止该线程的执行,而是被中断的线程依据中断状态自行处理。

  • void interrupt():中断线程,例如,当线程 A 运行时,线程 B 能够调用钱程 interrupt() 办法来设置线程的中断标记为 true 并立刻返回。设置标记仅仅是设置标记, 线程 A 理论并没有被中断,会持续往下执行。如果线程 A 因为调用了 wait() 系列函数、join 办法或者 sleep 办法阻塞挂起,这时候若线程 B 调用线程 A 的 interrupt()办法,线程 A 会在调用这些办法的中央抛出 InterruptedException 异样而返回。
  • boolean isInterrupted() 办法:检测以后线程是否被中断。
  • boolean interrupted() 办法:检测以后线程是否被中断,与 isInterrupted 不同的是,该办法如果发现以后线程被中断,则会革除中断标记。

4、线程状态

下面整顿了线程的创立形式和一些罕用办法,能够用线程的生命周期把这些办法串联起来。

在 Java 中,线程共有六种状态:

状态 阐明
NEW 初始状态:线程被创立,但还没有调用 start()办法
RUNNABLE 运行状态:Java 线程将操作系统中的就绪和运行两种状态抽象的称作“运行”
BLOCKED 阻塞状态:示意线程阻塞于锁
WAITING 期待状态:示意线程进入期待状态,进入该状态示意以后线程须要期待其余线程做出一些特定动作(告诉或中断)
TIME_WAITING 超时期待状态:该状态不同于 WAITIND,它是能够在指定的工夫自行返回的
TERMINATED 终止状态:示意以后线程曾经执行结束

线程在本身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java 线程状态变动如图示:

5、线程上下文切换

应用多线程的目标是为了充分利用 CPU,但要意识到,每个 CPU 同一时刻只能被一个线程应用。

为了让用户感觉多个线程是在同时执行的,CPU 资源的调配采纳了工夫片轮转也就是给每个线程调配一个工夫片,线程在工夫片内占用 CPU 执行工作。当线程应用完工夫片后,就会处于就绪状态并让出 CPU 让其余线程占用,这就是上下文切换。

6、线程死锁

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的相互期待的景象,在无外力作用的状况下,这些线程会始终互相期待而无奈持续运行上来。

那么为什么会产生死锁呢?死锁的产生必须具备以下四个条件:

  • 互斥条件:指线程对己经获取到的资源进行它性应用,即该资源同时只由一个线程占用。如果此时还有其它线程申请获取获取该资源,则请求者只能期待,直至占有资源的线程开释该资源。
  • 申请并持有条件:指一个 线程己经持有了至多一个资源,但又提出了新的资源申请,而新资源己被其它线程占有,所以以后线程会被阻塞,但阻塞 的同时并不开释本人曾经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在本人应用完之前不能被其它线程抢占,只有在本人应用结束后才由本人开释该资源。
  • 环路期待条件:指在产生死锁时,必然存在一个线程——资源的环形链,即线程汇合 {T0,T1,T2,……,Tn} 中 T0 正在期待一 T1 占用的资源,Tl1 正在期待 T2 用的资源,…… Tn 在期待己被 T0 占用的资源。

该如何防止死锁呢?答案是 至多毁坏死锁产生的一个条件

其中,互斥这个条件咱们没有方法毁坏,因为用锁为的就是互斥。不过其余三个条件都是有方法毁坏掉的,到底如何做呢?

  • 对于“申请并持有”这个条件,能够一次性申请所有的资源。
  • 对于“不可剥夺”这个条件,占用局部资源的线程进一步申请其余资源时,如果申请不到,能够被动开释它占有的资源,这样不可抢占这个条件就毁坏掉了。
  • 对于“环路期待”这个条件,能够靠按序申请资源来预防。所谓按序申请,是指资源是有线性程序的,申请的时候能够先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

7、线程分类

Java 中的线程分为两类,别离为 daemon 线程(守护线程)user 线程(用户线程)

在 JVM 启动时会调用 main 函数,main 函数所在的钱程就是一个用户线程。其实在 JVM 外部同时还启动了很多守护线程,比方垃圾回收线程。

那么守护线程和用户线程有什么区别呢?区别之一是当最初一个非守护线程束时,JVM 会失常退出,而不论以后是否存在守护线程,也就是说守护线程是否完结并不影响 JVM 退出。换而言之,只有有一个用户线程还没完结,失常状况下 JVM 就不会退出。

8、ThreadLocal

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创立了 ThreadLocal,那么拜访这个变量的每个线程都会有这个变量的一个本地正本,当多个线程操作这个变量时,实际操作的是本人本地内存外面的变量,从而防止了线程平安问题。创立 ThreadLocal 变量后,每个线程都会复制 到本人的本地内存。

能够通过 set(T)办法来设置一个值,在以后线程下再通过 get()办法获取到原先设置的值。

上面来看一个 ThreadLocal 的应用实例:

public class ThreadLocalTest {
    // 创立 ThreadLocal 变量
    static ThreadLocal<String> localVar = new ThreadLocal<String>();

    // 打印函数
    static void print(String str) {
        // 打印以后线程本地内存中 localVar 变量值
        System.out.println(str + ":" + localVar.get());
        // 革除火线程本地内存中 localVar 变量值
        //localVar.remove();}

    public static void main(String[] args) {Thread thread1 = new Thread(new Runnable() {public void run() {
                // 设置线程 1 中本地变量 localVal 的值
                localVar.set("线程 1 的值");
                // 调用打印函数
                print("线程 1");
                // 打印本地变量的值
                System.out.println("线程 1 打印本地变量后:" + localVar.get());
            }
        });

        Thread thread2 = new Thread(new Runnable() {public void run() {
                // 设置线程 2 中本地变量 localVal 的值
                localVar.set("线程 2 的值");
                // 调用打印函数
                print("线程 2");
                // 打印本地变量的值
                System.out.println("线程 2 打印本地变量后:" + localVar.get());
            }
        });

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

9、Java 内存模型

在 Java 中,所有实例域、动态域和数组元素都存储在堆内存中,堆内存在线程之间共享。

Java 线程之间的通信由 Java 内存模型管制,Java 内存模型决定一个线程对共享变量的写入何时对另一个线程可见。

从形象的角度来看,Java 内存模型定义了线程和主内存之间的形象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个公有的本地内存(Local Memory),本地内存中存储了该线程以读 / 写共享变量的正本。本地内存是 Java 内存模型的 一个抽象概念,并不实在存在。它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。

Java 内存模型的形象示意如图:

在理论实现中线程的工作内存如下图:

10、synchronized

synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都能够把它当作同步锁来应用,这些 Java 内置的使用者看不到的锁被称为外部锁,也作监视器锁。

线程的执行代码在进入 synchronized 代码块前会主动获取外部锁,这时候其余线程拜访该同步代码块 被阻塞挂起。拿到外部锁的线程会在失常退出同步代码块或者抛出异样后或者在同步块调用了该内置锁资源 wait 系列办法时开释该内置锁。内置锁是排它锁,就是当一个线程获取这个锁后,其余线程必须期待该线程开释锁后能力获取该锁。

synchronized 的内存语义:这个内存语义就能够解决共享变量内存可见性问题,进入 synchronized 块的内存语义是把在 synchronized 块内应用到的变量从线程的工作内存中革除,这样在 synchronized 块内应用到该变量时就不会从线程的工作内存中获取,而是间接从主内存中获取。退出 synchronized 块的内存语义是把在 synchronized 块内对共享变批改刷新到主内存。

11、volatile

下面介绍了应用锁的形式能够解决共享内存可见性问题,然而应用锁太轻便,因为它会带来线程上下文的切换开销,对于解决内存可见性问题,Java 还提供了 volatile 种弱模式的同步,也就是应用 volatile 关键字,该关键字能够确保对一个变量的更新对其余线程马上 可见

当一个变量被申明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其余中央,而是会把值刷新回主内存,当其它线程读取该共享变量,会从主内存从新获取最新值,而不是应用以后线程的工作内存中的值。

volatile 尽管提供了可见性保障,但并不保障操作的原子性。

12、Java 中的原子性操作

所谓原子性操作,是指执行一系列操作时,这些操作要么全副执行,要么全副不执行,不存在只执行其中一部分的状况。

例如在设计计数器个别都先读取以后值,而后+1,再更新。这个过程是读 - 改 - 写的过程,如果不能保障这个过程是原子性的,那么就会呈现线程安问题。

那么如何能力保障多个操作的原子性呢?最简略的办法就是应用 synchronized 关键字进行同步。还能够用 CAS 操作。从 Java 1.5 开始,JDK 的并发包里也提供了一些类来反对原子操作。

synchronized 是独占锁,没有获取外部锁的线程会被阻塞掉,大大降级了并发性。

13、Java 中的 CAS 操作

在 Java 中,锁在并发解决中占据了一席之地,然而应用锁有有个不好的中央,就是当线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和从新调度开销。

Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在肯定水平上补救了锁带来的开销问题,然而 volatile 只能保 共享变量可见性,不能解决读 - 改 - 写等的原子性问题。

CAS 即 Compre and Swap,其是 JDK 提供的非阻塞原子性操作,它通过硬件保障了 比拟 - 更新 操作的原子性。JDK 外面的 Unsafe 类提供了一系列的 compareAndSwap *办法,以 compareAndSwapLong 办法为例,看一下什么是 CAS 操作。

  • boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update): CAS 有四个操作数,别离为对象内存地位、对象中 变量的偏移量、变量预期值和新的值。其操作含意是:只有当对象 obj 中内存偏移量为 valueOffset 的变量预期值为 expect 的时候,才会将 ecpect 更新为 update。这是处理器提供的一个原子性指令。

CAS 有个经典的ABA 问题。因为 CAS 须要在操作值的时候,查看值有没有发生变化,如果没有发生变化,则更新,然而如果一个值原来是 A,变成了 B,又变成了 A,那么应用 CAS 进行查看时会发现它 的值没有发生变化,然而实际上却变动了。ABA 问题的解决思路就是应用版本号。在变量后面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A。

14、锁的概述

14.1、乐观锁与乐观锁

乐观锁和乐观锁是在数据库中引入的名词,然而在并发包锁外面引入了相似的思维。

乐观锁指对数据被外界批改持激进态度,认为数据很容易就会被其余线程批改,所以在数据被解决前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。乐观锁的实现往往依附数据库提供的锁机制,即在数据,在对数据记录操作前给记录排它锁。如果获取锁失败,则阐明数据正在被其它线程批改,以后线程则期待或者抛出异样。如果获取锁胜利,则对记录进行操作,而后提交事务后开释排它锁。

乐观锁绝对乐观锁来说的,它认为数据在个别状况下不会造成抵触,所以在拜访记录前不会加排它锁,而在进行数据提交更新时,才会正式对数据冲 与否进行检测。具体来说,依据 update 返回的行数让用户决定如何去做。

14.2、偏心锁与非偏心锁

依据线程获取锁的抢占机制,锁能够分为偏心锁和非偏心锁,偏心锁示意线程获取锁的程序是依照线程申请锁的工夫早晚来决定的,也就是最早申请锁的线程将最早获取到锁。

而非偏心锁是在运行时闯入,也就是先来不肯定先得。

ReentrantLock 提供了偏心锁和非偏心锁的实现:

  • 偏心锁:ReentrantLock pairLock =new eentrantLock(true)
  • 非偏心锁:ReentrantLock pairLock =new ReentrantLock(false)。构造函数不传数,则默认是非偏心锁。

例如,假如线程 A 曾经持有了锁,这时候线程 B 申请该锁其将被挂起。当线程 A 开释锁后,如果以后有线程 C 也须要取该锁,如果采纳非偏心锁式,则依据线程调度策略,线程 B 和线程 C 两者之一可能获取锁,这时候不须要任何其余干预,而如果应用偏心锁则须要把 C 挂起,让 B 获取以后锁。

在没有公平性需要的前提下尽量应用非偏心锁,因为偏心锁会带来性能开销。

14.3、独占锁与共享锁

依据锁只能被单个线程持有还是能被多个线程独特持有,锁能够分为独占锁和共享锁。

独占锁保障任何时候都只有一个线程能失去锁,ReentrantLock 就是以独占形式实现的。

共享锁则能够同时由多个线程持有,例如 ReadWriteLock 读写锁,它容许一个资源能够被多线程同时进行读操作。

独占锁是一种乐观锁,共享锁是一种乐观锁。

14.4、可重入锁

当一个线程要获取一个被其余线程持有的独占锁时,该线程会被阻塞。

那么当 一个线程再次获取它本人己经获取的锁时是否会被阻塞呢?如果不被阻塞,那么咱们说该锁是可重入的,也就是只有该线程获取了该锁,那么能够有限次数(严格来说是无限次数)地进入被该锁锁住的代码。

14.5、自旋锁

因为 Java 中的线程是与操作系统中的线程 一一对应的,所以当一个线程在获取锁(比方独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又须要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比拟大的,在肯定水平上会影响并发性能。

自旋锁则是,以后线程在获取锁时,如果发现锁曾经被其余线程占有,它不马上阻塞本人,在不放弃 CPU 使用权的状况下,屡次尝试获取(默认次数是 10,能够应用 -XX:PreBlockSpinsh 参数设置该值),很有可能在前面几次尝试中其余线程己经开释了锁,如果尝试指定的次数后仍没有获取到锁则以后线程才会被阻塞挂起。由此看来自旋锁是应用 CPU 工夫换取线程阻塞与调度的开销,然而很有可能这些 CPU 工夫白白浪费了。

<big>参考:</big>

【1】:瞿陆续,薛宾田 编著《并发编程之美》

【2】:极客工夫《Java 并发编程实际》

【3】:方腾飞等编著《Java 并发编程的艺术》

退出移动版