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