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。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老马说编程

(69) 线程的中断 / 计算机程序的思维逻辑

本节主要讨论一个问题,如何在Java中取消或关闭一个线程? 取消/关闭的场景 我们知道,通过线程的start方法启动一个线程后,线程开始执行run方法,run...

1799
来自专栏IT技术精选文摘

Java多线程知识小抄集(一)

本文主要整理平时遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子...

1889
来自专栏IT技术精选文摘

深入理解 Java 并发之 synchronized 实现原理

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了...

2727
来自专栏java相关

线程安全问题分析

计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址。当多个线程并发访问同一个内存地址并且内存地址...

662
来自专栏老马说编程

(65) 线程的基本概念 / 计算机程序的思维逻辑

在之前的章节中,我们都是假设程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束。从本节开始,我们讨论并发,在程序中创建线程来启动多条执行流,并...

2097
来自专栏noteless

-1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),

java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法...

804
来自专栏苦逼的码农

线程安全(上)--彻底搞懂synchronized(从偏向锁到重量级锁)

接触过线程安全的同学想必都使用过synchronized这个关键字,在java同步代码快中,synchronized的使用方式无非有两个:

1032
来自专栏编程

Java并发编程实战

在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者、写相同的数据,访问相同的文件等等。对于这种情况如果我们不加以控制,是非常容易导致错误的。在jav...

2215
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十四天 多线程-多线程&线程安全【悟空教程】

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

1005
来自专栏好好学java的技术栈

bat等大公司常考java多线程面试题

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程.进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切...

694

扫码关注云+社区