初步了解多线程


进程,线程,任务
进程(Process)是程序运行实例,比如一个正在运行的QQ程序就是一个进程。进程是程序向操作系统申请资源的基本单位。

线程(Thread)是进程中可独立执行的最小单位,比如QQ程序中的语音服务可以由多个线程完成。

一个进程可以包含多个进程,在同一个进程中的所有线程共享该进程的资源。

一个线程需要完成的计算称为任务,特定的线程执行特定的任务,比如QQ中的语音任务。

线程的创建,启动,运行
线程的任务处理逻辑可以在Thread类的run实例方法中直接实现或者通过该方法进行调用,因此run方法相当于线程的任务逻辑入口,它由Java虚拟机在运行相应线程时直接调用,不是由应用代码进行调用。

要运行一个线程,实际上是执行该线程的run方法。首先我们需要启动线程,使用Thread类的start方法。调用start方法启动线程实际上是请求Java虚拟机运行相应的线程,但是这个线程何时能够运行是由线程调度器(Scheduler)决定的。因此使用start方法,并不表示该线程的已经开始运行,这个线程可以稍后运行,也可能永远不会运行。

Thread类有两个构造器:Thread() 与 Thread(Runnable target),因此创建线程也有两种方式。一种是继承Thread,定义Thread的子类并重写run方法,在run方法中实现线程任务的处理逻辑;另一种则是实现 Runnable 接口,并在该实例的run方法中实现线程任务的处理逻辑,将这个实例作为参数传入Thread(Runnable target)构造其中。

以下是创建线程的两种示例
(1)继承Thread类

public class Client {
   public static void main(String[] args) {
      Thread myThread = new MyThread();
      myThread.start();//启动线程
      System.out.println("ThreadName is  "+Thread.currentThread().getName());
   }
}

class MyThread extends Thread{
   //重写run方法
   @Override
   public void run() {
      System.out.println("ThreadName is  "+ Thread.currentThread().getName() );
   }
}

(2)实现 Runnable 接口

public class Client {
   public static void main(String[] args) {
      Thread myThread = new Thread(new MyThread());
      myThread.start();//启动线程
      System.out.println("1ThreadName is  "+Thread.currentThread().getName());
   }
}

class MyThread implements Runnable{
   //重写run方法
   @Override
   public void run() {
      System.out.println("2ThreadName is  "+ Thread.currentThread().getName() );
   }
}

除了这两种外,还有一种是实现Callable接口,并与Future、线程池结合使用,在这里不谈及。

输出结果可能有两种情况

第一种:
1ThreadName is  main
2ThreadName is  Thread-0

第二种:
2ThreadName is  Thread-0
1ThreadName is  main

这也验证了,即使是使用start方法,并不表示该线程的已经开始运行,这个线程可以稍后运行,也可能永远不会运行。

但是不管使用哪种方式创建线程,一旦线程的run方法执行完毕,相应的线程也就运行结束。

线程属于一次性用品,我们不能在一个已经运行结束的线程上再次调用其start方法使其重新运行,实际上一个线程的start方法也只能被调用一次,若多次调用则会抛出 java.lang.IllegalThreadStateException 异常。

Java语言并不阻止我们直接调用run方法,这是因为在Java平台中,线程也是一个对象;其次run方法为一个public方法,但是即便如此,我们也要避免直接调用run方法。

public class Client {
   public static void main(String[] args) {
      Thread myThread = new Thread(new MyThread());
      myThread.start();//启动线程
      myThread.run();//在main方法中直接调用run方法
      System.out.println("1ThreadName is  "+Thread.currentThread().getName());
   }
}
----output-----
2ThreadName is  main
1ThreadName is  main
2ThreadName is  Thread-0

输出结果显示,run方法被调用了两次,一次是Java虚拟机直接调用,一次是应用代码直接调用。前者是运行在自己的线程中,后者是运行在main线程中,这样就违背了线程的意义。

Thread类实际上是 Runnable 接口的一个实现类

public class Thread implements Runnable

在Thread类的run方法源码如下:

public void run() {  // target 的类型为 Runnable
    if (target != null) {
        target.run();  //调用 Runnable 接口中的 run 方法
    }
}

根据上面代码可以看出,run方法的代码逻辑决定了创建线程的两种方式:
一种是在Thread类的子类中实现,这时候target为空,run方法的具体逻辑在其子类中实现
另一种若target不为空,那么Thread的run方法将调用 Runnable 接口的实现类中的run方法

