thread

java多线程

线程的基础

线程进程区别

  1. 进程是操作系统的分配和调度系统内存资源、cpu时间片等资源的基本单位,为正在运行的应用程序提供运行环境。
  2. 线程程序内部有并发性的顺序代码流,是cpu调度资源的最小单元

Java线程模型

thread_第1张图片
20160506143812820.jpg

Linux,windows 操作系统下都是使用内核线程 - Kernel Thread

内核线程

内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。

  1. 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
  2. 用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
    线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作

轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。

线程的状态


教科书上线程的状态

[图片上传失败...(image-7717e8-1547719360944)]

  1. Ready 代表当前的调度实例在可执行队列中,随时可以被切换到占用处理器的运行状态。
  2. Running代表当前的调度实例正在占用处理器运行中。
  3. Blocked(waiting)代表当前的调度实例在等待相应的资源。

linux线程的状态:

  • 就绪:线程分配了CPU以外的全部资源,等待获得CPU调度
  • 执行:线程获得CPU,正在执行
  • 阻塞:线程由于发生I/O或者其他的操作导致无法继续执行,就放弃处理机,转入线程就绪队列
  • 挂起:由于终端请求,操作系统的要求等原因,导致挂起。

java 线程的状态

 public enum State {
        /**
         * 线程被new出来
         */
        NEW,
        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         * 线程的runnable状态是指线程正在被虚拟机执行,但是它可能正在等待操作系统的资源比如处理器
         */
        RUNNABLE,
        BLOCKED,
        /**
         * 线程处于WAITING状态是在调用Object.wait,Thread.join,LockSupport#park()后
         */
        WAITING,
        /**
          * Thread.sleep,Object.wait带时间参数,Thread.join 带时间参数,
      * LockSupport.parkNanos,LockSupport.parkUntil时处于TIMED_WAITING
         */
        TIMED_WAITING,
        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

How does the thread state of Java map to linux or windows ? If the state of Java is runnable, what is on Linux or windows ?

A thread can be in only one state at a given point in time.
These states are virtual machine states which do not reflect
any operating system thread states.

java线程的状态和操作系统线程没有映射关系(来自java doc)

NEW

线程的创建方式

  1. 线程的创建与运行
    • java中有三种线程创建方式,实现Runnable接口的run方法,继承Thread类并重写run方法,使用FutureTask方式,JAVA 8可以使用CompletableFuture

    • Thread

      • 好处:获取当前线程方便,直接this
      • 坏处:不能继承其他类了,任务与代码没区分,无返回值
    • Runnable接口

      • 好处:可继承其他类,多任务
      • 坏处:无返回值
    • callable接口

      • 好处:有返回值
    • CompletableFuture的优势
      提供了异步程序执行的另一种方式:回调,不需要像future.get()通过阻塞线程来获取异步结果或者通过isDone来检测异步线程是否完成来执行后续程序。
      能够管理多个异步流程,并根据需要选择已经结束的异步流程返回结果。

RUNNABLE

        /**
         * 线程在等待监视器锁的状态,线程进入同步代码块或同步方法,或者调用wait()方              法后再次进
         * 入同步代码块或同步方法
         */

为什么只有runnable状态 没有区分running 和 ready状态

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。
这个时间分片通常是很小的,一个线程一次最多只能在 CPU上运行比如10ms-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)
由于线程切换的如此的快,因此把这两个统一为runnable状态
传统概念中的阻塞状态也可以映射到runnable状态

线程的阻塞

隐式锁(Synchronized)

相对JDK提供的concurrent包中的实现Lock接口的锁工具类,是jvm所实现的锁

隐式锁的使用

    //对普通方法同步
    public synchronized void sayGoodbye() {
        System.out.println("say good bye");
    }
    //对静态方法同步
    public synchronized static void sayHi() {
        System.out.println("say hi");
    }
    //对方法块同步
    public void sayHello() {
        synchronized (LockTest.class) {
            System.out.println("say hello");
        }
    }

synchronized的实现简单说明

  1. 从字节码角度分析
  • 方法块同步
 0 ldc #6 
  2 dup
  3 astore_1
  4 monitorenter
  5 getstatic #2 
  8 ldc #7 
 10 invokevirtual #4 
 13 aload_1
 14 monitorexit
 15 goto 23 (+8)
 18 astore_2
 19 aload_1
 20 monitorexit
 21 aload_2
 22 athrow
 23 return

synchronized代码块是由monitorenter和monitorexit两个指令实现的。关于这两个字节码虚拟机规范是这么说的

monitorenter:任何对象都有一个 monitor(这里 monitor 指的就是锁) 与之关联(规范上说,对象与其 monitor 之间的关系有很多实现,如 monitor 可以和对象一起创建销毁,也可以线程尝试获取对象的所有权时自动生成)。当且仅当一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取 objectref 所对应的 monitor 的所有权,那么:如果 objectref 的 monitor 的进入计数器为 0,那线程可以成功进入 monitor,以及将计数器值设置为 1。当前线程就是 monitor 的所有者。如果当前线程已经拥有 objectref 的 monitor 的所有权,那它可以重入这个 monitor,重入时需将进入计数器的值加 1。如果其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到 monitor 的进入计数器值变为 0 时,重新尝试获取 monitor 的所有权。

