Java并发之线程

     在前面我们介绍的一些内容中,我们的程序都是一条执行流,一步一步的执行。但其实这种程序对我们计算机的资源的使用上是低效的。例如:我们有一个用于计算的程序,主程序计算数据,在计算的过程中每得到一个结果就需要将其保存到外部磁盘上,那么难道我们的主程序每次都要停止等待CPU将结果保存到磁盘之后,再继续完成计算工作吗?要知道磁盘的速度可是巨慢的(相对内存而言),我们如果能分一个线程去完成磁盘的写入工作,主线程还是继续计算的话,是不是效率更高了呢?其实,并发就是这样的一种思想,使用时间片分发给各个线程CPU的使用时间,给人感觉好像程序在同时做多个事情一样,这样做的好处主要在于它能够对我们整个的计算机资源有一个充分的利用,在多个线程竞争计算机资源不冲突的前提下,充分的利用我们的资源。本篇文章首先来介绍并发的最基本的内容-----线程。主要涉及以下一些内容:

  • 定义线程的两种不同的方法及它们之间的区别
  • 线程的几种不同的状态及其区别
  • Thread类中的一些线程属性和方法
  • 多线程遇到的几个典型的问题

     一、创建一个线程
     首先我们看创建一个线程的第一种方式,继承Thread类并重写其run方法。

public class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println("this is mythread");
    }
}

现在我们来看看在主程序中如何启动我们自定义的线程:

public static void main(String[] args) {
    Thread myThread = new MyThread();
    myThread.start();
}

我们首先构建一个Thread实例,调用其start方法,调用该方法会为线程分配其所必须的堆栈资源,计数器,时间片等,并在该方法的结束时刻调用我们重写的run方法,完成线程的启动。

但是在Java中类是单继承的,也就是如果某个类已经有了父类,那么它就不能被定义成线程类。当然,Java中也提供了第二种方法来定义一个线程类,这种方式实际上更加的接近本质一些。通过继承接口Runnable并在其内部重写一个run方法。

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("this is mythread");
    }
}

启动线程的方式和上一种略微有点不同,但是本质上都是一样的。

public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.start();
    }

这里我们利用Thread的一个构造函数,传入一个实现了Runnable接口的参数。下面我们看看这个构造函数的具体实现:

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

调用init方法对线程的一些状态优先级等做一个初始化的操作,我们顺便看看使用第一种方式创建线程实例的那个无参的构造函数:

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

可以看到,两个构造函数的内部调用的是同一个方法,只是传入的参数不同而已。所以他们之间的区别就在于初始化的时候这个Runnable参数是否为空,当然这个参数的用处在run方法中也可以看出来:

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

如果我们使用第二种方式构建Thread实例,那么此处的target肯定不会是null,自然会调用我们重写的run方法。如果使用的是第一种方式构建的Thread实例,那么就不会调用上述的run方法,而是调用的我们重写的Thread的run方法,所以从本质上看,两种方式的底层处理都是一样的。

这就是创建一个线程类并启动该线程的两种不同的方式,表面上略有不同,但是实际上都是一样的调用init方法完成初始化。对于启动线程的start方法的源码,由于调用本地native方法,暂时并不易解释,有兴趣的可以使用jvm指令查看本地方法的实现以了解整个线程从分配资源到调用run方法启动的全过程。

     二、线程的多种状态
     线程是有状态的,它会因为得不到锁而阻塞处于BLOCKED状态,会因为条件不足而等待处于WAITING状态等。Thread中有一个枚举类型囊括了所有的线程状态:

public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

NEW状态表示线程刚刚被定义,还未实际获得资源以启动,也就是还未调用start方法。

RUNNABLE表示线程当前处于运行状态,当然也有可能由于时间片使用完了而等待CPU重新的调度。

BLOCKED表示线程在竞争某个锁失败时被置于阻塞状态

WAITING和TIMED_WAITING表示线程在运行中由于缺少某个条件而不得不被置于条件等待队列等待需要的条件或资源。

