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

原文地址:https://www.cnblogs.com/EtherealWind/p/14718149.html