Java多线程(一)--线程的一些基本概念

这周赶项目,暂停了一下微博。结果今天看到简书app的图标竟然有负罪感!趁着周末,更新一波。。。

从本文开始,我们开始分析一个新的Java的知识点--多线程。要研究这个,首先我们要知道,什么是线程。

线程的定义

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,拥有自己的数据和代码。

线程是指进程中的一个任务,一个进程中可以运行多个线程。线程是属于某个进程,进程中的多个线程共享此进程的内存。

拿手机举个例子,我们拿着手机可以一边听歌一边看微信,此时,微信和听歌软件就构成了多进程。即同一时刻,有不同的进程在工作。而多线程就是指在同一个程序中,同时执行多个任务,通常,每个任务称为一个线程。线程跟进程的区别是,每个进程都有自己独立的数据空间,而同一类的线程共享数据。多进程是为了提高CPU的使用率,而多线程是为了提高应用程序的使用率。

线程的状态

线程的状态如下:

image

线程的状态分为五种:

新建状态:当使用某种方式创建一个线程对象后,该线程就是新建状态

就绪状态:当已创建的线程的start()方法被调用后,就进入就绪状态。这种状态也叫做"可执行状态"。在这种状态下,该线程随时等待被CPU调度执行(注意,这个时候不是立即执行,而是等待CPU的调度)。

可运行状态:在Java虚拟机中执行的线程处于此状态。即线程取得CPU的权限,开始执行。线程只能从就绪状态进入运行状态。(在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行)

阻塞状态:当线程因为某种关系,无法获得CPU的使用权限时,就处于这种状态。比如调用的wait()方法(等待)、有同步锁(阻塞)及调用了sleep()方法(计时等待)等。

死亡状态:以退出的线程处于此状态。退出可能是线程执行完毕,或者是发生了异常等。

当一个线程开始运行的时候,它并不是始终运行的。因为Java中多线程是抢占式调度,即多个线程抢占时间片来执行任务。当一个线程的时间片用完后,它就会被系统剥夺其运行权限,并与其他线程共同争夺下一个时间片的使用权。

线程的创建

创建线程的方式有:继承Thread类、实现Runnable接口及通过Callable和Future新建一个线程。

继承Thread类

创建的步骤为:

1、继承Thread类

2、重写run方法

3、实例化我们写的Thread类的子类,并调用start方法启动线程

具体代码如下:

//继承Thread类
public class MyThread extends Thread{

   //重写run方法(线程的执行部分)
   @Override
    public void run() {
       //自己的代码逻辑
       ........
    }

}

public class Main{

    public static void main(String [] args){
    
        //实例化一个MyThread类的子类
        MyThread myThread = new MyThread();
        //调用start方法启动线程
        myThread.start();
        
    }

}

实现Runnable接口

创建步骤为:

1、定义一个类,实现Runnable接口,重写run方法;

2、创建一个Runnable的实现类的实例,并以此为参数创建一个Thread类

3、调用Thread类的start方法,启动线程

还可以创建一个实现Runnable接口的匿名类,或者创建一个实现Runnable接口的Java Lambda表达式(JDK8之后)。

具体代码如下:

//定义一个类,实现Runnable接口
public class MyRunnable implements Runnable {

    //重写run方法
    @Override
    public void run() {
        //自己的代码逻辑
        .......
    }
}

public class Main{

