创建和运行线程
方法一,直接使用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层面的状态。