Java同步锁的实现

    科技2024-04-20  11

    Java的内置锁一直都是备受争议的。 在JDK1.6之前,synchronized这个重量级锁的性能一直都是较为低下; 虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的; synchronized提供了便捷性的隐式获取锁、释放锁机制(基于JVM机制),但是它却缺少了获取、释放锁的可操作性、可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。


    实现自己的同步锁

    【先来看看这个栗子】在一个不加锁的情况下,多个线程对同一个资源的操作(如下代码),我们期望在程序执行结束后,能够输出打印结果sum为20000

    public class testDemo {//为了略缩行数,压缩了部分代码格式 volatile int sum=0;//统计的值 public void sum(){for(int i=0;i<=9999;i++) sum = sum + 1;} public static void main(String[] args){ testDemo testDemo = new testDemo(); Thread t1 = new Thread(()->testDemo.sum()); Thread t2 = new Thread(()->testDemo.sum()); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(testDemo.sum); } }

    输出结果:

    14979 Process finished with exit code 0

    结论 在多线程环境下,对同一个资源进行的操作,都不属于线程安全的。

    问题分析

    出现线程不安全的最佳可能是在sum = sum + 1这条语句上,从Java的角度分析,这是属于一条语句,但从操作系统指令上分析,这是由多条指令组成。

    所以,只需要在执行这条语句时,确保,同一时间,只有一个线程在执行。

    循环CAS解决方案

    我们引入Sync、CASLock类

    private static abstract class Sync{ //全局锁的标识,0代表无锁,1代表存在锁 volatile int status = 0; //真正用于同步处理的核心 private static final Unsafe unfase = getUnsafe(); //status值的偏移地址 private static long valueOffset = 0; //初始化拿取自身类内部成员status的偏移地址,在cas操作时,需要用到 static { try { valueOffset = unfase.objectFieldOffset (Sync.class.getDeclaredField("status")); } catch (Exception ex) { throw new Error(ex); } } //执行加锁 -- 核心,采用循环cas不断自旋式拿锁 abstract void lock(); //执行解锁 abstract void unlock(); boolean compareAndSet(int except,int newValue){ return unfase.compareAndSwapInt (this, valueOffset,except,newValue); } //获取Unsafe对象 public static Unsafe getUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe)field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; } } private static class CASLock extends Sync{ @Override void lock() { while(!compareAndSet(0,1)); } @Override void unlock() { this.status = 0; } }

    调整 testDemo代码如下

    public class testDemo { volatile int sum=0; CASLock casLock = new CASLock(); public void sum(){ casLock.lock(); try{ for(int i=0;i<=9999;i++) sum = sum + 1; }finally { casLock.unlock(); } } public static void main(String[] args){ testDemo testDemo = new testDemo(); Thread t1 = new Thread(()->testDemo.sum()); Thread t2 = new Thread(()->testDemo.sum()); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(testDemo.sum); } }

    输出结果:

    20000 Process finished with exit code 0

    缺点:在竞争过程中,如果没有拿取到锁,就会一直处于不断cas过程,直到成功才退出拿锁过程,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会花费Ns的cpu资源。 处理方式:让拿不到锁的进程,让出CPU时间片

    将循环中空语句改为yield

    调整CASLock中lock方法

    void lock() { while(!compareAndSet(0,1)){ Thread.yield(); } }

    优点:可以在循环中,当当前线程拿取不到锁的时候,可以让出CPU资源 缺点:你并不知道CPU的下一个选择,是不是就不选当前线程了,甚至有可能不会理你… 😃

    将循环中yield更改为sleep

    void lock() { while(!compareAndSet(0,1)){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }

    emm … 这样确实能够让出CPU时间片,不过让出之后是不是又会回来再次CAS? 对了,不要问我为什么是10,因为…很多时候,就算你是调用者本身,其实你也不知道这个时间是多少

    去除sleep 引入 park

    特别声明:下面的代码为伪代码,在两个线程之间运行是没问题的,超过了就不行啦~~

    private static class CASLock extends Sync{ Thread thread = null; @Override void lock() { while(!compareAndSet(0,1)){ thread = Thread.currentThread(); LockSupport.park();//阻塞当前线程 } } @Override void unlock() { this.status = 0; LockSupport.unpark(thread);//唤醒那个被阻塞的线程 thread = null; } }

    执行结果为20000,完美符合预期~~~

    好了,到这里本文已经结束了,后续多线程下 阻塞 - 唤醒来实现一个完整的同步锁(ReentrantLock),可参考文章:并发编程 - LockSupport工具类、CLH队列锁、AQS(AbstractQueuedSynchronizer)同步队列锁

    Processed: 0.038, SQL: 9