多线程之一:多线程基础

整理自炼数成金

源码连接:

 

一、为什么需要并行:

– 业务要求

– 性能

– 并行计算还出于业务模型的需要

并不是为了提高系统性能,而是确实在业务上需要多个执行单元。

比如HTTP服务器,为每一个Socket连接新建一个处理线程

让不同线程承担不同的业务工作

简化任务调度

二、几个重要的概念 

    同步(synchronous)和异步(asynchronous) :

 多线程之一:多线程基础_第1张图片

    并发(Concurrency)和并行(Parallelism):

多线程之一:多线程基础_第2张图片

    临界区:

        – 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程 使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

    多线程之一:多线程基础_第3张图片

    阻塞(Blocking)和非阻塞(Non-Blocking):

        – 阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要 这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如 果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。

        – 非阻塞允许多个线程同时进入临界区

    死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock) :

        饥饿是指某一个或 者多个线程因为种 种原因无法获得所 需要的资源,导致 一直无法执行。

    并行的级别:

        – 阻塞

            一个方法被称为阻塞的,即这个方法在其演进过程中不能正常运行直到其他(占有锁的)线程释放。

        – 无障碍

            无障碍是一种最弱的非阻塞调度

            自由出入临界区

            无竞争时,有限步内完成操作

            有竞争时,回滚数据

        – 无锁

            是无障碍的

            保证有一个线程可以胜出

            while (!atomicVar.compareAndSet(localVar, localVar+1))

            { localVar = atomicVar.get(); }

        – 无等待

            无锁的

            要求所有的线程都必须在有限步内完成

            无饥饿的

 

    三、2个重要的定理:

        Amdahl定律(阿姆达尔定律):

            – 定义了串行系统并行化后的加速比的计算公式和理论上限

            – 加速比定义:加速比=优化前系统耗时/优化后系统耗时

            假设要开发一个新任务,这个任务分为5步完成,每一步所需消耗CPU100毫秒的时间。如下图        

         多线程之一:多线程基础_第4张图片

         第一条任务链是以单线程实现的,第二条任务链其中步骤二和步骤五是以2个线程实现的。

        加速比=优化前系统耗时/优化后系统耗时=500/400=1.25

         多线程之一:多线程基础_第5张图片

 

        增加CPU处理器的数量并不一定能起到有效的作用 提高系统内可并行化的模块比重,合理增加并行处 理器数量,才能以最小的投入,得到最大的加速比。

 

        Gustafson定律(古斯塔夫森):

            – 说明处理器个数,串行比例和加速比之间的关系

             多线程之一:多线程基础_第6张图片

        只要有足够的并行化,那么加速 比和CPU个数成正比。

四、线程   

    什么是线程:

        – 线程是进程内的执行单元

    线程的状态:

     多线程之一:多线程基础_第7张图片

    新建状态(New)

        用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。

    就绪状态(Runnable)

        当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。

    运行状态(Running)

        处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。

    阻塞状态(Blocked)

        阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。

    死亡状态(Dead)

        当线程退出run()方法时,就进入死亡状态,该线程结束生命周期

 

五、java中线程的基本操作

    新建线程

[java] view plain copy

  1. Thread t1=new Thread(); t1.start();  
  2.   
  3. Thread t1=new Thread(Runnable r); t1.start();  

 

    终止线程

 

            – Thread.stop() 不推荐使用。它会导致临界区的最终数据不一致。

 

       例如有2个线程需要同时读一个临界区的A对象(里面有a和b两个变量),然后根据读到的数据再决定后续操作。

       当线程1读取了A对象,并根据业务操作需要对a和b变量做出修改,但修改了a之后被调用了Thread.stop(),此时b变量还没来得及修改,a变量又不能回滚修改的操作,所以导致线程2 读取到A对象的数据不一致。

    中断线程

          调用Thread.interrupt()方法对某一个线程中断之后,目标线程不会立刻停止运作,仅仅是对目标线程做了一个中断标记,需要目标线程调用isInterrupted()或者interrupted()方法判断当前线程是否需要中断了,然后做中断处理,例如数据回滚、数据备份。

      或者当目标调用了sleep()方法之后,会抛出一个InterruptedException异常,可以在catch块中做中断处理。

         public void Thread.interrupt() // 中断线程

         public boolean Thread.isInterrupted() // 判断是否被中断

         public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态

     挂起(suspend)和继续执行(resume)线程

            – suspend()不会释放锁

            – 如果加锁发生在resume()之前 ,则死锁发生

     等待线程结束(join)和谦让(yeild)

            join的本质

                – while (isAlive()) { wait(0); }

 

守护线程

    在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程

    当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出

[java] view plain copy

  1. Thread t=new DaemonT();  
  2.   
  3. t.setDaemon(true);  
  4.   
  5. t.start();  

 

线程优先级

   高优先级的线程更容易再竞争中获胜

    

[java] view plain copy

  1. //线程类中定义好的优先级:  
  2.   
  3.     public final static int MIN_PRIORITY = 1;  
  4.   
  5.     public final static int NORM_PRIORITY = 5;  
  6.   
  7.     public final static int MAX_PRIORITY = 10;  
  8.   
  9. //设置优先级:  
  10.   
  11.     Thread high=new HightPriority();  
  12.   
  13.     LowPriority low=new LowPriority();  
  14.   
  15.     high.setPriority(Thread.MAX_PRIORITY);  
  16.   
  17.     low.setPriority(Thread.MIN_PRIORITY);  
  18.   
  19.     low.start();  
  20.   
  21.     high.start();  

 

基本的线程同步操作

Synchronized

    – 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的

    – 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

    – 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

Object.wait(),Obejct.notify()

    – wait():会释放锁

    – notify():随机唤醒wait队列中的其中一个,当前Object释放锁之后,被唤醒的Object重新获取锁。

JVM锁机制

   当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:

    Contention List:所有请求锁的线程将被首先放置到该竞争队列, 该队列是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。

    Entry ListOwner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中

    Wait Set如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

    OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck

    Owner:获得锁的线程称为Owner

    !Owner:释放锁的线程

 

    新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现EntryList为空则从ContentionList中移动线程到EntryList。

 

    那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

 

你可能感兴趣的:(Java)