最近刚好辞职休息闲来无事刚好总结一下Java多线程中是东西,于是先从基础开始否则根基不牢固其他的都是瞎搞;
一、Java中线程创建与分类
至于什么是进程、什么是线程、进程与线程区别,多线程的好处之类的就不多说了,如有不明白的请放弃吧。
1.Java线程创建与启动
Java中创建的线程常见的方式有:
1.继承Thread类重写run方法;
2.实现Runnable接口;
3.借助Callable和Futurn实现多线程;
当然Java团队也封装了许多多线程的工具类例如:Timer等,但是内部开启线程方式依然是上述3种;
Java启动(start)的线程的方式有:
1.Thread类中的start()方法;
2.Java中的线程池提供的Api;
2.后台线程/守护线程与前台线程/用户线程
Java中线程又可以分为两种线程:
1.前台线程也称为用户线程UserThread
: 就是我们日常开发中并且未进行设置线程类型的线程;前台线程具有个体性,不具有提供公共服务的性质,即完成一个Task就自动退出了;Java工程中主线程(MainThread)和Android中的UI线程都属于前台线程;
2.后台线程也称为守护线程DaemonThread
: 指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程,Java中一个普通的线程通过setDaemon(true)设置为后台线程;
- 关于守护线程几个知识点
- 1.守护线程必须在Start之前调用setDaemon(true)设置为守护线程,即已经开始运行的线程不能设置为守护线程;
- 2.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常;
- 3.线程的继承性,即在守护线程中创建的线程还是守护线程;
- 4.守护线程不能单独存活在JVM,即当程序中用户线程全部结束后JVM会立即杀死所有守护线程【不管是否执行完毕,包括finally中也会不执行】然后App退出;
- 5.守护线程(setDaemon(true)),当守护线程中执行完毕后,同样会自动结束(Thread.isAlive()为false);[在网上看到有人说后台线程不会主动结束,只有所有的前台线程都死亡,后台线程会自动结束,个人测试这是个错误结论],以下是测试代码如有错误纰漏望诸位大佬及时批评指正:
public static void main(String[] args) {
Thread thread = new Thread(() -> {
long l = System.currentTimeMillis();
try {
System.out.println(“Daemon -------->START: " + l);
while (System.currentTimeMillis() - l < (3 * 1000)){}
}finally {
System.out.println(“Daemon --------> END :” + (System.currentTimeMillis()-l));
}
});
thread.setDaemon(true);
thread.start();
long l = System.currentTimeMillis();
while (System.currentTimeMillis() - l < (5 * 1000)){}
System.out.println(”--------> Alive: “+ thread.isAlive()+” isDaemon:"+thread.isDaemon()+" Priority:"+thread.getPriority());
}
代码执行结果:
Daemon -------->START: 1543073677716
Daemon --------> END :3000
--------> Alive: false isDaemon:true Priority:5
二、线程生命周期和状态
在Thread类[JDK8]中的内部枚举类State中定义了:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED这6种状态;
同样在网上可以看到很多人把Java线程状态分为:New、Runnable、Running、Blocked包括(blocked、wait、time_wait)、Dead这5中状态,在这里我们按照Java中Thread内部枚举类State为准;
1、新建(NEW):
创建线程时通过new方法来创建,处于新建(new)状态的线程JVM已经在内存空间分配好内存空间,创建好还没有没有执行start方法的线程状态;
之前有看到技术博客看到就绪状态,但是在jdk中的Thread源码中并没这个状态,这里只做记录一下,有机会验证;
对已经new好的Thread进行start()方法之后,Java虚拟机会为其创建方法调用栈和程序计数器,此时线程并没有开始运行,只是表示该线程可以运行了,该线程这时所处状态为:就绪状态;
至于该线程何时开始运行,取决于JVM里线程调度器的调度。
2、可运行(RUNNABLE):
Thread.Start()函数之后,线程进入RUNNABLE状态,可能是正在执行,也可能处于等待CPU资源阶段;
运行状态的线程下一步线程状态变化最为复杂,它随着CPU资源时间片等变量随时可能转变为阻塞状态、就绪状态和死亡状态。
补充:RUNNABLE这个状态其实包含了5种状态中的就绪Runnable和运行Running两个转态;
3、阻塞(BLOCKED):
处于运行状态的线程,由于等待监视器锁等原因让出CPU资源,并暂时停止自己的运行,等待再次运行(Running)的状态称为:阻塞状态;
由运行状态转阻塞可能原因有:
1.调用了wait()方法;
2.线程锁导致阻塞(synchronized、ReentrantLock),
3.等待网络 、I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
4、等待(WAITING)
在线程中主动执行线程等待API使得线程进入WAITING状态,例如:Object.wait()、Thread.join()、LockSupport.park(),对应的调用Object.notify()或者Object.notifyAll()、加入的线程进入TERMINATED、调用LockSupport.unpark()后线程再次转如RUNNABLE;
5、超时等待(TIMED_WAITING)
与等待(WAITING)不同是这个等待是不许要唤醒,而是当等待时间完成就会自动转为RUNNABLE;当然了在发起等待时必须设置等待超时时间,例如:Thread.sleep(long)、Object.wait(long) 、 Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil();
6、死亡(TERMINATED):
线程在run()方法执行结束后进入死亡状态。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。
Java线程函数与状态图
备注:上图来自《Java多线程编程核心技术》中第七章7.1中图7-3;
三、线程优先级
线程的优先级是和操作系统相关联的,在操作系统中线程优先级较高的线程在抢占CPU资源时抢占到CPU资源的概率更大。在代码中设置线程的优先级有助于线程调度器更加合理快速的分配CPU资源。
- Java线程调度
- 线程调度:指系统为线程分配处理器使用权的过程。JVM主要调度方式有:协同式线程调度 和 抢占式线程调度
- 协同式调度模型:指让所有的线程轮流获得cpu的使用权,并且按每个线程占用的CPU的时间片由线程自己决定;在此多线程系统中,线程的执行时间由线程本身控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上继续,据说在工业制造业中应用广泛;
- 抢占调度模型:指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU资源;在抢占调度模型中的多线程系统中,每个线程将由系统来分配时间,线程的切换不由线程本身决定,Java中Thread.yield() 可以让出执行时间,但是线程无法获取执行时间;
- JVM中默认使用的就时抢占调度模型
在抢占调度模型中线程执行时间是系统可控的,这样就不会出现一个线程导致整个进程阻塞的问题;
1.优先级的划分与设置;
a.在Java中线程优先级的范围从1到10,10是最高优先级,1是最低优先级,5是正常优先级;
b.Thread类中提供了静态方法setPriority(int newPriority)用于设置线程的优先级;
c.线程的优先级应该在start()方法调用之前被设置;
d.在Java中新建线程时默认优先级为NORM_PRIORITY =5,包括Main主线程、守护Daemon、Android主线程的优先级同样是NORM_PRIORITY;
JavaJDK【1.8】中setPriority(int newPriority)源码:
//JDK中预定义的3个常量优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();//检查是否可以修改线程优先级
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {//如果设置优先级不在范围内就抛出参数异常
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {/*与线程组优先级相比较*/
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);/*通过native方法设置优先级*/
}
}
在JDK源码中可以看到开发者设置的线程优先级必须在1-10之间反之就抛出IllegalArgumentException;
在实际开发中可以根据场景的需要设置线程优先级,提高代码的质量和用户体验,不过这是一个技术活,设置不当可能适得其反;个人只用过在一些优先级低的Task中将线程的优先级设置的小于NORM_PRIORITY ,尽量降低和View线程抢占导致界面卡顿便于GC回收;
2.线程优先级的继承性
线程是继续性很好理解,类似Java的继承特性一样,即在子线程继承了父线程后,同时也继承了父线程的优先级;
- 这里的继承不仅仅是指Java中extends类继承特性,
新线程同样会"继承"创建它的线程的优先级
;即:实际开发中开启新线程必然是在一个线程中创建一个新的线程,即如果A线程中创建B线程,如果没有给B线程设置优先级,那么B线程默认继承A线程的优先级,前台线程和守护线程中遵循这规则
;
3.线程优先级的随机性
优先级的随机性其实是指:多线程中线程执行顺序并不是完全按照线程优先级从高到低的顺序执行,线程执行顺序具有随机性;
- 线程在抢占资源时的随机性,即多个不同的优先级线程同时抢占CPU等计算机资源时具有随机性,简单的说就是不同的优先级线程抢占资源时并不是优先级高的线程就一定会先获取资源,而是获取资源的线程具有随机性;
- 刚看到这里可能会有人疑惑的是:随机性是不是和优先级相矛盾;
实际情况是多线程中优先级高的线程在抢占资源时获取资源的概率随着优先级而提升,并不是一定是优先级高的就一定优先获得资源;
四、线程中几个重要的函数
在Java的Thread类中提供了诸多对线程操作的函数,在下文中选择在开发中使用频繁比较重要的函数进行介绍;
1.sleep();
线程修眠:sleep是Thread中的一个静态函数,线程中调用sleep()后,当前立即程序停止执行,让出CPU资源给其他线程,当前线程转入TIMED_WAITING超时等待状态,但是线程并没有释放持有的资源锁(线程锁),休眠结束后线程返回就 绪状态,重新开始抢占CPU资源;sleep()也是响应中断;
补充:
- Thread.Sleep( long time)会阻塞当前线程一段时间(time)后转为就绪状态;
- Thread.Sleep(0)可实现“触发JVM立刻重新进行一次线程调度,即线程之间重新竞争CPU等资源”;
2.yield();
线程让步:yield()也是Thread中的一个静态函数,线程中调用yield()后,当前线程会立即让出拥有的CPU资源,并转入就绪状态;提供所有等待的线程重新进行一次CPU竞争,但是抢占结果依然是随机的,同时JVM立刻重新进行一次线程调度即线程之间重新进行一次CPU资源竞争;
补充
- Thread.yield()函数可以让当前正在运行线程暂停转为就绪状态,并不会阻塞线程;
- yield()只能使相同优先级或更高优先级的线程参与CPU资源竞争而有执行的机会。
- 执行yield()的线程在经过一次线程调度后,有可能再次获取CPU资源而马上又被执行;
3.join();
线程插队:join()也是Thread中的一个静态函数,在一个正在执行的线程中调用其他线程的join()函数,当前运行的线程会被立即堵塞,直到目标线程目标线程执行完成进入TERMINATED状态后,再次执行该线程;
3.wait();
等待:线程中执行了wait()函数后,线程立即转为WAITING等待状态,即时释放对象锁,解除对资源对象的占用;只用等到wait时间结束或者通过notify()、notifyAll()函数换醒线程,再次开始抢占CPU和对象锁等资源进行下一次运行;wait()函数是响应中断的,即在WAITING状态是调用interrupt()后立即抛出InterruptedException异常,线程进入TERMINATED,等待JVM回收;
5.interrupt();
中断线程、
wait()和sleep()比较
:
a.都支持超时响应和中断响应,中断抛出异常并转入TERMINATED;
b.wait让出cpu和锁资源,当是sleep休眠后任然持有CPU和锁资源;
c.sleep超时后仍然持锁,只需重新先抢占CPU锁资,wait超时或者notify、notifyAll需要重新先抢占CPU和锁资源;
d.wait、notify、notifyAll必须在synchronized代码中,sleep可以在随意地方调用;
使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
参考文章/资料
《Java多线程编程核心技术》