作者:黄钰朝
邮箱:[email protected]
日期:2019年7月16日
今天的学习内容是多线程编程的基础知识,包括对线程和进程,并发和并行,多线程和多进程的基本理解,创建线程的三种方式的原理和区别,线程的生命周期,线程的属性:守护线程,线程优先级,线程的三个方法:sleep,yield,join,线程池的使用。
线程是建立在进程的基础上的,因此要了解线程首先需要知道进程。
进程是指一个运行中的程序,是程序运行的实例,是正在执行的一串指令,也就是说,程序并不是直接被运行的,程序只是一些指令,数据的集合,真正在运行的时候,是被操作系统加载进入内存,以进程的形式在计算机中被CPU执行,一个程序通常会产生多个进程,可以由多个用户同时使用。每个进程都需要占用一定的系统资源,比如CPU资源,内存资源等等,操作系统会给每一个进程分配一定的内存空间,并且分配一个进程id(PID),每个进程独自占有这些资源,不能共享。因此进程是资源分配的基本单位。
线程是属于进程的,线程是一串正在被执行的指令,一个程序的进程往往要同时完成不同的任务,而进程就是这些一个个的任务,不同的线程共享父进程的资源,但是为了不相互干扰,每个线程也有自己私有的资源,比如独立的堆栈,程序计数器和局部变量,一个进程至少拥有一个线程(一个程序启动当然需要执行一定的指令),通常被系统启动的线程称为主线程,一个线程可以被启动,挂起,停止等等操作,因此线程是运行和调度的基本单位。
一个CPU在同一时间只能执行一个线程,因此,只有一个CPU核心时,只能是把时间“分片”,轮流执行不同的线程,从而达到同时进行的效果,实际在时间上是”不同步“的,这就是"并发"
并行是多个处理器同时执行多条线程,也就是把多个线程分配给不同的处理器同时执行,在时间上是真正“同时”的
多线程和多进程都可以实现并发,但是进程拥有自己的独立资源,有独立的内存空间,多个进程间不能共享数据。
而线程虽然也有自己独立的堆栈,程序计数器和局部变量,但是多个线程共享父进程的共享变量和部分环境,因此使用多线程进行并发编程更加方便,但是也因为存在共享的资源,尤其是数据,并发时因为实际上多个线程在时间上是不同步的,因此也更容易出现问题,这就是线程安全问题。
上下文切换也就是环境切换。包括进程的上下文切换和线程的上下文切换。
CPU从一个线程切换到另外一个线程执行,需要先保存当前的线程的状态和数据,然后载入另外一个进程的状态和数据进行执行,这个过程就是上下文切换,需要消耗一定的系统资源,这便是多线程带来的额外开销。
线程在Java中的抽线是Thread类,启动线程其实只需要调用Thread类对象的start方法。如下:
Thread thread = new Thread();
thread.start();
上面的代码已经启动了一个线程,但是因为没有给这个线程添加执行体代码,因此这个线程一开始执行就结束了。其实我打开看了一下源码,start方法其实只是做了一些检查和准备工作,而且是被当前线程执行的,新启动的线程实际执行的是Thread对象的run方法。而里面的run方法是这样的:
/**
* If this thread was constructed using a separate
* Runnable
run object, then that
* Runnable
object's run
method is called;
* otherwise, this method does nothing and returns.
*
* Subclasses of Thread
should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
public void run() {
if (target != null) {
target.run();
}
}
从注释和源代码可以看出来,如果我们像上面那样启动线程,那么它会先看看target是否存在,如果存在,那么执行target的run方法,如果为null,那么啥也不做。而且,注释提示,子类应该重写run方法。看一下target变量:
private Runnable target;
可以看到target其实是一个实现了Runnable接口的类的对象(并且其实这个target是通过Thread的有参构造方法传进来的)
因此,为线程添加执行体代码有两种基础的方式:
后面为了能够接收线程执行的返回值,并且能够声明抛出异常,所以又多弄了个FutureTask和Callable接口来添加执行体代码。
这个感觉不用说了,把你想执行的代码写到重写的run方法中,然后用这个子类的对象来启动线程就行了
//继承Thread类
public class MyThread extend Thread{
@Override
public void run(){
do something...
}
}
然后:
MyThread myThread = new Thread();
myThread.start();
知道了上面的原理之后,贼简单:
//实现Runnable接口
public class MyRunnableClass implements Runnable{
public void run(){
do something...
}
}
然后:
//把Runnable对象传进去
Thread thread = new Thread(new MyRunnableClass());
thread.start();
完事了。
Callable接口其实也是从Runnable接口发展来的,底层还是Runnable接口,因为Callable接口中的call方法是被FutureTask的run方法调的,而FutureTask其实RunnableFuture的实现类,而RunnableFuture又是继承自Runnable接口的。
说白了,FutureTask的对象其实也就是target对象,只是它在run方法中调用了Callable接口实现类的对象中的call方法,所以最终执行的是call方法,但是为什么要加一个FutureTask对象做中介?目的就是在中介FutureTask的run方法中实现接收返回值,接收call方法抛出的异常等等扩展的功能,也就是说,Callable通过FutureTask的辅助,实现了Runnable接口的增强。
也正因为有了一层中介,Callable的对象要使用FutureTask再包装一层再传给Thread对象。
因为时间有限,我就不自己写示例代码了,直接摘了菜鸟教程的一段代码,需要注意的是,call方法重写时的返回值类型,需要通过泛型传递给FutureTask对象,比如这段代码中的Integer类型返回值。
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
线程并不是一创建就开始执行,也不是一直在执行。线程在生命周期的不同时期具有不同状态。
新建状态就是刚刚创建(刚刚被new出来),还没有被执行,也还没有”等待执行“。
线程创建之后,调用start方法,就进入”等待执行“的状态了,就是”就绪“,也就是可以执行了,但是还不是开始执行。什么时候才开始,取决于JVM线程调度器的调度,相当于开始"排队"等待被执行了。
也就是线程中的代码被执行的过程,但是这个状态不是一直保持,会有中断。
阻塞也就是运行状态中断了,停止执行,并且释放出所占有的资源,进入的等待状态,其实不会被执行,也不是“等待执行”,它需要再次进入就绪状态,才能被接着执行。
线程可以主动进入阻塞状态,比如调用sleep方法,也可以被动进入,比如由于阻塞式I/O的方法没有返回,该线程被阻塞。
顾名思义,就是线程结束了。包括:
死亡的线程无法再次启动。
线程拥有一些属性,包括:守护线程,线程优先级,线程组和处理未捕获异常的处理器。用来指示JVM如何对待这个线程。
守护线程其实就是后台线程,也叫“精灵线程”,把一个线程设置为守护线程是为了让该线程为其他线程服务,并且不要阻碍JVM的关闭。
普通线程在停止前JVM不会自动关闭,而普通线程全部死亡时,JVM就会自动关闭,而不会理会是否还有守护线程在运行。
具体的设置方法为:setDaemon(boolean isDaemon);
注意:这个方法必须在start方法之前调用,否则会报错IllegalThreadStateException
线程优先级越高,被执行的机会越多。
线程优先级的范围是1·10,Thread类中有三个静态常量:
PS:后面两个属性太啰嗦了,不想写了,用的时候再说
Thread类提供了一些方法来方便我们对类进行控制。
让类停止运行一段时间(进入阻塞状态),参数为停止的毫秒数
这个方法是用来线程让步,不会阻塞该线程,会进入就绪状态让JVM线程调度器重新调度,也就是让优先级高的线程执行,如果该线程刚好就是优先级最高的,那么继续执行。
让当前线程等待被调用了jion方法的线程执行完毕,再继续执行。
关于线程池的知识,这篇博客:深入理解 Java 线程池:ThreadPoolExecutor 讲得很明白了,我还是等做了项目实践之后,再来写心得吧,这一块暂且留空。
之前训练营和考核时期,没有系统地学过多线程编程,基本都是遇到问题再去查,现查现用,今天比较完整地学习了多线程的基础知识,还专门去看了源码一探究竟,感觉对线程的理解比以前更加深刻了,但是多线程的知识远远不止这些,接下来的打算是明天学习线程安全和同步机制的知识,后天去学更深入的同步器和一些Java并发集合类,多线程的实践任务已经发下来了,主要还是要多在项目中实践。
小结今日收获: