Java-线程2

概述

在说线程的概念之前,先说下进程的概念,进程是代码在数据集合上的一次运行活动,它是系统进行资源分配和调度的基本单位。一个进程至少有一个线程,线程是进程中的实体,线程本身是不会独立存在的,进程中的多个线程可以共享进程的资源(例如:内存地址、文件I/O等),也可以独立调度。

有以下三种方式实现线程:

使用内核线程实现
使用用户线程实现
使用用户线程和轻量级线程混合实现

Java语言统一处理了不同硬件和操作系统平台的线程操作,一个线程是一个已经执行start()方法而且还没结束的java.lang.Thread类的实例,其中Thread类的所有关键方法都是本地方法(Native Method)来的,这意味着这些方法没有使用或者无法使用平台相关的手段来实现。


线程状态切换

Java语言定义了六种线程状态,要注意的是,在任意一个时间点,一个线程有且只有五种线程状态的其中一种,这五种线程状态如下所示:

  • 新建(New):线程创建后尚未启动的状态。

  • 运行(Runable):线程正在等待着CPU为它分配执行时间,进入就绪(Ready)状态,等到CPU分配执行时间后,线程才真正执行,进入正在运行(Running)状态。

  • 无限期等待(Waiting):这种状态下的线程不会被CPU分配执行时间,它们需要其他线程显示地唤醒。以下方法会让线程进入这种状态:

       没有设置参数Timeout的Object.wait()方法
       没有设置参数Timeout的Thread.join()方法
       LockSupport.park方法
    
  • 限期等待(Timed Waiting):这种状态下的线程不会被CPU分配执行时间,但是它无需其他线程显示地唤醒,会在一定时间内由系统自动唤醒。以下方法会让线程进入这种状态:

     Thread.sleep()方法
     有设置参数Timeout的Object.wait()方法
     有设置参数Timeout的Thread.join()方法
     LockSupport.parkNanos()方法
     LockSupport.parkUntil()方法
    
  • 阻塞(Block):线程被阻塞的状态,在等待着一个排他锁。

  • 结束(Terminated):线程已终止,并且已经结束执行的状态。


线程创建和运行

Java语言提供了三种创建线程的方式,如下所示:

继承Thead类并且重写run方法

class ZThreadTest {

    private static class ZThread extends Thread {

        @Override
        public void run() {
            super.run();
            System.out.println("SharkZ");
        }

    }

    public static void main(String[] args) {
        // 创建线程
        ZThread thread = new ZThread();
        // 启动线程
        thread.start();
    }

}

这种方式的优点是在run方法内可以使用this获取当前线程,无须使用Thread.currentThread方法;缺点是因为Java的类只能继承一个类,所以继承Thread类之后,就不能继承其他类了,而且因为任务和代码没有分离,如果多个线程执行相同的任务时,需要多份任务代码。

要注意的是,调用了start方法后,线程正在等待着CPU为它分配执行时间,进入就绪(Ready)状态,等到CPU分配执行时间后,线程才真正执行,进入正在运行(Running)状态。

实现Runnable接口的run方法

class ZRunnableTest {

    private static class ZRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("SharkZ");
        }

    }

    public static void main(String[] args) {
        // 创建TanJiaJunRunnable对象
        ZRunnable runnable = new ZRunnable();
        // 创建线程
        Thread thread = new Thread(runnable);
        // 启动线程
        thread.start();
    }

}

这种方式的优点是因为Java的类可以实现多个接口,所以这个类就可以继承自己需要的类了,而且任务和代码分离,如果多个线程执行相同的任务时,可以公用同一个任务的代码,如果需要对它们区分,可以添加参数进行区分。

使用FutureTask类

class ZFutureTaskTest {

    private static class ZCallable implements Callable<String> {

        @Override
        public String call() {
            return "SharkZ";
        }

    }

