进程与线程
进程和线程的概念:
进程:是操作系统执行的最小单元,每启动一个应用程序就会在内存中创建一个进程。一个进程中包括:一个或多个线程、堆、方法区。进程的崩溃,在进程之间不会相互影响。
线程:是CPU执行任务的最小单元。线程是进程的结构之一,多个线程共享同一进程的堆、方法区的数据。线程的崩溃整个进程也会被杀死。
进程和线程的结构
进程的结构包括:堆、方法区、线程
堆是存放创建对象的地方,方法区是存放常量、静态变量、被加载的类信息等的地方。堆和方法区在同一进程的多个线程之间是共享的数据。
由上图可知线程的结构包括:程序计数器、虚拟机栈、本地方法栈。
程序计数器:记录线程执行位置,在切换线程后可以在相应的位置开始,而不是重头执行。
虚拟机栈:执行java层方法,方法的开始与完成,就会有对应的入栈出栈。
本地方法栈:执行的是native层方法
CPU核心数与线程数的关系
CPU一个核心数对应一个物理线程,一个物理线程通过超线程技术可以模拟出两个线程,所以一个2核的CPU可以模拟四个线程,一个4核的CPU可以模拟八个线程。
CPU时间片轮转
从上面的[进程和线程]图中可以看到,CPU只有一个,而且每次只能执行一个进程。所以在有多个进程时进程可以排队的执行CPU。
CPU会分配给进程的时间叫做时间片,如果进程到时间片结束了还没有执行完,CPU就会记录此次进程的执行的情况,为下次执行做好准备,然后把CPU分配给队列的下一个进程。如果进程在时间片结束之前进程结束或阻塞,CPU会立刻切换其他进程。
CPU的进程的切换也叫上下文切换,每个进程的时间片就很短,而上下文切换时要保存的信息消耗的时间也不少。所以时间片不能太短也不能太长,时间片太短了切换的时间次数增加了,就要花费很多做保存工作,时间片太长了,进程等待执行的时间太长,影响用户体验。
并发和并行的概念
并发是一种程序的结构,表示线程可以交替执行,比如执行到线程A一半后放一旁,开始执行线程B,之后再执行线程A。
并行表示在同一时间内,多个线程可以同时执行。
线程
Java线程一共有两种实现方式,一种是继承Thread,一种是实现Runnable。两种方式都是在run()方法中执行耗时的操作。调用线程对象的start()方法,线程就在进程中等待CPU调用执行了,这种状态也叫做就绪状态。
线程可以分为:新建状态、就绪状态、运行状态、阻塞状态、结束状态,如下图就是线程的五种状态的情况。
新建状态:new一个线程对象实例
就绪状态: 调用线程对象.start()方法,线程就进入了就绪状态,在进程中等待着被执行。
运行状态:此时线程已经得到CPU的执行资源。
阻塞状态:线程被调用sleep()方法、IO请求、wait()方法、等待synchroized锁的时候,线程就会处于阻塞状态。
死亡状态:线程执行结束或异常时,线程就会进入死亡状态。
sleep(long millis):使线程睡眠进入阻塞状态,并在睡眠结束后进入进入就绪状态重新竞争CPU。
wait()、wait(long millis):使当前线程进入阻塞状态,直到其他线程调用此对象的notify()方法或超过设置的等待时间,进入就绪状态。
join():在当前线程调用另一个线程的join()方法,则当前线程进入阻塞状态,直到另一个线程执行完成,当前线程才进入就绪状态
yield():暂停执行的线程对象,并转为就绪状态,让其他排队的线程来执行CPU资源。
interrupt():把线程的中断标识位设置true
,但它并不会中断正在执行的线程,而是需要自己通过isInterrupted()
判断标志位后中断。如果此时线程调用了wait/sleep/join处于阻塞状态,则会触发、InterruptedException
异常事件。
setDaemon:设置线程为后台(守护)线程,后台线程就是为其他线程提供服务的线程,前台线程就是接受服务的线程。
setPriority:设置线程的优先级,范围为1-10,正常的线程的优先级是5
多线程的协作
wait/notify
场景: 假设有线程A和线程B,线程A正在执行。不过线程A下一步操作需要线程B执行
某一段的代码后的结果
,此时需要线程A停止运行,知道线程B执行某段代码后
,线程A才重新执行往下的代码。此时就可以通过(wait,notify)来完成线程间的这种协作。
wait()和notify():是Object类的方法,执行这两个代码都需要在synchronized
同步代码中执行。
wait():使当前线程
进入阻塞状态,并且会释放调用wait()的对象锁。注wait()方法必须在synchronized(调用wait的对象)的同步方法里面,不然会报异常。同时有wait(long timeout)、wait(long timeout, int nanos)
同级方法,就是多了过了时间后会自动转成成就绪状态。
notify():此方法也需要synchronized同步代码块中调用,它会把此对象锁上等待的一个线程释放进入就绪状态。
示例:线程A,等待线程B运行计算结果后的值,重新运行。
public class ThreadA {
public void main() throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
// A拥有了线程b的对象锁,线程为了调用wait()/notify()必须是对象锁的拥有者
synchronized (b) {
System.out.println("等待对象b完成计算。。。");
// 当前线程A等待,并且释放了b的对象锁
b.wait();
System.out.println("b对象计算的总和是:" + b.total);
}
}
}
public class ThreadB extends Thread {
int total;
@Override
public void run() {
// 获取了该对象的锁,才能进入代码块
synchronized (this) {
for (int i = 0; i <= 10; i++) {
total += i;
}
// 完成了计算,唤醒在此对象监视器等待的单个线程
notify();
System.out.println("计算完成");
}
}
}
运行结果:
I/System.out: 等待对象b完成计算。。。
I/System.out: 计算完成
I/System.out: b对象计算的总和是:55
join()
跟上面的wait()的使用场景一样,区别在于此方法的调用并不需要
synchronized
同步代码内进行,并且需要在线程B完成运行完之后,线程A才会由阻塞状态进入就绪状态。
join():使当前运行的线程A
进入阻塞状态,并在调用join的线程B执行完成后,线程A才由阻塞进入就绪状态,继续往下执行代码。
示例:等待线程B完成后才继续执行
public class ThreadA {
public void main() throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
System.out.println("等待对象b完成计算。。。");
// 当前线程A等待,等待线程B执行完成
b.join();
System.out.println("b对象计算的总和是:" + b.total);
}
}
public class ThreadB extends Thread {
int total;
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
total += i;
}
System.out.println("计算完成");
}
}
运行结果
I/System.out: 等待对象b完成计算。。。
I/System.out: 计算完成
I/System.out: b对象计算的总和是:55
sleep(long millis)
场景:使当前运行的线程进入阻塞状态X毫秒后,进入就绪状态,重新竞争CPU资源往下执行代码。
sleep(long millis):是Thread的静态方法,跟前面的wait(),join()一样,也是阻塞当前运行的线程
。不过sleep()方法并不会释放当前线程的拥有的对象锁,则其他线程给予访问不了被锁住的方法。
示例: 让当前线程阻塞1S,再重新执行往下的代码:
public class ThreadA {
public void main() throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
System.out.println("等待对象b完成计算。。。");
Thread.sleep(1000);
System.out.println("b对象计算的总和是:" + b.total);
}
}
public class ThreadB extends Thread {
int total;
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
total += i;
}
System.out.println("计算完成");
}
}
I/System.out: 等待对象b完成计算。。。
I/System.out: 计算完成
I/System.out: b对象计算的总和是:55
可以看到在当前线程A调用sleep()阻塞后,线程B开始执行,线程B执行完后,线程A才往下继续执行。
下面使线程A获取b的对象锁,并调用sleep()不释放锁,看看线程B是否可以调用run()方法。
public class ThreadA {
public void main() throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
// A拥有了线程b的对象锁
synchronized (b) {
System.out.println("等待对象b完成计算。。。");
Thread.sleep(1000);
System.out.println("b对象计算的总和是:" + b.total);
}
}
}
public class ThreadB extends Thread {
int total;
@Override
public void run() {
// 获取了该对象的锁,才能进入代码块
synchronized (this) {
for (int i = 0; i <= 10; i++) {
total += i;
}
System.out.println("计算完成");
}
}
}
运行结果
I/System.out: 等待对象b完成计算。。。
I/System.out: b对象计算的总和是:0I/System.out: 计算完成
因为线程A拥有b的对象锁,并且在阻塞时并没有释放,所以线程B也不能访问run()方法。
yield
场景:当前执行的线程,让出CPU资源,并重新就绪状态,重新和其他线程竞争CPU
yield:是Thread类的静态方法,让当前线程让出CPU资源,进入就绪状态重新和其他线程竞争CPU,所以也有可能再次执行,也可能不执行。
线程总结
- 进程包含:线程、堆(对象)、方法区(常量)
- 线程有五种状态:新建、就绪、运行、阻塞、结束状态
- 线程的wait/notify方法:会使当前的线程阻塞,并释放当前线程所持有的锁,两方法必须在synchronized{}内中调用。
- 线程sleep方法:使当前线程阻塞一定时间后重新竞争,此方法不会释放线程所持有的锁。
- 线程join方法:使当前线程进入阻塞状态,直到调用join的线程执行完成后重新开始竞争。
- 线程yield方法:使当前线程退出CPU执行,重新竞争资源
网友评论