Java多线程并发
什么是线程和进程
进程
是程序执行的一次过程,即进程从创建到运行再到消亡的过程。
是系统进行资源分配的独立单位
进程之间是相互独立的,因此很难进行进程间的通信。
线程
是比进程更小的执行单位,通常一个进程可以包含多个进程。
是进行资源调度和保护的基本单位
进程拥有独立的内存单元,线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈,多个线程之间共享进程的堆和方法区。
多线程
多线程是多个线程的集合。一个程序(进程)中同时执行一个以上的线程,一个线程不必等待另一个线程执行完毕之后才执行,所有的线程在同一时刻发生。
多线程程序中,多个线程共享内存,带来好处就是提高程序的运行效率。
线程的实现和创建
继承Thread类
Thread类实际是Runnable接口的实现类,继承此类的类或子类的对象都是一个线程对象。继承Thread类需要重写run方法。
启动线程的方法:通过Thread类的start()实例方法。
线程启动后会自行调用run方法中的业务。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
实现Runnable接口
Runnable接口提供唯一的run方法,如何实现该接口的类都需要重写run方法。
多个线程共同处理数据
//如果一个类继承父类,不能够使用Thread类来实现线程,那么可以同实现Runnable接口来完成。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//实现Runnable接口如何启动线程。
public static void main(String[] args) {
/*使用同一个Runnable实现对象构建Thread线程对象*/
MyRunnable myRunnable = new MyRunnable();
/**
* 多个线程使用同一个run方法
*/
Thread thread = new Thread(myRunnable,"第一个线程");
Thread thread1 = new Thread(myRunnable,"第二个线程");
thread.start();
thread1.start();
}
public class MyRunnable implements Runnable {
@Override
public void run() {
//获取当前正在执行的线程对象
Thread current = Thread.currentThread();
System.out.println("当前线程名字:"+current.getName());
}
}
线程的生命周期
线程的生命周期分为:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Block)、死亡(Death)
当一个线程被创建后并不是立刻开始运行,在运行之前,它处于就绪状态,通过调用启动线程方法,获得cpu后进入运行态,当然线程也不是一直处于运行状态,因为多个线程之间的切换,线程让出CPU资源,让线程从运行态进入阻塞态,所有的线程都逃脱不了死亡。
-
新建态
同new关键字创建一个线程,JVM为线程分配内存,初始化成员变量。
-
就绪态
线程调用start()进入就绪态,等待获取cpu资源,JVM为其创建方法调用栈和程序计数器。
-
运行态
获得cpu资源,执行**run()**方法中的任务。
-
阻塞态
因为某些原因让出cpu资源,从运行态进入暂时的阻塞状态,当再次获取cpu时,又从阻塞态进入运行态。
阻塞情况
-
等待阻塞(执行wait())
运行态的线程执行了wait(),线程被放入等待队列(waiting queue)
-
同步阻塞
运行态的线程在获取对象的同步锁时,发现该对象的同步锁已经被其他线程占用,因此该线程进入阻塞状态。
该线程被放入锁池(lock pool)
-
其他阻塞
运行态的线程执行了sleep方法或join方法,或者发出IO请求
-
-
死亡态
-
正常结束
run()或call()结束
-
异常结束
线程抛出异常(Exception)或错误(Error)
-
调用stop方法
线程调用stop方法(容易产生死锁)
-
- 就绪状态—>运行状态:获得处理机资源(分派处理机的时间片)
- 运行状态—>就绪状态:1)处于运行状态的进程时间片用完 2)当有更高优先级的进程就绪时
- 运行状态—>阻塞状态:1)进程请求资源(外设)使用和分配 2)等待某一事件的发生(IO操作完成)
- 阻塞状态—>就绪状态:当进程等待事件到来(IO操作结束或者中断的结束)
终止线程的方式
-
正常结束
程序正常结果,线程自动销毁
-
自定义标志控制
正常情况下run方法执行完毕,线程就会终止,有些特殊情况下,线程需要很长的运行时间,只有在满足外部某些条件下,才能结束进程。
可以通过自定义变量来控制执行的时间。
例如:设定一个boolean类型的变量,执行while()语句,控制循环。
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { //设定变量exit来控制run() while (!exit){ //需要执行的业务 } } } //当exit为true时,while语句不成立,run()方法结束,线程结束。 //exit被volatile关键字修饰,实现同步代码,保护线程的安全。
-
Interrupt()方法结束线程
-
线程处于阻塞状态时
使用sleep()、同步方法wait()等方法,进入阻塞状态。
当调用线程的Interrupt()方法时,会抛出异常(InterruptException),通过在代码块中catch捕获异常,通过break关键字跳出循环状态。(调用Interrupt方法一定要捕获异常通过break跳出循环,才能正常结束run方法)
-
线程处于运行状态时
通过isInterrupted()方法判断线程是否处于阻塞状态,如果进入阻塞状态,用阻塞状态的方法来完成线程终止。
public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕获到异常之后,执行 break 跳出循环 } } } }
-
-
stop方法
前面也说了stop方法虽然可以终止线程但是会让线程不安全。
stop方法是强制线程终止,就像把计算机的电源断了,而不是正常关机,很可能产生数据上的异常。
比如一个线程中有其n个子线程,子线程被同步锁绑定,为了保证数据的安全性,如果执行stop方法,停止该线程,那么其子线程会释放所有的同步锁,**同时抛出TheadDeatherror的错误,**可能导致子线程中数据的不一致。
start()和run()区别
start()
是用来启动线程,new Thread通过调用start方法让线程处于就绪状态,等待获取cpu。
它不需要等待run方法结果就可以去执行其他任务,实现了多线程的过程。
Thread thread = new Thread(myRunnable,"第一个线程");
Thread thread1 = new Thread(myRunnable,"第二个线程");
thread.start();
thread1.start();
//两个线程争夺cpu,在控制台你可以看到两个线程交替输出
run()
当线程获取cpu时,线程进入运行态,执行run方法中内容,run方法是线程需要完成的工作内容。run方法结束,线程终止。
线程的基本方法
线程等待(wait)
当前拥有对象监视器的线程调用wait方法,会释放对象监视器的所有权,进入等待队列。
只能等待其他拥有该对象监视器的线程唤醒或者被中断,该线程重新获取对象监视器的所有权后才开始执行。
wait方法一般用在同步方法或同步代码中
补充:调用wait方法会释放cpu资源和锁资源,这两个资源。
线程睡眠(Sleep)
线程调用sleep方法进入睡眠状态,与wait方法不同之处在于sleep方法不会让线程释放所占有的对象锁,线程进入Time_Waiting状态,
wait方法让线程进入Waiting状态。
补充:sleep方法会让当前线程释放cpu资源,但是不会释放当前线程所占有的锁资源(重要)
锁是用来同步的,保证线程的安全,如果当前线程调用sleep方法后,其他的线程如果需要锁才能够运行,那么它们只能继续等待,如果说某些线程只需要cpu的资源就可以运行,一旦得到cpu资源它们就会立刻执行。
线程让步(yield)
yield让线程让出当前所持有的cpu资源,和其他线程一起重新争取cpu资源。一般情况下,优先级高的线程有更大的概率获取到cpu资源,但这不是绝对的,不同操作系统对线程优先级有着不同的敏感度。
线程中断(interrupt)
中断一个线程,但是interrupt方法本身并不是改变线程的状态,而是给对应线程设置一个中断标志位,通过这个中断标志位来判断是否需要中断线程。
runnable状态
如果一个线程处于运行状态,使用方法Thread.interrupt()只是将这个线程的中断标志位设置为true,此时线程还是处于runnable状态,
具体在线程的run方法中合使的位置检查中断标志位,若为true,说明需要进行中断响应处理,若为false,说明不需要进行中断处理。
例如:
public class InterruptRunnableDemo extends Thread {
@Override
public void run() {
//若为true,进行中断,若为false,执行while()循环体
while (!Thread.currentThread().isInterrupted()) {
// ... 单次循环代码
}
System.out.println("done ");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new InterruptRunnableDemo();
thread.start();
Thread.sleep(1000);
thread.interrupt();//true
}
}
/*
interrupt()设置thread线程的中断标志位为true
isInterrupted()方法判断当前线程的中断标志为是否为true,
*/
Waiting / Timed-Waiting状态
使用sleep、同步wait、join进入等待状态,当线程处于这种状态时,调用Thread.interrupt(),会抛出对应的InterruptedException异常。注意:异常抛出后进行处理,此时线程的中断标志位会被清空(true -> false),线程提前结果Waiting / Timed-Waiting状态。
例如:
Thread t = new Thread (){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//exception被捕获,但是为输出为false 因为标志位会被清空
//此时线程的中断标志为false
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();//置为true
Join等待其他线程终止
join()方法:调用join方法的线程获取cpu资源,其他线程被迫阻塞,只有等当前调用join方法的线程结束释放cpu资源后,其他线程才可以运行。
若在当前线程中的子线程调用join方法,当前线程需要等待子线程运行完后,才可以运行,因此join方法也称为:等待其他线程终止。
很典型的主线程中有子线程,子线程调用join方法,主线程就需要等待调用join的线程运行终。
public static void main(String[] args){
System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6();
thread1.setName("线程 B");
thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");
}
线程唤醒(notify)
和wait方法配套使用,应用与同步场景。
持有锁资源的线程使用wait方法释放锁资源,进入Waiting状态等待唤醒,只能是拥有当前对象锁的线程调用notify方法唤醒等待获取当前对象锁的线程,notify()只能唤醒单个线程并且是随机的,被唤醒的线程并不能立刻执行,需要等待唤醒它的线程将当前对象锁释放之后,它获取到对象锁之后才继续运行。**notifyAll()**唤醒等待同一个对象锁上的所有的线程。
Java后台线程(守护线程)
-
定义
守护线程又叫做服务线程,它是运行在后台的一个特殊线程。
主要为用户线程(普通线程)提供服务,当JVM中只剩下守护线程时,JVM将自动退出。
-
优先级
守护线程的优先级较低,用于为JVM中其他的对象和线程服务(垃圾回收就是一个守护线程)
-
设置
通过 setDaemon(true)来设置线程为“守护线程”;可以将一个用户线程设置为守护线程,守护线程中产生的新线程也是守护线程。
-
Example
垃圾回收就是一个经典的守护线程,它是一个优先级较低的线程,JVM中运行的普通线程产生的垃圾就由它来回收,当JVM中不再运行用户线程时,垃圾回收线程就会自动退出。
线程上下文切换
利用时间片轮转的方式,实现了一个CPU可以为多个线程服务。
CPU个每个线程设置一定的服务时间,当前服务的线程时间到了,就将当前线程的状态保存下来,接着去为下一个线程服务,多个线程轮巡,当下次再遇到这个线程时,加载上一次状态继续服务。这就是上下文切换过程。
进程
是程序执行的一次过程,即进程从创建到运行再到消亡的过程。
是系统进行资源分配的独立单位
进程之间是相互独立的,因此很难进行进程间的通信。
上下文
某一时间点上cpu寄存器和程序计数器内容
寄存器
CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
程序计数器
专用寄存器,专门用来表明正在执行的cpu指令序列,用于存储cpu正在执行的指令位置或下一次执行的指令。
PCB-“切换帧”
进程是由内核来管理和调度,进程的切换只能发生在内核中。线程的上下文的切换也是在内核(操作系统的核心)中,上下文切换中信息内容是保存在进程控制块中(PCB)的。当再次用到时直接从这里调用。
上下文切换的活动
- 挂起一个线程,将挂起的线程在CPU中的状态(上下文)存储在内存中
- 在内存中检索下一个线程的上下文并将在CPU的寄存器中恢复
- 在程序计数器中找寻上一次执行指令位置,用来进行这次运行的开头。
线程上下文切换的原因
- 在当前线程的cpu时间片用完之后,cpu正常去调用下一个线程。
- 多个线程抢占锁资源,获取锁的线程执行,其他线程挂起。
- 当前线程遭遇IO阻塞,调度器将当前线程挂起,继续执行一下。
- 用户代码挂起当前任务,让出cpu时间片
- 硬件中断
java中的线程调度
抢占式调度
- 线程执行的时间、线程的切换都是由系统来决定调度的
- 不会出现一个线程阻塞导致后续的都无法进行
协同式调度
- 一个线程执行完毕后通知系统切换到下一个线程继续执行(如同接力赛一般)
- 线程执行的时间由其本身决定,线程的切换是可预知的,不存在线程同步问题
- 若一个线程出现问题阻塞了会导致其不会通知系统导致后续线程无法运行
java中线程采用抢占式调用,java中线程是按照线程的优先级来分配cpu时间片,优先级越高可能可以获取到更多的时间片,优先级低可能获得少量的时间片,但不会存在得不到时间片的情况。