文章目录
- 一、基本概念
- 1、进程与线程
- 2、并发与并行
- 二、线程的生命周期
- 新建→就绪:
- 就绪→运行:
- 运行→就绪:
- 运行→阻塞:
- 阻塞→就绪:
- 运行→死亡:
- 三、创建线程
- 1、继承 Thread 类创建线程类
- 2、实现 Runnable 接口创建线程类
- 3、使用 Callable 和 Future 创建线程
- 附:创建线程的方式比较
- 四、线程调度
- 线程的强制运行
- 后台线程
- 线程睡眠
- 线程让步
- 改变线程优先级
- 五、线程同步
- 1、同步代码块
- 2、同步方法
- 附:关于释放同步监视块的锁定
- 3、同步锁
- 4、死锁
- 六、线程通信
- 1、传统的线程通信
- 2、使用 Condition 控制线程通信
- 3、使用阻塞队列控制线程通信
一、基本概念
1、进程与线程
进程: 是系统进行资源分配和调度的基本单位。
线程: 也被称为轻量级进程,是进程执行的最小单位。
联系: 线程是进程的组成部分,一个进程可以有多个线程,但是至少要包含一个主线程。
区别:
- 地址空间:每一个进程都拥有自己独立私有的地址空间;线程没有地址空间,线程包含在进程的地址空间中。
- 资源:每一个进程都拥有自己独立私有的资源;线程拥有自己的堆栈、程序计数器和局部变量,不拥有系统资源,同一进程的所有线程共享该进程的资源。
- 健壮性:一个进程的崩溃不会影响其它进程,但是一个线程崩溃会导致其所在的进程崩溃,从而导致该进程中的所有线程崩溃。
- 开销方面:进程占用内存多,进行进程间切换时开销大;而线程占用内存少,进行线程间切换时开销小。
2、并发与并行
并行:在同一时刻,有多条指令在多个处理器上同时执行。
并发:在同一时刻,只有一条指令在一个处理器上被执行,但多个进程指令被快速轮换执行,是的在宏观上具有多个进程同时执行的效果。
二、线程的生命周期
线程的生命周期中有新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead) 5 种状态。线程状态的转换图如下所示:
当程序使用 new 关键字新建一个线程之后,线程会处与新建状态。
新建→就绪:
- 线程对象调用 start() 方法后会进入就绪状态。
说明:①就绪状态并不是线程开始运行,只是进入了可以运行的状态,具体运行时间取决于虚拟机的调度。②启动线程使用 start() 方法而不是 run() 方法。直接调用 run() 方法时,系统会将线程当成普通对象立即执行其 run() 方法。
就绪→运行:
- 就绪状态的线程得到 CPU 调度时会进入运行状态。
运行→就绪:
- 线程失去处理器资源时会转回就绪状态。
- 调用 yield() 方法让线程主动放弃占用的处理器资源时会转入就绪状态。
运行→阻塞:
- 调用 sleep() 方法主动放弃占用的处理器资源。
- 调用了一个阻塞式 IO 方法,在该方法返回前,该线程被阻塞。
- 线程想获得同步锁的时候发现已被占用。
- 线程正在等待某个通知(notify)。
- 程序调用 suspend() 方法将该线程挂起。(容易导致死锁)。
阻塞→就绪:
与上一个状态转换一一对应。
- 经过了调用 sleep() 方法时指定的时间,睡眠结束。
- 调用的阻塞式 IO 方法已经返回。
- 成功获得了该线程想要的同步锁。
- 收到了线程正在等待的通知。
- 处于挂起状态的线程被调用了 resume() 方法恢复。
运行→死亡:
- run() 或 call() 方法执行完成,流程正常结束。
- 线程抛出一个未捕获的异常。
- 直接调用 stop() 方法结束该线程。(容易导致死锁)。
说明:①可以使用 isAlive() 来判断该线程是否死亡:线程处于就绪、运行、阻塞时返回 true,处于新建、死亡时返回 false。②不能对已死亡的线程再次调用 start() 方法来重新启动,否则会抛出异常。
三、创建线程
Java 使用 Thread 类来表示线程,所有的线程对象都必须是 Thread 类或其子类的实例。三种创建线程的方式如下:
1、继承 Thread 类创建线程类
- 先定义 Thread 类的子类并重写 run() 方法,run() 方法的方法体即是线程需要完成的任务。
- 然后创建实例并使用 start() 方法启动该线程即可。
通过继承 Thread 类来创建并启动多线程的示例如下:
//继承 Thread 类创建线程类
public class FirstThread extends Thread{
private int i;
//重写 run() 方法
public void run(){
for( ; i < 100; i++){
//在 Thread 的子类中,使用 this 即可获取当前线程
//Thread 对象的 getName() 方法可以返回当前线程的名字
System.out.printLn(this.getName() + " " + i);
}
}
public static void main(String[] args){
//调用 Thread 的 currentThread() 方法获取当前线程
System.out.printIn(Thread.currentThread().getName() + " " + i);
if(i == 20){
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
//运行代码可以看到三个独立的 i 的值
说明:默认情况下,主线程的名字为 main,启动的多个线程的名字为 Thread-0、Thread-1、…、Thread-n 等。用户可以通过 setName(String name) 方法为线程设置名字。
2、实现 Runnable 接口创建线程类
- 先定义 Runnable 接口的实现类,重写该接口的 run() 方法。
- 创建 Runnable 实现类的实例,将其作为 Thread 的 target 来创建 Thread 对象,然后使用 start() 方法启动该线程即可。
通过实现 Runnable 接口来创建并启动多线程的示例如下:
//实现 Runnable 接口创建线程类
public class SecondThread implement Runnable{
private int i;
//重写 run() 方法
public void run(){
for( ; i < 100; i++){
//在实现 Runnable 接口时,只能使用 Thread.currentThread() 方法获取当前线程
System.out.printLn(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args){
System.out.printIn(Thread.currentThread().getName() + " " + i);
if(i == 20){
SecondThread st = new SecondThread();
//通过 new Thread(target, name) 方法创建新线程
new Thread(st, "新线程2").start();
new Thread(st, "新线程2").start();
}
}
}
//运行代码可以看到两个子线程共用一个 i 值
说明:采用 Runnable 接口的方式创建的多个线程可以共享线程类的实例变量。因为这种方式下,程序所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target,所以多个线程可以共享同一个线程类的实例变量。
3、使用 Callable 和 Future 创建线程
Callable 接口提供了一个 call() 方法作为线程执行体,call() 方法比 run() 强大:call() 方法可以有返回值;call() 方法可以声明抛出异常。Java 提供 Future 接口代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该类实现了 Future 接口和 Runnable 接口。
使用 Callable 和 Future 创建线程的步骤:
- 创建 Callable 接口的实现类,实现 call() 方法,创建 Callable 实现类的实例。(也可直接用 Lambda 表达式创建 Callable 对象)。
- 使用 FutureTask 类包装 Callable 对象,该 FutureTask 对象封装 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程。
- 使用 FutureTask 对象的 get() 方法来获取子线程执行结束后的返回值。
使用 Callable 和 Future 来创建并启动多线程的示例如下:
public class ThirdThread{
public static void main(String[] args){
//创建 Callable 对象
ThirdThread rt = new ThirdThread();
//使用 Lambda 表达式创建 Callable<Integer> 对象,然后使用 FutureTask 对象封装 Callable 对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i = 0;
for( ; i < 100; i++){
System.out.printLn(Thread.currentThread().getName() + " 的循环变量 i 的值" + i);
}
//call() 的方法可以有返回值
return i;
});
for (int i = 0; i < 100; i++){
System.out.printLn(Thread.currentThread().getName() + " 的循环变量 i 的值" + i);
if(i == 20){
//实际上还是以 Callable 对象来创建并启动线程
new Thread(task, "有返回值的线程").start();
}
}
try{
//获取线程返回值
System.out.printLn("子线程的返回值:" + task.get());
}
catch(Exception ex){
ex.printStackTrace();
}
}
}
//运行程序可以看到主线程和 call() 方法所代表的线程交替执行的情形。
附:创建线程的方式比较
- 通过 Runnable、Callable 接口实现多线程的时候还可以继承其他父类,而通过继承 Thread 类实现多线程时不能再继承其它父类。
- 通过 Runnable、Callable 接口实现多线程时,多个线程可以共享同一个 target 对象,适合多个线程处理同份资源的情况。
- 在访问当前线程时,通过 Runnable、Callable 接口实现的多线程必须使用 Thread.currentThread() 方法,而通过继承 Thread 类实现的多线程可以直接用 this。
四、线程调度
线程的强制运行
当在某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被调用的线程执行完毕。
join() 方法的重载形式:
- join():等待被 join 的线程执行完成。
- join(long millis):等待被 join 的线程的最长时间为 millis 毫秒。超过这个时间则不再等待。
后台线程
运行在后台,为其他线程提供服务的线程称为后台线程,例如垃圾回收线程。当所有的前台线程全部死亡时,后台线程会自动死亡。
- 调用 Thread 对象的 setDaemon(true) 方法可以将指定线程设置成后台线程。
- 调用 Thread 对象的 isDaemon() 方法可以判断指定线程是否为后台线程。
说明:将进程设置为后台进程时必须在 start() 方法调用前,否则会抛出异常。
线程睡眠
- static void sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态。
线程让步
- void yield():让当前正在执行的线程暂停,并进入就绪状态。(因此该线程有可能被暂停后立即又被系统调度运行)。
改变线程优先级
在线程调度时,优先级高的线程可以获得更多的执行机会。每个线程默认的优先级与创建它的父线程的优先级相同。
- void setPriority(int newPrioruty):设置线程的优先级,newPriority 为 1-10 之间的整数。
- int getPriority():获取线程的优先级。
Thread 类定义了三个与优先级相关的静态常量:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIOROTY:5
由于不同操作系统上的优先级不同,因此使用静态常量设置线程优先级的程序具有更强的可移植性。
五、线程同步
多个线程同时对一个数据进行操作时,需要人为的添加同步操作从防止造成数据破坏。
1、同步代码块
同步代码块的语法如下
synchronized(obj){
...
//此处的代码即为同步代码块
}
obj 即是同步监视器,线程在执行同步代码块之前必须获得对同步监视器的锁定。在任何时刻,最多只有一个线程可以获得对同步监视器的锁定,同步代码块执行完之后,线程会释放对同步监视器的锁定。
2、同步方法
通过对方法添加 synchronized 关键字使之成为同步方法,该同步方法的同步监视器是 this,即调用该同步方法的对象。
附:关于释放同步监视块的锁定
- 当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器。
- 当线程执行同步代码块或同步方法时,程序调用 sleep()、yield() 方法来暂停当前线程时,当前线程不会释放同步监视器。
- 当线程执行同步代码块或同步方法时,其他线程调用该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器。
3、同步锁
同步锁是指 Lock 对象,通过显示定义同步锁对象来实现同步。Java 为 Lock 提供了 ReentrantLock(可重入锁)实现类,其使用格式如下:
class X{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//...
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
//... method body
}
//使用 finally 块来保证释放锁
finally{
lock.unlock();
}
}
}
ReentrantLock 锁具有可重入性,因此一个线程可以对已被 ReentrantLock 加锁的对象再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套使用。
4、死锁
死锁产生的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
六、线程通信
1、传统的线程通信
Object 类提供了以下三个方法用于线程间通信:
- wait():导致当前线程等待,直到被 nofity() 方法或者 nofityAll() 方法唤醒。可以无参数(一直等待),也可以带毫秒或者耗微秒(指定时间后自动苏醒)。线程调用此方法会释放对同步监视器的锁定。
- nofity():唤醒此同步监视器上等待的单个线程,如果同时有多个线程在等待,则随机唤醒任意一个。
- nofityAll():唤醒此同步监视器上等待的所有线程。
这三个方法必须由同步监视器对象来调用:
- 同步方法:该类的默认实例为同步监视器,所以可以在同步方法中直接调用者三个方法。
- 同步代码块:同步监视器是 synchronized 后括号里的对象,因此必须使用该对象来调用这三个方法。
2、使用 Condition 控制线程通信
Condition 实例绑定在 Lock 对象上,与 Lock 配套使用。其提供以下三个类似传统线程通信的方法:
- await():类似于 wait()。
- singal():类似于 nofity()。
- singalAll():类似于 nofityAll()。
3、使用阻塞队列控制线程通信
Java 提供了一个 BlockingQueue 接口作为线程同步的工具。当生产者试图向 BlockingQueue 中放入元素时,若该队列已满,则该线程被阻塞;当消费者试图从 BlockingQueue 中取出元素时,若队列为空,则该线程被阻塞。
BlockingQueue 包含的方法如下:
抛出异常 | 不同返回值 | 阻塞线程 | 指定超出时长 | |
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e, time, uint) |
队头删除元素 | remove() | poll() | take() | poll(time, uint) |
获取、不删除元素 | element() | peek() | 无 | 无 |