线程与进程区别
主要区别
- 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
- 线程是进程的一个实体,一个进程中一般拥有多个线程。线程之间共享地址空间和其它资源(所以通信和同步等操作,线程比进程更加容易)
- 线程一般不拥有系统资源,但是也有一些必不可少的资源(使用ThreadLocal存储)
- 线程上下文的切换比进程上下文切换要快很多。
线程上下文切换比进程上下文切换快的原因
- 进程切换时:涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置
- 线程切换时,仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作
线程通信同步有几种方式
线程通信
- 使用全局变量:主要由于多个线程可能更改全局变量,因此全局变量最好声明为volatile。
- 使用消息实现通信:每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环),因此可以采用消息进行线程间通信sendMessage,postMessage。
- 使用事件CEvent类实现线程间通信:Event对象有两种状态分别是有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
线程同步方式
-
临界区
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操 作共享资源的目的。 仅能在同一进程内使用
-
互斥量
只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
-
信号量
信号允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。
-
事件
事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。
线程之间制约关系
- 直接制约关系:即一个线程的处理结果,为另一个线程的输入,因此线程之间直接制约着,这种关系可以称之为同步关系
- 间接制约关系:即两个线程需要访问同一资源,该资源在同一时刻只能被一个线程访问,这种关系称之为线程间对资源的互斥访问,某种意义上说互斥是一种制约关系更小的同步
进程通信同步有几种方式
- 通过使用套接字Socket来实现不同机器间的进程通信
- 通过映射一段可以被多个进程访问的共享内存来进行通信
- 通过写进程和读进程利用管道进行通信
多线程共享数据
-
每个线程执行流程相同,使用同一个Runnable对象,Runnable对象中有共享数据
public class Main { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(ticket).start(); new Thread(ticket).start(); } } class Ticket implements Runnable { private int ticket = 10; @Override public synchronized void run() { while (ticket > 0) { ticket--; System.out.println("当前线程窗口" + Thread.currentThread().getName() + "剩余票为: " + ticket); } } } /** 当前线程窗口Thread-0剩余票为: 9 当前线程窗口Thread-0剩余票为: 8 当前线程窗口Thread-0剩余票为: 7 当前线程窗口Thread-0剩余票为: 6 当前线程窗口Thread-0剩余票为: 5 当前线程窗口Thread-0剩余票为: 4 当前线程窗口Thread-0剩余票为: 3 当前线程窗口Thread-0剩余票为: 2 当前线程窗口Thread-0剩余票为: 1 当前线程窗口Thread-0剩余票为: 0 /
-
每个线程执行流程不相同,使用不同的Runnable对象
public class Main { public static void main(String[] args) { ShareData data = new ShareData(); new Thread(new Runnable1(data)).start(); new Thread(new Runnable2(data)).start(); } } class Runnable1 implements Runnable { private ShareData data; public Runnable1(ShareData data) { this.data = data; } @Override public void run() { data.increment(); } } class Runnable2 implements Runnable { private ShareData data; public Runnable2(ShareData data) { this.data = data; } @Override public void run() { data.decrement(); } } class ShareData { private int j = 10; public synchronized void increment() { j++; System.out.println("线程:" + Thread.currentThread().getName() + "加操作之后,j = " + j); } public synchronized void decrement() { j--; System.out.println("线程:" + Thread.currentThread().getName() + "减操作之后,j = " + j); } } /** 线程:Thread-0加操作之后,j = 11 线程:Thread-1减操作之后,j = 10 /
信号量和互斥量的区别
主要区别
- 互斥量用于线程的互斥,信号量用于线程的同步
- 互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
- 互斥量值只能为0/1,信号量值可以为非负整数。
- 一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
线程是怎么实现的
主要有三种方式实现线程
在用户空间中实现线程
结构图
原理
把整个线程包放在用户空间,内核对线程包一无所知。用户调用库函数实现线程,,在用户空间管理线程时,每个进程有个专用的线程表,用来跟踪进程中的线程,这些表和内核中的进程类似,它记录着各个线程属性,每个线程的程序计算器、堆栈、指针、寄存器和状态等。
优点
- 用户线程包可以在不支持线程的操作系统上实现,只需要调用函数库实现线程即可。
- 由于线程表在本地,启动线程比进行内核调用效率更高,并且不需要陷入内核,不需要本地上下文切换,也不用对内存进行高速缓存进行刷新。
- 允许每个进程都有自己的调度算法。
缺点
- 无法实现阻塞系统调用
- 会发生缺页中断问题,如果有一个线程引起页面故障,由于线程不在内核,内核不知道线程的存在,内核会把整个进程进行阻塞,直到磁盘IO完成为止,导致其他可以运行的线程也被阻塞。
- 一旦一个线程开始运行,那么该进程中的其他线程不能运行,除非第一个线程自动放弃CPU。
在内核中实现线程
结构图
原理
每个进程中没有线程表,线程表在哪喝中。当某个线程希望创建一个新线程或者撤销一个已有线程的时候,它会进行一个系统调用,这个系统调用会通过对线程表的更新完成线程创建或者撤销工作。
优点
- 内核线程不需要任何新的,非阻塞系统的调用。
- 可以回收线程(当某个线程被撤销,可以标志它为不可运行)
缺点
- 一个多线程创建新的进程时,难以确定新进程时拥有与原进程相同数量的线程,或者是只有一个线程。
- 当进程接收到一个信号时候,难以确定用哪一个线程处理它。
混合实现
结构图
原理
内核只识别内核级别线程,并对其进行调度。一些内核级线程会被多个用户线程多路复用。内线线程可以创建,撤销,调度这些用户线程,每个内核级线程有一个可以轮流使用的用户级线程集合。
优点
- 结合了内核实现线程和用户空间实现线程的优点。
- 多路复,每个内核级线程有一个可以轮流使用的用户级线程集合。
线程死锁
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非Runnable)。死锁的产生必须满足如下四个必要条件:
- 资源互斥:一个资源每次只能被一个线程使用
- 请求和保持::一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺:线程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待:若干线程之间形成一种头尾相接的循环等待资源关系
如何避免死锁
- 粗锁法:使用一个粒度粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
- 锁排序法:比如某个线程只有获得A锁和B锁,通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
- 使用显式锁中的**ReentrantLock.try(long,TimeUnit)**来申请锁
wait和sleep的区别
- wait:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
- sleep:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
用start()方法去执行run()方法而不是直接调用run()方法
start方法
用 start方法来启动线程,是真正实现了多线程, 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。但要注意的是,此时无需等待run()方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。
run方法
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。
上下文切换的种类
上下文切换的种类 | |
---|---|
线程切换 | 同一进程中的两个线程之间的切换 |
进程切换 | 两个进程之间的切换 |
模式切换 | 在给定线程中,用户模式和内核模式的切换 |
地址空间切换 | 将虚拟内存切换到物理内存 |
线程有几种状态和它的上下文切换
Java线程的6种状态
线程有六种状态:
- NEW:线程创建,但没有启动
- RUNNABLE:代表线程正在运行或者不处于阻塞、等待状态的可以被运行的状态。线程创建后或脱离阻塞、等待状态后进入可运行状态。
- BLOCKED:代表线程尝试获得一个锁时无法对锁占用,进入阻塞状态;当该线程能够获得锁时脱离阻塞状态。
- WAITING:等待线程主动进入等待条件达成的状态,可以使用join、wait、sleep方法。
- TIMED_WAITING:等待状态添加计时器,当等待条件达成或计时器耗尽时脱离等待状态。
- TERMINATED:线程任务结束或手动设置中断标记。
操作系统的5种状态
- 新建状态(New):线程创建但没有启动
- 就绪状态(Runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权 。
- 运行状态(Running):就绪状态( runnable )的线程获得了cpu时间片(timeslice ),执行程序代码。
- 阻塞状态(Blocked):线程放弃CPU的时间片(一直到某个条件达成),主动进入阻塞的状态,有三种阻塞状态,分别是
- 同步阻塞:线程由于尝试获得对象的同步锁但无法取得时,进入锁池,等待其他线程释放该对象的锁。
- 等待阻塞:线程主动放弃对对象上的锁的占用,进入等待对象通知的队列。指wait方法
- 其他阻塞:线程主动进入休眠状态,等待条件达成。指sleep、join方法或I/O请求。
- 线程死亡(Dead):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程运行状态图
线程上下文切换
概念
上下文切换是值CPU通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。(任务从保存到再加载的过程就是一次上下文切换)。
线程切出
一个线程被剥夺处理器的使用权而被暂停运行,操作系统会将线程的进度信息保存到内存。
线程切入
一个线程被系统选中占用处理器开始或继续运行,操作系统需要从内存中加载线程的上下文。
上下文切换的种类
自发性上下文切换
线程由自身因素导致的上下文切换
- Thread.sleep()
- Object.wait()
- Thread.yeild()
- Thread.join()
- LockSupport.park()
非自发性上下文切换
线程由于线程调度器的原因被迫切出
- 线程的时间片用完,导致线程切出。
- 有一个比线程优先级更高的线程需要被运行,导致线程切出。
- 虚拟机的垃圾回收动作。
上下文切换开销
简接开销
- 处理器高速缓存重新加载内存时的开销。
- 可能导致整个一级高速缓存中的内容被冲刷,即被写入到下一级高速缓存或主存。
直接开销
- 操作系统保存回复上下文所需的开销
- 线程调度器调度线程的开销
ThreadLocal是什么
使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
内部实现机制
- 每个线程内部都会维护一个类似HashMap的对象,称为ThreadLocalMap,里边会包含若干了Entry(K-V键值对),相应的线程被称为这些Entry的属主线程
- Entry的Key是一个ThreadLocal实例,Value是一个线程特有对象。Entry的作用是为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系
- Entry对Key的引用是弱引用;Entry对Value的引用是强引用。
就绪状态和阻塞状态有什么区别
就绪状态
当线程对象创建后,其他线程调用它的start方法,其进入线程等待池,等待cpu使用权
阻塞状态
线程放弃cpu的使用权,进入阻塞状态直到重新进入就绪状态,阻塞分为三种:
- 同步阻塞:线程由于尝试获得对象的同步锁但无法取得时,进入锁池,等待其他线程释放该对象的锁。
- 等待阻塞:线程主动放弃对对象上的锁的占用,进入等待对象通知的队列。指wait方法
- 其他阻塞:线程主动进入休眠状态,等待条件达成。指sleep、join方法或I/O请求。
两者可以互相切换吗
阻塞状态可以通过获得锁、sleep()方法结束、调用join()的线程运行结束方法后可以切换到就绪状态,但是就绪状态要先获取cpu时间片变成运行状态后,才能转换到阻塞状态。
进程和线程切换开销对比
虚拟内存
虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,操作系统是通关页表来记住这种映射关系。每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。
进程切换和线程切换的对比
- 最主要区别是进程切换涉及虚拟地址空间的切换而线程不涉及。由于每个进程都有自己的虚拟地址空间,而线程是共享虚拟地址空间。因此进程切换的时候涉及虚拟地址空间的切换,而同一个进程的线程切换不涉及虚拟地址空间的切换。
为什么虚拟地址空间切换会比较耗时
每个进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就TLB(translation Lookaside Buffer,TLB本质上就是一个cache,是用来加速页表查找的)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低