sychronized与锁的区别
sychronized
sychronized的作用是:用来确保多线程下的线程安全。
sychronized使用方法
- 对象锁:用sychronized修饰代码块 (手动去指定锁对象 入口monitorenter 出口monitorexit )
- 类锁:用sychronized修饰普通方法(类锁只能在同一时刻被一个对象拥有 通过方法的 ACC_SYNCHRONIZED 标志符是否被设置,这里会隐形调用monitorenter、monitorexit这两个指令)
sychronized锁的状态:锁升级的过程
无锁状态:锁没有线程进行争夺。
偏向锁状态:第一个线程获取该锁,存储该线程id,并改变对象的对象头Mark Word中的状态。
偏向锁可以撤销成无锁状态, 线程是不会主动释放偏向锁,需要等待其他线程来竞争
这种机制可以减小只有一个线程下,CAS操作的性能消耗
轻量级锁状态:第二个线程尝试获取该锁,当检测到对象头Mark Word已存在偏向锁,进入自旋。
重量级锁状态:线程自旋失败,进入阻塞状态。
对象实例在堆中会被划分三个组成部分:对象头,实例数据,和对其填充。
对象头中Mark Word中可以获取该对象sychronized锁的状态。
sychronized锁升级到重量级锁时就是进入不可逆状态。
sychronized在jdk1.6之后的优化
- 自旋锁
- 自适应自旋锁:根据自旋状况,自动调整自旋的次数。
- 锁消除: JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除(无锁状态)
- 锁粗化: JVM检测到同一个对象有连续的加锁、解锁操作,会合并成为一个更大范围的加锁、解锁操作,例如将加锁解锁操作移到for循环外面。
- 轻量级锁
- 偏向锁
- 重量级锁
乐观锁与悲观锁 (上锁的的位置不同)
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放. sychronized提供的是悲观锁
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作.
如何实现乐观锁:
a.在数据库 添加version字段
b.通过CAS(自旋锁)操作:内存位置、期望原值、新值 (CAS称为无锁栈、无锁队列 是一个原子性操作(一段内存同时只能有一个CPU方位))
update status=3 where id=111 and status=1;(增加筛选条件)
注:悲观锁 适合写入频繁场景,
乐观锁 适合读取频繁场景。
如何进一步 确保乐观锁事务原子性:
a.借助C语言的ASC方式
b.加锁:加入悲观锁
CAS操作是一种无锁的方式来实现线程安全的方式,它有以下实现类:
- AtomicInteger :用于整形
- AtomicReference:用于引用类
- AtomicStampedReference:引入时间戳用于对时间敏感的引用类 ##这个可以解决ABA问题
- AtomicIntegerArray:用于数组
- AtomicIntegerFieldUpdater:借助反射机制,让普通变量也能使用原则操作,如User类中的age;
CAS操作有那些问题:
1.ABA问题 (修改后又被修改回来)
2.每个线程都会尝试去执行,对CPU的消耗比较大
3.只能保证一个共享变量的原子操作
读写锁
读写锁的规则是“ 读读不互斥,读写互斥,写写互斥”
可以理解为: 读锁相当于一个共享锁,写锁相当于互斥锁。
读写锁是基于AQS原理
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(FIFO先进先出),虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
公平锁和非公平锁
公平锁和非公公平锁的区别在于公平锁会保证先来的线程会先获取资源(其内部实现时AQS原理),而非公平不能保证。
sychronized是非公平锁,ReenterLock(默认也是非公平锁)中的读写锁是公平锁。
自旋锁
自旋锁,是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁的状态是通过自旋锁的保持者来改变(命令模式).
互斥锁
互斥锁: 同一时刻只能有一个线程获得互斥锁,其余线程处于睡眠状态.
自旋锁vs互斥锁
- 互斥锁和自旋锁都能够保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
- 自旋锁没有上下文的切换性能比较高,但是存在死锁的风险,只适用保持锁时间比较短的情况下。
- 互斥锁会让获取不到资源的线程sleep(不放弃锁,放弃对CPU的争夺),进入线程阻塞状态。
不放弃锁:就意味着已经获得该锁的线程可以再次进入被该锁锁定的代码块。
这种锁被称为可重入锁, synchronized和ReenterLock都是可重入锁。