参考链接:
https://www.cnblogs.com/takumicx/p/9338983.html
乐观锁与悲观锁https://blog.csdn.net/qq_34337272/article/details/81072874
CAS:https://www.jianshu.com/p/ae25eb3cfb5d
https://www.cnblogs.com/qjjazry/p/6581568.html
1. 乐观锁(CAS)
https://blog.csdn.net/q5706503/article/details/84558343
- 概述:CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。
- 缺点:
-
循环时间长开销很大:
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。 -
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
1.1 ABA问题
- 首先T1线程想把栈顶改为B
- 此时T2线程在T1之前,取出A,B;然后推入D,C,A。(此时B游离)
- 这时T1把栈顶改为B,但是B.Next()为空,这时候我们就丢失了C,D。
2. 偏向锁、轻量锁、重量锁
1.锁总是被第一个占用(很少竞争)他的线程拥有,这个线程就是锁的偏向线程。
使用流程:在第一次获得锁的时候,记录下线程id,当下次使用,对比线程id,一致的话,就还是偏向锁,就不需要使用cas去更新对象头。如果不一致,则变成轻量级锁
-
轻量级锁参看上面的自旋锁,由于cas(对比与竞争)算法,每次都会循环竞争,每次进入退出同步块都需要CAS更新对象头
-
当上面的自旋次数太多,则会变成重量级锁,把一直循环的线程变为阻塞状态,等待唤醒。
3. synchronized
- 为什么要使用synchronized
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
- synchronized的三种应用方式
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
3.1 synchronized缺陷和对应的lock
- 首先说一下synchronized的缺陷
当线程被synchronized修饰的话,除非1.当前线程执行完;2.线程被异常中端,否则不会释放当前资源的锁。
再举一个比较常见的例子:对于一个文件,我们两个线程同时去读,应该没有关系的(不会改变文件内容),但是加了synchronized只能一个读的时候,另外一个堵塞。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低
4.Lock
https://blog.csdn.net/takemetofly/article/details/48086069
https://blog.csdn.net/qq_34337272/article/details/79714196
https://blog.csdn.net/qq_34337272/article/details/79714196
- 注意lock时一个类,不是java的关键字
- 注意加锁后,要主动释放锁,不然会死锁
4.1 lock()方法,普通的获得锁
//新建锁对象(还不是获得锁),注意Lock时接口,不能直接用他来赋值,然后不要在try中新建lock。
Lock lock = ...
try{
//给当前代码块加锁。
lock.lock();
}catch(){
}finally{
//释放锁
lock.unlock();
}
4.2 trylock(),如果能获得锁,就获取,不然直接释放锁
Lock lock = ...
//尝试给当前代码块加锁。
if(lock.trylock()){
try{
}catch(){
}finally{
//释放锁
lock.unlock();
}else{
//无法获得锁就做其他事
}
4.3 lockInterruptibly(),当获取锁的时候,获取失败了,可以被interrupt方法中断。
1, 注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
2, 因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
3. 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
方法名称 | 描述 |
---|---|
void lock() | 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。 |
void lockInterruptibly() | 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。 |
boolean tryLock() | 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。 |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false. |
void unlock() | 释放锁。 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
5. lock锁的实现类
5.1. ReentrantLock类(可重入锁)
构造方法:
ReentrantLock() 创建一个 ReentrantLock的实例。
ReentrantLock(boolean fair) 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例
- 普通的锁
5.2 ReentrantReadWriteLock(读写锁)
(只要有写就会互斥)
5.3 lock于Condition的合作
方法名称 | 描述 |
---|---|
void await() | 相当于Object类的wait方法 |
boolean await(long time, TimeUnit unit) | 相当于Object类的wait(long timeout)方法 |
signal() | notify() |
signalAll() | notifyAll() |
- 创建一个condition调用
public class UseSingleConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() throws InterruptedException {
lock.lock();
try {
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("这是condition.signal()方法之后的语句");
} finally {
lock.unlock();
}
}
}
static public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
}
- 新建多个condition来实现顺序等待/通知机制
public class UseMoreConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyserviceMoreCondition service = new MyserviceMoreCondition();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
static public class ThreadA extends Thread {
private MyserviceMoreCondition service;
public ThreadA(MyserviceMoreCondition service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
static public class ThreadB extends Thread {
private MyserviceMoreCondition service;
public ThreadB(MyserviceMoreCondition service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
}
死锁的四个必要条件
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的避免和预防
1. 死锁避免(类似于银行家算法):
系统每次进行动态检查,查看是否为安全序列,是的话就可以,不是则不给予分配
2. 死锁预防(针对于死锁产生的四个条件)
- 破坏“不可剥夺”:进程在申请新的资源,得不到满足的时候,进程会暂时释放当前占用资源(也就是进程所占有的资源可以被占有)
缺点:可能会造成上一步的任务失效 - 破坏“占有与等待":两种解决方案
- 静态解决方案:进程每次只能一次性获取自己进程所需要的全部资源,不然不允许占有资源
缺点:会很浪费系统资源,导致部分进程可能仅在程序初期或者末期有机会投入运行 - 动态:进程在占有新的资源时,必须释放当前资源
- 破坏”循环等待“条件:给系统资源设置一个编号,规定每个进程按编号顺序请求资源,同类资源一次性申请完。
缺点:限制了新设备的加入,而且有一些进程可能不会按照规定顺序执行,造成资源浪费。