TERMINATED表示线程运行结束,当线程的run方法结束之后,该线程就会是TERMINATED状态。

我们可以调用Thread的getState方法返回当前线程的状态:

/*定义一个线程类*/
public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("myThread's state is : "+Thread.currentThread().getState());
    }
}
/*启动线程*/
public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new MyThread());
        myThread.start();
        Thread.sleep(1000);
        System.out.println("myThread's state is : "+myThread.getState());
    }
这里写图片描述

我们两次输出myThread线程的当前状态,在run方法中输出结果显示该线程状态为RUNNABLE,当该run方法执行结束时候,我们又一次输出该线程的当前状态,结果显示该线程处于TERMINATED。至于更加复杂的线程状态,我们将在后续的文章中逐渐进行介绍。

     三、Thread类中的其他一些常用属性及方法
     以上我们介绍了创建线程的两种不同的方式以及线程的几种不同状态,有关于线程信息属性的一些方法还没有介绍。本小节将来简单介绍下线程所具有的基本的一些属性以及一些常用的方法。

首先每个线程都有一个id和一个name属性,id是一个递增的整数,每创建一个线程该id就会加一,该id的初始值是10,每创建一个线程就会往上加一。所以该id也间接的告诉了我们当前线程在所有线程中的位置。name属性往往是以“Thread-”+编号作为某个具体线程的name值。例如:

public static void main(String[] args){
        for (int i=0;i<10;i++){
            Thread myThread = new Thread(new MyThread());
            myThread.start();
            System.out.println(myThread.getName());
        }
    }

输出结果:

这里写图片描述

除此之外,Thread中还有一个属性daemon,它是一个boolean类型的变量,该变量指示了当前线程是否是一个守护线程。守护线程主要用于辅助主线程完成工作,如果主线程执行结束,那么它的守护线程也会跟着结束。例如:我们的main程序在执行的时候,始终有一个垃圾回收线程作为守护线程辅助一些对象的回收工作,当main程序执行结束时,守护线程也将退出内存。关于守护线程有几个方法:

public final boolean isDaemon() :判断当前线程是否是守护线程

public final void setDaemon(boolean on):设置当前线程是否作为守护线程

还有一个方法较为常见,join。该方法可以让一个线程等待另一个线程执行结束之后再继续工作。例如:

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("myThread is running");
    }
}
public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.start();

        //主线程等待myThread线程执行结束
        myThread.join();

        System.out.println("waiting myThread done....");
    }

输出结果:

这里写图片描述

有人可能会疑问,我们使用多线程不就是为了充分利用计算机资源,使其同时执行多个任务,为什么又要让一个线程等待另一个线程呢?其实某些时候,主线程需要拿到所有分支线程计算的结果再一次进行计算,各个分支线程的进度各有快慢,主线程唯有等待他们全部执行结束之后才能继续。此时就需要使用join方法了,所以说每一个方法的存在都有其可应用的场景。至于这个join的源代码也是很有研究价值的,我们将在后续的文章中对其源代码的实现进行进一步的学习。

还有一些属性和方法,限于篇幅,本文不再继续学习,大家可以自行查看源码进行学习。下面我们看看多线程之后可能会遇到的几个经典的问题。

     四、多线程遇到的几个典型的问题
     第一个可能遇到的问题是,竞态条件。也就是说,当多个线程同时访问操作同一个对象的时候,最终的结果可能正确也可能不正确,具体的执行情况和线程实际的执行时序有关。
例如:

/*我们定义一个线程*/
public class MyThread implements Runnable{
    public static int count;

