JAVA多线程·基础知识

一、线程基本概念

1. 线程的五种状态

  • 新建状态(new): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  • 就绪状态(runnable): "可执行状态",线程对象对创建后,其他线程调用了该线程的start()方法来启动该线程。处于就绪状态的线程,随时可能被CPU调度执行。
  • 运行状态(running): 线程获取cpu权限进行执行。
  • 阻塞状态(blocked): 线程由于某种原因放弃cpu使用权,暂时进入等待状态。分为三种情况:
    • 等待阻塞 - 调用线程的wait方法,等待某项工作的完成。
    • 同步阻塞 - 获取锁失败进入同步阻塞状态。
    • 其他阻塞 - 调用线程的sleep、join或者发出I/O请求,线程进入阻塞状态。
  • 终止状态(dead): 线程执行完毕或者因异常退出运行,该线程结束生命周期。

2. 线程状态图:

basic01.jpg

3. 线程使用方式:

通过Thread使用多线程

class MyThread extends Thread {
  public void run(){
    //my task
  }
}

public class ThreadTest{
  public static void main(String[] args){
    MyThread thread1 = new Mythread();
    MyThread thread2 = new Mythread();
    thread1.start();
    thread2.start();
  }
}

通过Runnable使用线程

class MyTask implements Runnable{
  public void run(){
    //my task
  }
}

public class ThreadTest{
  public static void main(String[] args){
    MyTask task = new MyTask();
    Thread thread1 = new Thread(task);
    Thread thread2 = new Thread(task);
    thread1.start();
    thread2.start();
  }
}

Thread方法start()和run()的区别

start(): 启动一个新线程并且在新线程中调用run方法执行任务

     public synchronized void start() {
        //判断是否为新建状态(new)
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);
        started = false;
        try {
            //调用本地方法启动新线程
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }

run(): 直接调用Runnable对象的run()方法,在当前线程中执行任务

public void run() {
    if (target != null) {
        target.run();
    }
}

二、同步基础

1. 互斥锁

概念

在Java中,所有对象都能够被作为"监视器monitor"——指一个拥有一个独占锁,一个入口队列和一个等待队列的实体entity。

对于对象的非同步方法来说,在任意时刻都能被任意线程调用,不需要考虑加锁的问题。

对于对象的同步方法来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们。例如,一个同步方法是独占的。如果在线程调用某一对象的同步方法时,对象的独占锁被其他线程拥有,那么当前线程将处于阻塞状态,并添加到对象的入口队列中。

分类

同步分为类级别和对象级别,分别对应着类锁和对象锁。每个类只有一个类锁,每一个对象有且仅有一个对象锁。也可以说互斥锁依赖于对象/类存在。

类锁:

pulbic class Something {
    public static synchronized void cSyncA(){}
    public static synchronized void cSyncB(){}
}

对象锁:

pulbic class Something {
    public synchronized void isSyncA(){}
    public synchronized void isSyncB(){}
}

2. synchronized关键字

当我们调用某对象的synchronized方法时,就获取了该对象的互斥锁。例如,synchronized(obj)就获取了“obj这个对象”的互斥锁。

不同线程对互斥锁的访问是互斥的。也就是说,某时间点,对象的互斥锁只能被一个线程获取到。通过互斥锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的互斥锁”。假设,在某一时刻,线程A获取到“obj的互斥锁”并在执行一些操作;而此时,线程B也企图获取“obj的互斥锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的互斥锁”之后线程B才能获取到“obj的互斥锁”从而才可以运行。

3. 线程等待与唤醒

wait与notify是java同步机制中重要的组成部分。结合与synchronized关键字使用,可以建立很多优秀的同步模型。

wait()允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变.而且只有在一个notify()或notifyAll()发生变化的时候,线程才会被唤醒,并检查条件是否有变.

wait()

在调用wait的时候,线程自动释放其占有的对象锁,同时不会去申请对象锁。当线程被唤醒的时候,它才再次获得了去获得对象锁的权利。

notify()/notifyAll()

notify仅唤醒一个线程并允许它去获得锁,notifyAll是唤醒所有等待这个对象的线程并允许它们去获得对象锁,只要是在synchronied块中的代码,没有对象锁是寸步难行的。其实唤醒一个线程就是重新允许这个线程去获得对象锁并向下运行。

