基础概念
什么是进程和线程
进程是程序运行资源分配的最小单位
桌面双击启动一个程序,该程序运行了一个进程,该进程里有多个线程。
这些线程共享该进程中的全部系统资源。资源包括:CPU、内存空间、 磁盘 IO 等。而进程和进程之间是相互独立的。
进程分为系统进程和用户进程。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
线程本身拥有很少的资源,大部分都是共享进程所拥有的全部资源。
CPU 核心数和线程数的关系
多核心:也指单芯片多处理器。多个 CPU 同时并行地运行程序。
多线程: 让同一个处理器上的多个线程同步执行并共享处理器的执行资源(并发)。
CPU 时间片轮转机制(RR调度)
并行和并发
并发:指应用能够交替执行不同的任务。线程>核数的去执行
并行:指应用能够同时执行不同的任务。双核cpu,两线程分别去一个核执行
高并发编程的意义、好处和注意事项
(1)充分利用 CPU 的资源
(2)加快响应用户的时间
(3)可以使你的代码模块化,异步化,简单化
例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,
将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。
这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。
多线程程序需要注意事项
(1)线程之间的安全性
在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。
(2)线程之间的死锁
为了解决线程之间的安全性引入了 Java 的锁机制。而锁用的不好就容易死锁,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。
(3)线程太多了会将服务器资源耗尽形成死机当机
线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU的“过渡切换”,造成系统的死机。
解决方法:示例数据库连接池。
只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。
Java 里的线程
精华就是这个图!!!!!
Java 程序天生就是多线程的
啥也不干就启动一个main(),java就会启动好多线程。main()主线程、和众多守护线程( 分发处理发送给 JVM 信号的线程、GC线程、内存 dump线程等)
启动
启动线程的方式有两种:
1、继承Thread, X extends Thread;然后 X.start
2、实现Runnable,X implements Runnable;然后交给 Thread 运行
Thread 和 Runnable 的区别:
Thread 才是 Java 里对线程的唯一抽象
Runnable 只是对任务(业务逻辑)的抽象
Thread 可以接受任意一个 Runnable 的实例并执行。
中止
自然终止
run 执行完成,或者抛出一个未处理的异常导致线程提前结束。
stop
暂停suspend()、恢复resume()和停止stop()。这三方法不建议使用。
原因:这些方法是强制执行的。suspend、stop的时候。线程占着资源(锁和其他资源等)直接睡眠,停止了,这样会导致死锁、内存泄露等等问题。
中断 interrupt()
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作。
中断 interrupt()是一个中断标志位,只是通知一下线程应该中断了,但是不会对线程有任何影响,然后根据代码判断控制是否该结束线程释放资源。
因为只是一个标志,所以当调用interrupt()方法时,不会影响改变线程的生命周期,线程可以完全不理会继续执行,或者结束。
interrupt() 发起中断请求,将
boolean isInterrupted() 判断是否被中断
boolean Thread.interrupted() 判断当前线程是否被中断,不过 Thread.interrupted()会同时将中断标识位改写为 false
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。
不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调
用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取
消标志。这种情况下,使用中断会更好,因为,
一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可
以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断!!!
重要方法
run()和 start()
Thread类是Java里对线程概念的抽象。
通过new Thread()其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。
只有执行了 start()方法后,才实现了真正意义上的启动线程。
start()方法不能重复调用,如果重复调用会抛出异常。
run() 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方
法并没有任何区别,可以重复执行,也可以被单独调用。
run()=普通方法
yield()
使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也不会释放锁资源。让完之后该线程与其他线程一起竞争cpu使用权。
注意:并不是每个线程都需要这个锁的,而且执行 yield( )的线
程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。
所有执行 yield()的线程有可能在进入到就绪状态后会被操作系统再次选中
马上又被执行。
join()
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续
执行线程 B。(此处为常见面试考点)
线程的优先级
setPriority()设置优先级,1~10,默认5。但是不同系统有不同的优先级,所以不可靠。聊胜于无
守护线程
线程分为用户线程和守护线程。自己启动的叫用户线程,后台为用户线程服务的是守护线程。
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
当用户线程全死亡时,他的守护线程也会死亡。
所以try{}finally{}代码块,当线程为守护线程时,他的用户线程死亡,他被清除,不一定会执行finally{}代码块,所以这种情况下是不安全的。
可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。
wait()和 notify/notifyAll()
wait、notify是Object里的方法。线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法--- 所以是通过某对象(Obj)来让线程等待,更新的
必须放在同步方法里(synchronized)
wait()调用后会释放锁资源!!!---因为别人要更新不放就死锁了
notify()调用后不会释放锁资源---所以一般notify()放在代码的最后一行,全部run()都跑完了,才释放锁
notify()随机更新一个用到该对象的线程,notifyAll()通知所有线程
wait(long millis) ---等待超时就抛一个异常
是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B
调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()
方法返回,进而执行后续操作。
上述两个线程通过对象 O 来完成交互,而对象
上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通
知方之间的交互工作。
标准范式
生产/消费模式案例
连接池(生产/消费)模式的实现!
线程生命周期总结!!!
这图不够完善,有错误
补充完整版
只有synchronized才会让线程进入阻塞状态,Lock让锁进入等待状态。阻塞:被动进入(没有抢到锁)。等待:主动进入(wait、sleep等)
线程间的共享和协作
synchronized 内置锁
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字
synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线
程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量
访问的可见性和排他性(原子性),又称为内置锁机制。
synchronized叫做对象锁,所以它锁的是一个对象,可以用在方法上或者代码块中。
但是也能用在static类(静态类) 上,叫他 类锁。实质锁的是静态类的Class对象。 所以还是对象锁。
错误使用
比如锁Integer等对象时,失效。因为Integer等对象,实现i+1的时候,是将Integer->转成int,然后 int+1在new 一个Integer赋值进去的,所以是新的对象。
volatile,最轻量的同步机制
volatile关键词试用于一写多读。它保证了可见性不保证原子性
volatile关键词的作用是,一个线程里修改,能通知其他线程这个变量改变了。
ThreadLocal
ThreadLocal线程局部变量,在多线程试用的时候,每个线程都有自己的这个变量。为每个线程提供变量副本/线程隔离
ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
ThreadLocal的使用,get、set、remove(删除并回收资源)
使用案例: Spring 的事务就借助了 ThreadLocal 类。Spring 会从数据库连接池中获得一个 connection,然会把 connection 放进 ThreadLocal 中,也就和线程绑定了,事务需 要提交或者回滚,只要从 ThreadLocal 中拿到 connection 进行操作。
ThreadLocal的get方法
ThreadLocalMap方法
1.使用不当引发内存泄漏
看上图和上上图(ThreadLocalMap方法)得出
引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap
还持有 ThreadLocal 的引用,如果没有手动删除,ThreadLocal 的对象实例不会
被回收,导致 Entry 内存泄漏。(remove手动删除,set、get方法可能会删除一些key为null的map)
由于 ThreadLocalMap 的生命周期跟
Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏。而不是因为弱引用,强引用必会发生内存泄露。所以取舍之间设计师选择了弱引用
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。
2.错误使用 ThreadLocal 导致线程不安全
当设置为static静态时,全局共用一份变量,没法建变量副本。所以会导致ThreadLocal失效。大家都改的同一个变量导致线程不安全。
死锁
概念
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
1.操作者≥2 && 资源≥2 && 操作者≥资源(1.多线程、2.多资源、3.多线程抢夺较少的资源)
2.争夺资源的顺序不对(1线程占有1资源,2线程占有2资源。然后1抢2,2抢1)
3.争夺者拿到资源不放手。
学术化的定义
1)互斥:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
解决
根据上面形成死锁的原因,可以任选一个解决。
1.操作者、资源数量:无法改变。
2.争夺资源的顺序不对。内部通过顺序比较,确定拿锁的顺序。
3.争夺者拿到资源不放手。采用尝试拿锁的机制。(没拿到锁就释放,用tryLock{}finally{释放锁})
尝试拿锁的机制
新启两线程,抢两资源,1线程先抢1锁后2,2线程先抢2锁后1锁
活锁
概念
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
如果把上面例子。随机睡眠的时间去掉,运行结果:
结果就是很长的拿锁,释放锁,这也就是所谓的活锁。线程12分别拿到12锁,去取时发现没有了,然后都一起释放了12锁,再同步去取,又没有锁了,这样无限循环。
解决
各个线程取锁的时间稍晚错开一点。
线程饥饿
低优先级的线程,总是拿不到执行时间
CAS (compare and swap比较并且交换)乐观锁
原子操作:不可分割的操作。 --- 从外部看该操作是不可分割的,要么执行完,要么不执行(synchronized就是原子操作)
想要实现原子操作,用锁机制完全可以实现,但是synchronized太笨重了。想要高效的执行,CAS是一个不错的选择。
CAS现代CPU给的一个原子指令(i的内存地址,期望的值(旧值),新值) i跟旧值比较(compare),如果是旧值那就swap(交换)成新值。否的话就自旋(循环)
synchronized被称为悲观锁,CAS被称为乐观锁。
悲观锁:总感觉有别的线程会改变我的值,所以我先抢占锁,然后修改。
乐观锁:不认为有别的线程会改变我的值,所以我先修改,改完以后再去比较compare,如果之前没有被修改那就交换swap。否则就一直循环。
CAS比synchronized快的关键是,synchronized在抢锁的过程中会频繁的上下文切换。而CAS则是一直占有着cpu进行自旋(无限循环)
CAS的三大问题和解决
-
ABA问题 值从A-->C-->A,但是监测不到值的变化。老王桌子上放了一杯水,被别人喝了,然后又灌满放回原位。老王回来看到水没有变化。
解决:用版本戳,AtomicStampedReference(每次改动都有一个版本戳记录版本号)或者AtomicMarkableReference(每次改动只告诉你有没有改,没有具体的版本号) -
循环时间长开销大。 这个没法解决,日常开放不会遇到,如果遇到就使用synchronized。
-
只能保证一个共享变量的原子操作。 CAS每次只能改变一个变量。 解决:将多个变量放到一个bean里面
CAS的使用
系统给我们定义好了很多类,我们直接用就行,凡是Atomic开头的类,都是CAS
阻塞队列与线程池
阻塞队列
队列
先进先出FIFO的线性表。
阻塞队列
当队列空去取的时候,或者队列满去插入的时候。队列会阻塞线程,直到可操作。这样的队列称为阻塞队列。
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
生产者和消费者模式
原先生产和消费一一对应,很浪费资源,而且生产者速度过快,让消费者来不及消费导致生产者等待。或者消费者过快,生产者来不及。这样生产消费能力不均衡的问题很严重。
为了解决生产消费能力不均衡的问题便有了生产者消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
BlockingQueue的方法
BlockingQueue阻塞队列里不仅有阻塞方法,还有非阻塞方法,他们是成对出现的。
常用的阻塞队列
● ArrayBlockingQueue:数组有界阻塞队列。 (常用)
● LinkedBlockingQueue:链表有界阻塞队列。 (常用)
区别:
1.锁的实现不同。ArrayBlockingQueue生产和消费用的是同一个锁。Linked两个锁。
2.在生产或消费时操作不同。ArrayBlockingQueue直接将枚举对象插入或移除的。Linked把枚举对象转换为Node进行插入或移除,会影响性能。
3.队列大小初始化方式不同。Array必须指定初始化大小,Linked默认Integer.MAX_VALUE。
● SynchronousQueue:一个不存储元素的阻塞队列。
队列大小1,现拿现取。
● PriorityBlockingQueue:优先级无界阻塞队列。
● DelayQueue:优先级无界阻塞队列实现的延时队列。
是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
● LinkedTransferQueue:链表无界阻塞队列。
● LinkedBlockingDeque:链表双向阻塞队列。
有界无界
有界就是队列长度有限。满了以后就阻塞。
无界也会阻塞,因为取的时候队列空。
自己做的时候选有界,因为无界容易挤爆资源。
线程池
线程池好处
第一:降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行,减少线程创建,销毁时间。
第三:提高线程的可管理性。 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
ThreadPoolExecutor 的类关系
Executor(翻译执行器)是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。
ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;
AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。 ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
ThreadPoolExecutor创建线程池
提交任务
不要返回值,用execute()
要返回值,submit(),submit会返回一个future对象
关闭线程池
shutdown或shutdownNow。原理:遍历工作线程,然后逐一调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
区别:shutdown:启动线程池的关闭序列。被关闭的执行其不再接受新任务,全部任务执行完,线程死亡。 shutdownNow:取消所有尚未开始的任务。
合理地配置线程池
- CPU密集型任务
大量的计算,所以要减少上下文切换,最大线程数一般为cpu的个数+1;
为啥要+1呢? 因为操作系统会将一部分磁盘当内存来用,当任务存在磁盘里,需要将磁盘里读取到内存,然后在交给cpu处理。读取过程很慢,为了充分利用cpu,就多一个线程,当在需要读取的时候,就切换另一个线程让他跑在cpu。
获取cpu个数方法:Runtime.getRuntime().availableProcessors() - IO密集型任务(IO:网络IO,磁盘IO等)
2*n (cpu个数),因为io读写很慢,所以可以多一些线程 - 混合型任务
当CPU密集型任务和IO密集型任务五五开的时候,应该拆分成两种线程池。当一个很小,一个很大时,可以看成其中一种处理。
AbstractQueuedSynchronizer
AQS概述
AbstractQueuedSynchronizer(翻译 抽象、队列、同步)队列同步器(同步器或AQS)。是用来构建锁或者其他同步组件的基础框架。 它使用了一个int成员变量(state)表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
AQS使用方式和设计模式
使用方式:子类通过继承AQS并实现它的抽象方法来管理同步状态。
(同步状态:AQS里由一个int型的state,比如锁的状态是否被持有)
(一般是自定义同步组件的静态内部类继承AQS)
(同步状态重要的三方法:getState()、setState(int newState)和compareAndSetState(int expect,int update),如锁0表示没锁,1表示有线程持有锁,234表示有重入锁-一个线程持好几次锁)
compareAndSetState方法 翻译:比较并且设置state
AQS只是一个抽象类,本身没有实现任何同步接口,他只是定义了若干同步状态获取和释放的方法来供自定义同步组件使用(getState)。同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。
同步器和锁:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
锁需要继承同步器,并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
模板方法模式
同步器的设计基于模板方法模式。模板方法模式:定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中。(也就是abstract抽像出一堆方法,让子类去具体执行)
AQS中的方法主要分为三类:独占式获取与释放同步状态、共享式获取与释放、查询同步队列中的等待线程情况。
CLH队列锁
AQS是根据CLH队列锁设计的。(CLH 三个人的名字的缩写)
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
新来一个等锁的线程,就会被包装成这样的块,然后排到队列的末尾。所以这是一个先来后到的取锁队列,所以是公平锁。
公平和非公平锁
在ReentrantLock的构造函数中,有两内置对象。NonfairSync对象(非公平锁)和FairSync对象(公平锁)。
JMM
JMM(Java Memory Model)Java内存模型
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
a=a+1。这个简单的运算,cpu在执行的过程中,cpu从内存读取数据需要100纳秒,但是运算确只要0.6纳秒。为了解决这个读取时间长的问题,现代CPU在其内部开辟了3个缓存空间。
L1,L2,L3高速缓存区
为了适应cpu这种操作方式,java设计了JMM(java 内存模型)
JMM引发的线程安全问题
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
volatile还有抑制重排序(CPU运行代码时,不是按照顺序执行的,这样很小可能会出现问题,用volatile就强制顺序执行)。(cpu流水线等)
Synchronized的实现原理
Synchronized在JVM里的实现都是基于进入和退出 Monitor对象 来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
对同步块,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。
对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
synchronized使用的锁是存放在Java对象头里面
对象的头存储的信息:锁的状态、hashCode、对象分带年龄(GC次数所标记的老对象还是新对象)、锁标志位等。
各种锁
锁一共有四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态
Synchronized在设计的过程中,因为太笨重了(所有线程阻塞、cpu频繁的上下文切换-两倍的次)等原因优化了。将锁设计成偏向锁,轻量级锁和重量级锁三种状态。
Synchronized关键词下,(很多情况是单线程在反复的抢锁放锁,所以不必竞争,这时会使用)偏向锁,但是一旦线程多了以后,竞争激烈了,偏向锁就会膨胀成轻量锁(类似CAS),当轻量锁计算量很大,或者等待时间过长,超过一个时间片轮转周期的时候,这样还不如不用,就又会轻量锁就会膨胀成重量锁。锁只能升级,不能降级。
无锁状态-->偏向锁状态-->轻量级锁状态-->重量级锁状态
偏向锁
引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。
轻量级锁
类似于CAS,因为上下文切换很费cpu时间(5000-20000)而计算只要0.6纳秒,所以与其cpu一直上下文切换,还不如占着CPU一直做自旋。
自旋锁
先执行计算的代码,然后在CAS(比较并且交换),如果是错的(没有抢到锁),那就在重复回去初始化,计算,CAS。详情见CAS。
优点:对占用锁时间短的线程来说,提升很大
但是如果竞争大,持有锁的时间长(超过一个上下文切换的时间),那就还不如用重量级锁去阻塞线程了。因为自旋占着CPU做无用功还是挺浪费资源的。