一,相关概念
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为操作系统中运行的一个程序。
线程
线程(Thread)是进程的一个执行单元。
一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支。
关系
进程时线程的容器,一个进程至少有一个线程。一个进程中也可以有多个线程。
在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等,每个线程都有各自的线程栈,都有自己的寄存器环境,自己的线程本地存储。
主线程与子线程
JVM启动时会创建一个主线程,该主线程负责执行main方法。主线程就是运行main方法的线程
Java中的线程不孤立,线程之间存在一些联系。如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
串行
先做任务A,完成后再做任务B,完成后再做任务C,所有的任务逐个完成,共耗时
15 + 10 + 10 = 35分钟
并发
先做任务A,准备了5分钟后,再等待A完成的这段时间内就开始做任务B,任务B准备了2分钟,再等待B完成的过程中开始做任务C,10分钟后任务C结束,共耗时:
5 + 2 + 10 = 17分钟
可以提高我们对事物的处理效率,即一段时间内可以处理或者完成更多的事情。
并行
三个任务同时开始,总耗时取决于需要时间最长的那个任务,总耗时:15分钟
并行是一种更为严格,理想的并发
从硬件的角度来说,如果单核CPU,一个处理器一次只执行一个线程的情况下,处理器可以使用时间片转换技术,可以让CPU快速的在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时进行,如果是多核心CPU,可以为不同的线程分配不同的CPU内核。
二,线程的创建与启动
在Java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)。
Thread类有两个常用的构造方法:Thread()与Thread(Runnable)对应的创建线程的两种方式:
定义Thread类的子类
定义一个Runnable接口的实现类
这两个创建线程的方式没有本质区别
使用继承Thread类创建线程
start执行来启动线程,调用结束并不意味着子线程开始运行,新开启的线程会执行run方法
多线程运行结果与代码执行顺序或调用顺序无关
定义一个Runnable接口的实现类来创建线程
也可创建匿名内部类的方式创建线程
三,线程常用方法
1,currentThread()方法
Thread.currentThread()方法可以获得当前线程
Java中的任何一段代码都是执行在某个线程当中的.执行当前代码的线程就是当前线程
同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象。
在main线程中调用构造方法,所以构造方法中的当前线程就是main线程
启动子线程,子线程会调用run()方法,所以run()方法中的当前线程的子线程
在main方法中直接调用run()方法,没有开启新的线程,所以在run方法中的当前线程就是main线程。
复杂使用
为什么最后一步线程名称是Thread-1,而类名称却还是Thread-0?
因为确实是创建了一个线程且调用了run方法,我们查看创建线程的底层构造方法
这下应该一目了然了吧。
2,setName()/getName()
thread.setName(线程名称) 设置线程名称
thread.getName() 返回线程的名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称。
3,isAlive()
thread.isAlive()判断当前线程是否处于活动状态
活动状态就是线程已启动并且尚未终止
4,sleep()
Thread.sleep(millis);让当前线程休眠指定的毫秒数,
当前线程是指Thread.currentThread()返回的线程
当我们不调用start,直接run
因为没有启动线程,那么就按部就班地执行咯。
使用sleep完成建议的倒计时
5,getId()
Thread.getId()获得线程的唯一编号。
注意:
某个编号的线程运行结束后,该编号可能被后续创建的线程使用。
重启的JVM后,同一个线程的编号可能会不一样
6,yield()
Thread.yield()方法的作用是放弃当前的CPU资源
当两个线程正常执行时:
可见时间是差不多的
当子线程使用yield()线程让步时,
子线程时间由于做出让步,时间明细慢了不少
7,setPriority()
Thread.setPriority(num);设置线程的优先级
Java线程的优先级取值范围是1~10,如果超出这个范围会抛出异常illegalArumentException.
在操作系统中,优先级较高的线程获得CPU的资源越多。
线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程。注意不能保证优先级高的线程先运行。
Java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级(默认为5)即可,即在开发时不必设置线程的优先级。
线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级与A线程的优先级是一样的。
8,interrupt()
中断线程.
注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
9,setDaemon()
Java中的线程分为用户线程与守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出。
子线程按理说无限循环的,因为子线程是守护线程,所以当main线程执行结束后,子线程也很快结束了,因此并没有无限循环。
四,线程的生命周期
线程的生命周期是线程对象的生老病死,即线程的状态
线程生命周期可以通过getState()方法获得,线程的状态是Thread.State枚举类型定义的,由以下几种:
- NEW(新建):创建了线程对象,在调用start()启动之前的状态;
- RUNNABLE(可运行):它是一个复合状态,包含:READY和RUNNING两个状态,READY状态该线程可以被线程调度器进行调度使它处于RUNNING状态,RUNNING状态表示该线程正在执行
,Thread.yield()方法可以把线程由RUNNING状态转换为READY状态; - BLOCKED(阻塞):线程发起阻塞的I/O操作,或者申请由其他线程占用的独有资源,线程会转换为BLOCKED阻塞状态.处于阻塞状态的线程不会占用CPU资源。当阻塞I/O操作执行完,或者线程获得了其申请的资源,线程可以转换为RUNNABLE;
- WAITING(等待):线程执行了object.wait(), thread.join()方法会把线程转换为WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态;
- TIMED_WAITING(等待):与WAITING状态类似,都是等待状态。区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为RUNNABLE状态;
- TERMINATED(终止):线程结束处于终止状态。
线程状态图
五,多线程编程的优势与存在的风险
1,优势
- 提高系统的吞吐量:多线程编程可以使一个进程有多个并发的操作;
- 提高响应性:Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间;
- 充分利用多核处理器资源:通过多线程可以充分的利用CPU资源,避免资源浪费。
2,风险
- 线程安全问题:多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新;
- 线程活性问题:由于程序自身的缺陷或者由资源的稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有一下几种:
(1)死锁(Deadlock):类似鹬蚌相争问题;
(2)锁死(Lockout):类似于睡美人故事,如果王子挂了,那么睡美人就一直是睡眠状态;
(3)活锁(LIvelock):类似于小猫咬自己的尾巴;
(4)饥饿(Starvation):类似于健壮的雏鸟总是从母鸟中抢到实物。 - 上下文切换问题:处理器从执行一个线程切换到执行另外一个线程
- 可靠性问题:可能会由一个线程导致JVM意外终止,其他的线程也无法执行