这两种创建线程的区别
①Thread类的子类创建线程是一种基于继承的方法,而Runnable 接口的实现类创建线程是一种基于组合的方式,后者灵活性更强,耦合性更低。
②Runnable 接口的实现类创建线程意味着多个线程实例可以共享同一个实例。
③Runnable 接口的实现类创建线程比Thread类的子类创建线程成本更低。
④线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类。

守护线程与用户线程
按照线程是否会阻止Java虚拟机正常停止,可以将线程划分为守护线程与用户线程,使用daemon属性设置,true表示守护线程,该属性默认与父类线程的属性值相同。用户线程会阻止Java虚拟机正常停止,因此Java虚拟机必须在所有用户线程已经停止的情况下才会正常停止。守护线程不会影响Java虚拟机的正常停止。

一般情况下,子线程是否是守护线程取决于父线程,父线程是什么类型的线程,子线程就是什么类型的线程,也可以通过setDaemon设置,线程的默认优先级也会跟随父线程,但是子线程的生命周期跟父线程生命周期没什么必然联系。

线程的生命周期
Java线程的状态可以通过Thread.getState()来获取,返回值是一个枚举类,线程状态包括以下几种:
①NEW:一个已经创建而未启动的线程(未调用start),线程只能被启动一次,因此一个线程只有一次处于该状态的机会。

②RUNNABLE:该状态包括两个子状态:READY 和 RUNNING 。READY状态可以被线程调度器(Scheduler)进行调度从而变成 RUNNING 状态。反之使用 Thread.yield() 转换。RUNNING 状态表示该线程的run方法正在被执行。

③BLOCKED:线程发起阻塞式I/O操作后,或者申请一个锁资源时就会处于该状态。

④WAITING:等待其他线程执行的状态,调用wait(),join(),park()等方法就会处于该状态

⑤TIMED_WAITING:与WAITING状态类似,差别在于该状态是有时间限制的等待

⑥TERMINATED:已经执行结束的线程处于该状态。一个线程只有一次处于该状态的机会。

线程的生命周期.png

串行,并行,并发
串行:先做完事情A,再做完事情B,再做完事情C,依此类推。
并发:先做事情A,在事情A还没做完时,开始做事情B,在事情B还没做完时开始做事情C,依次类推。
并行:事情A,B,C同时开始进行。

串行,并行,并发.png

竞态
定义:指计算的正确性依赖于相对时间顺序或者线程的交错。
竞态往往会伴随读取脏数据的问题。
产生的条件:访问(读取更新)同一组共享变量的多个线程所执行的操作相互交错。

注:局部变量不会导致竞态,因为不同线程访问的是各自的那一份局部变量。

线程安全
如果一个类在单线程环境下能够运行正常,且在多线程环境下,不必为其做任何更改的情况下也能运行正常,我们将该类其称为具有线程安全。
如果一个类是线程不安全的,那么在多线程下会导致线程安全问题,概括来讲包括以下三个方面

①原子性:对于涉及共享变量访问的操作,若该操作从执行线程以外的任意线程来看都是"不可分割"的,那么说该操作具有原子性。所谓"不可分割"指的是①该操作在外部线程看来要么已经完成,要么未发生,即其他线程是不会看到该操作的中间状态,②访问同一组共享变量的线程不能被交错。原子性的存在排除了一个线程在对一个共享变量进行读写操作时,另一个线程也对该变量进行读写或更新操作而带来的干扰问题。线程不可能进行交错,因此也消除了竞态的可能性。

注意点:原子操作针对的是访问共享变量,对局部变量的访问无所谓原子性;原子操作只有在多线程的情况下才有意义。原子操作+原子操作不等于原子操作。

原子性实现方式:①软件上使用锁(lock)②硬件上利用CAS指令

②可见性:一个线程对共享变量的更新的结果对于读取该共享变量的线程是否可见称为可见性。在Java平台使用volatile关键字保证可见性。Java语言规范保证父线程在启动子线程之前对共享变量的更新对子线程来说是可见的,同样保证一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程是可见的。

③有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

有序性实现方式:①volatile 具有禁止指令重排序,在一定程度上具有有序性。②synchronized锁

