Java多线程
用户态和内核态,如何切换,为什么要切换?
什么是系统中断?
进程与线程的区别?
串行 -> 批处理 -> 进程(进程独占内存空间,保存各自运行状态,相互间互不干扰且可以互相切换,使得操作系统可以并发处理任务) -> 线程(共享进程的内存资源,相互间切换更快速,比进程粒度更小,使进程内的子任务可以并发执行)
-
进程是资源分配的最小单位,线程是CPU调度的最小单位;
- 所有与进程相关的资源,都被记录在PCB(进程控制块)中:
-
进程是抢占处理机的调度单位,不同的进程有不同的虚拟地址空间,同一进程内的不同线程共享该地址空间;线程属于某个进程,同一进程下线程将共享该进程资源;
-
线程只由堆栈寄存器、程序计数器和TCP(线程控制块)组成:
寄存器可以存储线程内的局部变量,但不能存储其它线程的相关变量。
区别
- 进程是资源分配的基本单位;线程是CPU调度的基本单位;
- 进程有独立的地址空间,相互之间并不影响,某个进程崩溃之后并不会影响其它进程的运行;线程没有独立的地址空间,只是进程的不同执行路径,当某个线程崩溃后,其所在进程也会挂掉,因此同一进程下的所有线程也都将停止运行;因此多进程程序比多线程程序更健壮;
- 进程的切换比线程的切换开销大。
Java中进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程;
- 每运行一个Java程序会产生一个进程,每个进程包含至少一个线程(主线程);
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆;
- Java采用单线程编程模型,程序会自动创建主线程;
- 主线程可以创建子线程,原则上要后于子线程完成执行。
线程的start()
和run()
方法的区别
run()
方法并不会创建一个线程,将在调用线程中执行,仅仅只是一个普通方法;start()
方法真正创建一个线程,并会在新建线程中调用run()
方法。
从源码层面来看:
-
在
Thread
类中找到start()
方法,发现该方法中调用了一个native
方法start0()
; -
到Open JDK上找到该方法,以
jdk8u
版本为例:
然后进入src/share/native/java/lang/
目录,点击Thread.c
文件:
可以看到start0()
方法调用了jvm.h
中的JVM_StartThread
方法,然后我们定位到JVM的jvm.cpp
文件:
返回到http://hg.openjdk.java.net/jdk8u/
,点击hotspot
:
然后点击browse
,进入到src/share/vm/prims
目录,打开jvm.cpp
文件,定位到JVM_StartThread
方法:
发现该方法中会通过传入thread_entry
来新建一个新的Java线程,然后定位到thread_entry
:
可以看到,该方法会调用run()
方法。
Thread
和Runnable
是什么关系
Runnable
是一个接口,只声明了一个run()
方法,而Thread
是一个实现了Runnable
接口的类;- 由于Java是单继承的,推荐多使用
Runnable
接口。 - 如何给
run()
方法传参:- 构造函数传参;
- 成员变量传参(setter);
- 回调函数传参:在
run()
方法中调用。
创建线程的三种方法
-
实现
Runnable
接口,并实现run()
方法,然后作为参数传递给Thread
实例:MyRunnable.java
public class MyRunnable implements Runnable { @Override public void run() { // do something } }
Main.java
public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); } }
-
继承
Thread
类:public MyThread extends Thread { @Override public void run() { // do something } }
Main.java
public class Main { public static void main(String[] args) { Thread thread = new MyThread(); thread.start(); } }
-
实现
Callable
接口:MyCallable.java
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { Thread.currentThread().sleep(5000); return "Hello World"; } }
Main.java
public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<String> task = new FutureTask<String>(new MyCallable()); new Thread(task).start(); // 当call()未执行完毕时,get()会阻塞直到其执行完毕 System.out.println(task.get()); } }
如何实现主线程等待子线程返回处理结果
-
主线程等待法:当待处理变量太多时,会导致代码变得臃肿,降低程序可读性,且无法做到精准控制主线程等待的时间;
-
使用
Thread
类中的join()
方法阻塞当前线程以等待子线程处理完毕:比主线程等待法实现简单,控制粒度不够细; -
实现
Callable
接口,通过FutureTask
或线程池获取:FutureTask
见上面创建线程方式三;线程池:
public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService cachedThreadPool = new Executors.newCachedThreadPool(); Future<String> future = cachedThreadPool.submit(new MyCallable()); try { System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } finally { cachedThreadPool.shutdown(); } } }
线程的状态
- NEW:新建态,创建后未启动的线程的状态,即
new Thread()
创建线程对象后调用start()
方法启动线程前; - RUNNABLE:运行态,调用
start()
后正在JVM中执行的线程,包含操作系统中线程的Running
和Ready
态,即正在占用CPU或者等待CPU为其分配时间片; - WAITING:无限期等待,不会被分配CPU执行时间,需要显式被唤醒;
- 没有设置
timeout
参数的Object.wait()
方法; - 没有设置
timeout
参数的Thread.join()
方法; LockSupport.park()
方法。
- 没有设置
- TIMED_WAITING:有限期等待,在一定时间后由系统自动唤醒;
- Thread.sleep(timeout),通过
sleep()
方法进入休眠的线程不会释放持有的锁; - 设置了
timeout
参数的Object.wait(timeout)
方法,通过wait()
方法,在调用wait()
方法之前必须获得对象上的锁,否则程序会在运行时抛出IllegalMonitorStateException
异常,通过wait()
方法进入休眠的线程,会释放该对象的锁; - 设置了
timeout
参数的Thread.join(timeout)
方法; LockSupport.parkNanos()
方法;LockSupport.parkUntil()
方法。
- Thread.sleep(timeout),通过
- BLOCKED:阻塞态,等待获取排它锁的线程的状态;
- TERMINATED:已终止线程的状态,线程已经结束执行,在一个已经终止的线程上调用
start()
方法会抛出java.lang.IllegalThreadStateException
。
sleep()
和wait()
的区别
sleep()
方法属于Thread
类,且必须传入参数,表示休眠时长,线程会进入TIMED_WAITING
有限期等待状态;wait()
方法属于Object
类,当调用无参wait()
方法时,线程会进入WAITING
无限期等待状态,当调用有参wait(long timeout)
时,线程将进入TIMED_WAITING
有限期等待状态。
二者都会让出CPU的使用权:
-
调用
Thread.sleep(milli)
方法进入休眠的线程不会释放其持有的锁; -
调用
thread.wait()
方法进入休眠的线程会释放其持有的锁。 -
sleep()
方法可以在任何地方使用; -
wait()
方法必须在synchronized
方法或synchronized
块中使用,即在调用wait()
方法之前必须获得对象上的锁,否则程序会在运行时抛出IllegalMonitorStateException
异常。
notify()
和notifyAll()
的区别
JVM中的每个对象都会有两个数据结构:
- 锁池:EntryList;
- 等待池:WaitSet。
对象的锁池会存储等待该对象锁的线程,如线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized
方法(或者块),由于B、C线程在进入对象的synchronized
方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入该对象的锁池。
对象的等待池会存储不会竞争该对象锁的线程,如线程A调用了某个对象的wait()
方法,线程A就会释放该对象的锁,同时线程A就会进入该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
notify()
和notifyAll()
的区别:
obj.notifyAll()
会让等待池的所有线程全部进入锁池去竞争获取锁的机会;obj.notify()
只会从等待池中随机选取一个线程进入锁池去竞争获取锁的机会。
Thread.yield()
方法
让当前调用Thread.yield()
方法的线程让出CPU的执行权,与其它线程一起争夺CPU使用权。
interrupt()
方法
已经被抛弃的方法:stop()
方法,因为由外部stop()
来停止线程,可能会导致正在工作的线程突然被中断,导致不可预估的后果。
调用t1.interrupt()
,通知线程t1
应该中断了:
- 如果线程已处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个
InterruptedException
异常; - 如果线程处于正常活动状态,那么会将线程的中断标志设置为
true
,被设置中断标志的线程将继续正常运行,不受影响。
由目标线程自主检查本线程的中断标志位,如果被设置为中断就自行停止线程。