1 synchronized的工作过程
synchronzed是一个自适应的锁,应该根据具体情况来决定选取那种锁策略;
synchronzed一开始是一个轻量级锁(不在操作系统内核),如果冲突比较严重,就会转化成重量级锁(悲观锁)。synchronzed不是读写锁,是可重入锁,sychronzed是轻量级锁的时候,大概率采用的是自旋锁的方式来实现,当synchronzed是重量级锁的时候,大概率是以挂起等待锁的方式来实现
(1)一开始是以偏向锁(乐观锁)的方式来实现的,偏向锁只是在对象头里面设计了一个偏向锁标记,这个只是做标记要比真正的加锁高效很多,如果赌赢了,那么这次就不加锁了。直到锁的释放,因为这个过程根本就没有加锁,因此就很快;
但是如果赌输了,线程一尝试获取这把锁,锁进入偏向锁状态,此时有一个线程二也尝试竞争这把锁,那么线程一就会抢先把这把锁拿到,线程二就会进行阻塞等待(相当于是有偏向锁变成了真加锁)
(2) 如果出现了竞争,但这时竞争比较小此时我们就进入轻量级锁状态,此时的轻量级锁,就是基于CAS实现的自旋锁,是完全属于用户态完成的操作,因为这里面不涉及用户态和内核态之间的交互,也不涉及到线程等待和调度,只是多费了一些CPU而已,此时就保证更高效的获取到锁(线程一释放锁,线程二就立即获取到锁);
(3)但是如果是当前的场景,锁之间的冲突比较大,锁之间的竞争比较激烈,此时的锁会进一步的膨胀成重量级锁;如果锁之间的冲突和竞争比较大,轻量级自旋锁,就会浪费大量的CPU(在等待的时候CPU是空转的),此时我们就用更重量级的挂起等待锁,当线程等待的过程中,是释放CPU,代价就是引入了线程等待和调度开销;所以说,锁升级锁膨胀的过程中,完全是自适应的;
synchronized除了这个自适应的锁升级之外,还有一些重要的优化手段
1 锁消除:其实就是编译器和JVM自行判断一下看看这个代码是否真的需要加锁(JVM和编译器不相信程序员的真实水平),因此对于写代码中出现的synchronized,编译器和JVM都会自行衡量一下,看看这个代码加锁是否有必要,比如,只有一个线程,此时就算写了加锁,编译器也会自动地把锁去掉,不会真的执行加锁(但是有时编译器也不能100%的判定)
2 锁的粒度:主要指synchronized这个代码块中包含了多少代码,如果包含的代码比较多,就认为锁的粒度比较粗,如果锁包含的代码少就认为锁的粒度比较细,如果锁的粒度比较细,就意味着代码持有锁的时间比较短,这样其他线程冲突的概率就比较小 但是有一个例子
function()
{
synchronized(this){
任务1
}
synchronized(this){
任务2
}
synchronized(this){
任务3
}
}
这样写还不如把三个任务直接加到一个锁中呢
function()
{ synchronized(this)
{ 任务一();
任务二();
任务三():
}
}
2 下面我们来介绍一下 CAS(轻量级自旋锁)
面试题:1 介绍一下你理解的CAS机制?
2 说一下如何解决CAS机制中ABA问题?
CAS机制:(Compare and set)比较和替换
简单来说–>使用一个期望值来和当前变量的值进行比较,如果当前的变量值与我们期望的值相等,就用一个新的值来更新当前变量的值
CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时(条件),将内存值修改为B并返回true,否则条件不符合返回false。条件不符合说明该变量已经被其它线程更新了。多个线程访问相同的数据时,如果使用锁来进行并发控制,当某一个线程(T1)抢占到锁之后,那么其他线程再尝试去抢占锁时就会被阻塞,当T1释放锁之后,下一个线程(T2)再抢占到锁后并且重新恢复到原来的状态线程从阻塞到重新准备运行有很长的时间开销。而假设我们业务代码本身并不具备很复杂的操作,并发量也不高,那么当我们使用CAS机制来代替加锁操作,当多个线程操作同一变量时每个线程会首先会读取到地址值对应的原值作为自己的期望值,然后进行操作,操作完以后在更新的时候他会判断现在地址值对应的值是否与自己的期望值相同,如果相同,就认为自己操作的过程中别人没有进行过操作。则将自己操作后的值更新到地址值对应的位置,如果不同则说明自己操作的过程中一定有其他的线程更新过数据,然后会把当前地址值对应的值返回给用户,用户拿到新值重新上次的操作。不断循环直到更新成功。
当多个线程同时对某个资源进行CAS操作时,只有一个线程可以修改成功,但是并不会阻塞其他线程,其他线程只会受到操作失败的信号,可以看待成乐观锁的一种实现方式;
CAS的一种伪代码(不是Java代码):
boolean CAS(address,expectvalue,swapvalue){
if(&address==exceptedvalue){
&adress==swapvalue;
return true;
}
return false;
}
//就例如我之前写的那个自增操作的代码,address就相当于是内存中的值,excepted就相当于各种线程中寄存器的值
1 保证线程安全:例如用违代码(自己想象的代码)实现多线程同时修改一个值(不用加锁操作保证原子性,例如我想把count的值从零加到2,运用两个线程,两个线程分别进行+1操作,如何解决并发执行线程安全问题呢(在不加锁的情况下);CAS此时既可以高效地完成自增操作,又可以不用加锁;
原来数据的值a=内存中的值 旧的预期值=b=线程中CPU寄存器的值 需要修改的新值=c=count+1;
boolean CAS(a,b,c)
{ if(a==b)
{
a=(c=count+1);
return true;
}
return false;
}
public static void main()
{ private int 内存中的值;
int 当前线程寄存器的值=内存中的值;
if(CAS(内存中的值,线程中寄存器的值,count++)!=true)
{ 当前线程中寄存器的值=此时内存中的值;
}
}
AtomicInteger num=new AtomicInteger(0);
num.getAndIncrement();//相当于num++
num.getAndIncrement();//相当于++num
System.out.println(num);
内部通过CAS实现的
2 实现自旋锁
public Thread owner=null;
public void lock()
{ while(!CAS(this.owner,null,Thread,currentThread())
{
}
}
public void unlock()
{ this.owner=null;
}
只要有其他线程占用锁,this.owner就是空
CAS中的ABA问题:
假设滑稽老铁的账户余额中有100块钱,想要在ATM机中取50块钱,突然ATM机卡了一下
现在有两个线程,线程一和线程二都长时-50的操作;
第一步:线程一的寄存器和线程二的寄存器都读到了内存中的值是100;
第二步:线程一的寄存器的值会与内存中的值进行比较(发现在这个线程执行前没有其他线程来修改内存中的值),发现都是100,就将内存中的值减50;
第三步:这时线程二也想进行减50操作,但是发现当前线程二的值和当前内存中的值不相等,加偶无法进行减50操作了;
但是如果这时候,再进行判断之前,突然有个人,给这个滑稽老铁转了50元钱,此时内存中的值就变成100了(就算变成101)都没这事;
那如何解决这个问题呢?
此时我们就要引入版本号,为了解决ABA问题,无论此时如何对账户余额进行操作都会对版本号加一,如果当前的版本号和读到的版本号相同,就修改数据,就让版本号加1,如果当前的版本号高于读到的版本号,就认为操作失败;
那么此时:线程一的寄存器和线程二的寄存器都读到了100元钱,和版本号是1,线程1扣款成功内存中的钱变成50,版本号改成2,此时线程二还在阻塞等待中,这时突然有人给滑稽老铁转账50,虽然此时的钱数是100,但是版本号也进行加1操作,变成了3,此时线程二的版本号是1,小于读到的内存中的版本号,就操作失败;