    @Override
    public void run(){
        try {
            Thread.currentThread().sleep((int)(Math.random()*100));
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/*main方法中启动多个线程*/
public static void main(String[] args){

        Thread[] threads = new Thread[100];
        for (int i=0;i<100;i++){
            threads[i] = new Thread(new MyThread());
            threads[i].start();
        }

        for (int j =0;j<100;j++){
            threads[j].join();
        }

        System.out.println(MyThread.count);
    }

首先在我们自定义的线程类中,有一个static公共变量,而我们的run方法主要就做两个事情,随机睡一会和count增一。再来看main函数,首先定义了一百个线程并逐个启动,然后主线程等待所有的子线程完成之后输出count的值。

按照我们一般的思维,这一百个线程,每个线程都是为count加一,最后的输出结果应该是100才对。但是实际上我们多次运行该程序得到的结果都是不一样的,但几乎都是小于100的。

这里写图片描述

这里写图片描述

这里写图片描述

为什么会出现这样的情况呢?主要原因还是在于为count加一这个操作,它并非是原子操作,也就是说想要为count加一需要经过起码两个步骤:

  • 取count的当前值
  • 为count加一

因为每个线程都是随机睡了一会,有可能两个线程同时醒来,都获取到当前的count的值,又同时为其加一,这样就导致两个不同的线程却只为count增加了一次值。这种情况在多线程的前提下,发生的概率就更大了,所以这也是为什么我们得到的结果始终小于100但又每次都不同的原因。

第二个问题是,内存的可见性问题。就是说,如果两个线程共享了同一个参数,其中一个线程对共享参数的修改而另一个线程并不会立马能够看到。原因是这些修改会被暂存在CPU缓存中,而没有立马写回内存。例如:

public class MyThread extends Thread{
    public static boolean flag = false;

    @Override
    public void run(){
        while(!flag){
            //just running
        }
        System.out.println("my thread has finished ");
    }
}
public static void main(String[] args) throws InterruptedException {
        Thread myThread = new MyThread();
        myThread.start();

        Thread.sleep(1000);
        MyThread.flag = true;
        System.out.println("main thread has finished");
    }

首先我们定义一个线程类,该线程类中有一个静态共享变量flag,run方法做的事情很简单,死循环的做一些事情,等待外部线程更改flag的值,使其退出循环。而main方法首先启动一个线程,然后修改共享变量flag的值,按照常理线程myThread在main线程修改flag变量的值之后将退出循环,打印退出信息。但是实际的输出结果为:

这里写图片描述

main线程已经结束了,而整个程序并没有结束,线程myThread的结束信息也没有被打印,这就说明myThread线程还困在while循环中,但是实际上主线程已经将flag的值修改了,只是myThread无法看见。这是什么原因呢?

我们知道,每个线程都有一些缓存,往往为了效率,对一个变量值的修改并不会立马写会内存,而是注入缓存中,等到一定的时候才写回内存,而当别的线程来修改这些共享的变量的时候,他们是从内存进行读取的,修改后可能也没有及时的写回内存中,这就很容易导致其他线程根本就看不到你所做的修改。这就是典型的内存可见性问题。

本小节简单的介绍了多线程的两个典型的问题,解决办法其实有多种,我们将在下篇文章中涉及。

以上的本篇内容主要介绍了线程的基本概念,如何创建一个线程,如何启动一个线程,还有与线程相关的一些基本的属性和方法,总结不到之处,望大家指出,相互学习。下篇文章将介绍一个用于解决多线程并发问题的关键字synchronized。

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

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,448评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,952评论 1 18
  • 前面的几篇文章主要介绍了线程的一些最基本的概念,包括线程的间的冲突及其解决办法,以及线程间的协作机制。本篇主要来学...
    Single_YAM阅读 476评论 0 3
  • 未有过的压郁 总是在不知不觉间沦陷 不知是我太过于煽情还是现实真的总是会让人感触良多 或许 在一个压郁...
    小丑模样阅读 197评论 0 0
  • 介绍对象,刚刚接到了之前邻居家的电话,说给我介绍对象,其实吧我内心很拒绝,但是还是要礼貌啊,毕竟邻居还是看得上我,...
    啊貴阅读 189评论 0 0