    public static void main(String[] args) {
        // 创建FutureTask对象
        FutureTask<String> futureTask = new FutureTask<>(new ZCallable());
        // 创建线程
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            // 等待任务执行完毕,并且得到返回值 
            // 这个等待是线程阻塞的 会阻塞UI线程
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}

Activity

public class MainActivity extends AppCompatActivity {
    
    private static final String TAG="SHARK_THREAD";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Log.i(TAG, "onCreate: 开始执行"+System.currentTimeMillis());
        main();
        Log.i(TAG, "onCreate: 执行完毕"+System.currentTimeMillis());

    }
    
    private static class TanJiaJunCallable implements Callable<String> {

        @Override
        public String call() {
            try {
                Log.i(TAG, "call: 开始睡眠");
                Thread.sleep(1000);
                Log.i(TAG, "call: 结束睡眠");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Log.i(TAG, "call: 当前线程的ID ="+Thread.currentThread().getId());
            return "SharkZ";
        }

    }

    public static void main() {

        Log.i(TAG, "main: 主线程的ID ="+Thread.currentThread().getId());

        // 创建FutureTask对象
        FutureTask<String> futureTask = new FutureTask<>(new TanJiaJunCallable());
        // 创建线程
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            // 等待任务执行完毕,并且得到返回值
            // 阻塞UI线程了 
            String result = futureTask.get();
            //System.out.println(result);
            Log.i(TAG, "main: 结果输出 result="+result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}

结果

    I: onCreate: 开始执行1603787084730
    I: main: 主线程的ID =2
    I: call: 开始睡眠
    I: call: 结束睡眠
    I: call: 当前线程的ID =3598
    I: main: 结果输出 result=SharkZ
    I: onCreate: 执行完毕1603787085731

前面两种方式都没有返回值,FutureTask可以有返回值。


wait和notify

wait()方法、wait(long timeoutMillis)方法、wait(long timeoutMillis, int nanos)方法、notify()方法和notifyAll()方法都是Object类的方法。

wait系列方法

当一个线程调用共享变量的wait系列方法时,这个线程进入等待状态,直到使用下面两种方式才会被唤醒:

      其他线程调用该共享变量的notify系列方法(notify()方法或者notifyAll()方法)。
      其他线程调用该共享变量所在的线程的interrupt()方法后,该线程抛出InterruptedException异常返回。

要注意的是,需要获取到该共享变量的监视器锁才能调用wait方法,否则会抛出IllegalMonitorStateException异常,可以使用以下两种方式获得对象的监视器锁:

调用条件是必须在 synchronized 关键字修饰的情况下

wait()

源码如下所示:

// Object.java
public final void wait() throws InterruptedException {
    wait(0L);
}

这个方法实际上调用了wait(long timeoutMillis)方法,参数timeoutMillis的值是0L。它的行为和调用wait(0L, 0)方法是一致的。

wait(long timeoutMillis)

源码如下所示:

// Object.java
public final native void wait(long timeoutMillis) throws InterruptedException;

参数timeoutMillis是等待的最大时间,也就是超时时间,单位是毫秒。它的行为和调用wait(timeoutMillis, 0)方法是一致的。
要注意的是,如果传入了负数的timeoutMillis,就会抛出IllegalArgumentException异常。

wait(long timeoutMillis, int nanos)

源码如下所示:

// Object.java
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
    if (timeoutMillis < 0) {
        throw new IllegalArgumentException("timeoutMillis value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
        timeoutMillis++;
    }

    wait(timeoutMillis);
}

这个方法实际上调用了wait(long timeoutMillis)方法;参数timeoutMillis是等待的最大时间,也就是超时时间,单位是毫秒;参数nanos是额外的时间,单位是纳秒,范围是0~999999(包括999999)。

只有在参数nanos大于0的时候,参数timeoutMillis才会自增。

notify系列方法

在一个线程上调用共享变量的notify方法后,会唤醒这个共享变量上调用wait系列方法后进入等待状态的线程。要注意的是,一个共享变量可能有多个线程在等待,具体唤醒哪个等待的线程是随机的。

被唤醒的线程不能立即从wait系列方法返回后继续执行,它需要获取到该共享变量的监视器锁才能返回,也就是说,唤醒它的线程释放了该共享变量的监视器锁,被唤醒的线程不一定能获取到该共享变量的监视器锁,因为该线程还需要和其他线程去竞争这个监视器锁,只有竞争到这个监视器锁后才能继续执行。

只有当前线程获取到该共享变量的监视器锁后,才能调用该共享变量的notify系列方法,否则会抛出IllegalMonitorStateException异常。

notify()方法
// Object.java
@HotSpotIntrinsicCandidate
public final native void notify();
notifyAll()方法
// Object.java
@HotSpotIntrinsicCandidate
public final native void notifyAll();

notifyAll()方法可以唤醒所有在该共享变量上因为调用wait系列方法而进入等待状态的线程。


sleep()方法--让线程睡眠

sleep()方法是Thread类的一个静态方法。当一个正在执行的线程调用了这个方法后,调用线程会暂时让出指定睡眠时间的执行权,不参与CPU的调度,但是不会让出该线程所拥有的监视器锁。指定的睡眠时间到了后,sleep()方法会正常返回,线程处于就绪状态,然后参与CPU调度,获取到CPU资源后继续运行。

要注意的是,如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,就会在调用sleep方法的地方抛出InterruptedException异常而返回。

// Thread.java
public static native void sleep(long millis) throws InterruptedException;

join系列方法--等待线程执行终止

join系列方法是Thread类的一个普通方法。它可以处理一些需要等待某几个任务完成后才能继续往下执行的场景。

// Thread.java
public final void join() throws InterruptedException {
    join(0);
}

这个方法实际上调用了join(final long millis)方法,参数millis的值是0。

join(final long millis)方法
// Thread.java
public final synchronized void join(final long millis)
throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}

参数millis是等待时间,单位是毫秒。

要注意的是,如果传入了负数的millis,就会抛出IllegalArgumentException异常。

join(long millis, int nanos)方法

// Thread.java
public final synchronized void join(long millis, int nanos)
throws InterruptedException {

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0 && millis < Long.MAX_VALUE) {
        millis++;
    }