顺便说一下notifyall,虽然是对每个wait的对象都调用一次notify,但是这个还是有顺序的,每个对象都保存这一个等待对象链,调用的顺序就是这个链的顺序。其实启动等待对象链中各个线程的也是一个线程。

注意点

  1. Object.wait()的作用是让“当前线程”等待,即正在cpu上运行的线程。
  2. 必须要在synchronized(obj){......}的内部才能够去调用obj的wait、notify、notifyAll等同步方法,即只有在已经获取到对象的互斥锁的情况下,才能执行该对象的同步相关方法。否则会抛出 java.lang.IllegalMonitorStateException
  3. 在同步块中调用了notify()/notifyAll()方法,会在当前同步块执行完成后释放锁并且唤醒其他等待线程。

4.使用

public class ThreadTest{
  //需要独占的对象
  private static final Object lock = new Object();
  public static void main(String[] args){
    MyThread myThread = new MyThread();
    myThread.start();
    //获取myThread的互斥锁
    synchronized(lock){
      try{
        //放弃锁,进入等待状态,即当前线程进入阻塞状态
        lock.wait();
      }catch(InterruptedException e){
        //do nothing
      }
    }
  }
  private static class MyThread extends Thread{
    public void run(){
      //获取this的互斥锁,即myThread的互斥锁
      synchronized(lock){
        //do something
        //执行完毕,通知等待线程
        lock.notify();
      }
    }
  }
}

synchronized(lock){...};的意思是定义一个同步块,使用lock作为互斥锁。lock.wait()的意思是临时释放锁,并阻塞当前线程,好让其他使用同一把锁的线程有机会执行,在这里要用同一把锁的就是lock对象.这个线程在执行到一定地方后用notify()通知wait的线程,锁已经用完,待notify()所在的同步块运行完之后,wait所在的线程就可以继续执行。

三、线程常用操作

1. sleep()

Thread类静态方法,让当前线程进入休眠,从运行状态进入阻塞状态。可以指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,等待cpu的调度执行。

public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("millis < 0: " + millis);
        }
        if (nanos < 0) {
            throw new IllegalArgumentException("nanos < 0: " + nanos);
        }
        if (nanos > 999999) {
            throw new IllegalArgumentException("nanos > 999999: " + nanos);
        }

        //时间=0时不需要进入阻塞状态
        if (millis == 0 && nanos == 0) {
            //检查线程是否被请求中断,如果被请求中断,抛出InterruptedException
            if (Thread.interrupted()) {
              throw new InterruptedException();
            }
            return;
        }

        long start = System.nanoTime();
        long duration = (millis * NANOS_PER_MILLI) + nanos;

        //获取当前线程的锁
        Object lock = currentThread().lock;

        //在同步块中调用sleep本地方法进入阻塞状态
        synchronized (lock) {
            while (true) {
                sleep(lock, millis, nanos);

                long now = System.nanoTime();
                long elapsed = now - start;

                if (elapsed >= duration) {
                    break;
                }

                duration -= elapsed;
                start = now;
                millis = duration / NANOS_PER_MILLI;
                nanos = (int) (duration % NANOS_PER_MILLI);
            }
        }
    }

2. yield()

Thread类静态方法,使当前线程由运行状态进入就绪状态,从而让其它具有相同优先级的等待线程获取执行权,但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权。

3. join()

Thread实例方法,让调用线程等待目标线程结束之后才能继续运行。

