基础知识
一、并发编程
并发编程的优点
- 充分利用多核CPU的计算能力:
通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。 - 方便进行业务拆分,提升系统并发能力和性能:
在特殊的业务场景下,先天的就适合与并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。
并发编程的缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。
并发编程三要素(线程的安全性问题的体现)
-
原子性: 一个或多个操作要么全部执行成功要么全部执行失败。
-
可见性: 一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized、 volatile)
-
有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因 及解决办法
- 线程切换带来的原子性问题
JDK Atomic 开头的原子类、synchronized、LOCK 可以解决原子性问题 - 缓存导致的可见性问题
synchronized、volatile、LOCK 可以解决可见性问题 - 编译优化带来的有序性问题
Happens-Before 规则可以解决有序性问题
并行和并发有什么区别
- 并发: 多个任务同一个 CPU 核上,按细分的时间片轮流执行,从逻辑上来看哪些任务时同时执行。
- 并行: 单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的 “同时进行”。
- 串行: 有n个任务,由一个线程按顺序执行。由于任务、方法都在 一个线程执行,所以不存在线程不安全情况,也就不存在临界区的问题。
什么是多线程 及多线程的优劣
多线程:
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同任务。
多线程的好处:
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说,允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
- 线程也是程序,所以线程需要占用内存,线程越多占用的内存也越多。
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程。
- 线程之间对共享资源的访问会互相影响,必须解决竞争共享资源的问题。
线程和进程区别
进程
一个在内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中, 一个运行的 .exe 就是一个进程。
线程
进程中一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
根本区别
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
守护线程和用户线程
用户线程:
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
守护线程:
运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。
一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
区别:
用户线程结束,JVM 退出,不管这时候有没有守护线程运行。
而守护线程不会影响 JVM 的退出。
什么是线程死锁
死锁
是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们将无法推进下去。 此时称为系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
形成死锁的四个必要条件
- 互斥条件:
进程(线程)对于所分配到的资源具有排它性,即一个资源只能被一个进程(线程)占用,直到被该进程(线程)释放。 - 请求与保持条件:
一个进程(线程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 - 不剥夺条件:
进程(线程)已获得的资源在未使用完之前不能被其它线程其强行剥夺,只有自己使用完毕后才释放资源。 - 循环等待条件:
当发生死锁时,所等待的进程(线程)必定会形成一个环路,造成永久阻塞。
创建线程的四种方式
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 使用 Executors 工具类创建线程池
二、线程安全
什么是线程安全
指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使得程序功能正确完成。
在 Java 程序中怎么保证线程的运行安全
- 使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
- 使用自动锁 synchronized 关键字
- 使用手动锁 Lock java类
synchronized 和 Lock 有什么区别
- synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;
而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 - 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
三、悲观锁和乐观锁
当出现多个用户同时执行更新等操作时,会出现事务交叉更新操作的冲突,会破坏业务和数据的完整性。可以使用悲观锁和乐观锁解决这类问题。
- 悲观锁机制:
在进行数据查询时追加一个锁机制,进行业务操作,此时其他用户不能进行增删改操作,在事务结束时会自动将锁释放,其他用户可以继续执行此类操作。
悲观锁特点:
将用户操作一个一个处理,可以解决更新并发问题,缺点是处理效率比较低。
- 乐观锁机制:
多个不同用户都可以同时对数据库记录进行查看和更新操作,但是最先commit提交的用户会执行成功,后续用户会以异常形式提示失败。
乐观锁特点:
允许多个用户同时操作,处理效率相对较高。
乐观锁使用步骤:
- 将原有数据表追加一列版本字段,初始值0
- 在实体类中添加版本属性
- 在映射描述文件中采用元素定义版本属性和版本字段的映射
- 当发生多个事务并行交叉执行时,第一个提交的成功,后续提交的会抛出异常。可以异常捕获给用户一个友善的提示。
四、AQS(AbstractQueuedSynchronizer)
- 这个类在 java.util.concurrent.locks 包下面。
- AQS 是一个用来构建锁和同步器的框架,
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就绪要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是 用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 - CLH 队列是一个虚拟的双向队列。AQS 是将每条请求共享资源的线程封装成一个CLH 锁队列的一个结点(Node)来实现锁的分配。
五、并发容器
ConcurrentHashMap
CopyOnWriteArrayList
ThreadLocal
BlockingQueue
ConcurrentLinkedQueue
六、线程池
事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Executors 类创建四种常见线程池
- newSingleThreadExecutor
创建一个单线程的线程池 - newFixedThreadPool
创建固定大小的线程池 - newCachedThreadPool
创建一个可缓存的线程池 - newScheduledThreadPool
创建一个大小无限的线程池
线程池的优点
- 降低资源消耗:
重用存在的线程,减少对象创建销毁的开销。 - 提高响应速度:
可以有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免阻塞。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 提高线程的可管理性:
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配,调优和监控。 - 附加功能:
提供定时执行、定期执行、单线程、并发数控制 等功能。
综上所述:
使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
Executor 框架
提供线程的抽象,基于生产者–消费者模式
线程池是线程的所有者
什么是 以及 为什么使用 Executor 框架
- 是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。
- 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。
- 所以创建一个线程池是个更好的解决方案,因为可以限制线程的数量,并且可以回收再利用这些线程。利用 Executor 接口可以非常方便的创建一个线程池。
在 Java 中 Executor 和 Executors 的区别
-
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
-
Executor 接口对象能执行我们的线程任务。
CAS
以上均为基于锁的阻塞的原子考虑。而CAS (compare and swap)比较并交换,是针对当下流行的多处理器而设计,是乐观技术,即多个线程尝试使用CAS同时更新时,只有一个能更新成功,且不会挂起。