【Java并发编程笔记】初入了解Java中的线程以及Java中线程的创建与使用

初入了解Java中的线程以及线程的创建与使用


在总结线程之前,之前的内容都是顺序编程的知识,既所有操作都是串行的。在学习了线程的知识后,才算真正步入Java编程,进入并发编程的概念。这里仅仅是是从Java的角度讲述了Java线程的基本创建方法和使用方法~

  • 前提概念
    • 进程和线程的概念
      • 什么是进程?
      • 什么是线程?
      • 进程和线程的关系?
    • 单线程和多线程
      • 单线程的概念
      • 多线程的概念
      • 为什么要有多线程编程?
  • 初入Java中的线程的概念和实现
    • Java中线程的5个状态
    • 任务(Tasks)和线程(Thread)
    • Runnable接口和Thread类
    • Java实现线程的几种方式
    • Java线程的中断
  • Thread线程的部分方法实现
    • start()
    • join()
    • sleep()
    • yeild()
  • Java线程的一些问题
    • Thread和Runnable在代码层面上的联系
  • 并发编程学习路线图


前提概念


进程和线程的概念

什么是进程?

进程就是程序运行的过程,一个程序也可以有多个进程在运行。进程是系统进行资源分配的基本单位。

通俗说明:
比如我们平时使用的QQ。qq.exe文件不是一个进程,它是一个程序,只有当我们点击这个qq时,程序被运行了,程序运行的这个过程,就是一个进程。

什么是线程?

线程是操作系统中能够进行运算调度的最小单位,它被包含在进程之中,共享进程中的资源,是进程中实际运作的单位。

通俗说明:
用户A和用户B在使用QQ进行交流。假设他们的聊天框的运行期间是一个进程。那么他们在视频的同时还发送文件,执行发送文件的操作就是一个线程在执行。而视频则是另一个线程的执行。

进程和线程的关系
  • 线程是进程里的具体执行的单位。
  • 一个进程可以有很多线程,每条线程可以执行不同的任务。进程拥有系统资源,同一进程的线程共享这一个进程的资源。
  • 每个程序至少拥有一个进程,每一个进程至少拥有一个线程。
  • 我们可以把进程看做是一个拥有系统资源的容器,而线程就是这个容器里具体去执行的基本单位。

单线程和多线程

单线程的概念

程序进程中只存在一个线程,既主线程。就比如我们Java开发中,如果没有自行new一个线程去start,那么我们的程序也只有一个main方法的主线程在执行(当然这里排除了GC线程等等)

多线程的概念

在一个程序进程中,拥有多个线程在执行,比如我们在Java开发中,new了很多个线程去start,执行其他的任务

为什么要有多线程编程?

多线程编程拥有很大的好处,在系统支持的情况下,我们可以节省大量的时间。比如我们今天要做3件事情

行为 步骤和用时
烧开水 准备烧开水(1分钟),等水烧开(8分钟),关掉烧水机(1分钟)
举杠铃 举杠铃100下(10分钟)
洗衣服 准备洗衣服(1分钟),洗衣服(5分钟),关掉洗衣机(1分钟)

如果是(同步)单线程的情况下:
先烧开水,再举哑铃,再洗衣服,1+8+1+10+1+5+1=27分钟

如果是(同步)多线程的情况下:

