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,被设置中断标志的线程将继续正常运行,不受影响。
由目标线程自主检查本线程的中断标志位,如果被设置为中断就自行停止线程。
线程状态转换


本文深入探讨Java多线程的概念,解析用户态与内核态切换原理,系统中断及进程与线程的区别。阐述Java中进程与线程的关系,线程创建方法,线程状态及其转换,以及线程同步机制,如sleep(), wait(), notify()和interrupt()等方法的使用技巧。
1582

被折叠的 条评论
为什么被折叠?