上下文切换
时间片:每个线程占用处理器的时间称为时间片(Time Slice)。
时间片决定了一个线程可以连续占用处理器运行的长度,当这个线程的时间片用完或者被迫暂停时(切出),另一个线程就会被线程调度器选中并运行(切入),这种方式叫做线程上下文切换。

处理器上连续运行的多线程实际上是每个线程以时间片断断续续的进行,线程的切出与切入都需要操作系统保存和恢复此线程的进度信息,这个信息就叫做上下文。

一个线程的生命周期状态在RUNNABLE,BLOCKED,WAITING,TIMED_WAITING这四个状态之间切换的过程就是一个上下文切换的过程。

在三种情况下可能会发生上下文切换:中断处理,多任务处理,用户态切换。在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。

多线程运行必须有上下文切换,但上下文切换会带来性能消耗,因此多线程不一定比单线程计算效率高。

线程活性故障
一般情况下,我们都希望线程一直处于RUNNABLE状态,但是事实并非如此。线程有可能会因为上下文切换,程序自身的错误,资源的有限等导致一个线程处于非RUNNABLE状态,这种现象就称为线程活性故障,常见的线程活性故障有以下几种:
①死锁(Deadlock):线程A持有a资源并等待线程B释放b资源再运行,但线程B虽然持有b资源却也在等待线程A释放a资源,因此两线程永远无法进行,导致线程一直处于非RUNNABLE状态。

②锁死(Lockout):线程A持有a资源并等待线程B释放b资源再运行,但是线程B因某种原因而被终止运行了,这样导致线程A永远无法运行。

③活锁(Livelock):线程A一直处于RUNNABLE状态,但是线程A并没有在执行任务,而是在做无用功。

④饥饿(Starvation):由于线程优先级的原因,导致某些低优先级的线程永远获取资源而导致任务无法进行。

线程中断
interrupt():用于中断线程,将调用该方法的线程的状态设置为“中断”状态。但是并不会停止线程运行。
isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志。
interrupted():测试当前线程是否已经是中断状态,执行后具有状态标志清除为false的功能。

“中断”状态只是一个标志,正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。

关于interrupted() 和 isInterrupted()的源码如下

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

private native boolean isInterrupted(boolean ClearInterrupted); //② 参数代表是否要清除状态位

public boolean isInterrupted() {
    return isInterrupted(false); //③
}
nterrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。

interrupted()实际上是调用②中的isInterrupted(boolean ClearInterrupted),默认值为true;isInterrupted()其实也是调用的②,默认值为false。

注意以下说明

InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.  
当一个线程处于中断状态时,如果再由wait、sleep以及jion三个方法引起的阻塞,那么JVM会将线程的中断标志重新设置为false,并抛出一个InterruptedException异常

如何捕获线程中的异常
背景:线程中的异常是不能抛出到调用该线程的外部方法中捕获的,线程方法的异常(无论是checked还是unchecked exception),都应该在线程代码边界之内(run方法内)进行try catch并处理掉。

工具:Thread.UncaughtExceptionHandler 新接口,它允许我们在每一个Thread对象上添加一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()方法会在线程因未捕获的异常而面临死亡时被调用。

使用:
自定义线程异常捕获处理器

//定义这个线程异常捕获的处理器
class ThreadExceptionhandler implements Thread.UncaughtExceptionHandler{
   @Override
   public void uncaughtException(Thread t, Throwable e) {
      System.out.println("捕获线程 "+t+" 异常:"+e);
   }
}

有三种方式使用该线程的异常捕获器

1 在创建线程时进行设置
Thread mythread = new MyThread();
mythread.setUncaughtExceptionHandler(new ThreadExceptionhandler());
mythread.start();

2 使用Executors创建线程时,还可以在TreadFactory中设置。
TreadFactory:线程工厂类,有默认实现,如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。
 ExecutorService exec = Executors.newCachedThreadPool(new ThreadFactory(){
 @Override
            public Thread newThread(Runnable r) {
                Thread thread = newThread(r);
                 thread.setUncaughtExceptionHandler(new ThreadExceptionhandler());
                return thread;
             }
});
exec.execute(new ExceptionThread());

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

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,797评论 3 53
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,442评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,952评论 1 18
  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,662评论 0 12
  • 你是否有过这样的经历? 日复一日,忙忙碌碌,奔赴在职场和生活的战场之间,西装革履的微笑外表下却身心疲惫甚至麻木… ...
    咖喱电影阅读 907评论 1 2