thread

作者: c7d122ec46c0 | 来源:发表于2019-01-17 18:02 被阅读0次

    java多线程

    线程的基础

    线程进程区别

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

    Java线程模型

    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 <thread/ByteCodeDemo>
      2 dup
      3 astore_1
      4 monitorenter
      5 getstatic #2 <java/lang/System.out>
      8 ldc #7 <say hello>
     10 invokevirtual #4 <java/io/PrintStream.println>
     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 <java/lang/System.out>
    3 ldc #5 <say hi>
    5 invokevirtual #4 <java/io/PrintStream.println>
    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)
      
    企业微信截图_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 ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。


    584866-20170419194339446-1408410540.png

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


    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 <java/lang/Object.notifyAll>
    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

    待续

    最后的状态图

    企业微信截图_15477095399150.png

    相关文章

      网友评论

          本文标题:thread

          本文链接:https://www.haomeiwen.com/subject/cgaidqtx.html