前言
本文会介绍Java中多线程与并发的基础,适合初学者食用。
线程与进程的区别
在计算机发展初期,每台计算机是串行地执行任务的,如果碰上需要IO的地方,还需要等待长时间的用户IO,后来经过一段时间有了批处理计算机,其可以批量串行地处理用户指令,但本质还是串行,还是不能并发执行。
如何解决并发执行的问题呢?于是引入了进程的概念,每个进程独占一份内存空间,进程是内存分配的最小单位,相互间运行互不干扰且可以相互切换,现在我们所看到的多个进程“同时"在运行,实际上是进程高速切换的效果。
那么有了线程之后,我们的计算机系统看似已经很完美了,为什么还要进入线程呢?如果一个进程有多个子任务,往往一个进程需要逐个去执行这些子任务,但往往这些子任务是不相互依赖的,可以并发执行,所以需要CPU进行更细粒度的切换。所以就引入了线程的概念,线程隶属于某一个进程,它共享进程的内存资源,相互间切换更快速。
进程与线程的区别:
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。所有与进程相关的资源,均被记录在PCB中。
- 线程隶属于某一个进程,共享所属进程的资源。线程只由堆栈寄存器、程序计数器和TCB构成。
- 进程可以看作独立的应用,线程不能看作独立的应用。
- 进程有独立的地址空间,相互不影响,而线程只是进程的不同执行路径,如果线程挂了,进程也就挂了。所以多进程的程序比多线程程序健壮,但是切换消耗资源多。
Java中进程与线程的关系:
- 运行一个程序会产生一个进程,进程至少包含一个线程。
- 每个进程对应一个JVM实例,多个线程共享JVM中的堆。
- Java采用单线程编程模型,程序会自动创建主线程 。
- 主线程可以创建子线程,原则上要后于子线程完成执行。
线程的start方法和run方法的区别
区别
Java中创建线程的方式有两种,不管使用继承Thread的方式还是实现Runnable接口的方式,都需要重写run方法。调用start方法会创建一个新的线程并启动,run方法只是启动线程后的回调函数,如果调用run方法,那么执行run方法的线程不会是新创建的线程,而如果使用start方法,那么执行run方法的线程就是我们刚刚启动的那个线程。
程序验证
public class Main { public static void main(String[] args) { Thread thread = new Thread(new SubThread()); thread.run(); thread.start(); } } class SubThread implements Runnable{ @Override public void run() { // TODO Auto-generated method stub System.out.println("执行本方法的线程:"+Thread.currentThread().getName()); } }
Thread和Runnable的关系
Thread源码
区别
通过上述源码图,不难看出,Thread是一个类,而Runnable是一个接口,Runnable接口中只有一个没有实现的run方法,可以得知,Runnable并不能独立开启一个线程,而是依赖Thread类去创建线程,执行自己的run方法,去执行相应的业务逻辑,才能让这个类具备多线程的特性。
使用继承Thread方式和实现Runable接口方式分别创建子线程
使用继承Thread类方式创建子线程
public class Main extends Thread{ public static void main(String[] args) { Main main = new Main(); main.start(); } @Override public void run() { System.out.println("通过继承Thread接口方式创建子线程成功,当前线程名:"+Thread.currentThread().getName()); } }
运行结果:
使用实现Runnable接口方式创建子线程
public class Main{ public static void main(String[] args) { SubThread subThread = new SubThread(); Thread thread = new Thread(subThread); thread.start(); } } class SubThread implements Runnable{ @Override public void run() { // TODO Auto-generated method stub System.out.println("通过实现Runnable接口创建子线程成功,当前线程名:"+Thread.currentThread().getName()); } }
运行结果:
使用匿名内部类方式创建子线程
public class Main{ public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("使用匿名内部类方式创建线程成功,当前线程名:"+Thread.currentThread().getName()); } }); thread.start(); } }
运行结果:
关系
- Thread是实现了Runnable接口的类,使得run支持多线程。
- 因类的单一继承原则,推荐使用Runnable接口,可以使程序更加灵活。
如何实现处理多线程的返回值
通过刚才的学习,我们知道多线程的逻辑需要放到run方法中去执行,而run方法是没有返回值的,那么遇到需要返回值的状况就不好解决,那么如何实现子线程返回值呢?
主线程等待法
通过让主线程等待,直到子线程运行完毕为止。
实现方式:
public class Main{ static String str; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { str="子线程执行完毕"; } }); thread.start(); //如果子线程还未对str进行赋值,则一直轮转 while(str==null) {} System.out.println(str); } }
使用Thread中的join()方法
join()方法可以阻塞当前线程以等待子线程处理完毕。
实现方式:
public class Main{ static String str; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { str="子线程执行完毕"; } }); thread.start(); //如果子线程还未对str进行赋值,则一直轮转 try { thread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(str); } }
join方法能做到比主线程等待法更精准的控制,但是join方法的控制粒度并不够细。比如,我需要控制子线程将字符串赋一个特定的值时,再执行主线程,这种操作join方法是没有办法做到的。
通过Callable接口实现:通过FutureTask或者线程池获取
在JDK1.5之前,线程是没有返回值的,通常程序猿需要获取子线程返回值颇费周折,现在Java有了自己的返回值线程,即实现了Callable接口的线程,执行了实现Callable接口的线程之后,可以获得一个Future对象,在该对象上调用一个get方法,就可以执行子线程的逻辑并获取返回的Object。