Java多线程(1)-- 基本概念

一、使用线程

有三种使用线程的方法:

* 实现 Runnable 接口;

* 实现 Callable 接口;

* 继承 Thread 类。

实现 Runnable 和Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread来调用。可以说任务是通过线程驱动从而执行的。


1、实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用start() 方法来启动线程。

public class MyRunnable implements Runnable {

    public void run() {

        // ...

    }

}

public static void main(String[] args) {

    MyRunnable instance = new MyRunnable();

    Thread thread = new Thread(instance);

    thread.start();

}


2、实现 Callable 接口

与 Runnable 相比,Callable可以有返回值,返回值通过FutureTask 进行封装。

public class MyCallable implements Callable {

    public Integer call() {

        return 123;

    }

}

public static void main(String[] args) throws ExecutionException, InterruptedException {

    MyCallable mc = new MyCallable();

    FutureTask ft = newFutureTask<>(mc);

    Thread thread = new Thread(ft);

    thread.start();

    System.out.println(ft.get());

}


Callable与Runnable

     先说一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法:

public interface Runnable {

      public abstract void run();

}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。


Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

public interface Callable {

    /**

     * Computes a result, orthrows an exception if unable to do so.

     *

     * @return computedresult

     * @throws Exception ifunable to compute a result

     */

    V call() throwsException;

}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

Future submit(Callable task);

Future submit(Runnable task, T result);

Future submit(Runnable task);


Future

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

Future类位于java.util.concurrent包下,它是一个接口,在Future接口中声明了5个方法,下面依次解释每个方法的作用:

cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。

isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。

isDone方法表示任务是否已经完成,若任务完成,则返回true;

get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

也就是说Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。


FutureTask

先来看一下FutureTask的实现:

public class FutureTask implementsRunnableFuture

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture extends Runnable,Future {

    void run();

}

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。

事实上,FutureTask是Future接口的一个唯一实现类。


使用示例:

1.使用Callable+Future获取执行结果

public class Test {

    public static voidmain(String[] args) {

        ExecutorServiceexecutor = Executors.newCachedThreadPool();

        Task task = newTask();

       Future result = executor.submit(task);

        executor.shutdown();

    }


    try {

       System.out.println("task运行结果"+result.get());

    } catch(InterruptedException e) {

        e.printStackTrace();

    } catch(ExecutionException e) {

        e.printStackTrace();

    }

}


class Task implements Callable{

    @Override

    public Integer call()throws Exception {

       System.out.println("子线程在进行计算");

        Thread.sleep(3000);

        int sum = 0;

        for(inti=0;i<100;i++)

            sum += i;

        return sum;

    }

}   


2.使用Callable+FutureTask获取执行结果

public class Test {

    public static voidmain(String[] args) {

        //第一种方式

        ExecutorServiceexecutor = Executors.newCachedThreadPool();

        Task task = newTask();

       FutureTask futureTask = newFutureTask(task);

       executor.submit(futureTask);

        executor.shutdown();

    }

     try {

      System.out.println("task运行结果"+futureTask.get());

     } catch(InterruptedException e) {

       e.printStackTrace();

     } catch(ExecutionException e) {

       e.printStackTrace();

     }

}

class Task implements Callable{

    @Override

    public Integer call()throws Exception {

       System.out.println("子线程在进行计算");

        Thread.sleep(3000);

        int sum = 0;

        for(inti=0;i<100;i++)

            sum += i;

        return sum;

    }

}

3、继承 Thread

同样也是需要实现 run() 方法,因为 Thread类也实现了Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public class MyThread extends Thread {

    public void run() {

        // ...

    }

}

public static void main(String[] args) {

    MyThread mt = new MyThread();

    mt.start();

}


实现接口 VS 继承Thread

实现接口会更好一些,因为:

Java 不支持多重继承,因此继承了 Thread类就无法继承其它类,但是可以实现多个接口;

类可能只要求可执行就行,继承整个 Thread 类开销过大。


Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

主要有三种 Executor:

CachedThreadPool:一个任务创建一个线程;

