CAS
概述
CAS的全称为Compare-And-Swap ,它是一条CPU并发原语,
- 比较工作内存值(预期值)和主物理内存的共享值是否相同,相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。这个过程是原子的
- AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升
CAS并发原语体现在Java语言中就是sun.misc包下的UnSaffe类中的各个方法. 调用UnSafe类中的CAS方法,JVM会帮我实现 CAS汇编指令.
-
这是一种完全依赖于硬件 功能,通过它实现了原子操作
-
由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题
-
例子,比如 unSafe 中的 getAndIncrement 方法
-
public final boolean compareAndSet(int expect, int update)
: 如果当前状态值等于预期值,则原子方式将同步状态设置为给定的更新值。
/*
* CAS:Compare and swap [比较并交换]
* */
public class AtomicIntegerDemo {
public static void main(String[] args) {
//当前状态值为5
AtomicInteger atomicInteger=new AtomicInteger(5);
//当前状态值为:5;预期值为:5,更新值为2019
//结果:true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
//当前状态值为:2019;预期值为:5,更新值为2022
//结果:false 2019(更新失败所以返回 false)
System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
}
}
UnSafe类
UnSafe类 是 CAS 的核心类,由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问, UnSafe相当于一个后门,基于该类可以直接操作特定的内存数据.
-
UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作依赖于UnSafe类的方法
-
UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
-
public final int getAndIncrement
:
变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的 -
变量
value
和volatile修饰
,保证了多线程之间的可见性
atomic
atomic是原子类,主要有如下
Java开发手册中说明:
基本类型原子类(AtomicInteger、AtomicBoolean、AtomicLong)
方法 | 解释 |
---|---|
public final int get() | 获取当前的值 |
public final int getAndSet(int newValue) | 获取到当前的值,并设置新的值 |
public final int getAndIncrement() | 获取当前的值,并自增 |
public final int getAndDecrement() | 获取到当前的值,并自减 |
public final int getAndAdd(int delta) | 获取到当前的值,并加上预期的值 |
public final int incrementAndGet( ) | 返回的是加1后的值 |
boolean compareAndSet(int expect,int update) | 如果输入的数值等于预期值,返回true |
-
AtomicInteger 解决 i++ 多线程下不安全问题
代码如下:public class AtomicIntegerDemo { AtomicInteger atomicInteger=new AtomicInteger(0); public void addPlusPlus(){ atomicInteger.incrementAndGet(); } public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch=new CountDownLatch(10); AtomicIntegerDemo atomic=new AtomicIntegerDemo(); // 10个线程进行循环100次调用addPlusPlus的操作,最终结果是10*100=1000 for (int i = 1; i <= 10; i++) { new Thread(()->{ try{ for (int j = 1; j <= 100; j++) { atomic.addPlusPlus(); } }finally { countDownLatch.countDown(); } },String.valueOf(i)).start(); } //(1). 如果不加上下面的停顿3秒的时间,会导致还没有进行i++ 1000次main线程就已经结束了 //try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();} //(2). 使用CountDownLatch去解决等待时间的问题 countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t"+"获取到的result:"+atomic.atomicInteger.get()); } }
其中 CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
- CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
-
AtomicBoolean 可以作为中断标识停止线程的方式
//线程中断机制的实现方法 public class AtomicBooleanDemo { public static void main(String[] args) { AtomicBoolean atomicBoolean=new AtomicBoolean(false); new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t"+"coming....."); while(!atomicBoolean.get()){ System.out.println("=========="); } System.out.println(Thread.currentThread().getName()+"\t"+"over....."); },"A").start(); new Thread(()->{ atomicBoolean.set(true); },"B").start(); } }
-
AtomicLong 的底层是 CAS+自旋锁 的思想,适用于低并发的全局计算,高并发后性能急剧下降,原因如下:N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了(AtomicLong的自旋会成为瓶颈)
在高并发的情况下,我们使用LoadAdder
数组类型原子类 (AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray)
数组类型原子类,主要有三个AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
//(1). 创建一个新的AtomicIntegerArray,其长度与从给定数组复制的所有元素相同。
int[]arr2={1,2,3,4,5};
AtomicIntegerArray array=new AtomicIntegerArray(arr2);
//(2). 创建给定长度的新AtomicIntegerArray,所有元素最初为零。
//AtomicIntegerArray array=new AtomicIntegerArray(5);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
System.out.println();
System.out.println("=======");
array.getAndSet(0,1111);
System.out.println("============");
System.out.println("将数字中位置为0位置上的元素改为:"+array.get(0));
System.out.println("数组位置为1位置上的旧值是:"+array.get(1));
System.out.println("将数组位置为1位置上的数字进行加1的处理");
array.getAndIncrement(1);
System.out.println("数组位置为1位置上的新值是:"+array.get(1));
}
}
引用类型原子类 (AtomicReference、AtomicStampedReference、AtomicMarkableReference)
引用类型原子类主要有三个: AtomicReference、AtomicStampedReference、AtomicMark ableReference
-
使用AtomicReference来实现自旋锁案例
//自旋锁 public class AtomicReferenceThreadDemo { static AtomicReference<Thread>atomicReference=new AtomicReference<>(); static Thread thread; public static void lock(){ thread=Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t"+"coming....."); while(!atomicReference.compareAndSet(null,thread)){ } } public static void unlock(){ System.out.println(Thread.currentThread().getName()+"\t"+"over....."); atomicReference.compareAndSet(thread,null); } public static void main(String[] args) { new Thread(()->{ AtomicReferenceThreadDemo.lock(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();} AtomicReferenceThreadDemo.unlock(); },"A").start(); new Thread(()->{ AtomicReferenceThreadDemo.lock(); AtomicReferenceThreadDemo.unlock(); },"B").start(); } }
-
AtomicStampedReference
解决ABA问题
携带版本号的引用类型原子类,可以解决ABA问题
解决修改过几次
状态戳原子引用 -
AtomicMarkableReference
不建议用它解决ABA问题
原子更新带有标志位的引用类型对象
解决是否修改(它的定义就是将状态戳简化位true|false),类似一次性筷子
状态戳(true/false)原子引用
不建议用它解决ABA问题 -
AtomicStampedReference和AtomicMarkableReference
区别- stamped – version number 版本号,修改一次+1
- Markable – true、false 是否修改过
对象的属性修改原子类 (AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater)
以一种线程安全的方式操作非线程安全对象内的某些字段是否可以不要锁定整个对象,减少锁定的范围,只关注长期、敏感性变化的某一个字段,而不是整个对象,已达到精确加锁+节约内存的目的。即仅对某个操作进行加锁
使用对象属性修改原子类的要求:
-
更新的对象属性必须使用
public volatile修饰符
-
因为对象的属性修改类型都是抽象类,所以每次使用都必须使用静态方法newUpdater( )创建一个更新器,并且需要设置想要更新的类和属性。如下所示:
-
例子: 我们利用 AtomicIntegerFieldUpdater 将 Score 对象中的 int 元素设置为原子对象。
/*** 1.从AtomicIntegerFieldUpdaterDemo代码中我们不难发现,通过AtomicIntegerFieldUpdater更新score我们获取最后的int值时相较于AtomicInteger来说不需要调用get()方法! 2.对于AtomicIntegerFieldUpdaterDemo类的AtomicIntegerFieldUpdater是static final类型也就是说即使创建了100个对象AtomicIntegerField也只存在一个不会占用对象的内存,但是AtomicInteger会创建多个AtomicInteger对象,占用的内存比AtomicIntegerFieldUpdater大, 所以对于熟悉dubbo源码的人都知道,dubbo有个实现轮询负载均衡策略的类AtomicPositiveInteger用的就是AtomicIntegerFieldUpdater。 */ @SuppressWarnings("all") public class AtomicIntegerFieldUpdaterDemo { private static final int THREAD_NUM = 1000; //设置栅栏是为了防止循环还没结束就执行main线程输出自增的变量,导致误以为线程不安全 private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); Score score=new Score(); public static void main(String[] args)throws InterruptedException { Score score = new Score(); for (int j = 0; j < THREAD_NUM; j++) { new Thread(() -> { score.addTotalScore(score); countDownLatch.countDown(); }).start(); } countDownLatch.await(); System.out.println("totalScore的值:" + score.totalScore); } } class Score { String username; public volatile int totalScore = 0; //public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName) private static AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Score.class, "totalScore"); public void addTotalScore(Score score){ //public int incrementAndGet(T obj) { atomicIntegerFieldUpdater.incrementAndGet(score); } }
-
AtomicReferenceFieldUpdater:
原子更新引用类型字段的值
例子: 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次。//需求:多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次 public class AtomicReferenceFieldUpdaterDemo { public static void main(String[] args) { MyCar myCar=new MyCar(); AtomicReferenceFieldUpdater<MyCar,Boolean>atomicReferenceFieldUpdater= AtomicReferenceFieldUpdater.newUpdater(MyCar.class,Boolean.class,"flag"); for (int i = 1; i <= 5; i++) { new Thread(()->{ if(atomicReferenceFieldUpdater.compareAndSet(myCar,Boolean.FALSE,Boolean.TRUE)){ System.out.println(Thread.currentThread().getName()+"\t"+"---init....."); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over"); }else{ System.out.println(Thread.currentThread().getName()+"\t"+"------其它线程正在初始化"); } },String.valueOf(i)).start(); } } } class MyCar{ public volatile Boolean flag=false; }
-
AtomicIntegerFieldUpdater与AtomicInteger使用引发的思考
通过下面代码我们不难得知使用AtomicIntegerFieldUpdater与AtomicInteger其实效果是一致的,那既然已经存在了AtomicInteger并发之神又要写一个AtomicIntegerFieldUpdater呢?- 从AtomicIntegerFieldUpdaterDemo代码中我们不难发现,通过AtomicIntegerFieldUpdater更新score我们获取最后的int值时相较于AtomicInteger来说不需要调用get()方法!
- 对于AtomicIntegerFieldUpdaterDemo类的AtomicIntegerFieldUpdater是static final类型也就是说即使创建了100个对象AtomicIntegerField也只存在一个不会占用对象的内存
- 但是AtomicInteger会
创建多个AtomicInteger对象,
占用的内存比AtomicIntegerFieldUpdater大 - 所以对于熟悉dubbo源码的人都知道,dubbo有个实现轮询负载均衡策略的类AtomicPositiveInteger用的就是AtomicIntegerFieldUpdate,在netty底层大量使用了这个类
public static class Candidate { int id; volatile int score = 0; AtomicInteger score2 = new AtomicInteger(); } public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score"); public static AtomicInteger realScore = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { final Candidate candidate = new Candidate(); Thread[] t = new Thread[10000]; for (int i = 0; i < 10000; i++) { t[i] = new Thread() { @Override public void run() { if (Math.random() > 0.4) { candidate.score2.incrementAndGet(); scoreUpdater.incrementAndGet(candidate); realScore.incrementAndGet(); } } }; t[i].start(); } for (int i = 0; i < 10000; i++) { t[i].join(); } System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score); System.out.println("AtomicInteger Score=" + candidate.score2.get()); System.out.println("realScore=" + realScore.get()); } } /** AtomicIntegerFieldUpdater Score=5897 AtomicInteger Score=5897 realScore=5897 */
原子操作增强类(DoubleAccumulator 、DoubleAdder 、LongAccumulator 、LongAdder)
方法 | 解释 |
---|---|
void add(long x) | 将当前的value加1 |
void increment( ) | 将当前的value加1 |
void decrement( ) | 将当前value减1 |
long sum( ) | 返回当前的值,特别注意,在没有并发更新value的情况下。 |
long longvale | 等价于long sum( )。 |
long sumThenReset() | 获取当前value,并将value重置为0 |
-
LongAdder 和 LongAccumulato
LongAdder只能用来计算加法、减法,且从零开始计算
LongAccumulator提供了自定义的函数操作public class LongAdderDemo { public static void main(String[] args) { // LongAdder只能做加减法,不能做乘除法 LongAdder longAdder=new LongAdder(); longAdder.increment(); longAdder.increment(); longAdder.increment(); longAdder.decrement(); System.out.println(longAdder.longValue()); System.out.println("========"); //LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) //LongAccumulator longAccumulator=new LongAccumulator((x,y)->x+y,0); LongAccumulator longAccumulator=new LongAccumulator(new LongBinaryOperator() { @Override public long applyAsLong(long left, long right) { return left*right; } },5); longAccumulator.accumulate(1); longAccumulator.accumulate(2); longAccumulator.accumulate(3); System.out.println(longAccumulator.longValue()); } }
-
LongAdder高性能对比code演示
import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAccumulator; import java.util.concurrent.atomic.LongAdder; class ClickNumber { int number=0; //(1). 使用synchronized实现number++ public synchronized void add_synchronized(){ number++; } //(2). 使用AtomicInteger AtomicInteger atomicInteger=new AtomicInteger(); public void add_atomicInteger(){ atomicInteger.incrementAndGet(); } //(3). 使用AtomicLong AtomicLong atomicLong=new AtomicLong(); public void add_atomicLong(){ atomicLong.incrementAndGet(); } //(4). 使用LongAdder LongAdder adder=new LongAdder(); public void add_longAdder(){ adder.increment(); } //(5). 使用LongAccumulater LongAccumulator accumulator=new LongAccumulator((x, y)->x+y,0); public void add_longAccumulater(){ accumulator.accumulate(1); } } /** * 50个线程,每个线程100w次,总点赞数出来 * */ public class LongAdderCalcDemo { // 50个线程和每个线程点在100w次 public static final int SIZE_THREAD=50; public static final int _1w=10000; public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch=new CountDownLatch(SIZE_THREAD); ClickNumber clickNumber=new ClickNumber(); long startTime = System.currentTimeMillis(); for (int i = 1 ; i <=SIZE_THREAD ; i++) { new Thread(()->{ try{ for (int j = 1; j <=10*_1w; j++) { //我们明显可以看到调用LongAdder性能最好 //clickNumber.add_synchronized(); clickNumber.add_longAdder(); } }finally { countDownLatch.countDown(); } },String.valueOf(i)).start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("-----consTime:"+(endTime-startTime)+"毫秒"+"\t"); System.out.println(clickNumber.adder.longValue()); } }
-
结果显示
// clickNumber.add_longAdder() 的结果 -----consTime:107毫秒 5000000 // clickNumber.add_synchronized(); 的结果 -----consTime:447毫秒 5000000
CAS的缺点
- 循环时间长开销很大
我们可以看到getAndInt
方法执行时,有个do while
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
- 能保证一个共享变量的原子性,但无法保证多个共享变量的原子性。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
ABA问题
产生原因
假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后再修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
new Thread(() -> {
atomicInteger.compareAndSet(100, 101);
atomicInteger.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
//暂停一会儿线程
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
;
System.out.println(atomicInteger.compareAndSet(100, 2019) + "\t" + atomicInteger.get());
}, "t2").start();
}
}
结果:
true 2019
所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了虽然结果没问题,但是 线程 B 判断时,atomoicInteger其实已经是被修改过的了。如果我们用B的结果判断 atomoicInteger
是否发生修改就无法获得正确答案。
ABA问题解决方案
-
①. ABA问题解决方案是增加版本号:使用 AtomicStampedReference 每修改一次都会有一个版本号
-
②. 注意:AtomicStampedReference用来解决AtomicInteger中的ABA问题,该demo企图将integer的值从0一直增长到1000,但当integer的值增长到128后,将停止增长。出现该现象有两点原因:
- 使用int类型而非Integer保存当前值
- Interger对-128~127的缓存[这个范围才有效,不在这个范围comareAndSet会一直返回false
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1); public static void main(String[] args) { new Thread(()->{ int stamp = stampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference()); //暂停1秒钟t3线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference()); stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference()); },"t3").start(); new Thread(()->{ int stamp = stampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference()); //保证线程3完成1次ABA try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp()); System.out.println("最新的值\t"+stampedReference.getReference()); },"t4").start(); } }
六大锁
乐观锁和悲观锁
悲观锁
- synchronized关键字和Lock的实现类都是悲观锁
- 什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改=
- 适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
- 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁在Java中通过使用无锁编程来实现,最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升
乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
例子:
//悲观锁的调用方式
public synchronized void m1(){
//加锁后的业务逻辑
}
//保证多个线程使用的是同一个lock对象的前提下
ReetrantLock lock=new ReentrantLock();
public void m2(){
lock.lock();
try{
//操作同步资源
}finally{
lock.unlock();
}
}
//乐观锁的调用方式
//保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicIntege=new AtomicInteger();
atomicIntege.incrementAndGet();
公平锁和非公平锁
概念
-
公平锁: 是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到
-
非公平锁: 是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象
注意:synchronized 和 ReentrantLock 默认是非公平锁 -
例子: 排队抢票案例(公平出现锁饥饿)
锁饥饿:我们使用5个线程买100张票,使用ReentrantLock默认是非公平锁,获取到的结果可能都是A线程在出售这100张票,会导致B、C、D、E线程发生锁饥饿class Ticket { private int number = 50; private Lock lock = new ReentrantLock(true); //默认用的是非公平锁,分配的平均一点,=--》公平一点 public void sale() { lock.lock(); try { if(number > 0) { System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number); } }finally { lock.unlock(); } } /*Object objectLock = new Object(); public void sale(){ synchronized (objectLock) { if(number > 0) { System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number); } } }*/ } public class SaleTicketDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start(); new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start(); new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start(); new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start(); new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start(); } }
源码解析
- 公平锁: 排序排队公平锁,就是判断同步队列是否还有先驱节点的存在。如果没有先驱节点才能获锁
- 先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以运行了
ReentrantLock默认是非公平锁,公平锁要多一个方法,所以非公平锁的性能更好(aqs源码)
为什么会有公平锁、非公平锁的设计?为什么默认非公平?、
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销线程的开销
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用
可重入锁(又名递归锁)
概念
可重入锁:
- 可: 可以
- 重: 再次
- 入: 进入 ,进入什么:进入同步域(即同步代码块、方法或显示锁锁定的代码)
- 锁: 同步锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞
- 如果是1个有synchronized修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中
ReentrantLock和Synchronized
都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
代码验证synchronized是可重入锁
//synchronized 是可重入锁
class Phone{
public synchronized void sendSms() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendSms");
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendEmail");
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* * 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
* **/
public class ReenterLockDemo {
/**
* t1 sendSms
* t1 sendEmail
* t2 sendSms
* t2 sendEmail
* @param args
*/
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
}
}
ReentrantLock 是可重入锁
//ReentrantLock 是可重入锁
class Phone implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tget");
set();
} finally {
lock.unlock();
}
}
private void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tset");
} finally {
lock.unlock();
}
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* <p>
* 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
**/
public class ReenterLockDemo {
/**
* Thread-0 get
* Thread-0 set
* Thread-1 get
* Thread-1 set
*/
public static void main(String[] args) {
Phone phone = new Phone();
Thread t3 = new Thread(phone);
Thread t4 = new Thread(phone);
t3.start();
t4.start();
}
}
可重入锁的种类
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,在同步块、同步方法使用
在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的 - 显示锁(即Lock)也有ReentrantLock这样的可重入锁
lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程
Synchronized的重入的实现机理
Synchronized的重入的实现机理(为什么任何一个对象都可以成为一个锁)
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁。 这个计数器的数字也可以叫做所得重入次数。
- 当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放
死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
产生死锁的原因
-
系统资源不足
-
进程运行推进的顺序不合适
-
资源分配不当
-
例子如下:
public class DeadLockDemo{ static Object lockA = new Object(); static Object lockB = new Object(); public static void main(String[] args){ Thread a = new Thread(() -> { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功"); } } }, "a"); a.start(); new Thread(() -> { synchronized (lockB){ System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA){ System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功"); } } },"b").start(); } }
死锁产生的必要条件
- 互斥条件: 进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件: 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
死锁的避免
-
加锁顺序(线程按照一定的顺序加锁)
-
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
-
死锁检测
死锁的排除方案1:jstack 进程号
如下所示:D:\studySoft\Idea201903\JavaSelfStudy>jps 10048 Launcher 6276 DeadLockDemo 6332 Jps 9356 D:\studySoft\Idea201903\JavaSelfStudy>jstack 6276 (最后面有一个发现了一个死锁) 2021-07-28 16:05:36 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode): "DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003592800 nid=0x830 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "b" #15 prio=5 os_prio=0 tid=0x00000000253d5000 nid=0x1ba8 waiting for monitor entry [0x0000000025c8e000] java.lang.Thread.State: BLOCKED (on object monitor) at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31) - waiting to lock <0x0000000741404050> (a java.lang.Object) - locked <0x0000000741404060> (a java.lang.Object) at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "a" #14 prio=5 os_prio=0 tid=0x00000000253d3800 nid=0xad8 waiting for monitor entry [0x0000000025b8e000] java.lang.Thread.State: BLOCKED (on object monitor) at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20) - waiting to lock <0x0000000741404060> (a java.lang.Object) - locked <0x0000000741404050> (a java.lang.Object) at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #13 daemon prio=9 os_prio=0 tid=0x000000002357b800 nid=0x1630 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x00000000234f6000 nid=0x1fd4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x00000000234f3000 nid=0x5c0 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x00000000234ed800 nid=0x1afc waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x00000000234eb800 nid=0x2ae0 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x0000000023464800 nid=0xc50 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x000000002345f800 nid=0x1b0c runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000000023451000 nid=0x2028 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000002343f800 nid=0x1ea0 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000233eb800 nid=0x10dc runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000233d3000 nid=0xafc in Object.wait() [0x000000002472f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143) - locked <0x0000000741008e98> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000021d0d000 nid=0x28ec in Object.wait() [0x000000002462f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x0000000741006b40> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference.tryHandlePending(Reference.java:191) - locked <0x0000000741006b40> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) JNI global references: 2504 Found one Java-level deadlock: ============================= "b": waiting to lock monitor 0x0000000021d10b58 (object 0x0000000741404050, a java.lang.Object), which is held by "a" "a": waiting to lock monitor 0x0000000021d13498 (object 0x0000000741404060, a java.lang.Object), which is held by "b" Java stack information for the threads listed above: =================================================== "b": at com.xiaozhi.juc.DeadLockDemo.lambda$main$1(DeadLockDemo.java:31) - waiting to lock <0x0000000741404050> (a java.lang.Object) - locked <0x0000000741404060> (a java.lang.Object) at com.xiaozhi.juc.DeadLockDemo$$Lambda$2/2101440631.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "a": at com.xiaozhi.juc.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20) - waiting to lock <0x0000000741404060> (a java.lang.Object) - locked <0x0000000741404050> (a java.lang.Object) at com.xiaozhi.juc.DeadLockDemo$$Lambda$1/1537358694.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock.
可以看到,通过
jstack 6276
我们找到了一个死锁:“b” and “a” -
死锁的排除方案2:jconsole
jconsole为java自带的分析工具:
自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
- 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
//自旋锁
public class AtomicReferenceThreadDemo {
static AtomicReference<Thread>atomicReference=new AtomicReference<>();
static Thread thread;
public static void lock(){
thread=Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
while(!atomicReference.compareAndSet(null,thread)){
}
}
public static void unlock(){
System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
atomicReference.compareAndSet(thread,null);
}
public static void main(String[] args) {
new Thread(()->{
AtomicReferenceThreadDemo.lock();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
AtomicReferenceThreadDemo.unlock();
},"A").start();
new Thread(()->{
AtomicReferenceThreadDemo.lock();
AtomicReferenceThreadDemo.unlock();
},"B").start();
}
}
自旋锁是一种 基于 CAS 的锁的实现