zoukankan      html  css  js  c++  java
  • java多线程高并发学习从零开始——新建线程

    java多线程高并发学习从零开始——新建线程

    本笔记就本人学习中的一些疑问进行记录,希望各位看官帮忙审查,如有错误欢迎评论区指正,本人将感激不尽!

    一、对线程和进程概念的理解

    1.1 首先总结与线程比较相似也经常出现的另一个概念——进程

    进程:计算机上的一个应用程序的载体就是一个进程,比如:QQ,微信、各种游戏等支持这些应用运行的就是其对应的进程。

    进程是计算机资源分配的最小单位。

        

    (1)在一台计算机上可能有多个进程同时运行,比如:QQ、微信、浏览网页、听歌同时在运行,因为在同时操作所以进程的特点是具有并发性的;

    (2)上述每个进程执行互不影响,相互独立的;

    (3)进程可以随时关闭和开启,这可以理解为动态性

    (4)每个进程是由程序、数据、进程控制块等组成的,所以说进程是具有结构性的;

    1.2 接下来理解另一个概念 —— 线程

    线程:随着技术的发展,CPU性能的提升和对时间效率的要求提高,出现了线程的概念。

    线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程。

    1.3  线程和进程有什么区别呢:

    (1)线程是CPU调度的最小单位,而进程是资源分配的最小单位;

    (2)一个进程是由一个或者多个线程组成的,但是线程是程序代码中不同的执行逻辑块;

    (3)进程之间是相互独立的,但是同类的线程是共享代码和数据空间,每个线程都是有独立的栈和程序计数器;

    (4)进程的切换开销较大,线程的切换开销小;

    二、线程详细学习和总结

    2.1 线程的状态有哪些?

      书本和网上都可以搜索到线程的基本状态有五种:新建、就绪、运行、死亡、阻塞。

    其实作为初学者,我深刻知道这些概念总是比较模糊,可能是因为比较抽象,因此我试着跟接地气的理解这些状态:

    新建:这个无需多做理解,你要使用线程,你总得有一个线程吧,怎么拥有一个线程呢?那自然就是新建。

    就绪:要理解这个状态,需要知道CPU在运行的时候在一个较长时间段,并不是一直操作唯一的一段程序的,可能采用时间片轮转机制(把cpu要操作的进程排列成一个圈,圈是由一段一段的要执行的进程组成的,cpu在运转的过程中每次只在当前片段执行对应的程序,转过这个片段就切换成下一个程序——上下文切换),所以就绪就可以理解为,当前的线程已经进入了CPU即将操作的队列中,时刻准备着CPU开始执行该程序。还有另外一种情况要考虑到:当前线程的时间片用完,就再次进入到就绪队列,等待下一次时间片到达。

    运行:根据就绪的理解,当CPU的时间片切换到本线程,线程开始运行。

    死亡:当线程的run()和程序的main()方法执行结束,或者线程执行过程中抛出异常,退出了程序运行。这种状态的线程被称为死亡,死亡的线程无法再次复生。

    阻塞:线程的阻塞概念比较复杂些,当线程由于某种操作让出了cpu的执行权,也即放弃了当前的时间片队列,暂时停止运行。导致这种情况的有以下这些操作:

        (1)使用Object.wait();方法阻塞线程,等待Object.notify()或者notifyAll()唤醒线程,重新进入就绪队列;

        (2)使用Thread.sleep();方法,睡眠线程,等待线程睡眠时间结束,重新进入就绪队列;

        (3)使用了同步锁,例如:synchronized关键字,线程等待抢占锁资源也属于阻塞,等占有了锁进入就绪队列;

        (4)线程内的子线程使用了join();方法,等待子线程执行结束返回后,重新进入就绪队列;

        (5)线程执行中有了I/O请求,即需要等待第三方(用户)输入,等输入结束后,重新进入就绪队列;

    2.2 线程的使用——新建

     新建线程的方法一般有三种:继承Thread类,实现Runable接口和实现Callable接口。

    2.2.1 继承Thread类:简单的新起一个线程

    下面粘贴一个本人简单的学习示例:

     1 package com.shine.study;
     2 
     3 public class MutipleThreadTask {
     4     //使用继承Thread类自定义线程
     5     static class ShowoutThread extends Thread{
     6         private String name;
     7         //自定义线程的构造函数
     8         public ShowoutThread (String name){
     9             this.name = name;
    10         }
    11         //run方法的实现,其内部用于实现真正的业务代码
    12         public void run(){
    13             for (int i = 0; i<3 ; i++){
    14                 System.out.println("Thread [" + name + "] 内部打印第"+i+"次");
    15             }
    16         }
    17     }
    18 
    19     public static void main(String[] args) {
    20         System.out.println("欢迎使用学习线程程序课堂!这里是main调用线程前的描述");
    21         Thread threada = new ShowoutThread("A");
    22         Thread threadb = new ShowoutThread("B");
    23         threada.start();
    24         threadb.start();
    25         System.out.println("线程调用后的语句,这里是main调用线程后的描述");
    26     }
    27 }

    运行结果:

        

    运行两次之后,结果如上图所示,

    执行结果我们可以看出两个问题:

    (1)两次结果并不是一样的,为什么呢?

    (2)"线程调用后的语句,这里是main调用线程后的描述" ,这句话明明放在threadb.start();方法后面,为什么实际却是先打印了呢?

    答:(1)这里就看以看出每个代码中线程A 和线程B是两个不同的新线程,当main方法执行到 threada.start(); threadb.start(); 时候两个进程都被放进CPU的就绪队列里面等待,执行过程中由于时间片的上下文切换导致线程AB的执行顺序并不是每一次都是相同的。

    (2)这个现象其实也是好理解的,对于main方法,它自己就是一个线程,我们称之为主线程。主线程在执行到  threada.start(); threadb.start(); 所做的操作仅仅是把两个进程都被放进CPU的就绪队列里面等待,自己后续的工作还没有执行完,只要CPU对主线程的时间片没用完,当然会继续执行其后面的方法了。

     这里讲述一个我在学习的时候困惑的一个问题:我们发现使用 threada.run(); threadb.run(); 程序也是不会报错,那为什么不能这么用呢?我不知道其他初学者有没有过这样的疑问,但是我还是在这里描述一下我现在的理解吧!

      其实很简单,我们看到在上面的程序中 ShowoutThread 这个类虽说是我们用来继承Thread类,用于我们后面新起线程用的,但是不要忘记了它本身还是一个class,name是它的成员变量,run也是它的成员函数啊!我们正常情况下怎么去调用成员函数呢?不就是通过 对象.成员函数(); 来的吗?这种情况下去调用run();方法不就是普通的函数调用了吗?这又怎么能称为启动线程呢?

      并且我们会发现,如果写成 threada.run();threadb.run(); 这种形式,其执行结果就变成了顺序输出了:("线程调用后的语句,这里是main调用线程后的描述"这句话也始终在最后了)

    所以学习千万不能想当然,学习还是要基于我们最基本的知识点去理解。

     ------------------------------------------------------

    这里还要补充一个现象,反复的调用同一个线程对象会抛出异常!java.lang.IllegalThreadStateException:

              

    这里反复的启动了同一个线程,结果抛出异常——java.lang.IllegalThreadStateException。

    为什么出现了这种情况呢?我们稍微看一下start方法的实现:

     1     /* Java thread status for tools,
     2      * initialized to indicate thread 'not yet started'
     3      */
     4 
     5     private volatile int threadStatus = 0;
     6 
     7     public synchronized void start() {
     8         /**
     9          * This method is not invoked for the main method thread or "system"
    10          * group threads created/set up by the VM. Any new functionality added
    11          * to this method in the future may have to also be added to the VM.
    12          *
    13          * A zero status value corresponds to state "NEW".
    14          */
    15         if (threadStatus != 0)
    16             throw new IllegalThreadStateException();
    17 
    18         /* Notify the group that this thread is about to be started
    19          * so that it can be added to the group's list of threads
    20          * and the group's unstarted count can be decremented. */
    21         group.add(this);
    22 
    23         boolean started = false;
    24         try {
    25             start0();
    26             started = true;
    27         } finally {
    28             try {
    29                 if (!started) {
    30                     group.threadStartFailed(this);
    31                 }
    32             } catch (Throwable ignore) {
    33                 /* do nothing. If start0 threw a Throwable then
    34                   it will be passed up the call stack */
    35             }
    36         }
    37     }

    很明显可以看到线程在执行start();方法的时候首先会判断 threadStatus 是否为0,但是在我们第一次使用 threada.start();的时候 已经  * A zero status value corresponds to state "NEW".

    这时候再一次使用这个线程对象的start方法,判断 threadStatus 不为 0 所以抛出异常 :throw new IllegalThreadStateException();

    引申:volatile 关键字。其实要讲这个关键字就涉及了另外的一些概念,我们只要知道有了这个关键字,当程序中这个变量被改变,那么内存中其他正在使用这个变量的程序都会同时发现:

    1.内存模型 2.并发编程的三个需要注意的问题:原子性问题,可见性问题,有序性问题

    这里想要了解请参考搜索“java关键字volatile”。

    或者本人学习总结的文章,欢迎指导:https://www.cnblogs.com/EtherealWind/p/14856493.html

    2.2.2 实现Runable接口:常见的线程创建方式

    同样粘贴本人学习的代码:

     1 package com.shine.study;
     2 
     3 public class MutipleRunableTask {
     4     
     5     static class ShowTask implements Runnable {
     6         private String name;
     7         public ShowTask(String name) {
     8             this.name = name;
     9         }
    10 
    11         @Override
    12         public void run() {
    13             for (int i = 0; i < 2 ; i++){
    14                 System.out.println("线程["+name+"],第"+ i +"次打印。");
    15             }
    16         }
    17     }
    18 
    19     public static void main(String[] args) {
    20         ShowTask stask1 = new ShowTask("thread1");
    21         ShowTask stask2 = new ShowTask("thread2");
    22         Thread thread1 = new Thread(stask1);
    23         Thread thread2 = new Thread(stask2);
    24         thread1.start();
    25         thread2.start();
    26     }
    27 }

    两次运行结果:

        

     从两次运行的结果不相同可以知道实现了多线程。

    这里发现采用实现Runable接口的方式创建对象的方式不一样,在这里详细说明一下实现Runable接口的特点

    • 因为是采用实现接口的方式,而Java语言是单继承多实现的,所以一般都采用实现Runable接口的方式。
    • 网上说采用实现Runable接口的方式降低了线程对象和线程任务之间的耦合,我们可以看见线程任务都是写在ShowTask中的,而我们的Thread类声明的对象是用来启动线程的,从这里可看出线程任务和线程对象是松耦的。
    • 线程的声明方式符合面向对象编程的思想。

    2.2.3 实现Callable接口:带有返回值

    贴上学习代码:

     1 package com.shine.study;
     2 
     3 import java.util.concurrent.Callable;
     4 import java.util.concurrent.ExecutionException;
     5 import java.util.concurrent.FutureTask;
     6 
     7 public class MutipleCallableTask {
     8     static class ShowTask implements Callable{
     9         private String name;
    10         public ShowTask(String name) {
    11             this.name = name;
    12         }
    13 
    14         @Override
    15         public String call() throws Exception {
    16             for (int i = 0; i<2; i++){
    17                 System.out.println("线程["+name+"],第"+ i +"次打印。");
    18             }
    19             return "调用线程[ "+ name +" ] 返回值";
    20         }
    21     }
    22 
    23     public static void main(String[] args) {
    24         ShowTask stask1 = new ShowTask("thread1");
    25         ShowTask stask2 = new ShowTask("thread2");
    26         FutureTask<Integer> ft1=new FutureTask<Integer>(stask1);
    27         FutureTask<Integer> ft2=new FutureTask<Integer>(stask2);
    28         Thread thread1 = new Thread(ft1);
    29         Thread thread2 = new Thread(ft2);
    30         thread1.start();
    31         thread2.start();
    32         try {
    33             System.out.println(ft1.get());
    34             System.out.println(ft2.get());
    35         } catch (InterruptedException e) {
    36             e.printStackTrace();
    37         } catch (ExecutionException e) {
    38             e.printStackTrace();
    39         }
    40     }
    41 }

    贴上运行结果:

            

     从结果可以看到主程序接收了由线程任务中返回的值,其返回值在FutureTask对象中使用get()的方法获取到。

    注意这里使用的FutureTask类来配合Callable接口获取线程的返回值,发现这里也可以使用Future接口来配合使用。两者的区别想要了解也可以搜索“FutureTask 和 Future区别”。

  • 相关阅读:
    Sum Root to Leaf Numbers 解答
    459. Repeated Substring Pattern
    71. Simplify Path
    89. Gray Code
    73. Set Matrix Zeroes
    297. Serialize and Deserialize Binary Tree
    449. Serialize and Deserialize BST
    451. Sort Characters By Frequency
    165. Compare Version Numbers
    447. Number of Boomerangs
  • 原文地址:https://www.cnblogs.com/EtherealWind/p/14718149.html
Copyright © 2011-2022 走看看