    public static void main(String [] args){
    
        //实例化一个Runnable实现类的实例
        MyRunnable myRunnable = new MyRunnable();
        //以myRunnable为参数实例化一个Thread类
        Thread thread = new Thread(myRunnable);
        //调用start方法启动线程
        thread();
        
        /**
          *--------分割线------
          */
          //创建Runnable的匿名实现
          Runnable myRunnable =new Runnable(){
                public void run(){
                     //自己的代码逻辑
                      .......                 
                      }
              }
             
          //Runnable的Lambda实现
          Runnable runnable =() -> { //自己的代码逻辑};      
        
}

注意:

1、使用"myThread.run();"时,run()方法并非是由刚创建的新线程执行,而是被创建新线程的当前线程所执行了。想要让创建的新线程执行run()方法,必须调用新线程的start()方法。且start()方法不可以多次调用。

2、调用start()方法后,并不是让线程立刻执行,而是将线程变为可执行状态,等待CPU的调度。

问题来了,当用此方式创建线程后,线程执行的run()方法是Runnable接口中的还是Thread类中的?

我们看一下Thread类的定义及其run()方法:

public class Thread implements Runnable {

private Runnable target;

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
       ......
        this.target = target;
       ......
    }

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

}

可以看到,在run()方法中,会判断target是否为空,不为空则执行Runnable的run()方法,否则此方法不执行任何操作并返回。也是因为如此,Java提示Thread的子类要重写此方法。

实现Runnable接口比继承Thread类的优势:

1、避免了Java中的单继承限制

2、适合多个相同的程序代码的线程去处理同一个共享资源

3、代码可以被多个线程共享且代码和数据独立,增加了程序的健壮性

通过Callable和FutureTask

创建步骤为:

1、创建一个Callable接口的实现类,并实现call()方法

2、使用FutureTask类包装Callable实现类的对象,封装了Callable的call()方法的返回值

3、以FutureTask对象为Thread的参数创建线程,并启动线程

4、调用FutureTask对象的get()方法,获取线程执行结束后的返回值

代码如下:

//创建一个Callable接口的实现类,并实现call()方法
public class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //自己的代码逻辑
        return value;
    }
}

public class Main{

    public static void main(String [] args){
    
        //使用FutureTask类包装Callable实现类的对象
        MyThread myThread = new MyThread();

        FutureTask futureTask = new FutureTask<Integer>(myThread);

        //以FutureTask对象为Thread的参数创建线程
        Thread thread = new Thread(futureTask);

        thread.start();  
        
        //获取值
        try {
            //get()方法会阻塞,直到子线程执行结束才返回
            int x = (int) futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }      
    }

}

其中,Callable的类型参数为返回值的类型,Future保存异步计算的结果。

在计算过程中,可以使用isDone方法判断Future任务是否结束(包括正常结束或者中途退出),返回true表示完成,返回false表示未完成。可以用cancal方法取消计算。

一般推荐使用实现Runable或Callable接口的方式来创建多线程。因为这样既可以继承其他类,而且多线程可以共享一个target,即多线程可以共同处理同一份资源。

线程的优先级及守护线程

线程优先级

每个线程都有优先级,且默认情况下,线程继承其父类的优先级。我们可以使用setPriority方法为线程设置优先级。可以将线程的优先级设置在MIN_PRIORITY(1)和MAX_PRIORITY(10)之间。NORM_PRIORITY表示线程优先级为5,为默认优先级。数字越大,表示优先级越高。高优先级的线程被CPU调用的概率大于低优先级的线程。不过要注意的是线程优先级无法保证线程的执行顺序,它是依赖于平台的。比如在Linux下,线程优先级仅适用于Java6之后,在这之前线程优先级没有作用。

守护线程

在Java中,线程可以分为用户线程和守护线程,可以使用Thread类的setDaemon方法将一个线程设置为守护线程。守护线程在后台运行,当JVM中没有其他非守护线程时,守护线程会和JVM一起结束。守护线程的作用是为其他线程提供服务,比如我们熟悉的GC就是这样。要注意的是,不要用守护线程访问文件或数据库等资源。因为守护线程可能在任何时候发生中断,而这个时候,我们对资源文件的读写有可能还没有完成。

有时候主线程都结束了,守护线程还在执行,这是因为线程结束是需要时间的。

Thread类的部分方法

start()方法

start方法为将线程由新建状态转变为可运行状态,其源码如下:

//表示Java线程状态的工具,初始化为线程未启动
private volatile int threadStatus = 0;

//线程是否运行的标志
boolean started = false;

//此线程的线程组
private ThreadGroup group;

public synchronized void start() {

        //判断线程是否未启动或者已经运行,满足一项则抛出异常
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        //添加次线程到其线程组
        group.add(this);
        
        //将运行状态置为false
        started = false;
        try {
              //调用本地方法启动线程
            nativeCreate(this, stackSize, daemon);
            //将线程的运行状态置为true
            started = true;
        } finally {
            try {
                   //如果启动失败,从其线程组中移除此线程
                   //此线程组的状态将回滚,就像从未尝试启动线程一样。该线程再次被视为线程组的未启动成员,允许随后尝试启动该线程。
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                            }
        }
    }

可以看到,当一个线程是未启动或者已运行时,调用start方法将抛出异常。当线程启动失败后,会将其从线程组中移除,以让其有机会重新启动。

sleep()方法

使当前正在执行的线程休眠(暂时停止执行),该线程不会失去任何监视器的所有权。源码如下:

