• 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区别”。

  • 相关阅读:
    TCP/IP和HTTP协议代理
    HTTP与HTTPS的区别
    HTTP头HOST
    租户、租户管理员、部门管理员和开发者在APIGW中的角色
    HTTP协议扫盲(八 )响应报文之 Transfer-Encoding=chunked方式
    Java的Socket通信
    开发一个http代理服务器
    需求迭代:迭代需求矩阵
    产品功能对标
    GIT入门笔记(20)- 使用eclipse 基于 git 开发过程梳理
  • 原文地址:https://www.cnblogs.com/EtherealWind/p/14718149.html
Copyright © 2020-2023  润新知