- **提升多核CPU的利用率:**一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论
上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了
CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。 - 比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可
以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,
而并发编程更能吻合这种业务拆分 。 - 简单来说就是:
充分利用多核CPU的计算能力;
方便进行业务拆分,提升应用性能
- 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提
高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安
全、死锁等问题。
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
- 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是
同时执行。 - 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
- 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题
- 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
- 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程
而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程; - 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
守护线程和用户线程有什么区别呢?- 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用
户线程 - 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线
程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
- 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等
待,直至占有资源的进程用毕释放。
占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过
来。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在
等B,B在等C,C在等A)
如何避免线程死锁
避免一个线程同时获得多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程
体。通过调用Thread类的start()方法来启动一个线程。 - start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()
只能调用一次。 - start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执
行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此
Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度
其它线程。 - run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实
就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下
面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用
start()方法而不是run()方法
- Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无
法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值
可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 - Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生
结果,Future 用于获取结果。
- FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可
以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
-
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU
的时间片这个也比较好理解。 -
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可
运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线
程会一直运行,直至它不得不放弃 CPU -
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建
一个线程并启动它,它的执行便依赖于线程调度器的实现。 -
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线
程优先级或者线程等待的时间。
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理
InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一
个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它
们竞争,只有获得锁的线程才能进入就绪状态;
两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long
timeout)超时后线程会自动苏醒。
为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法
用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象
调用方法一定定义在Object类中。
在java中有以下3种方法可以终止正在运行的线程:
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期
作废的方法。
使用interrupt方法中断线程。
什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是
一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后
才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
- 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
- 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。
- 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用
某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到
占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保
一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不
允许执行同步代码。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次
只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
- 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
- Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保
证共享变量的线程安全性的。
程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),
它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和
代码顺序执行的结果是一致的。
- synchronized关键字最主要的三种使用方式:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 - 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个
实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一
份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个
实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态
synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实
例对象锁。 - 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成,
每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就
会处于锁定状态并且尝试获取monitor的所有权 ,过程:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为
monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再
重新尝试获取monitor的所有权。
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线
程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0
时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候
threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断
threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为
轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的
对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;
而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
synchronized 和 ReentrantLock 区别是什么?
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,
synchronized 操作的应该是对象头中 mark word
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同
一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子
性
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的
可见性;禁止指令重排序。
区别
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改
可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
- 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进
行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存
中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从
Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,
效率低于 synchronized。
java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程
原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。
比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int
变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该
方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、
递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况
下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。
简单来说就是原子类来实现CAS无锁模式的算法
AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引
用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线
程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
线程池
什么是线程池?Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用
线程池。在开发过程中,合理地使用线程池能够带来许多好处。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降
低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建
和销毁线程所需的时间,从而提高效率。
如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是
不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还
不能控制线程池中线程的开始、挂起、和中止。
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,
避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统
的稳定性,使用线程池可以进行统一的分配,调优和监控。
ThreadPoolExecutor就是线程池
ThreadPoolExecutor其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入
不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池)