线程 步骤
线程1 准备烧开水(1) 休眠(1) 休眠(5) 休眠(1) 休眠(2 关闭烧水机(1)
线程2 休眠(1) 休眠(1) 举杠铃50下(5) 休眠(1) 举杠铃20下(2) 休眠(1) 举杠铃30下(3)
线程3 休眠(1) 准备洗衣服(1) 休眠(5) 关洗衣机(1)

多线程同步的情况下共用时14分钟

所以我们得出结论,多线程同步的情况下比单线程同步总共节约了13分钟,这是一个巨大的提升!!
为什么会剩下这么多时间呢,因为比如等水烧开(8),洗衣服(5)的操作就相当于IO的耗时操作,这些操作时不需要用到CPU的资源的,我们这里就是利用同步的多线程实现了异步的概念


初入Java中的线程的概念和实现


Java中线程的5个状态

引用至《深入理解Java虚拟机》:
Java语言定义了5种线程的状态,在任意一个时间点,一个线程只能有且只有其中一种状态:

  • 新建(New)
    线程被创建后但尚未启动的状态,既new了但没有start
  • 运行(Runable)
    包括操作系统中线程的就绪(ready)和运行状态(running),也是就处于该状态的线程有可能在运行,也有可能在等待CPU为它分配时间
  • 无限期等待(Waiting)
    处于当前状态的线程不会被分配CPU时间,他们需要等待其他线程显式唤醒。有如下实现方法:没有设置时间参数的Object.wait(),Thread.join()和LockSupport.park()方法
  • 限期等待(Timed Waiting)
    处于这个状态的线程不会被分配CPU时间,不过无须其他线程显式唤醒,而是在等待一定时间后由系统自动唤醒,有如下实现方法:设置了时间参数Thread.sleep(),Object.wait(),Thread.join(),LockSupport.parkNanos(),LockSupport.parkUntil()方法
  • 阻塞(Blocked)
    线程被阻塞了,在程序等待进入同步区域的时间发生。阻塞状态和等待状态的区别是:阻塞状态在等待获取一个排他锁,这个事件将在另外一个线程放弃该锁时发生。而等待状态则是在等待一段时间或者唤醒动作的发生。
  • 结束(Terminated)
    已终止线程的线程状态,线程已经结束执行。

任务(Tasks)和线程(Thread)

在进入代码实战之前,我们来区别一下什么是任务,什么是线程?这里引用《Java编程思想》 的说法:

任务(Tasks):
任务就是需要执行的操作,比如说我想发送一个文件。那么发送文件就是一个任务,这个任务就是由我们的代码构成的。放在Java线程知识中,就如实现一个Runnable接口的run方法,run方法里要执行的东西就是一个要去完成的任务
线程(Thread)
线程就是一个具体的执行单位,我们定义好一个任务,交给线程去帮我们完成它。

它们之间的关系就类似任务是一个需要去完成的事情,线程就是具体去完成这个事情的人。

Runnable接口和Thread类

刚刚我们理清了什么是任务,什么是线程。那我们再具体化些,在Java中,Runnable就是一个任务Thread就是一个线程

  • 我们需要实现一个Runnable接口和其run方法,这个过程就类似于定义了一个任务
  • 然后我们new了一个Thread对象,将Runnable对象传入Thread的构造方法中。这个过程就是将定义的任务交给线程去处理

代码如下:

//通过匿名内部类的方式实现了Runnable接口,并重写了run方法,相当定义了一个任务
Runnable runnable = new Runnable() {
        @Override
        public void run() {        //重写run方法,输出“我是一个任务” 就是这个任务需要完成的事情
            System.out.println("我是一个任务");
        }
}

//定义了两个线程实例,将Runnable对象传入Thread构造方法中,相当于把任务委托给了线程去完成
Thread myThread1 = new Thread(runnable);  
Thread myThread2 = new Thread(runnable);

myThread1.start();//Thread.start代表开启线程,去完成被委托的任务。
myThread1.start();

从上面可以看到,我们定义了一个任务Runnable对象,分别交给了两个Thread去处理。可以看到,这也明确了告诉了我们,我们的任务是可以交给不同的线程去实现的。

Java实现线程的几种方式

  • 实现Runnable接口
  • 继承Thread类
  • 线程池
通过实现Runnable接口的方式实现线程
public static void myRunnable(){
        //通过匿名内部类的方式实现Runnable方法,并做为参数传入Thread的实例化中
        Thread myThread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "running");
            }
        });
        myThread.start();
    }

通过实现Runnable接口的方式实现线程分为两个步骤:

  • 实现Runnable接口,重写run方法。相当定义了一个任务
  • 实例化Thread对象,传入刚实现的Runnable对象,然后start。相当与把这个任务交给这个线程去执行
通过继承Thread接口重写run方法的方式实现线程
public static void myThread(){
        //通过匿名内部类的方式继承Thread并重写run方法    
        Thread myThread = new Thread(){

            @Override
            public void run() { //重写run方法,内容是线程要执行的任务
                System.out.println(getName() + "running"); 
            }
        };
        myThread.start(); //启动线程
    }

通过继承Thread实现线程的方式很简单,只要你重写了run方法,并start线程既可

Java线程的中断