    join(millis);
}

这个方法实际上调用了join(final long millis)方法;参数millis是等待时间,单位是毫秒;参数nanos是额外的时间,单位是纳秒,范围是0~999999(包括999999)。

只有在参数nanos大于0的时候,参数millis才会自增。


yield()--让出CPU执行权

yield()方法是Thread类的一个静态方法。当一个线程调用这个方法后,当前线程告诉线程调度器让出CPU执行权,但是线程调度器可以无条件忽略这个请求,如果成功让出后,线程处于就绪状态,它会从线程就绪队列中获取一个线程优先级最高的线程,当然也有可能调度到刚刚让出CPU执行权的那个线程来获取CPU执行权。源码如下所示:

// Thread.java
public static native void yield();

它和sleep()方法的区别是:当线程调用sleep()方法时,它会被阻塞指定的时间,在这个期间线程调度器不会去调度其他线程,而当线程调用yield()方法时,线程只是让出自己剩余的CPU时间片,线程还是处于就绪状态,并没有被阻塞,线程调度器在下一次调度时可能还会调度到这个线程执行。


线程中断

在Java中,线程中断是一种线程间的协作模式。

要注意的是,通过设置线程的中断标志并不能立刻终止线程的执行,而是通过被中断的线程的中断标志自行处理。

interrupt()方法

interrupt()方法可以中断线程,如果是在其他线程调用该线程的interrupt()方法,会通过checkAccess()方法检查权限,这有可能抛出SecurityException异常。假设有两个线程,分别是线程A和线程B,当线程A正在运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并且立即返回,前面也提到过,设置标志仅仅是设置标志而已,线程A实际上还在运行,还没被中断;如果线程A因为调用了wait系列方法、join()方法或者sleep()方法而被阻塞,这时候线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常。源码如下所示:


// Thread.java
public void interrupt() {
    if (this != Thread.currentThread()) {
        checkAccess();

        // 线程可能在IO操作中阻塞
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0(); // 设置中断标志
                b.interrupt(this);
                return;
            }
        }
    }

    // 设置中断标志
    interrupt0();
}
isInterrupted()方法

