目录
3.2 为什么调用Thread的start()会执行run(),为什么不直接执行run()?
1. 进程、线程
- 进程:进程即程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程的创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。
- 线程:线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作 时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
1.1 并发和并行的区别:
- 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)。
- 并行:单位时间内,多个任务同时执行。
1.2 多线程可能带来的问题
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但同时也会造成死锁、内存泄漏、 上下⽂切换等问题。
2. 线程的生命周期
- NEW(新建状态): 当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。
- RUNNABLE(就绪状态): 当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。
- RUNNING(运行状态): 如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
- BLOCKED(阻塞状态): 阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状 态。阻塞的情况分三种:
-
- 等待阻塞(o.wait->等待队列): 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。
- 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线 程放入锁池(lock pool)中。
- 其他阻塞(sleep/join) :运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态
- DEAD(线程死亡): 线程会以下面三种方式结束,结束后就是死亡状态。
-
- run()或 call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的 Exception 或 Error 。
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
3. Java中实现多线程的实现
常用方法:
start():启动当前线程;调用当前线程的run()方法
run():需要重写Thread类中的此方法,将创建线程需要执行的操作声明在此方法中
currentThread():返回执行当前代码的线程
getName():获取当前线程的名字
setName(String name):设置当前线程的名字
yield():释放当前CPU的执行权
join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完之后,线程a在结束阻塞状态
sleep(int millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前进程是阻塞状态
isAlive():判断当前线程是否存活(线程执行完之前都是存活的
3.1 实现多线程常用的有四种:
1. 继承Thread类,重写run()方法
/**
* 继承Thread类实现多线程
*/
public class Demo01 extends Thread{
@Override
public void run() {
System.out.println("线程正在工作中..........");
}
public static void main(String[] args) {
Demo01 demo = new Demo01();
demo.start();
}
}
2. 实现Runnable接口
public class Demo02 {
public static void main(String[] args) {
Thread thread01 = new Thread(new Thread01());
Thread thread02 = new Thread(new Thread02());
thread01.start();
thread02.start();
}
}
class Thread01 implements Runnable {
@Override
public void run() {
for (int i=0;i<5;i++) {
try {
System.out.println("我是线程一,正在运行中...........");
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Thread02 implements Runnable {
@Override
public void run() {
for (int i=0;i<5;i++) {
try {
System.out.println("我是线程二,正在运行中...........");
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3. 实现Callable接口(JDK>=1.5)
/**
* 实现Callable接口
*/
public class Demo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new MyThread());
Thread thread = new Thread(task);
thread.start(); //开启线程
System.out.println(task.get()); //获取返回值
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 2;
}
}
4. 线程池方式创建
3.2 为什么调用Thread的start()会执行run(),为什么不直接执行run()?
new Thread()时,线程进入了新建状态。 调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅法,会把 run() ⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线 程⼯作。
总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会 以多线程的⽅式执⾏
3.3 sleep()和wait()方法的区别?
- 两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁 。
- 两者都可以暂停线程的执⾏。
- wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
- wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long timeout) 超时后线程会⾃动苏醒。
3.4 runnable接口和callable接口的异同。
- runnable接口重写run()方法,callable接口重写call()方法
- runable没有返回值,callable有返回值并且接受泛型
- callable接口的call()方法可以抛出异常,runnable的run()方法不能继续向上抛出异常
3.5 volatile和synchronized
- 原子性:确保线程互斥的访问同步代码
- 有序性:有效解决重排序问题。
- 可见性:保证共享变量的修改能够及时可见;
3.5.1 volatile
vilatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。
- 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令就行重排序。
指令重排序简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。
常见的重排序有下面 2 种情况:
- 编译器优化重排 :编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
- 指令并行重排 :和编译器优化重排类似,
3.5.2 synchronized
作用:原子性、可见性、有序性。
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁。
- 修饰静态方法:作用于当前类,进入同步代码前要获得当前对象的锁,synchronized关键字加到static静态方法和synchronized(class)代码块上都是给Class类上锁。
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块库前要获得给定对象的锁。
3.5.3 volatile和synchronized的区别
- volatile只能用在变量上,synchronized可以在类,变量,方法和代码块上。
- volatile只保证可见性,synchronized保证原子性和可见性。
- volatile禁用指令重排序,synchronized不禁用。
- volatile不会造成阻塞,synchronized会。
4. 死锁
4.1 什么是死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
4.2 产生死锁的条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
4.3 如何预防和避免线程死锁
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。(操作系统课上会细讲)。