private static final int NANOS_PER_MILLI = 1000000;

public static void sleep(long millis) throws InterruptedException {
     
        Thread.sleep(millis, 0);
    }

//实际调用的方法    
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);
        }
        //零睡眠
        if (millis == 0 && nanos == 0) {
            //如果线程为中断状态,则抛出异常并返回
            if (Thread.interrupted()) {
              throw new InterruptedException();
            }
            return;
        }
        //返回运行的Java虚拟机的高分辨率时间源的当前值,以纳秒计。
        long start = System.nanoTime();
        //将传入的时间转为纳秒级
        long duration = (millis * NANOS_PER_MILLI) + nanos;
         //获取锁
        Object lock = currentThread().lock;

        //等待可能会提前返回,所以循环直到睡眠时间结束。
        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);
            }
        }
    }

由上面的源码可以看出,我们最终调用的是sleep(long millis, int nanos)方法。在sleep方法中,是通过循环不断判断当前时间跟起始时间的差值,直到这个值大于等于我们传入的休眠时间,则线程可以继续工作。在此期间,当前线程为阻塞状态。

yield()方法

线程执行此方法的作用是暂停当前正在执行的线程,使其他具有相同优先级的线程获得运行的机会。但是在实际中,我们不能保证其功能可以完全实现,因为yield是将线程从运行状态变为可运行状态,在这种情况下,当前线程可能会被CPU再次选中。此方法为本地方法,jdk中源码如下:

public static native void yield();

join()方法

此方法为让一个线程加入到另一个线程的后面,在前面的线程没有结束的时候,后面的线程不被执行。调用此方法会导致线程栈发生变化,当然,这些变化都是瞬时的。

//负责此线程的join / sleep / park操作的同步对象
private final Object lock = new Object();

public final void join() throws InterruptedException {
        //调用join(long millis)方法
        join(0);
    }
    
public final void join(long millis, int nanos)
    throws InterruptedException {
        synchronized(lock) {
        //判断传入的参数是否正确
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        //根据条件判断millis是否要加1
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
          //调用join(long millis)方法
        join(millis);
        }
    }
 
//真正被调用的方法    
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");
        }
          //以下就是根据isAlive(线程是否存活),调用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;
            }
        }
        }
    }      

wait()方法的作用:导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法或指定的等待时间已经过去。

由源码可知,我们可以自己设置等待时间。但是如果我们不设置等待时间或者设置的等待时间为0,则线程会永远等待。直到被其join的线程结束后,会调用this.notifyAll方法,使其结束等待。

未捕获异常处理器

UncaughtExceptionHandler:是在Java Thread类中定义的,当Thread由于未捕获的异常而突然终止时调用的处理程序接口。这个接口只有一个方法:

//当给定线程由于给定的未捕获异常而终止时调用的方法。Java虚拟机将忽略此方法抛出的任何异常
void uncaughtException(Thread t, Throwable e);

当一个线程由于未捕获的异常而即将终止时,Java虚拟机将使用getUncaughtExceptionHandler向线程查询其UncaughtExceptionHandler并将调用处理程序的uncaughtException方法,将线程和异常作为参数传递。如果某个线程没有显式设置其UncaughtExceptionHandler,则其ThreadGroup对象将充当其UncaughtExceptionHandler。如果ThreadGroup对象没有处理异常的特殊要求,它可以将调用转发到getDefaultUncaughtExceptionHandler默认的未捕获异常处理程序。我们可以用setUncaughtExceptionHandler方法为任何线程设置一个处理器。也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程设置一个默认的处理器。我们可以通过实现Thread.UncaughtExceptionHandler接口并重写其uncaughtException方法来自定义一个未捕获异常处理器。

小结

本文主要是简单的介绍一下线程的相关概念,使大家对线程有基本的了解。本文中涉及到的锁及介绍的相关方法,会在后期的分析中一一讲解。

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

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,084评论 0 23
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,796评论 3 53
  • 单任务 单任务的特点是排队执行,也就是同步,就像再cmd输入一条命令后,必须等待这条命令执行完才可以执行下一条命令...
    Steven1997阅读 1,162评论 0 6
  • 前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要...
    嘟爷MD阅读 7,302评论 21 272
  • ➡️小感动 1.慎思园的阿姨每次看见我都超开心,我觉得像家人需要我不时回去看看~今天还非要送给我小橘子,觉得好温暖...
    wutacooper阅读 180评论 0 0