Java多线程和并发
进程和线程的区别
基本概念见https://cloud.tencent.com/developer/article/1688297
进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的?
1. 基本概念:
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
2. 进程和线程的由来
进程对应一个程序。每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程空间间互不干扰,并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能,当进程暂停时,它会保存进程当前的状态如进程的标识,进程的使用的资源,等重新切换回来时,便根据之前保存的状态进行恢复和执行。进程让操作系统的并发变成可能。对单核CPU而言,在宏观上看也有多个任务在同时运行,但事实上任一个时刻只有一个任务在占用CPU资源,这是因为CPU分配给单一任务执行的时间片很短,任务切换的频次高,造成多个任务并发执行的假象。
并发和并行的区别: 并行是指两个或者多个时间在同一时刻发生;并行是指两个或者多个时间在同一时间间隔内发生。 在单处理机系统中的多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,微观上只是在分时交替执行。 并发是轮流处理多个任务,并行是同时处理多个任务。
进程的出现解决了操作系统的并发问题,但是随着电脑的普及,人们逐渐对实时性有了要求,因为一个进程在一个时刻只做一个任务,如果一个进程有多个任务,只能逐个地执行这些子任务,而这些子任务往往不存在顺序上的依赖,是可以并发执行的,既然CPU可以按照时间片的方式轮流切换来跑进程,那能不能给这些子任务打个标签,让CPU按照更细的时间片来执行子任务呢?这是可以的。由于子任务共享进程的内存等资源,因此隶属于同一个进程间的子任务的切换是不需要切换页目录以使用新的地址空间的。这样就为子任务间更快速的切换提供了可能,此时人们就发明了线程,让线程去执行一个子任务。线程让进程的内部并发成为可能。
3.进程和线程的区别
-
进程是资源分配的最小单位,线程是CPU调度的最小单位。
-
所有以进程有关的资源,都被记录在PCB(进程控制块)中,以表示表示该进程拥有这些资源,或者正在使用他们。
-
进程是抢占处理机的调度单位,它拥有完整的虚拟内存地址空间,当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。线程与资源分配无关,线程属于某个进程,与进程内的线程共享资源。 -
**线程只由堆栈与寄存器,程序计数器和TCB(线程控制块)**组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。通常,一个进程中可以包含若干个线程,他们可以拥有进程所拥有的资源
-
4.总结
-
线程不能看做独立应用,而进程可以看做独立应用。
-
进程有独立的地址空间,一个进程崩溃后,在保护模式下,不会对其他进程产生影响,而线程只是一个进程中不同的执行路径。
-
线程有自己的堆栈和局部变量(寄存器存储?),但线程没有自己独立的地址空间,一个线程挂掉相当于一个进程挂掉,所以多进程的程序比多线程的程序健壮。
-
进程的切换比线程的切换开销大。对一些要求同时进行,并且要求共享某些变量的并发操作,只能用线程不能用进程,每个独立的线程,有一个程序运行的入口,顺序执行序列,和程序的出口,但是线程不能独立执行,必须依存于某一个应用程序当中,由应用程序,提供对多个线程的执行控制。
5. Java中进程和线程的关系
- Java作为与平台无关的编程语言,会对操作系统提供的功能进行封装成与平台无关的编程接口供程序员使用。进程和线程也是如此。
- 每运行一个Java程序会产生一个Java进程,每一个Java进程包含至少一个线程。
- 每一个进程对应一个JVM实例,每个JVM实例唯一对应一个堆,每个线程有自己私有的栈,多个线程共享JVM中的堆。
- Java采用单线程编程模型,即自己的程序中如果没有主动创建线程,只会创建一个线程,通常称为主线程,因此耗时的操作最好放在子线程中执行,以免阻塞子线程。
- Java程序启动时,主线程立刻运行,主线程是产生其他线程的线程,通常它需要最后完成执行,因为它需要执行各种关闭动作。
备注:
Java 中,Thread.currentThread()可以获取到当前的线程。
Thread.currentThread().getName()可以获取到当前线程的名字。
一个程序是一个可执行的文件,而一个进程是一个执行中程序的实例。
既然说Java采用的是单线程编程模型,是不是意味着JVM虚拟机也是单线程的?要注意的是,虽然只有一个线程执行任务,并不代表JVM中只有一个线程,JVM实例在创建的时候同时会创建很多其他线程,比如GC是由一个垃圾收集器线程专门负责的,所以JVM是多线程的。
Thread中的start和run方法的区别
public class ThreadTest {
private static void attack(){
System.out.println("Fight");
System.out.println("Current Thread is : "+Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t = new Thread(){
public void run(){
attack();
}
};
System.out.println("Current Thread is :"+Thread.currentThread().getName());
t.run();
}
}
直接调用线程中run(),函数run还是在当前线程(主线程执行),结果:
Current Thread is :main
Fight
Current Thread is : main
public class ThreadTest {
private static void attack(){
System.out.println("Fight");
System.out.println("Current Thread is : "+Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t = new Thread(){
public void run(){
attack();
}
};
System.out.println("Current Thread is :"+Thread.currentThread().getName());
t.start();
}
}
调用start,执行attack()的线程是新的线程。
Current Thread is :main
Fight
Current Thread is : Thread-0
总结:调用run会沿用主线程执行方法,而用start()会用一个非main的线程执行这个方法。
- 调用start()方法会创建一个新的子线程并启动
- run()方法只是Thread的一个普通方法的调用
Thread和Runnable是什么关系
Thread是一个类,runnable是一个接口。
class Thread implements Runnable{
}
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Thread是实现了Runnable接口的类,使得run支持多线程,因为类的单一继承原则,推荐多使用runnable接口
如何实现处理线程的返回值
实现的方式主要有三种
-
构造函数传参
-
成员变量传参
-
回调函数传参
如何实现处理线程的返回值
实现的方式有三种
主线程等待法
(手动实现,不用看)
-
使用Thread类的join()阻塞当前线程以等待子线程处理完毕
在当前线程函数里写 要等待的线程.join() Thread t = new Thread(cw)//cw是一个实现了runnable接口的实例 t.start(); t.join();//等待t线程完成
-
通过Callable接口实现:通过FutureTask or 线程池获取
-
Java实现了有返回值的任务,即实现了callable接口的任务,执行callable的任务后,可以获取一个Future对象,在该对象上调用get,即可获得Callable任务返回的Object。
-
关于通过Callable接口实现线程的返回值,我们可以有以下两种方式去实现,1.futureTask,2.线程池。
FutureTask implements RunnableFuture,RunnableFuture entends Runnable,所以Thread的构造函数可以接收FutureTask。
-
FutureTask
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
String value = "test";
System.out.println("Ready to work");
Thread.currentThread().sleep(5000);
System.out.println("task done");
return value;
}
}
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<String>(new MyCallable());
new Thread(task).start();
if(!task.isDone()){
System.out.println("task has not finished,please wait!");
}
System.out.println("task return: "+task.get());
}
}
流程:
实现Callable类,实现callable类的方法call(),把callable类带入FutureTask的构造函数,把FutureTask带入Thread的构造函数,if FutureTask.isdone(),通过FutureTask.get()得到返回值
线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCachedThreadPool.submit(new MyCallable());
if(!future.isDone()){
System.out.println("task has not finished, please wait!");
}
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
newCachedThreadPool.shutdown();
}
}
}
线程的状态
- 新建(new):创建后尚未启动的线程的状态,即新创建一个线程但没有调用start方法
- 运行(runnable):包括操作系统中的Running(执行中)和Ready(等待CPU为它分配执行时间)
- 阻塞(blocked):等待获取排他锁。
- 无限期等待(waiting):不会被分配CPU执行时间,需要被显式被唤醒,以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object.wait()方法
- 没有设置Timeout参数的Thread.join()方法
- LockSupport.part()方法
- 限期等待(Timed Waited):在一定时间后会右系统自动唤醒,以下方法会让线程陷入限期的等待状态
- Thread.sleep()方法
- 设置了Timeout参数的Object.wait()方法
- 设置了Timeout参数的Thread.join()方法。
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
- 结束状态(teminated):已终止线程的状态,线程已经结束执行。在一个终止的线程上调用start()方法会抛出异常
sleep和wait的区别
基本差别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
最主要的本质区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
wait()可以用notify来唤醒
notify和notifyall的区别
两个概念
-
锁池EntryList:
假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B,C想要调用这个对象的某个sychronized方法(或者块),由于B,C线程在进入对象的sychronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程A所占用,此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
-
等待池WaitSet:
假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会竞争该对象的锁。
对于Java虚拟机中运行程序的每个对象来说,都有两个池,锁池和等待池。这两个池用于Object.wait()和Object.notify(),Object.notifyAll()三个方法,以及sychronized相关。
- notify