并行是什么意思?与并发的区别是什么?
- 并行:指两个或两个以上事件或活动在同一时刻发生。如多任务在多个CPU或CPU的多个核上同时执行,不存在CPU资源的竞争,等待行为
- 并发:指两个或两个以上的事件在 同一时间间隔内发生
并发与并行的区别
- 并行指多个事件在同一时刻发生;并发指在某时刻只有一个事件在发生,某个时间段内由于CPU交替执行,可以发生多个事件
- 并行没有对CPU资源的抢占;并发执行的线程需要对CPU资源进行抢占
- 并行执行的线程之间不存在切换,并发操作系统会根据任务调度系统给线程分配CPU的执行时间,线程的执行会进行切换
Java中的多线程
- 通过JDK中的java.lang.Thread可以实现多线程
- Java中的多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源
- 不管是多线程是并发还是并行,都是为了提高程序的性能
什么是线程?什么是进程?有什么关系和区别?
进程:
- 程序执行时 的一个实例
- 每个进程都有独立的内存地址空间
- 系统进行资源分配和调度的基本单位
- 进程里的堆,是一个进程中最大的一块内存,被进程中所有线程共享,进程创建时分配,主要存放new创建的对象实例
- 进程里的方法区,是用来存放进程中的代码片段的,是线程共享的
- 在多线程OS中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码
为什么要有线程
- 每个进程都有自己的地址空间,即进程空间,一个服务器通常需要接收大量的并发请求,为每一个请求创建一个进程系统开销大,请求响应效率低
线程:
- 进程中的一个实体
- 进程中的一个执行路径
- CPU调度和分派的基本单位
- 线程本身不会独立存在
- 当前线程的CPU时间片用完后,会让CPU等下次轮到自己时候在执行
- 系统不会为线程分配内存,线程组之间只能共享所属进程的资源
- 线程里的程序计数器就是为了记录该线程的执行地址以及出CPU时的执行地址,待再次分配到时间片的时候就可以从自己私有的计数器指定地址继续执行
- 每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其他线程无法访问
关系
- 一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程中的资源
- Java中当我们启动main函数时就启动了一个JVM的进程,而main函数所在线程就是这个进程中的一个线程,也叫做主线程
- 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域
区别
- 本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
- 内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除CPU外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
- 资源拥有:进程之间的资源是独立的,无法共享;同一进程的多有线程共享本进程的资源
- 开销
- 通信:进程间以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信;同一进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
- 调度和切换
- 执行过程
- 健壮性
- 可维护性
进程与线程的选择
- 需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变CPU的执行
- 线程的切换速度快,需要大量计算,切换频繁时,用线程
- 耗时的操作使用线程可提高应用程序的响应
- 线程对CPU的使用效率更优
- 需要跨机移植,优先考虑用进程
- 需要更稳定、安全时优先考虑用进程
- 需要速度是,优先考虑用线程
- 并行性要求很高时,要求考虑使用线程
Java编程语言中线程是通过java.long.Thread类的实现的
- Thread类中包含tid, name, group, daemon(是否守护线程), priority(优先级)
什么是守护线程
- 守护线程拥有自动结束自己生命周期的特性
- 守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
Java线程分为用户线程和守护线程
- 守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
- Java中把线程设置为守护线程的方法;在start线程之前调用线程的setDaemon(true)方法
注意
- setDaemon(true)必须在start()之前设置,否则会抛出IIIlegalThreadStateException异常,该线程仍默认为用户线程,继续执行
- 守护线程创建的线程也是守护线程
- 守护线程不应该访问、写入持久化资源,如文件,数据库,因为它会在任何时间被停止,导致资源未释放,数据写入中断等问题
如何创建、启动Java线程
- Java中有4种常见的创建线程的方式
一,重写Thread类的run()方法
- new Thread对象匿名重写run()方法
- 继承Thread对象,重写run()方法
二,实现Runable接口,重写run()方法
- new Runable对象,匿名重写run()方法
- 实现Runable接口,重写run()方法
三,实现Callable接口,使用FutureTask类创建线程
四,使用线程池创建,启动线程
- 使用工具类Executors创建单线程线程池
并发编程
什么是并发编程
- 并发编程:用编程语言编写让计算机可以在一个时间段内执行多个任务的程序
为什么要用并发编程
- 并发编程可以提升CPU的计算能力的利用率
- 提升程序的性能,如:响应时间、吞吐量、计算机资源使用率
- 并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务
并发编程的缺点
- Java中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较销毁内存和时间
- 容易带来线程安全问题。如线程的可见性,有序性,原子性问题,会导致程序出现的结果与预期结果不一致
- 多线程容易造成死锁、活锁、线程饥饿等问题
导致并发程序出问题的根本原因
- CPU缓冲,在多核CPU的情况下,带来了可见性问题
- 操作系统对当前执行线程的切换,带来了原子性问题
- 编译器指令重排优化,带来了有序性问题
Java程序中怎么保证多线程的运行安全
线程的安全性问题体现在
- 原子性:一个或者多个操作在CPU执行的过程中不被中断的特性
- 可见性:一个线程对于共享变量的修改,另外一个线程能够立刻看到
- 有序性:程序执行的顺序按照代码的先后顺序执行
导致原因
- 缓冲导致的可见性问题
- 线程切换带来的原子性问题
- 编译优化带来的有序性问题
解决办法
- JDK Atomic开头的原子类,synchronized, LOCK可以解决原子性问题
- synchronized,volatile, LOCK可以解决可见性问题
- Happen-Before规则可以解决有序性问题
线程包括哪些状态?
线程的生命周期
- 线程包括哪些状态,不同的编程语言对线程的生命周期封装是不同的
Java中线程的生命周期
- NEW(初始化状态)
- RUNNABLE(可运行/运行状态)
- Blocked(阻塞状态)
- Waiting(无限时等待)
- Timed_Waiting(有限时等待)
- Terminated(终止状态)在操作系统层面,Java线程中Blocked, Waiting, Timed_waiting是一种状态(休眠状态),即只要Java线程处于这三种状态之一,就永远没有CPU的使用权
Java中线程的状态的转变
- NEW到RUNNABLE状态
-
- Java刚创建出来的Thread对象就是NEW状态,不会被操作系统调度执行.
-
- 从NEW状态转变到RUNABLE状态调用线程对象的start()方法就可以了
- RUNNABLE与BLOCKED的状态转变
-
- synchronized修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从Runnable转变到Blocked状态
-
- 当等待的线程获得synchronized隐式锁时,就会从Blocked转变到Runnable状态
-
- 在操作系统层面,线程是会转变到休眠状态的,但是在JVM层面,Java线程的状态不会发生改变,即Java线程的状态会保持Runnable状态
- Runnable与Waiting的状态转变
-
线程池
什么是线程池
- 线程池就是创建若干个可执行的线程放入一个池中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务
为什么要使用线程池
- 因为Java中创建一个线程,需要调用操作系统内核的API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁
- 使用线程池就能很好地避免频繁创建和销毁
线程池包含哪些状态
- RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED
- running:线程池一旦被创建,就处于Running状态,任务数为0,能够接收新任务,对已排队地任务进行处理
- shutdown: 不接受新任务,但能处理已排队的任务,调用线程池的shutdown()方法,线程池由running转变为shutdown状态
- stop:不接受新任务,不处理已排队的任务,并且会中断正在处理的任务,调用线程池的shutdown()方法,线程池由running或shutdown转变为stop状态
synchronized关键字
- Java中的关键字synchronized表示只有一个线程可以获取作用对象锁,执行代码,阻塞其他线程
- 作用:
-
- 确保线程互斥地访问同步代码
-
- 保证共享变量的修改能够及时可见
-
- 有效解决重排序问题
- 用法
-
- 修饰普通方法
-
- 修饰静态方法
-
- 指定对象,修饰代码块
- 特点:
-
- 阻塞未获取到锁,竞争同一对象锁的线程
-
- 获取锁无法设置超时
-
- 无法实现公平锁
-
- 控制等待和唤醒需要结合加锁对象的wait()和notify(), notifyAll()
-
- 锁的功能是JVM层面实现的
-
- 在加锁代码块执行完成或者出现异常,自动释放锁
- 原理
-
- 同步代码块是通过monitorenter和monitorexit指令获取线程的执行权
-
- 同步方法通过加ACC_SYNCHRONIZED标识实现线程的执行权的控制
volatile
- 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见
- 通过禁止编译器,CPU指令重新排序和部分happens-before规则,解决有序性问题
volatile可见性的实现
- 在生成汇编代码指令时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令
- Lock前缀的指令会引起CPU缓存写回内存
- 一个CPU的缓存回写到内存会导致其他CPU缓存了该地址的数据无效
- volatile变量通过缓存一致性协议保证每个线程获得最新值
- 缓存一致性协议保证每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
- 当CPU发现自己缓存对应的内存地址被修改,会将当前CPU的缓存行设置成无效状态,重新从内存中把数据读到CPU缓存
Java中的锁是什么
- 在并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况
- JDK1.5之前,使用synchronized关键字,拿到Java对象的锁,保护锁定的代码块,JVM保证同一时刻只有一个线程可以拿到这个Java对象的锁,执行对应的代码块
常见锁
- synchronized关键字锁定代码库
- 可重入锁java.util.concurrent.lock.ReentrantLock
- 可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock
Java中不同维度的锁分类
- 可重入锁
-
- 指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK中基本都是可重入锁,避免死锁的发生。上面提到的常见的锁都是可重入锁
- 公平锁/非公平锁
-
- 公平锁,指多个线程按照申请锁的顺序来获取锁,如java.util.concurrent.lock.ReentrantLock.FairSync
-
- 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程先获得锁
- 独享锁/共享锁
-
- 独享锁,指锁一次只能被一个线程所持有。synchronized, java.util.concurrent.locks.ReentrantLock都是独享锁
-
- 共享锁:指锁可被多个线程持有。ReadWriteLock返回的ReadLock就是共享锁
- 悲观锁/乐观锁
-
- 悲观锁:一律会对代码块进行加锁, synchronized
-
- 乐观锁:默认不会进行并发修改,通常采用CAS算法不断尝试更新
- 粗粒度锁/细粒度锁
-
- 粗粒度锁:就是把执行的代码块都锁定
-
- 细粒度锁:就是锁住尽可能小的代码块
- 偏向锁/轻量级锁/重量级锁
乐观锁与悲观锁
悲观锁
- 线程每次在处理共享数据时都会上锁,其他线程想要处理数据就会被阻塞直到获得锁
乐观锁
- 线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号等机制判断其他线程有没有更新数据。乐观锁适合读多写少的应用场景
两种锁各有优缺点
- 乐观锁适用于读多写少的场景,可以省去频繁加锁,释放锁的开销,提高吞吐量
- 在写比较多的场景下,乐观锁会因版本不一致,不断重试更新,产生大量自旋,消耗CPU,影响性能,这种情况下适合悲观锁