彻底学会Java并发编程——Java线程的理解与精通

47 阅读6分钟

创建和运行线程

方法一,直接使用thread

中间的thread t = new thread()部分是在主线程中创建了一个新的线程。

run()里面是创建的对象要执行的任务。

t.start是启动这个线程。

t.setName是给这个线程t起名。

运行结果如下:

方法二,把线程(thread)和线程要执行的任务(runnable)分开。

先创建任务,在创建线程。

如上图所示,先创建了一个任务r,把r的内容放在run里。

然后创建线程对象t,把任务r作为参数传进去。

然后t.start启动线程。

输出结果如下:

java8以后可以用lambda精简代码:

带@FunctionalInterface注解的接口可以被lambda简化。里面只有一个抽象方法。

Idea快捷简化:鼠标放在方法名上,用alt+enter。

简化完的代码如下:

也可以更简化:

thread和runnable的关系,从源码分析

方法一原理是重写了thread里面的run方法,方法二是把target传入thread里面的run方法。

更推荐第二种方法,因为1更容易和线程池等高级API配合,2让任务脱离了thread继承体系,更灵活。

方法三,FutureTask配合thread

结果如下所示:

task.get()主线程输出返回值,阻塞了2秒。

查看线程进程的方法

windows下用任务管理器来查看进程和线程;cmd中用tasklist指令查看线程;taskkill来杀死线程;

jps指令能看到所有的java进程。

jconsole

在cmd里输入jconsole打开。

需要以如下方式运行你的 java 类:

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote - 
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 - 
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

原理之线程运行

栈与栈帧

左边frame是栈帧,右边是当前方法的变量。

栈内存就是给线程使用的;每个线程启动后,虚拟机就会为其分配一块栈内存;栈是由栈帧组成的,每个栈帧对应每个方法调用时候占的内存;每个栈只能有一个活跃的栈帧。

下面是整个过程的图解:创建的对象都在堆里;下一条执行哪条方法就放到程序计数器里,在线程上下文切换的时候,依靠程序计数器能记住下一条JVM线程指令的执行地址。

每个线程都有自己的栈内存,是相互独立的,栈帧是以线程为单位的,主线程有自己的栈帧,别的线程也有自己的栈帧,在debug的时候可以分别查看。

线程上下文切换

引起上下文切换可能有四个原因:

1、垃圾回收机制

2、cpu时间片用完了

3、有更高优先的程序执行

4、线程自己执行了sleep、lock、yield、wait、join、park、synchronized等方法

线程上下文切换多了会影响性能。

线程上下文切换发生的时候,需要操作系统记录当前线程状态,包括通过程序计数器记录下一条指令,以及虚拟机栈中每条栈帧的信息:操作数栈、局部变量表、返回地址等。

常见的方法

start方法和run方法的对比:

run()是用主线程来执行run方法

例子如下:

输出结果如下:

可以看出都是main线程来执行的,并没有启动新的线程。

Sleep

让当前线程从running进入time waiting状态;其他线程可以用interrupt方法来打断正在睡眠的线程;睡眠结束后的方法不一定会立刻执行;用TIMEUNIT的sleep来代替thread的sleep;

下面是睡眠的线程被唤醒的实例:

上图可知,1秒后执行了interrupt,唤醒了线程。

下图展示用TIMEUNIT的sleep来增加可读性:

yield

让当前线程从running变成runnable,然后调度执行其他线程;和sleep不同的是,此时cpu仍然有可能给这个线程分时间片。

sleep是阻塞指定的时间,然后才能变成yield这种状态。

线程优先级

线程优先级只是提醒任务调度器该线程,调度器可以忽略他。

t1.setPriority(thread.minPriority/thread.maxPriority)

cpu比较忙的时候,优先级高的线程会分得更多的时间片,但是cpu不忙的时候,优先级就没啥用了。

可以用sleep或yield去防止while(true)空转浪费cpu。

join

join:等到该线程执行完再继续执行主线程

结果如下:

等到t1结束后才开始继续执行主线程,此时称主线程和t1是同步的。

下图显示的是有时间限制的join:

结果如下:

interrupt

interrupt打断sleep(阻塞):

结果如下:

interrupt打断正常运行的线程(死循环):

如果没有while(true)里的判断语句,打断后t1线程还会继续运行,打断后isInterrupt显示的是true,通过这个判断来结束这个线程t1。

结果如下:

用interrupt打断park

park是locksupport里一种可以让线程睡眠的方法,

结果如下:

isInterrupt为真的时候,park就会失效。也就是说在上面这个例子中,在isInterrupt后再加上park就会失效。

可以加上interrupt,把isInterrupt设为假,就能再继续park了。如下所示:

主线程和守护线程

java代码要等所有的线程运行完之后才能结束。

守护线程:其他非守护线程都执行完了之后,就算守护线程没有执行结束也会被强制结束。

例如下面这个例子:

通过t1.setDaemon(true)来把t1线程设置为守护线程。

垃圾回收器线程是一种守护线程。

Tomcat中的Accpter和Poller都是守护线程,所有Tomcat接收到shutdown命令后,不会等待他们处理完当前请求。

线程的五种状态

从操作系统的层面来讲:

1、初始状态:在语言层面创建了线程,还没和操作系统线程关联。

2、可运行状态(就绪状态):线程已经创建完毕,可以被cpu调度执行。

3、运行状态:此时线程获取了cpu的时间片,处于运行状态;cpu的时间片用完后,从运行状态状态转为可运行状态,此时对应上下文切换。

4、阻塞状态:由于执行一些阻塞API,BIO,阻塞状态不会用到cpu,解除阻塞状态会进入到可运行状态。

5、终止状态:线程执行完毕,生命周期结束了,不会再进入别的状态。

线程的六种状态

Java API来描述

Thread State枚举,分为六种

1、New:线程刚被创建,没有进行start操作。

2、Runnable:执行start操作后;对应操作系统线程状态的可运行状态、运行状态和阻塞状态,这是因为BIO导致的线程阻塞再java里没法区分;

3、terminated:线程代码执行结束的状态。

4、block、waiting、timed_waiting,java API层面对阻塞的细分。

打印时,用t1.getstate就可以获得该线程的JAVA API层面的状态。