public final void join(long millis) throws InterruptedException {
        //获取目标线程的锁
        synchronized(lock) {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        //wait方法进入休眠,若目标线程依然在运行,则继续休眠
        if (millis == 0) {       
            while (isAlive()) {
                lock.wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                lock.wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
        }
    }

四、线程中断

1. 中断原理

一个线程在未正常结束之前, 被强制终止是很危险的事情. 可能带来预料不到的严重后果。比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。 那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程关闭, 或者让它结束某种等待的状态,一个比较优雅而安全的做法是:

使用等待/通知机制或者给那个线程一个中断信号, 让线程自己决定该怎么办。

2. 中断实现

对于不同状态下的线程有着不同的中断实现方式:

线程处于运行状态中

对于运行状态的线程,一般需要在运行代码中设置中断检查,可以使用线程提供的interupt相关方法和变量,也可以是自己定义的检查方法和变量,当检查到状态被置为中断时程序停止运行或进行后续一些工作,当然可以出不做任何处理继续保持运行。

class MyThread1 extends Thread{
  public void run(){
    while(!isInterrupted()){
      //do task
    }
    //do something after interrupted
    System.out.println("thread is interrupted.");
  }
}

线程处于阻塞状态中

对于阻塞状态的线程,无法用代码检查来实现中断,为此java系统提供了InterruptedException。在调用wait()、sleep()等方法后进入阻塞状态后,被调用Thread.interrupt()会抛出InterruptedException,可捕获InterruptedException实现中断处理。

可中断的阻塞:阻塞库方法(例如Thread.sleep、Thread.join和Object.wait)

不可中断的阻塞:同步Socked I/O、同步I/O、Selector的异步I/O、获取内部锁

class MyThread2 extends Thread{
  public void run(){
     try{
       while(!isInterrupted()){
         //do task
          synchronized(this){
            wait();
          }
       }
     }catch(InterruptedException e){
       //do something after interrupted
       System.out.println("thread is interrupted.");
     }
  }
}

3. java中断相关方法

class Thread{
  public void interrupt(); //请求中断线程
  public boolean isInterrupted();  //返回线程是否被中断
  public static boolean interrupted(); //返回当前调用线程的中断状态,并清除中断状态
}

interrupt()

Thread实例方法,调用线程的interrupt()会将线程置为中断状态。

当线程在运行状态时,isInterrupted()将返回true。

当线程在阻塞状态时(可中断的阻塞状态),抛出InterruptedException异常,并清除中断状态,即isInterrupted()返回false。

isInterrupted()

Thread实例方法,返回当前实例线程是否处于中断状态。

interrupted()

Thread静态方法,返回调用线程的中断状态并清除中断状态,即在调用interrupted()后,isInterrupted()返回false。

五、常见问题

1. 为什么wait/notify/notifyAll要在同步块中调用

wait/notify是线程之间的通信,他们之间存在竞态,因此需要强制wait/notify在synchronized中调用。

假设要自定义一个blocking queue,如果不使用synchronized的话可以这样写(假设是可以运行的):

class BlockingQueue {
  Queue<String> buffer = new LinkedList<String>();
  
  public void give(String data){
    buffer.add(data);
    notify();
  }
  
  public String take(){
    while(buffer.isEmpty()){
      wait();
    }
    return buffer.remove();
  }
}

这段代码可能会导致如下问题:

  1. 一个消费者调用take,发现buffer.isEmpty
  2. 在消费者调用wait之前,由于cpu的调度,消费者线程被挂起,生产者调用give,然后notify
  3. 然后消费者调用wait
  4. 如果很不幸的话,生产者产生了一条消息后就不再生产消息了,那么消费者就会一直挂起,无法消费,造成死锁。

2. sleep方法和wait方法的异同

相同点:

  • 都可以使线程进入阻塞状态
  • 都可中断,抛出InterruptedException异常

不同点:

  • 所属类不同。sleep方法属于Thread类静态方法,wait属于Object类实例方法
  • 对锁处理不同。sleep方法不会释放已有的锁,wait方法会释放锁。

  • 调用范围不同。sleep方法可以在任何地方调用,wait方法需要在同步块中调用。

3. 为什么notify(), wait()等函数定义在Object中,而不是Thread中

Object中的wait(), notify()等函数,和synchronized一样,会对“对象的互斥锁”进行操作。

wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“互斥锁”,否则其它线程获取不到该“互斥锁”而无法运行!
OK,线程调用wait()之后,会释放它锁持有的“互斥锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的互斥锁”。

负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的互斥锁”(这里的互斥锁必须和等待线程的互斥锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的互斥锁”。必须等到唤醒线程释放了“对象的互斥锁”之后,等待线程才能获取到“对象的互斥锁”进而继续运行。

总之,notify(), wait()依赖于“互斥锁”,而“互斥锁”是对象所持有,并且每个对象有且仅有一个。这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

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

推荐阅读更多精彩内容

  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,727评论 12 45
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,955评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,452评论 1 15
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,345评论 3 87
  • 清晨,我看着绚丽的朝阳。 想象着,你是否会踏着云朵而来。 夜晚,我瞧着漆黑的天空。 想象着,你是否会执一盏灯而来。...
    六月栖栖阅读 266评论 4 4