线程内部的run方法的代码是串行执行的。相当于一个一次执行体。想让线程中断,目前推荐的有两种做法:

  • 让线程执行完所需要执行的任务
  • 如果线程所执行的任务是一个无限循环体,则设置一个flag判断,根据条件跳出循环。
    volatile static boolean isContinue = true; //flag,默认为true

    public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new Runnable() {
            int count = 0;

            @Override
            public void run() {
                while(isContinue){ //如果isContinue为true就一直循环,如果为false则跳出循环,结束线程
                    count++;
                    System.out.println(Thread.currentThread().getName() +"  count=  "+ count);
                }
            }
        });
        myThread.start();

        Thread.sleep(3000); //主线程休眠3s
        isContinue = false; //flag设置为false,myThread线程就会跳出循环,执行完剩余代码结束线程

        System.out.println(Thread.currentThread().getName() + " IsContinue = " + isContinue);
    }

输出为

...
...
Thread-0  count=  171514
Thread-0  count=  171515
Thread-0  count=  171516
Thread-0  count=  171517
Thread-0  count=  171518
Thread-0  count=  171519
Thread-0  count=  171520
Thread-0  count=  171521
Thread-0  count=  171522
Thread-0  count=  171523
main IsContinue = false

我们可以看到这里有两个线程,一个是main线程(主线程),另一个是我们自己定义的MyThread线程。MyThread线程中执行的任务是一个无限循环体。只有当主线程把isContinue修改为fasle时,MyThread才会跳出循环,执行剩下的代码,然后结束。最后主线程再结束

注意:
注意,停止线程,不要使用自带的stop方法,有缺陷,应该使用正确的方法去停止线程。Stop方法会使得线程戛然而止,就是线程还在运行中,还没执行至少一个周期,突然中断,被强行关闭,不利于很多事物的处理,所以我们可以选择让线程走完自己的周期,就会自己结束


Thread线程的部分方法实现


start()方法的使用

 public synchronized void start() {}

以上是start()方法的定义,我们可以看出Thread的方法是非静态的方法,所以可以说是Thread对象的方法。
用法很简答,只要你定义了一个Thread对象,通过Thread对象.start()即可,作用是启动该线程

//定义一个Thread实例化对象,重写run方法
Thread myThread = new Thread(){
            @Override
            public void run() {
                System.out.println(getName());
            }
};
myThread.start(); //启动myThread线程

join()方法的使用

public final synchronized void join(long millis){}

以上是join方法的定义,我们可以看到也是一个实例方法,属于对象的方法。它的作用是让当前线程等待调用线程运行结束或X秒后才恢复运行。,以下是三种重载的方式:

void join()                      //没有参数,当前线程必须等待调用线程终止才能运行
void join(long millis)           //含有参数则会给当前线程一个等待调用线程运行结束的最大的等待时间
void join(long millis,int nanos) //第二个参数可以额精确到纳闷的级别

既在线程A中执行线程B.join()方法,意味着线程A要等待线程B执行完毕才能恢复执行。

实际使用效果:

public class Demo{

    volatile static boolean isContinue = true; //flag,影响myThread的run方法的循环体

    public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new Runnable() { //定义myThread线程
            int count = 0;

            @Override
            public void run() {
                while(isContinue){
                    count++;
                    System.out.println(Thread.currentThread().getName() +"  count=  "+ count);
                }
            }
        });
        myThread.start();     //启动myThread线程
        myThread.join(5000);  //当前线程是主线程,让主线程等待myThread线程5000毫秒再继续运行

        isContinue = false;   //myThread线程执行5s后,主线程得以继续运行
                              //isContinue赋值为false,所以myThread线程结束
    }
}

从上面我们可以看出。myThread执行的任务是一个无线循环体,跳出条件是isContinue。为了不让myThread立马退出,所以我们在main线程中执行myThread.join(5000)。意思就是myThread的join方法在main线程里调用,作用就是让当前线程main线程等待myThread线程5s再执行。如果没有参数,则意思是让当前线程等待调用线程myThread执行完才恢复执行。

注意:
当前线程就是执行myThread.join()这条语句的线程。
调用线程就是调用join方法的那个对象线程

sleep()方法的使用

 public static native void sleep(long millis) throws InterruptedException;

