- Java 内存模型(JMM)
- 简述线程的可见性
- volatile 可以保证线程安全吗
- 并行和并发有什么区别?
- 线程和进程的区别?
- 进程间通信 IPC 方式
- CPU上下文切换
- 进程调度算法
- Linux 默认的进程调度方法
- 守护线程是什么?
- 创建线程有哪几种方式?
- 线程有哪些状态?
- sleep() 和 wait() 有什么区别?
- notify()和 notifyAll()有什么区别?
- 线程的 run()和 start()有什么区别?
- 创建线程池有哪几种方式?
- 线程池都有哪些状态?
- 线程池中 submit()和 execute()方法有什么区别?
- 线程池的优点
- 设计一个线程池的思路
- 在 java 程序中怎么保证多线程的运行安全?
- 多线程锁的升级原理是什么?
- 什么是死锁?
- 怎么防止死锁?
- ThreadLocal 是什么?有哪些使用场景?
- 说一下 synchronized 底层实现原理?
- synchronized 和 volatile 的区别是什么?
- synchronized 和 Lock 有什么区别?
- synchronized 和 ReentrantLock 的异同?
Java 内存模型(JMM)
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

-
所有的变量都存储在主内存中
-
每个线程都有自己的独立工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
-
两条规则:
-
线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
-
不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
-
简述线程的可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性
-
synchronized实现可见性
- synchronized能够实现:
- 原子性(同步)
- 可见性
- JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中。
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要的是同一把锁)
- synchronized能够实现:
-
volatile实现可见性
-
volatile关键字:
- 能够保证volatile变量的可见性
- 不能保证volatile变量的原子性
-
volatile如何实现内存可见性:
深入来说:通过加入内存屏障和禁止重排序优化来实现的。
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
- store指令会在写操作后把最新的值强制刷新到主内存中。同时还会禁止cpu对代码进行重排序优化。这样就保证了值在主内存中是最新的。
- 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
- load指令会在读操作前把内存缓存中的值清空后,再从主内存中读取最新的值。
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
-
volatile如何实现内存可见性:
通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当变量发生变化时,又强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新的值。
-
-
final实现内存可见性:
- Final 变量在并发当中,原理是通过禁止cpu的指令集重排序,来提供现成的可见性,来保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用。
-
synchronized和volatile的比较:
- synchronized锁住的是变量和变量的操作,而volatile锁住的只是变量,而且该变量的值不能依赖它本身的值,volatile算是一种轻量级的同步锁
- volatile不需要加锁,比synchronized更加轻量级,不会阻塞线程。
- 从内存可见性角度讲,volatile读相当于加锁,volatilexie相当于解锁。
- synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
注:由于voaltile比synchronized更加轻量级,所以执行的效率肯定是比synchroized更高。在可以保证原子性操作时,可以尽量的选择使用volatile。在其他不能保证其操作的原子性时,再去考虑使用synchronized。
volatile 可以保证线程安全吗
不能绝对保证
可以使用 volatile 的条件:
-
对变量的写操作不依赖当前值。如 i++ 是无法通过volatile保证结果准确性的;

