Java进阶——关于多线程需要了解的一些常识和操作整理(一)

引言

多线程在现代化的今天,相信很多人都不会陌生吧,利用多线程获取更多的CPU资源,如果总有些子任务是可以并发的,多个子任务并发执行了很可能避免CPU需要IO操作的完成,而且能够提高系统的吞吐量等等,无论是Web或者是移动开发,多线程都直接影响着程序执行效率和用户体验,于是乎打算整理下关于多线程的一些知识,本文内容可能来自某些书本(部分内容整理摘自《操作系统》和《Java核心技术》),甚至是自己以前做的笔记,结合自己的一些理解,尽量使用自己的语言总结下。

一、多线程概述

现代操作系统在运行一个程序时,通常会为其创建一个进程,无论是Linux、Windows、Android都可以把一个具体的程序看为一个独立的进程(Process),比如说打开微信、支付宝等程序的时候,操作系统就会为先其创建一个进程,一个进程里包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分,一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。)。而学过操作系统和计算机原理的都知道,操作系统中调度的最小单元是线程(Thread又叫轻量级进程),而在一个进程里可以创建多个线程,进程中这些线程都拥有各自的计数器,堆栈和局部变量等成员属性,但共享着进程的内存并且能够访问共享的内存多线程就是实现多任务的一种方式,但多线程使用了更小的资源开销。每一个进程执行都是有一个执行顺序(该顺序被称为一个执行路径或者控制单元),而线程就是进程中的一个独立的控制单元,总而言之,在CPU中真正把时间片分配到的是线程,真正执行是线程,在单核CPU某一时刻永远都是在执行一个程序(即永远只有一个控制单元在执行),在单核CPU中实际上总是在极短的时间内不停地切换执行路径即线程,所以单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。因此我们可以这样理解:

  • 进程——正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。每个进程都有独立的代码和内存空间,进程间的切换会有较大的开销,一个进程包含1–n个线程,进程是资源分配的最小单位。

  • 线程——是进程中的单个顺序控制单元,是一条执行路径,一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。同一进程内的线程共享进程的代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,线程是cpu调度的最小单位

二、线程的状态生命周期

如下图所示线程主要经历五个基本状态:新建状态就绪状态运行状态阻塞状态死亡状态
这里写图片描述

  • 新建状态——使用 new 关键字和 Thread 类或其子类创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态——线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,处于就绪队列中,等待JVM里线程调度器的调度获取CPU的使用权

  • 运行状态(Running)——如果就绪状态的线程获取了CPU,执行程序代码,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态(Blocked)——阻塞状态是线程因为某种原因(比如说如果一个线程执行了sleep()方法、suspend(挂起)、wait()、等待进入synchronized方法或synchronized 代码块 等)放弃CPU使用权,暂时停止运行,该线程就从运行状态进入阻塞状态;而在睡眠时间已到或获得设备资源后可以重新进入就绪状态。直到线程进入就绪状态,才有机会转到运行状态,阻塞的情况分三种:

    • 等待阻塞(Waiting):运行状态中的线程执行 wait() 方法,JVM会把该线程放入等待池中,使其进入到等待阻塞状态

    • 同步阻塞(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中,使其进入同步阻塞状态。

    • 其他(超时)阻塞通过调用线程的 sleep() 或 join() 发出了 I/O 请求或者通过Object的wait()方法,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时或者 I/O 处理完毕,线程重新转入就绪状态,而因为wait()方法而导致的阻塞,则需要调用Object的notify()或notifyAll()方法,才会重新进入就绪态。

  • 死亡状态(Dead)——线程执行完了run方法或者因异常退出了run()方法,该线程结束生命周期,由系统负责回收线程对象

三、线程的优先级、中断、守护线程

通常操作系统会维护一个就绪的线程队列(ready queue)且在某一时刻cpu只为ready queue中位于队列头部的线程服务。

1、线程的优先级

现代操作系统中基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是在MIN_PRIORITY (1)和MAX_PRIORITY =(10)之间的一个整数,而每一个线程都会分配一个默认的优先级 NORM_PRIORITY (5)。一般情况下,每当线程调度器有机会选择新线程时,虽然它首先会选择较高优先级的线程,且应该在低优先级的线程之前分配CPU资源。但是,线程优先级并不能总是确保线程执行的顺序(因为线程的优先级高度依赖于系统平台)。所以尽量不要把程序功能依赖于优先级,若确实要用setPriority(int priorty)方法设置优先级,应该注意:如果有几个高优先级的线程没有进入非活动状态,低优先级线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程可能永远不会被执行到。所以一般我们在设置优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者运算)的线程则设置较低的优先级,这样可以确保处理器不会被长久独占。

2、线程的中断

当线程的run()方法执行方法体中的最后一条语句后,并经由执行return语句返回时或者在方法体执行过程中出现没有捕获的异常时线程将终止,以上两种情况称为线程的中断。Java为提供了一种调用interrupt()方法来请求终止线程的方法。在每一个线程都有一个boolean类型标志,用来表明当前线程是否请求中断,当一个线程调用interrupt() 方法时,线程的中断标志将被设置为true。我们可以通过调用Thread.currentThread().isInterrupted()或者Thread.interrupted()方法来判断线程的是否请求中断这里需要注意的是调用线程的interrupt() 方法不会中断一个正在运行的线程,它只是设置了一个线程中断标志位,如果在程序中你不检测线程中断标志位,那么即使设置了中断标志位为true,线程也一样照常运行
那么如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程检查中断标志时如果发现中断标示为true,就会在这些阻塞方法(sleep、join、wait及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标志重新设置为false),而抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。但是如果每次迭代之后都调用sleep方法(或者其他可中断的方法),isInterrupted检测就没必要也没用处了,因为假如在中断状态被置位时调用sleep方法,它不会休眠反而会清除这一休眠状态并抛出InterruptedException。所以如果在循环中调用sleep,不要去检测中断状态,只需捕获InterruptedException,如以下范例代码:

public void run(){  
        while(more work to do ){  
            try {  
                Thread.sleep(5000);  
            } catch (InterruptedException e) {  
                //thread was interrupted during sleep  
                e.printStackTrace();  
            }finally{  
                //clean up , if required  
            }  
        }

但同时还有点要注意的就是我们在捕捉中断异常时不要留空白什么都不处理,所以下是反面典型


void mySubTask(){  
    ...  
   try{  
       sleep(delay)  
      }catch(InterruptedException e){  
   ...  
   }  
}

规范应该如此处理

/**代码范例*/
void mySubTask()throw InterruptedException{  
    sleep(delay)  
}
//或者
void mySubTask(){  
    ...  
    try{  
    sleep(delay)  
    }catch(InterruptedException e){  
     Thread.currentThread().interrupt();  
    }  
}     

四、Thread重要的相关方法

Thread方法名 说明
final void setPriority(int priority) 设置线程的优先级
void interrupt() 向线程发送中断请求并把线程的中断状态设置为true,如果当前线程被一个sleep调用阻塞,那么将会抛出interrupedException异常,一般不会主动调用
static boolean interrupted() 检测当前线程(当前正在执行命令的这个线程)是否被中断,是它会将当前线程的中断状态重置为false
boolean isInterrupted() 判断线程是否被中断,这个方法的调用不会改变线程的当前中断状态
static void yield() 暂停当前正在执行的线程对象,暂停当前正在执行的线程对象,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()方法将不会起作用。暂停当前正在执行的线程对象,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,yield()只能使同优先级或更高优先级的线程有执行的机会
public final boolean isAlive() 测试线程是否还在运行
final synchronized void join() 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程,比如说在主线程开启了线程A和线程B,此时他们会交替执行,但是假如在某一时刻在线程A中调用了线程B的join方法,此时线程A相当于是被阻塞住了,等到B执行完毕之后才开始执行
public final void setDaemon(boolean on) 标记为守护线程
/** 
   *  Waits at most <code>millis</code> milliseconds for this thread to   
   * die. A timeout of <code>0</code> means to wait forever.   此字面意思是永远等待,其实是等到t结束后。 
   */   
  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;  
          }  
      }  
  } 

join方法的简单验证演示

public class TestThread {
    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
    }

    static class ThreadA implements Runnable{
        public static int k=0;
        @Override
        public void run() {
            Thread b=new Thread(new ThreadB());
            b.start();
            while(k<10){
                System.out.println(new SimpleDateFormat("MM:ss:ms").format(new Date())+"执行A"+k);
                if(k==3){
                    try {
                        b.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                k++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    static class ThreadB implements Runnable{
        int n=0;
        @Override
        public void run() {
            while(n<10){
                System.out.println(new SimpleDateFormat("MM:ss:ms").format(new Date())+"执行B"+n);
                n++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

五、Java线程中sleep()、wait()与notify()和notifyAll()、await()和signal()、yield()等方法小结

线程方法名称 是否释放同步锁 是否需要在同步的代码块中调用 方法是否已废弃 是否可以被中断
sleep()
wait()
suspend
resume()
join()

1、sleep

在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

2、Object的wait() 与 notify()和notifyAll()

sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器,在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去了对象的机锁,可以允许其它的线程执行一些同步操作。但是wait()可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。 wait可以让同步方法或者同步块暂时放弃对象锁,释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁,而将它暂时让给其它需要对象锁的,当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛IllegalMonitorStateException异常。唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。wait()和notify()必须在synchronized函数或synchronized block中进行调用(JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁)。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。不仅wait方法会放弃锁,notify()和notifyAll()也会放弃锁,wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器,另外suspend()、resume()方法已经不推荐使用了。

3、Condition的await() 与 signal()和signalAll()

在Java中任何一个对象都继承自Object,在JDK5.0前线程间的通信往往是借助Object的wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制(与对象监视器配合完成线程间的等待/通知机制)也许是基于可控制性和扩展性考虑,在JDK 5.0时引进的Lock体系配合Condition的await() 与 signal()和signalAll()完成等待通知/机制。简单来说,await系方法就是加入AQS( AbstractQueuedSynchronized)的内部conditionObject实现类的等待队列里,signal就是唤醒该队列的第一个线程节点,signalAll是唤醒队列里所有的线程节点。

4、yield()

暂停当前正在执行的线程对象,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会

六、线程安全

何为线程安全?当你的代码无论是在多线程下执行和在单线程下执行永远都能获得一样的结果,即你的代码线程安全。而在Java中针对不同级别的对象,线程安全也有所不同

1、final类型的不可变的对象

比如String、Integer、Long等,一经初始化,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下确保是线程安全的

2、绝对线程安全

不管运行时环境如何,调用者都不需要额外的确保线程安全的措施(因为JDK本身已经做了大量的措施来确保线程安全)比如说CopyOnWriteArrayList、CopyOnWriteArraySet等就是绝对线程安全的,但是也有些在JDK中标注自己是线程安全的类,实际上绝大多数都不是线程安全的

3、相对线程安全

相对线程安全即我们通常意义上所说的线程安全,比如说Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、另一个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

4、线程非安全

ArrayList、LinkedList、HashMap等都是线程非安全的类,需要开发者自己采取必要的措施来保证线程安全。

七、线程同步

由于篇幅问题见下一篇。

猜你喜欢

转载自blog.csdn.net/crazymo_/article/details/79493840