1. 概念
线程,CPU调度的基本单位,被包裹在进程里面,同一个进程里的线程共享同一片内存空间。其中,守护线程是特殊的线程,守护线程自创建伊始会在后台为用户线程提供服务,其生命贯彻进程的整个生命周期。
2. 特点
- 轻型实体:这种轻体现在线程是程序执行流的最小单位,执行时仅向系统请求执行所必须的一点儿资源。
- 共享进程内存空间:同一个进程里的线程拥有同一个内存空间地址(进程空间地址),共享这片内存空间,同一个进程里的线程互相通信不需要调用内核。
- 并发执行:线程通过互夺CPU资源实现并发执行,这种并发效果不是严格意义的同时执行,而是在多个线程之间高速切换,以至于给人并发执行的错觉。
3. 组成
- 线程ID:线程标识符
- 当前指令指针(PC)
- 寄存器集合:存储单元寄存器的集合
- 堆栈:两种数据结构
4. 状态
- 运行:线程占有处理机正在执行
- 阻塞:线程在等待某个事件(如某个信号量),逻辑上不可执行
- 就绪:线程一切准备就绪,等待处理机执行,逻辑上可执行
5. 生命周期
- 新建状态
- 通过
new
方法新建一个线程,该线程被加载进堆内存,此时的线程不具有运行所必须的资源
- 可运行态
- 调用线程的
start()
方法,使该线程获得运行所需的一点系统资源,同时调用run()
方法。
- 不可运行态
- 以下方法会使线程进入不可运行态
- 线程调用
sleep()
方法进入睡眠 - 线程调用
wait()
方法 - 发生I/O阻塞
- 线程调用
- 以下方法可使线程脱离不可运行态
-
sleep()
时间结束 - 调用
notify()
方法唤醒线程 - 等待输入输出完成
-
- 消亡态
- 当线程的
run()
方法走到生命的尽头,线程进入消亡态,消亡了的线程不能再被start()
。
5. 多线程
- 多个线程并发执行的过程称作多线程
6. Java实现多线程的两个方法
1、 继承Thread类,重写run()
方法
代码示例
class SubThread extends Thread{
public void run(){
... ; //线程体,线程所要实现的功能
}
}
public class TestThread{
public static void main(String[] args){
SubThread st = new SubThread();
st.start();
}
}
2、 实现Runable方法,重写run()
方法,并使用Thread构造函数构造线程
代码示例
class SubThread implememts Runable{
public void run(){
... ; //线程体,线程所要实现的功能
}
}
public class TestThread{
public static void main(String[] args){
SubThread r = new SubThread();
Thread st = new Thread(r);
st.start();
}
}
7. 继承 VS 实现
- 实现的方式优于继承的方式,原因如下:
- 继承实现多线程难免会走进单继承的尴尬,而实现的方式则可以避免这个情况实现多继承。
- 在子线程中需要对共享数据进行操作的时候,实现的方式只要在实现Runable的类中定义一个普通的变量就可以实现数据共享,因为在实例化时实现Runable的对象只被创建一次,之后以该对象为参数实例化的线程共享同一片内存数据;而继承的方式每次实例化一个对象会重新开辟一个内存空间,如果要操作同一片内存空间就不得不用
public static final
修饰成常量,这种方式修饰的内存空间生命周期长。
8. 线程的方法
-
start()
:开启一个线程,获得运行所需的系统资源;调用run()
方法 -
run()
:线程体,线程要实现的主体功能 -
sleep(Long l)
:显式地让线程睡眠l毫秒,睡眠时让出CPU执行权 -
join()
:暂停当前线程,让调用该方法的线程参与进来,并在该线程执行结束后才开始执行当前线程 -
currentThread()
:返回当前占有处理机的线程 -
setName()
:设置线程名字 -
getName()
:返回线程名字 -
yield()
:让出CPU的执行权,但并不是说让出CPU执行权就一定是下一个线程执行,因为有可能让出执行权后又抢到执行权,继续执行。 -
isAlive()
:判断一个线程是否消亡
代码示例
public class TestThread{
public static void main(String[] args){
SubThread st = new SubThread();
st.start();
//st.run(); 该方法仅仅是调用st对象的run()方法,而不是一个线程,执行这一步的还是主线程
//st.start(); 一个线程在消亡之后就等同于人类的死亡,是不会有重新开始的
st.sleep(1000); //这里要阐述线程和对象的关系,线程是线程,对象是对象,即使这里使用SubThread对象
//调用的sleep()方法,但是实际上是主线程在执行这个方法,两者不冲突
}
}
9. 线程的优先级
线程的优先级并不是绝对的优先,而是混沌的。被给予了高优先级的线程仅仅只是在概率上有较大概率抢到CPU执行权,而非绝对。线程预设的优先级别有以下三个:
- NORM_PRIORITY = 5
- MIN_PRIORITY = 1
- MAX_PRIORITY = 10
其中,一般我们创建一个线程优先级都为NORM_PRIORITY。
设置和获得线程优先级的方法如下:
- setPriority(int i)
- getPriority()
PS:线程创建时继承父类线程优先级。线程优先级在1~10之间,离开这个范围虚拟机会报java.lang.IllegalArgumentException错误。
10. 线程的同步机制
1. 线程的安全问题
代码示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(true){
if(num >= 1){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}else{
break;
}
}
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
上述程序其中一次输出结果如下:
20
19
18
17
16
15
14
13
12
11
10
10
9
8
7
6
5
4
3
2
1
1
由此可见上述程序是存在安全隐患的,程序的本意是让两个线程相互协作打印出20到1,然而由程序可以看出,出现了一些重复值,这些重复值的出现意味着该程序是线程不安全的。
出现上述线程安全的原因是:当线程t1通过if条件判断后,还没来得及执行num--
的的操作就失去了CPU操作权,而后线程t2进来自然会导致一些数字被打印多次。解决这种线程不安全的问题的关键是:当某个线程进入某个共享区域后,对该区域数据进行操作期间其他线程必须不能再进入该区域,直到这个线程对该区域的操作完毕。
2. 解决线程安全问题的两个方案
- 当多个线程尝试操作共享数据时,将操作共享数据的代码块用
synchrosized(mutex)
声明为同步代码块。
代码示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(true){
synchronized(this){
if(num >= 1){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}else{
break;
}
}
}
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
其中mutex是互斥锁,它可以是任意对象,但必须是同一对象,不同对象代表不同锁。在这里用this表示用RunnableImpl实例化的对象本身。
- 将对共享数据操作部分封装成一个方法,用
synchrosized
修饰。
代码示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(num >= 1){
printNum();
}
}
public synchronized void printNum(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
上述解决方案是将共享数据操作放进一个synchronized声明的方法中,该解决方案的互斥锁默认为this。
其实,解决这种线程问题的方法就是提出“锁”的概念,当一个线程进入共享数据操作时就上锁(lockd),直到该线程对操作完毕。实质上当某个线程进入被synchronized修饰的代码块或方法时,该方法块的状态改变,只有被同步监视器(mutex)标记的线程可进入该区域,当此条线程离开,状态恢复。示意图如下(只是想试试GIF图制作,请多包涵):
11. 线程的通信
线程的通信主要是三个方法:
- wait():使线程挂起,并交出CPU执行权,被挂起的线程会被放进等待队列中等待唤醒
- notify():唤醒等待队列中优先级最高的一个线程
- notifyAll():唤醒所有线程
首先,这些方法不是Java.lang.Thread里的方法,而是Java.lang.Object的方法,也就是所有的对象都具有该方法,但该方法只能使用在同步代码块或同步方法中。