zoukankan      html  css  js  c++  java
  • 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。

  • 相关阅读:
    iTerm2使用技巧
    我的mac下有关php扩展的安装
    xmlhttprequest 1.0和2.0的区别,from qq前端哥
    PHP错误日志记录:display_errors与log_errors的区别
    目前php连接mysql的主要方式
    闭包介绍汇总
    接口设计知识总结
    git命令——从GitHub clone XXX分支,本地创建新分支push到远程仓库
    Spring错误——Junit测试——java.net.BindException: Address already in use: bind
    Java.util.Random生成随机数
  • 原文地址:https://www.cnblogs.com/yangming1996/p/7521665.html
Copyright © 2011-2022 走看看