thread

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,707评论 0 11
  • 目录 概念 线程:系统调度的最小单位,CPU资源分配的基本单位。进程:一段执行的程序,可以包含多个进程。 在And...
    单向时间轴阅读 538评论 0 1
  • 一. 前言   Thread类作为线程中最基础的类,本篇文章我们就来了解下该类的使用。 二、. Thread类 1...
    骑着乌龟去看海阅读 337评论 0 0
  • 1. 概念引入 Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 普通同步方法,锁是...
    Java旅行者阅读 826评论 0 4
  • 雨下着 我哭了 小雨懂得人情 好不容易相同的调调 一样斜着漂 一次次入侵 一次次失败 鱼伤了尾鳍 小小的战栗 雨絮...
    琴歌素简阅读 199评论 0 1