isInterrupted()方法可以用来检测当前线程是否被中断,如果是就返回true,否则返回false。源码如下所示:

// Thread.java
public boolean isInterrupted() {
    return isInterrupted(false);
}
interrupted()方法

interrupted()方法是Thread类的一个静态方法。它可以用来检测当前线程是否被中断,如果是就返回true,否则返回false。它和上面提到的isInterrupted()方法的不同的是:如果发现当前线程被中断,就会清除中断标志,并且这个方法是静态方法,可以直接调用Thread.interrupted()使用。源码如下所示:

// Thread.java
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

isInterrupted()方法和interrupted()方法都是调用了isInterrupted(boolean ClearInterrupted)方法,源码如下所示:

// Thread.java
@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean ClearInterrupted);

参数ClearInterrupted是用来判断是否需要重置中断标志。

从源码可得知,interrupted()方法是通过获取当前线程的中断标志,而不是获取调用interrupted()方法的实例对象的中断标志。


线程上下文切换

在多线程编程中,线程的个数一般大于CPU的个数,但是每一个CPU只能被一个线程使用,为了让用户感觉多个线程在同时执行,CPU的资源分配策略采用的是时间片轮换策略,时间片轮换策略是指给每个线程分配一个时间片,线程会在时间片内占用CPU执行任务。

线程上下文切换是指当前线程使用完时间片后,处于就绪状态,并且让出CPU给其他线程占用,同时保存当前线程的执行现场,用于再次执行时恢复执行现场。


线程死锁

线程死锁是指两个或者两个以上的线程在执行的过程中,因为互相竞争资源而导致互相等待的现象,如果没有外力的情况下,它们会一直互相等待,导致无法继续执行下去。

死锁的产生必须具备以下四个条件:

  • 互斥条件:指资源只能由一个线程占用,如果其他线程要请求使用该资源,就只能等待,直到占用资源的线程释放该资源。
  • 请求并持有条件:指一个线程已经占有至少一个资源,但是还想请求占用新的资源,而新的资源被其他线程占用,所以当前线程会被阻塞,但是阻塞的同时不释放自己获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完毕之前不能被其他线程占用,只有在自己使用完毕后才会释放该资源。
  • 环路等待条件:指发生在死锁时,必然存在一个线程——资源的环形链,举个例子:有一个线程集合{Thead0, Thread1, Thread2, ……, Threadn),其中Thread0等待Thread1占用的资源,Thread1等待Thread2占用的资源,Thread2等待Thread3占用的资源,……,Threadn等待Thread0占用的资源。

那如何避免死锁呢?只要打破其中一个条件就可以避免死锁,不过基于操作系统的特性,只有请求并持有条件和环路等待条件是可以破坏的。


用户线程和守护线程

Java中的线程分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。在Java虚拟机启动的时候会调用main方法,main方法所在的线程就是一个用户线程,同时Java虚拟机还会启动很多守护线程,例如:垃圾回收线程。

只需要调用Thread类的setDaemon(boolean on)方法,并且参数on设为true,就可以使该线程成为守护线程。

用户线程和守护线程的区别是当最后一个用户线程结束后,Java虚拟机进程才会正常结束,而守护线程是否结束不影响Java虚拟机进程的结束。

总结一下:如果我们希望在主线程结束后,子线程继续工作,等到子线程结束后才让Java虚拟机进程结束,我们可以把线程设为用户线程;如果我们希望在主线程结束后,Java虚拟机进程也立即结束,我们可以把线程设为守护线程。

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