FixedThreadPool:所有任务只能使用固定大小的线程;

SingleThreadExecutor:相当于大小为 1 的FixedThreadPool。


Executors.newCachedThreadPool();        //创建一个线程池,容量大小为        

                                                                    Integer.MAX_VALUE

Executors.newFixedThreadPool(int);    //创建固定容量大小的线程池

Executors.newSingleThreadExecutor();   //创建容量为1的线程池


public static void main(String[] args) {

    ExecutorService executorService =Executors.newCachedThreadPool();

    for (int i = 0; i < 5; i++) {

        executorService.execute(newMyRunnable());

    }

    executorService.shutdown();

}


守护线程(Daemon):

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。

在Java中有两类线程:User Thread(用户线程)、Daemon

Thread(守护线程)

用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆;

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)

守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。setDeamon(true)的唯一意义就是告诉JVM不需要等待它退出,让JVM喜欢什么退出就退出吧,不用管它。

Thread daemonTread= new Thread(); 

  //设定 daemonThread 为 守护线程,default false(非守护线程) 

 daemonThread.setDaemon(true); 

 //验证当前线程是否为守护线程,返回 true 则为守护线程 

 daemonThread.isDaemon(); 


这里有几点需要注意:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。

(2) 在Daemon线程中产生的新线程也是Daemon的。

(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。

因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了。


二、线程状态

      Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态)。

  New:新建状态,当线程创建完成时为新建状态,即new Thread(...),还没有调用start方法时,线程处于新建状态。

  Runnable:就绪状态,当调用线程的的start方法后,线程进入就绪状态,等待CPU资源。处于就绪状态的线程由Java运行时系统的线程调度程序(thread scheduler)来调度。

  Running:运行状态,就绪状态的线程获取到CPU执行权以后进入运行状态,开始执行run方法。

       Blocked:阻塞表示线程在等待Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。

       WAITING(等待):表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。Thread.join()也会令线程进入等待状态。

       TIMED_WAIT(计时等待):其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本。

       TERMINATED(终止):不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

以下是关系到线程运行状态的几个方法:

  1)start方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

一个线程两次调用start()方法会出现什么情况?

Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。

  2)run方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

        3)wait/notify/notifyAll方法的使用  

        wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized关键字使用,即一般在synchronized同步代码块里使用wait()、notify/notifyAll()方法。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。

     既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用

     notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。

    只有当notify/notifyAll()被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait(),再次释放锁。

也就是说,notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区,以唤醒其他线程。

    notify和wait的顺序不能错,如果A线程先执行notify方法,B线程再执行wait方法,那么B线程是无法被唤醒的。

     notify和notifyAll的区别

    notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

    notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。

    最后,有两点点需要注意:

    (1)调用wait方法后,线程是会释放对monitor对象的所有权的。

    (2)一个通过wait方法阻塞的线程,必须同时满足以下两个条件才能被真正执行:

                线程需要被唤醒(超时唤醒或调用notify/notifyll)。

                线程唤醒后需要竞争到锁(monitor)。


    4)sleep/yield/join方法解析

          这组方法跟上面方法的最明显区别是:这几个方法都位于Thread类中,而上面三个方法都位于Object类中。

    (1)sleep方法

         sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。

         最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

    (2)yield方法

         调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,

         yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。yield方法只是将Running状态转变为Runnable状态。

      注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

    (3)join方法

         join方法有三个重载版本:

         join()

         join(long millis)     //参数为毫秒

         join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒        

         join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。可以看出join方法就是通过wait方法来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起来,直到join的线程执行完成,当前线程才能执行。由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。

         不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

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

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,941评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,426评论 1 15
  • 林炳文Evankaka原创作品。转载自http://blog.csdn.net/evankaka 本文主要讲了ja...
    ccq_inori阅读 642评论 0 4
  • 文章来源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke阅读 1,461评论 0 1
  • 从中可以看出,中国对于这次工业革命是怎样的一个态度呢?历史上的技术浪潮有哪些呢?两次工业革命对中国的影响是怎样的呢...
    透透妈阅读 163评论 0 1