乐观锁(Optimistic Locking)在对一条数据进行修改时,为了避免其他人同时对这一条数据进行更改,我们就可以通过锁机制,对数据进行上锁防止以并发问题;
线程认为线程的安全问题是很容易发生的,会对代码上锁。
因为悲观锁的上锁和释放锁的过程是需要消耗很多资源的,因此会降低程序的性能。
(锁机制看上篇)
相对于悲观锁来说,乐观锁认为线程问题是不容易发生的,因此不会对代码上锁。
乐观锁本身是不会对 *** 作的数据进行加锁,而是通过一些业务的 *** 作实现的,如果线程过多,也不建议使用;
实现的方式:
-
版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
-
CAS (Compare And Swap)比较和交换算法
-
通过内存的偏移量获得数据的值
-
计算出一个预计的值
-
将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
-
悲观锁和乐观锁的对比
-
悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
-
乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
public class AtomicDemo { static int count = 0; public static void main(String[] args) { for (int i = 0; i < 100000; i++) { new Thread(() ->{ count++; }).start(); } System.out.println(count); } }
问题:多线程同时执行++ *** 作,最后结果少了
分析:
count++ 分解为三个指令:
-
从内存中读取count的值
-
计算count+1的值
-
将计算结果赋值给count
这三个指令不是原子性的,A线程读取count值10,加1后得到11,准备赋值给count;B线程进入读取count也是10,加1得到11,赋值给count为11;切换会A线程,赋值count为11。
解决方案:
-
悲观锁,使用同步方法、同步块、同步锁
-
乐观锁
使用原子整数
悲观锁解决方案:
public class Demo { static Integer count=0; static Object object=new Object(); Lock lock=new ReentrantLock(); public void aa(){ //同步锁 lock.lock(); try{ count++; }finally { lock.unlock(); } } // 同步方法 public synchronized void cc(){ count++; } // 同步代码块 public void dd(){ synchronized (object){ count++; } } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0; i < 100000; i++) { new Thread(()->{ demo.dd(); }).start(); } System.out.println("count:"+count); } }
乐观锁的解决方案:
原子类java.util.concurrent.atomic包下的类大多是使用 CAS *** 作来实现的;
如AtomicInteger类:
常用方法:
-
incrementAndGet 原子递增
-
decrementAndGet 原子递减
public class AtomicDemo { static AtomicInteger atomicInteger=new AtomicInteger(0); //乐观锁 public void bb(){ count++; atomicInteger.incrementAndGet(); } public static void main(String[] args) { AtomicDemo atomicDemo = new AtomicDemo(); for (int i = 0; i < 100000; i++) { new Thread(()->{ atomicDemo.bb(); }).start(); } System.out.println("count:"+count); System.out.println("atomic:"+atomicInteger.get()); } }
CAS算法存在的问题
1.ABA问题 :
count=3;Thread1获取count=3
Thread2获取count=3 再修改成 count=4;再将count=4 =>count=3;
Thread1再进行 *** 作,然后执行成功了,但这并不代表这个过程没有问题;
2.如果预期值和实际值不一致处于循环等待状态,对CPU的消耗比较大
编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)public class SingletonDemo { private static SingletonDemo singleton = null; private SingletonDemo() { System.out.println("创建对象"); } public static SingletonDemo getInstance(){ if(singleton==null){ singleton = new SingletonDemo(); } return singleton; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"-----"+SingletonDemo.getInstance().hashCode()); }).start(); } } }
我们会发现:
多线程的情况下,不止一次调用了构造方法,产生了多个对象,就不是单例了(如果没有发生这种情况,是因为第一次创建的过程太快了,让这个线程sleep,就会发现了)
这是因为第一次在创建对象过程中时 又执行了别的线程别的线程也进行了判断,也创建了对象
if(singleton==null){ singleton = new SingletonDemo(); }
解决问题:
可以通过同步方法、同步代码块、同步锁的方式的方式都可已解决
public synchronized static SingletonDemo getInstance(){ if(singleton==null){ singleton = new SingletonDemo(); } return singleton; }
但是这种方法就会产生一个问题:
每次执行的时候都需要大量上锁、释放锁;我们知道大量的上锁、释放锁会消耗资源,而单例模式用的是一个对象,因此这样的方式显然也不是最佳解。
那么我们的单例类的方法这样写:
public synchronized static SingletonDemo getInstance(){ //如果为空,再执行代码,提升性能 if(singleton==null){ //同步代码块 保证原子性 synchronized (SingletonDemo.class){ //创建对象 if(singleton==null){ singleton = new SingletonDemo(); } } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)