-
该变量没有包含在具有其它变量的不变式中
// 若同时设置了setLower(8)和setUpper(5),就会设置无效范围(8, 5) public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
并行和并发有什么区别?
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
线程和进程的区别?
进程时操作系统进行资源分配的最小单元;线程时操作系统进行任务分配的最小单元,线程隶属于进程
进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。同一进程中的多个线程之间可以并发执行。
进程间通信 IPC 方式
概念:每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
方式:
- 管道:内核中申请一块固定大小的缓冲区,无名管道一般使用fork函数实现父子进程的通信,命名管道用于没有血缘关系的进程也可以进程间通信;面向字节流、自带同步互斥机制、半双工,单向通信,两个管道实现双向通信。
- 消息队列:在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列;消息队列独立于发送与接收进程,可以通过顺序和消息类型读取,也可以fifo读取;消息队列可实现双向通信
- 共享内存:将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。目前最快的IPC形式,不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以,共享内存是临界资源,所以需要操作时必须要保证原子性。使用信号量或者互斥锁都可以。
- 信号量: 在内核中创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1,通过对临界资源进行保护实现多进程的同步
- 套接字:socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据。socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种”打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个”文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。是一种可以网间通信的方式。
- https://www.jianshu.com/p/c1015f5ffa74
- https://www.sohu.com/a/430956382_701814
CPU上下文切换
什么是 CPU 上下文:
CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。
- CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
- 程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
什么是 CPU 上下文切换:
把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
进程调度算法
为什么需要调度算法
用户进程数一般都多于处理机数,从而导致进程互相争夺处理机。同时,系统进程也需要使用处理机。
因此,这就需要进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,使之执行。
分类
- 抢占式
- 非抢占式
调度算法
- 批处理系统
- 先来先服务
- 短作业优先
- 最短剩余时间优先
- 交互式系统
- 时间片轮转
- 优先级调度
- 多级反馈队列
http://jalan.space/2019/03/16/2019/process-scheduling/
Linux 默认的进程调度方法
CFS完全公平调度: CFS的出发点基于一个简单的理念:即所有进程实际占用处理器CPU的时间应为一致,目的是确保每个进程公平的处理器使用比,即最大的利用了计算资源。FIFO先入先出队列:不基于时间片调度,处于可运行状态的SCHED_FIFO级别的进程比SCHED_NORMAL有更高优先级得到调度,一旦SCHED_FIFO级别的进程处于可执行的状态,它就会一致运行,直到进程阻塞或者主动释放。RR(Round-Robin):SCHED_RR级别的进程在耗尽事先分配的时间片之后就不会继续执行。即可以理解将RR调度理解为带有时间片的SCHED_FIFO。
https://zhuanlan.zhihu.com/p/75879578
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式?
-
继承Thread类重写run方法
public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); } } class MyThread extends Thread { @Overrride public void run() { System.out.println("Start new thread"); } } -
通过Runnable接口重写run方法
public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("Start new thread"); } } -
通过Callable和Future创建线程
- 实现 Callable 接口,重写 call 方法
- 创建 Callable 类的实例,使用 FutureTask 类对象包装该实例,FutueTask 对象封装了 Callable 对象 call 方法的返回值
- 将 FutureTask 作为 Thread 对象的 target 属性传入,并启动线程。
- 调用 FutureTask 对象的 get 方法,获取返回值。
public class Main { public static void main(String[] args) { FutureTask futureTask = new FutureTask<>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回的结果是:" + futureTask.get()); } catch (Exception e) { e.printStackTrace(); } } } class MyCallable implements Callable { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()); return 99; } } -
通过线程池创建线程
public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); MyRunnable myRunnable = new MyRunnable(); for(int i = 0; i < 10; i++){ executorService.execute(myRunnable); } System.out.println("=======任务开始======="); executorService.shutdown(); } } class MyRunnable implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } // 根据阿里开发手册,不推荐使用 executors 创建线程池,直接使用 threadPoolExecutor 创建,自己定义 队列的类型和长度(默认情况为 Integer.MAX_VALUE,)
线程有哪些状态?
线程通常都有 6 种状态,new, runnable, terminated, timed waiting, waiting, blocked。
-
创建状态。在生成(new)线程对象,此时仅由 JVM为其分配内存,并初始化其成员变量的值,并没有调用该对象的start方法,这是线程处于创建状态。
-
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
-
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
就绪状态和运行状态对应 runnable 状态(因为时间片很短,区分 ready 和 running 没什么意义,这里是虚拟机的状态,并不反映操作系统的真实线程状态)
-
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep, suspend, wait等方法都可以导致线程阻塞。
对应 timed waitting, waiting blocked 状态
发生情况:
- 等待阻塞(o.wait->等待对列)
- 同步阻塞(lock->锁池)
- 其他阻塞(sleep/join)
-
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
notify()和 notifyAll()有什么区别?
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
创建线程池有哪几种方式?
-
newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
-
newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
-
newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
-
newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:

线程池中 submit()和 execute()方法有什么区别?
- 接收的参数不一样
- submit有返回值,而execute没有
- submit方便Exception处理
线程池的优点
- 采用线程池的方案,将线程重复利用,从而保证系统的效率,避免过多资源浪费在创建销毁线程上。
- 提高相应速度,请求或者任务到达可以直接相应处理
- 将任务提交和执行分离,降低耦合
- 提高线程的可管理性。使用线程池统一分配,调优,监控。
设计一个线程池的思路
线程池的生命周期:
- 启动线程池时,我们需要预先创建并启动若干个线程以用来接收传入的任务
- 创建的线程名称,优先级等待属性需要统一设置,我们可以通过一个特定或默认的工厂进行批量生产
- 启动线程后,我们还要对线程进行重复利用,那么需要容器来存取线程
- 启动的线程数必须要有限制,不然无尽的线程数会使cpu频繁切换上下文,从而使cpu资源严重浪费,同时大量的线程也会占用大量的内存空间,导致 OOM
- 如果传入新任务,但所有线程都在执行任务中,无暇顾及传入的任务。需要将其缓存下来,等待任务完毕的某个线程接收该任务继续工作
- 继续传入新任务,导致超出缓存大小,或者缓存过大占用大量内存空间进而 OOM。那么可以增加若干个线程,加快处理任务的进度
- 如果还是不停的有任务加入,但是依照现在的资源状况,既不允许将任务缓存,也不能允许增加线程进行处理。就只好寻找一个策略来处理这些的任务
- 任务逐渐处理完毕,不需要新创建的线程就能够应对了,可以将这些线程销毁来降低资源的消耗。不过考虑到万一销毁后马上又有大批新任务处理不过来,于是设置一个空闲等待的销毁时间,这段时间里这些线程还是空闲,则销毁,反之则继续运行
- 如果运行过程中,需要将线程池停下来,要么所有线程马上停止,要么正在运行的线程运行完毕后停止
- 所有任务全部都处理完毕了,所有线程都处于空闲状态了,需要将所有线程封存起来,以尽力降低空闲线程的消耗
线程池中比较重要的一些属性和方法:
preStartAllCoreThreads()方法预热线程池,不断向线程池中加入新的worker线程,直到达到 核心线程数 corePoolSize- 线程工厂
threadFactory,批量创建线程,定制线程的名称。 - 工作线程池
workers,使用 HashSet 用于存取 work 线程。 - 核心线程数
corePoolSize,维持线程最基本的线程数,注意尽量不要设置数值很大,如 Integer.MAX_VALUE。不然创建大量线程导致CPU和内存资源过于紧张浪费。 - 工作队列
workQueue,是一个阻塞队列,保存等待执行的任务。有很多类,但是注意尽量不要使用无界队列 (界限最大值为Integer.MAX_VALUE),任务入队过多会导致内存紧张进而 OOM,并且会使 maximumPoolSize失效 - 最大线程数
maximumPoolSize,用于在阻塞队列满了后,继续创建新线程执行任务。同理不得设置过大 - 拒绝策略、饱和策略 handler,在线程池和队列都满的情况下,需要采取一种策略处理新提交的任务。Java 提供的策略有:
- AbortPolicy :直接抛异常
- DiscardPolicy:丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- CallerRunPolicy:当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,这样的话提交的任务就不会被丢弃而造成业务损失,同时这种谁提交谁负责的策略必须让提交线程来负责执行,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈。也有利于线程池中的线程来消化任务
- 空闲线程存活时间
keepAliveTime,单位是unit,在线程空闲后如果超过这个时间就将其销毁。 shutDown和shutDownNow方法
在 java 程序中怎么保证多线程的运行安全?
并发三大特性:
- 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
- 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示过程:


什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
怎么防止死锁?
死锁的四个必要条件:
- 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
- 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
- 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
- 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
方法:
- 互斥条件:使用读锁,运行数据被很多线程同时读取
- 请求和保持条件:对持有资源设置一定的时间限制
- 不可剥夺条件:对优先度高的进程可以剥夺其他进程的资源
- 环路等待条件:对资源进行编号,对于申请资源1,2,只有申请到了1才可以申请2
ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的
存在的问题
- 对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用, 造成一系列问题。
- 内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。
使用场景:最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
synchronized 和 volatile 的区别是什么?
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
synchronized 和 Lock 有什么区别?
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
synchronized 和 ReentrantLock 的异同?
-
可重入性:
两者都是可重入锁
-
功能:
- 都是用来协调线程对共享对象、变量的访问
- 都保证了可见性和互斥性
-
锁的实现:
对于 Synchronized,它是 Java 语言的关键字,是原生语法层面的互斥,需要 JVM 实现。而 ReentrantLock 是 JDK 1.5 以后提供的 API 层面的互斥锁,需要 lock 和 unlock 方法配合 try/finally 语句块来完成。
-
性能的区别:
- 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。
- 在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
-
功能的区别:
- 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
- 锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
-
ReentrantLock 独有的能力:
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
- 提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
Reference:
- https://mp.weixin.qq.com/s/ywOwdXuMG5rhEXMH_OXKEQ