monitorexit:objectref必须为reference类型数据。执行monitorexit指令的线程必须是objectref对应的monitor的所有者。指令执行时,线程把monitor的进入计数器值减1,如果减1后计数器值为0,那线程退出monitor,不再是这个monitor的拥有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

  • 方法同步
0 getstatic #2 
3 ldc #5 
5 invokevirtual #4 
8 return

对静态方法同步和方法块同步并没有 monitor 相关指令,而是多了 invokevirtual 指令。 invokevirtual 指令是用 来调用实例方法,依据实例的类型进行分派
Java 虚拟机规范上描述该指令:如果调用的是同步方法,那么与 objectref 相关的同步锁将会进入或者重入,就如同当前线程中执行了 monitorenter 指令一般。

虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

  1. synchronized锁的位置

    • 锁存放在对象头中

    对象实例由对象头、实例数据组成,其中对象头包括markword和类型指针,如果是数组,还包括数组长度。

    markword的结构,定义在markOop.hpp文件:

    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    //
    //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
    //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
    //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
    //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
    
thread_第2张图片
企业微信截图_15476924643731.png
  1. hotSpot 如何具体实现

    1. 源码中锁的入口

    在HotSpot的中有两处地方对monitorenter指令进行解析:一个是在bytecodeInterpreter.cpp#1816 ,另一个是在templateTable_x86_64.cpp#3667。

    JVM中的字节码解释器(bytecodeInterpreter),用C++实现了每条JVM指令(如monitorenterinvokevirtual等),其优点是实现相对简单且容易理解,缺点是执行慢。后者是模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。两者的原理是一致的,大家分析的时候可以基于字节码解释器的源码进行分析。

    1. 为什么要做优化

      monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高

    2. 有哪些优化

      Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

      锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

      锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。
      
      锁消除(Lock Elimination):JVM及时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这些锁。
      
      轻量级锁(Lightweight Locking):JDK1.6引入。在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。
      
      偏向锁(Biased Locking):JDK1.6引入。目的是消除数据再无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。
      
      适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗。产生了忙循环(循环时间固定),即自旋。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,用以期望能减少阻塞的时间。
      
    3. 加锁的流程

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。


thread_第3张图片
584866-20170419194339446-1408410540.png

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord.
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋.
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己


thread_第4张图片
584866-20170419191951321-2145960409.png

WAITING

        /**
         * Thread.sleep,Object.wait带时间参数,Thread.join 带时间参数,
         * LockSupport.parkNanos,LockSupport.parkUntil时处于TIMED_WAITING
         */
  • wait notify的实现
  • wait notify 必须在同步代码中使用
ObjectMonitor() {
    _header       = NULL;//markOop对象头
    _count        = 0;
    _waiters      = 0,//等待线程数
    _recursions   = 0;//重入次数
    _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _owner        = NULL;//指向获得ObjectMonitor对象的线程或基础锁
    _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set ObjectWaiter 类型;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//处于锁block状态的线程,会被加入到entry set;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
    _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  }
class ObjectWaiter : public StackObj {  
 public:  
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;  
  enum Sorted  { PREPEND, APPEND, SORTED } ;  
  ObjectWaiter * volatile _next;  
  ObjectWaiter * volatile _prev;  
  Thread*       _thread;  // ObjectWaiter 对应的线程的
  ParkEvent *   _event;   // 线程的ParkEvent
  volatile int  _notified ;  
  volatile TStates TState ;  
  Sorted        _Sorted ;           // List placement disposition  
  bool          _active ;           // Contention monitoring is enabled  

};  

在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor
ObjectMonitor的获取方法
ObjectMonitor * m = omAlloc (Self) ;//获取一个可用的ObjectMonitor

_WaitSet:
主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列
_EntryList:
所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。

  • wait的实现
    ObjectSynchronizer::wait方法
    通过object的对象中找到ObjectMonitor对象 调用方法
    void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS)
    通过ObjectMonitor::AddWaiter调用把新建立的ObjectWaiter对象放入到 _WaitSet 的队列的末尾中
    然后在ObjectMonitor::exit释放锁,接着 thread_ParkEvent->park 也就是wait

  • Notify方法的实现:
    找到ObjectMonitor对象调用ObjectMonitor::notify 摘除第一个ObjectWaiter对象从_WaitSet 的队列中
    并把这个ObjectWaiter对象放入_EntryList中,_EntryList 存放的是ObjectWaiter的对象列表,列表的大小就是那些所有在等待这个对象锁的线程数。**注意不管NotifyALL和Notify 并没有释放锁。锁的释放是在同步代码块结束的时候释放的这种可以从字节码看出

   synchronized (objectLock) {
                while (list.size() == 1) {
                    try {
                        objectLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("生产下");
                list.add(1);
                objectLock.notifyAll();// 通知所有在此对象上等待的线程
            }
81 invokevirtual #15 
84 aload_1
85 monitorexit
  • NotifyALL和Notify 的区别
    NotifyALL 会把所有的_WaitSet中的对象放入_EntryList,Notify是随机选取一个

  • join的实现

 public static void main(String[] args) {
        Thread t2 = new Thread();
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
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);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
  • 实现的原理以当前线程的为锁对象,jvm调用当前线程对象的notify方法释放锁通知

TIMED_WAITING

待续

最后的状态图

thread_第5张图片
企业微信截图_15477095399150.png

你可能感兴趣的:(thread)