以上是sleep方法的定义,这是一个静态的(native)本地方法。不管它是本地方法与否。要记住的是Sleep方法与start,join等方法不同。sleep方法是一个类方法,是静态方法。是可以直接通过类名调用的。不是属于具体对象的方法。

sleep的作用是使得当前线程休眠暂定一段时间再执行。比如在线程A中运行了Thread.sleep(3000);,意思是线程A将休眠3s再恢复运行。

    public static void main(String[] args){

        Thread myThread = new Thread(){

            @Override
            public void run() {

                count++;
                System.out.println(getName() + ",count = " + count);
            }

        };
        Thread.sleep(3000); //主线程休眠3秒
        myThread.start();   //主线程恢复运行之后才能启动myThread线程
    }

yield()方法的使用

    public static native void yield();

yield方法也是一个静态的本地方法,是属于类的方法,而不是对象的方法。所以该方法也是针对当前线程而言的。

如过在线程A的执行体中运行了Thread.yield()方法,该线程A会主动放弃已获得到的系统资源,重新去竞争资源,直到抢到才会轮到它。

public static void main(String[] args) {
        Thread myThread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("myThread running");
                Thread.yield();  //释放系统资源
                System.out.println("myThread end");
            }
        });

        myThread.start();;
    }

也许你会有疑问,如果它在线程执行任务到了一半,突然说释放资源。那么重新竞争到资源的时候,线程从何处开始执行呢?当然是从何处退出就从何处接上啦!所以上面程序的输出结果是:

myThread running
myThread end


Java线程的一些问题


Thread和Runnable在代码层面上的联系

我们首先来看Thread的部分构造函数(排除了ThreadGroup),看看有什么不同?

public Thread() 实例化一个空Thread对象

Thread(String name) 实例化一个空Thread对象并指定Thread的名字

Thread(Runnable target) 实例化一个Thread对象,并将 target 作为其运行对象

Thread(Runnable target,String name) 实例化一个Thread对象,指定运行对象和Thread名字

当然名称是无关紧要的啦,如果你没有显式的指定名称,Thread也是会有默认的名称的,默认名称由"Thread-" + nextThreadNum()实现。
重点就是有无传入Runnable对象,为什么呢?我们来看一下Thread的部分源码:


//Thread类实现了Runnable接口,重写了Run方法
public class Thread implements Runnable {

   private Runnable target;   //Thread内部会定义一个Runnable变量,用于接收构造函数初始化传入的Runnable对象

   @Override
   public void run() {
        if (target != null) { //如果target为null,Thread不做反应。反之,则执行target的run方法
            target.run();
        }
    }
}

以上是我挑出的部分Thread源代码,可以看出有三个亮点

  • Thread实现了Runnable接口
  • Thread中定义了一个Runnable变量target
  • Thread重写的Runnable接口的run方式里实际执行的是target的run方法

现在我们来说一下之前运用到的场景,我们都知道实现线程可以用继承Thread类重写其run方法去实现。也可以通过实现Runnable接口重写run方法,交给一个Thread实例去实现。

第一种场景为什么能实现呢?
很简单,Thread本身就是一个线程实际的执行单位,只要把任务交给它,它就能去完成。但Thread本身又实现了Runnable接口,具备run方法。也就是说Thread本身就可以定义任务,然后自己去处理,只是Thread run方法的内部默认执行的是target的run()。所以当我们重写了Thread的run方法,此时Thread的run方法不再需要执行其内部的target.run()。所以这样自然可以实现一个有任务的线程。

第二种场景为什么能实现?
这就更简单了,Thread内部的run方法实际执行的是target变量的run方法。你向Thread传入了一个Runnable对象,在Thread初始化时,target被赋值,target不再是null。自然Thread会去执行target的run方法的内容。

小结:
总结起来也很简单,一句话。Thread默认执行的就是传入的Runnable对象的run方法。如果没有传入也没关系。你只要重新我的run方法,修改原本的判断逻辑也是可以去实现的。

学习路线图


最后送上一个从网上缴来的并发编程学习路线图~
这里写图片描述


参考资料


《Java编程思想》
Java 多线程(二) Thread类与Runnable接口的关系 - 作者:@qq_33830123

猜你喜欢

转载自blog.csdn.net/snailmann/article/details/80620296
今日推荐