进程和线程的概念
关于线程和进程的前置知识:
1、在单CPU计算机中,CPU是无法被多个程序并行使用的。
2、操作系统中存在一种调度器,它可以负责拆分CPU为一段段时间的运行片,轮流分配给不同的进程。
3、程序的运行不仅仅需要CPU,还需要很多其他资源,如内存,显卡,GPS,磁盘等等,这些统称为程序的执行环境,也就是程序上下文。
4、多个程序没办法同一个时间共享CPU,那怎么办呢?这个时候比进程更小的线程就出来了,通过在不同线程的切换来达到共享CPU、共享程序上下文的目的。
5、大家都知道,CPU有单核和多核区别,单核CPU其实就是多个线程会轮流得到那一个CPU核心的支持;在多核CPU中,一个核心可以服务于一个线程,例如我的电脑是4核的话,有四个线程A、B、C、D需要处理,那CPU会将他们分配到核心1、2、3、4,如果还有其他更多的线程,也必须要等待CPU的切换执行。
单核CPU不能实现物理上程序之间真正的并行执行,在单核CPU中多线程是通过逻辑上的并发执行,物理上的顺序执行来实现的。以下为单线程程序和双线程程序的图示。
为什么引入线程而不只使用进程?
线程是程序执行的基本单位,进程是资源分配的基本单位。
线程之间的切换比进程之间的切换系统开销小很多,这是因为同一进程的不同线程之间共享资源,切换效率很高。
创建和启动线程
Java中的多线程是建立在Thread类,Runnable接口的基础上的,通常有两
种办法让我们来创建一个新的线程:
– 创建一个Thread类,或者一个Thread子类的对象;
– 创建一个实现Runnable接口的类的对象;
例(使用方法1):
class UdpRecv extends Thread{
@Override
public void run(){
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,1024);
try {
DatagramSocket ds = new DatagramSocket(3000);
ds.receive(dp);
String strRecv = new String(dp.getData(),0,dp.getLength()) + " from"
+ dp.getAddress().getHostAddress() + ":"+dp.getPort();
System.out.println(strRecv);
System.out.println("66666666666666666666666");
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上为线程的状态转换图示。
线程常用方法总结
Thread类中的核心方法
方法名称 | 是否static | 方法说明 |
---|---|---|
start() | 否 | 让线程启动,进入就绪状态,等待cpu分配时间片 |
run() | 否 | 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑 |
yield() | 是 | 线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片 |
sleep(time) | 是 | 线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断 |
join()/join(time) | 否 | 调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片 |
isInterrupted() | 否 | 获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记 |
interrupt() | 否 | 打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记 |
interrupted() | 否 | 获取线程的打断标记。调用后会清空打断标记 |
stop() | 否 | 停止线程运行 不推荐 |
suspend() | 否 | 挂起线程 不推荐 |
resume() | 否 | 恢复线程运行 不推荐 |
currentThread() | 是 | 获取当前线程 |
Object中与线程相关方法
方法名称 | 方法说明 |
---|---|
wait()/wait(long timeout) | 获取到锁的线程进入阻塞状态 |
notify() | 随机唤醒被wait()的一个线程 |
notifyAll(); | 唤醒被wait()的所有线程,重新争抢时间片 |
Q&A
-
start()方法和run()方法的区别?
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。 -
sleep方法和wait方法有什么区别?
sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器。 -
线程类的构造方法、静态块是被哪个线程调用的?
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的 -
Runnable接口和Callable接口的区别?
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
控制线程的执行顺序
- join方式
我们直接通过在每个Thread对象后面使用join方法就可以实现线程的顺序执行,用join方法来保证线程顺序,其实就是让main这个主线程等待子线程结束,然后主线程再执行接下来的其他线程任务。代码如下:
public static void main(String[] args) throws Exception {
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
thread4.start();
thread4.join();
thread5.start();
thread5.join();
}
线程的执行顺序为1-2-3-4-5
- ExecutorService方式
将线程用排队的方式扔进一个线程池里,让所有的任务以单线程的模式,按照FIFO先进先出、LIFO后进先出、优先级等特定顺序执行,但是这种方式也是存在缺点的,就是当一个线程被阻塞时,其它的线程都会受到影响被阻塞,不过依然都会按照自身调度来执行,只是会存在阻塞延迟。
static ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args) throws Exception {
executorService.submit(thread1);
executorService.submit(thread2);
executorService.submit(thread3);
executorService.submit(thread4);
executorService.submit(thread5);
executorService.shutdown();
}
线程执行顺序1-2-3-4-5