zoukankan      html  css  js  c++  java
  • 并发之ATOMIC原子操作--Unsafe解析(三)

    Atomic 类的原子操作是依赖java中的魔法类sun.misc.Unsafe来实现的,而这个类为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。

    获取Unsafe的实例

    查看Unsafe的源码我们会发现它提供了一个getUnsafe()的静态方法。

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    如果直接调用这个方法会抛出一个SecurityException异常,这是因为Unsafe仅供java内部类使用,外部类不应该使用它。通过反射拿到它的实例

    public class UnsafeTest {
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
        }
    }

    使用Unsafe实例化一个类

    假如我们有一个简单的类如下:

    class User {
        int age;
    
        public User() {
            this.age = 10;
        }
    }

    如果我们通过构造方法实例化这个类,age属性将会返回10。

    User user1 = new User();
    // 打印10
    System.out.println(user1.age);

    如果我们调用Unsafe来实例化呢?

    User user2 = (User) unsafe.allocateInstance(User.class);
    // 打印0
    System.out.println(user2.age);

    age将返回0,因为 Unsafe.allocateInstance() 只会给对象分配内存,并不会调用构造方法,所以这里只会返回int类型的默认值0。

    修改私有字段的值

    使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。

    public class UnsafeTest {
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
    
            User user = new User();
            Field age = user.getClass().getDeclaredField("age");
            unsafe.putInt(user, unsafe.objectFieldOffset(age), 20);
    
            // 打印20
            System.out.println(user.getAge());
        }
    }
    
    class User {
        private int age;
    
        public User() {
            this.age = 10;
        }
    
        public int getAge() {
            return age;
        }
    }

    一旦我们通过反射调用得到字段age,我们就可以使用Unsafe将其值更改为任何其他int值。(当然,这里也可以通过反射直接修改)

    抛出checked异常

    我们知道如果代码抛出了checked异常,要不就使用try...catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。

    // 使用正常方式抛出IOException需要定义在方法签名上往外抛
    public static void readFile() throws IOException {
        throw new IOException();
    }
    // 使用Unsafe抛出异常不需要定义在方法签名上往外抛
    public static void readFileUnsafe() {
        unsafe.throwException(new IOException());
    }

    使用堆外内存

    如果进程在运行过程中JVM上的内存不足了,会导致频繁的进行GC。理想情况下,我们可以考虑使用堆外内存,这是一块不受JVM管理的内存。

    使用Unsafe的allocateMemory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受JVM管理,因此我们要调用freeMemory()方法手动释放它。

    假设我们要在堆外创建一个巨大的int数组,我们可以使用allocateMemory()方法来实现:

    class OffHeapArray {
        // 一个int等于4个字节
        private static final int INT = 4;
        private long size;
        private long address;
    
        private static Unsafe unsafe;
        static {
            try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                unsafe = (Unsafe) f.get(null);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        // 构造方法,分配内存
        public OffHeapArray(long size) {
            this.size = size;
            // 参数字节数
            address = unsafe.allocateMemory(size * INT);
        }
        
        // 获取指定索引处的元素
        public int get(long i) {
            return unsafe.getInt(address + i * INT);
        }
        // 设置指定索引处的元素
        public void set(long i, int value) {
            unsafe.putInt(address + i * INT, value);
        }
        // 元素个数
        public long size() {
            return size;
        }
        // 释放堆外内存
        public void freeMemory() {
            unsafe.freeMemory(address);
        }
    }

    在构造方法中调用allocateMemory()分配内存,在使用完成后调用freeMemory()释放内存。

    使用方式如下:

    OffHeapArray offHeapArray = new OffHeapArray(4);
    offHeapArray.set(0, 1);
    offHeapArray.set(1, 2);
    offHeapArray.set(2, 3);
    offHeapArray.set(3, 4);
    offHeapArray.set(2, 5); // 在索引2的位置重复放入元素
    
    int sum = 0;
    for (int i = 0; i < offHeapArray.size(); i++) {
        sum += offHeapArray.get(i);
    }
    // 打印12
    System.out.println(sum);
    
    offHeapArray.freeMemory();

    最后,一定要记得调用freeMemory()将内存释放回操作系统。

    CompareAndSwap操作

    JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。

    比如,我们可以基于Unsafe的compareAndSwapInt()方法构建线程安全的计数器。

    class Counter {
        private volatile int count = 0;
    
        private static long offset;
        private static Unsafe unsafe;
        static {
            try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                unsafe = (Unsafe) f.get(null);
                offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        public void increment() {
            int before = count;
            // 失败了就重试直到成功为止
            while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) {
                before = count;
            }
        }
    
        public int getCount() {
            return count;
        }
    }

    我们定义了一个volatile的字段count,以便对它的修改所有线程都可见,并在类加载的时候获取count在类中的偏移地址。

    在increment()方法中,我们通过调用Unsafe的compareAndSwapInt()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。

    我们可以通过使用多个线程来测试我们的代码:

    Counter counter = new Counter();
    ExecutorService threadPool = Executors.newFixedThreadPool(100);
    
    // 起100个线程,每个线程自增10000次
    IntStream.range(0, 100)
        .forEach(i->threadPool.submit(()->IntStream.range(0, 10000)
            .forEach(j->counter.increment())));
    
    threadPool.shutdown();
    
    Thread.sleep(2000);
    
    // 打印1000000
    System.out.println(counter.getCount());

    park/unpark

    JVM在上下文切换的时候使用了Unsafe中的两个非常牛逼的方法park()和unpark()。

    当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。

    当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。

    我们之前在分析java中的集合时看到了大量的LockSupport.park()/unpark(),它们底层都是调用的Unsafe的这两个方法。

    总结

    使用Unsafe几乎可以操作一切:

    (1)实例化一个类;

    (2)修改私有字段的值;

    (3)抛出checked异常;

    (4)使用堆外内存;

    (5)CAS操作;

    (6)阻塞/唤醒线程;

    彩蛋

    论实例化一个类的方式?

    (1)通过构造方法实例化一个类;

    (2)通过Class实例化一个类;

    (3)通过反射实例化一个类;

    (4)通过克隆实例化一个类;

    (5)通过反序列化实例化一个类;

    (6)通过Unsafe实例化一个类;

    public class InstantialTest {
    
        private static Unsafe unsafe;
        static {
            try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                unsafe = (Unsafe) f.get(null);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        
        public static void main(String[] args) throws Exception {
            // 1. 构造方法
            User user1 = new User();
            // 2. Class,里面实际也是反射
            User user2 = User.class.newInstance();
            // 3. 反射
            User user3 = User.class.getConstructor().newInstance();
            // 4. 克隆
            User user4 = (User) user1.clone();
            // 5. 反序列化
            User user5 = unserialize(user1);
            // 6. Unsafe
            User user6 = (User) unsafe.allocateInstance(User.class);
    
            System.out.println(user1.age);
            System.out.println(user2.age);
            System.out.println(user3.age);
            System.out.println(user4.age);
            System.out.println(user5.age);
            System.out.println(user6.age);
        }
    
        private static User unserialize(User user1) throws Exception {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
            oos.writeObject(user1);
            oos.close();
    
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
            // 反序列化
            User user5 = (User) ois.readObject();
            ois.close();
            return user5;
        }
    
        static class User implements Cloneable, Serializable {
            private int age;
    
            public User() {
                this.age = 10;
            }
    
            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }
        }
    }

    附 Unsafe.class:

    package sun.misc;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.security.ProtectionDomain;
    import sun.reflect.CallerSensitive;
    import sun.reflect.Reflection;
    
    public final class Unsafe
    {
      static
      {
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[] { "getUnsafe" });
      }
      
      private static final Unsafe theUnsafe = new Unsafe();
      public static final int INVALID_FIELD_OFFSET = -1;
      
      @CallerSensitive
      public static Unsafe getUnsafe()
      {
        Class localClass = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {
          throw new SecurityException("Unsafe");
        }
        return theUnsafe;
      }
      
      @Deprecated
      public int getInt(Object paramObject, int paramInt)
      {
        return getInt(paramObject, paramInt);
      }
      
      @Deprecated
      public void putInt(Object paramObject, int paramInt1, int paramInt2)
      {
        putInt(paramObject, paramInt1, paramInt2);
      }
      
      @Deprecated
      public Object getObject(Object paramObject, int paramInt)
      {
        return getObject(paramObject, paramInt);
      }
      
      @Deprecated
      public void putObject(Object paramObject1, int paramInt, Object paramObject2)
      {
        putObject(paramObject1, paramInt, paramObject2);
      }
      
      @Deprecated
      public boolean getBoolean(Object paramObject, int paramInt)
      {
        return getBoolean(paramObject, paramInt);
      }
      
      @Deprecated
      public void putBoolean(Object paramObject, int paramInt, boolean paramBoolean)
      {
        putBoolean(paramObject, paramInt, paramBoolean);
      }
      
      @Deprecated
      public byte getByte(Object paramObject, int paramInt)
      {
        return getByte(paramObject, paramInt);
      }
      
      @Deprecated
      public void putByte(Object paramObject, int paramInt, byte paramByte)
      {
        putByte(paramObject, paramInt, paramByte);
      }
      
      @Deprecated
      public short getShort(Object paramObject, int paramInt)
      {
        return getShort(paramObject, paramInt);
      }
      
      @Deprecated
      public void putShort(Object paramObject, int paramInt, short paramShort)
      {
        putShort(paramObject, paramInt, paramShort);
      }
      
      @Deprecated
      public char getChar(Object paramObject, int paramInt)
      {
        return getChar(paramObject, paramInt);
      }
      
      @Deprecated
      public void putChar(Object paramObject, int paramInt, char paramChar)
      {
        putChar(paramObject, paramInt, paramChar);
      }
      
      @Deprecated
      public long getLong(Object paramObject, int paramInt)
      {
        return getLong(paramObject, paramInt);
      }
      
      @Deprecated
      public void putLong(Object paramObject, int paramInt, long paramLong)
      {
        putLong(paramObject, paramInt, paramLong);
      }
      
      @Deprecated
      public float getFloat(Object paramObject, int paramInt)
      {
        return getFloat(paramObject, paramInt);
      }
      
      @Deprecated
      public void putFloat(Object paramObject, int paramInt, float paramFloat)
      {
        putFloat(paramObject, paramInt, paramFloat);
      }
      
      @Deprecated
      public double getDouble(Object paramObject, int paramInt)
      {
        return getDouble(paramObject, paramInt);
      }
      
      @Deprecated
      public void putDouble(Object paramObject, int paramInt, double paramDouble)
      {
        putDouble(paramObject, paramInt, paramDouble);
      }
      
      public void setMemory(long paramLong1, long paramLong2, byte paramByte)
      {
        setMemory(null, paramLong1, paramLong2, paramByte);
      }
      
      public void copyMemory(long paramLong1, long paramLong2, long paramLong3)
      {
        copyMemory(null, paramLong1, null, paramLong2, paramLong3);
      }
      
      @Deprecated
      public int fieldOffset(Field paramField)
      {
        if (Modifier.isStatic(paramField.getModifiers())) {
          return (int)staticFieldOffset(paramField);
        }
        return (int)objectFieldOffset(paramField);
      }
      
      @Deprecated
      public Object staticFieldBase(Class<?> paramClass)
      {
        Field[] arrayOfField = paramClass.getDeclaredFields();
        for (int i = 0; i < arrayOfField.length; i++) {
          if (Modifier.isStatic(arrayOfField[i].getModifiers())) {
            return staticFieldBase(arrayOfField[i]);
          }
        }
        return null;
      }
      
      public static final int ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(boolean[].class);
      public static final int ARRAY_BYTE_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(byte[].class);
      public static final int ARRAY_SHORT_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(short[].class);
      public static final int ARRAY_CHAR_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(char[].class);
      public static final int ARRAY_INT_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(int[].class);
      public static final int ARRAY_LONG_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(long[].class);
      public static final int ARRAY_FLOAT_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(float[].class);
      public static final int ARRAY_DOUBLE_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(double[].class);
      public static final int ARRAY_OBJECT_BASE_OFFSET = theUnsafe
        .arrayBaseOffset(Object[].class);
      public static final int ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe
        .arrayIndexScale(boolean[].class);
      public static final int ARRAY_BYTE_INDEX_SCALE = theUnsafe
        .arrayIndexScale(byte[].class);
      public static final int ARRAY_SHORT_INDEX_SCALE = theUnsafe
        .arrayIndexScale(short[].class);
      public static final int ARRAY_CHAR_INDEX_SCALE = theUnsafe
        .arrayIndexScale(char[].class);
      public static final int ARRAY_INT_INDEX_SCALE = theUnsafe
        .arrayIndexScale(int[].class);
      public static final int ARRAY_LONG_INDEX_SCALE = theUnsafe
        .arrayIndexScale(long[].class);
      public static final int ARRAY_FLOAT_INDEX_SCALE = theUnsafe
        .arrayIndexScale(float[].class);
      public static final int ARRAY_DOUBLE_INDEX_SCALE = theUnsafe
        .arrayIndexScale(double[].class);
      public static final int ARRAY_OBJECT_INDEX_SCALE = theUnsafe
        .arrayIndexScale(Object[].class);
      public static final int ADDRESS_SIZE = theUnsafe.addressSize();
      
      public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
      {
        int i;
        do
        {
          i = getIntVolatile(paramObject, paramLong);
        } while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
        return i;
      }
      
      public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
      {
        long l;
        do
        {
          l = getLongVolatile(paramObject, paramLong1);
        } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
        return l;
      }
      
      public final int getAndSetInt(Object paramObject, long paramLong, int paramInt)
      {
        int i;
        do
        {
          i = getIntVolatile(paramObject, paramLong);
        } while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
        return i;
      }
      
      public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2)
      {
        long l;
        do
        {
          l = getLongVolatile(paramObject, paramLong1);
        } while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
        return l;
      }
      
      public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2)
      {
        Object localObject;
        do
        {
          localObject = getObjectVolatile(paramObject1, paramLong);
        } while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
        return localObject;
      }
      
      private static void throwIllegalAccessError()
      {
        throw new IllegalAccessError();
      }
      
      private static native void registerNatives();
      
      public native int getInt(Object paramObject, long paramLong);
      
      public native void putInt(Object paramObject, long paramLong, int paramInt);
      
      public native Object getObject(Object paramObject, long paramLong);
      
      public native void putObject(Object paramObject1, long paramLong, Object paramObject2);
      
      public native boolean getBoolean(Object paramObject, long paramLong);
      
      public native void putBoolean(Object paramObject, long paramLong, boolean paramBoolean);
      
      public native byte getByte(Object paramObject, long paramLong);
      
      public native void putByte(Object paramObject, long paramLong, byte paramByte);
      
      public native short getShort(Object paramObject, long paramLong);
      
      public native void putShort(Object paramObject, long paramLong, short paramShort);
      
      public native char getChar(Object paramObject, long paramLong);
      
      public native void putChar(Object paramObject, long paramLong, char paramChar);
      
      public native long getLong(Object paramObject, long paramLong);
      
      public native void putLong(Object paramObject, long paramLong1, long paramLong2);
      
      public native float getFloat(Object paramObject, long paramLong);
      
      public native void putFloat(Object paramObject, long paramLong, float paramFloat);
      
      public native double getDouble(Object paramObject, long paramLong);
      
      public native void putDouble(Object paramObject, long paramLong, double paramDouble);
      
      public native byte getByte(long paramLong);
      
      public native void putByte(long paramLong, byte paramByte);
      
      public native short getShort(long paramLong);
      
      public native void putShort(long paramLong, short paramShort);
      
      public native char getChar(long paramLong);
      
      public native void putChar(long paramLong, char paramChar);
      
      public native int getInt(long paramLong);
      
      public native void putInt(long paramLong, int paramInt);
      
      public native long getLong(long paramLong);
      
      public native void putLong(long paramLong1, long paramLong2);
      
      public native float getFloat(long paramLong);
      
      public native void putFloat(long paramLong, float paramFloat);
      
      public native double getDouble(long paramLong);
      
      public native void putDouble(long paramLong, double paramDouble);
      
      public native long getAddress(long paramLong);
      
      public native void putAddress(long paramLong1, long paramLong2);
      
      public native long allocateMemory(long paramLong);
      
      public native long reallocateMemory(long paramLong1, long paramLong2);
      
      public native void setMemory(Object paramObject, long paramLong1, long paramLong2, byte paramByte);
      
      public native void copyMemory(Object paramObject1, long paramLong1, Object paramObject2, long paramLong2, long paramLong3);
      
      public native void freeMemory(long paramLong);
      
      public native long staticFieldOffset(Field paramField);
      
      public native long objectFieldOffset(Field paramField);
      
      public native Object staticFieldBase(Field paramField);
      
      public native boolean shouldBeInitialized(Class<?> paramClass);
      
      public native void ensureClassInitialized(Class<?> paramClass);
      
      public native int arrayBaseOffset(Class<?> paramClass);
      
      public native int arrayIndexScale(Class<?> paramClass);
      
      public native int addressSize();
      
      public native int pageSize();
      
      public native Class<?> defineClass(String paramString, byte[] paramArrayOfByte, int paramInt1, int paramInt2, ClassLoader paramClassLoader, ProtectionDomain paramProtectionDomain);
      
      public native Class<?> defineAnonymousClass(Class<?> paramClass, byte[] paramArrayOfByte, Object[] paramArrayOfObject);
      
      public native Object allocateInstance(Class<?> paramClass)
        throws InstantiationException;
      
      @Deprecated
      public native void monitorEnter(Object paramObject);
      
      @Deprecated
      public native void monitorExit(Object paramObject);
      
      @Deprecated
      public native boolean tryMonitorEnter(Object paramObject);
      
      public native void throwException(Throwable paramThrowable);
      
      public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
      
      public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
      
      public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
      
      public native Object getObjectVolatile(Object paramObject, long paramLong);
      
      public native void putObjectVolatile(Object paramObject1, long paramLong, Object paramObject2);
      
      public native int getIntVolatile(Object paramObject, long paramLong);
      
      public native void putIntVolatile(Object paramObject, long paramLong, int paramInt);
      
      public native boolean getBooleanVolatile(Object paramObject, long paramLong);
      
      public native void putBooleanVolatile(Object paramObject, long paramLong, boolean paramBoolean);
      
      public native byte getByteVolatile(Object paramObject, long paramLong);
      
      public native void putByteVolatile(Object paramObject, long paramLong, byte paramByte);
      
      public native short getShortVolatile(Object paramObject, long paramLong);
      
      public native void putShortVolatile(Object paramObject, long paramLong, short paramShort);
      
      public native char getCharVolatile(Object paramObject, long paramLong);
      
      public native void putCharVolatile(Object paramObject, long paramLong, char paramChar);
      
      public native long getLongVolatile(Object paramObject, long paramLong);
      
      public native void putLongVolatile(Object paramObject, long paramLong1, long paramLong2);
      
      public native float getFloatVolatile(Object paramObject, long paramLong);
      
      public native void putFloatVolatile(Object paramObject, long paramLong, float paramFloat);
      
      public native double getDoubleVolatile(Object paramObject, long paramLong);
      
      public native void putDoubleVolatile(Object paramObject, long paramLong, double paramDouble);
      
      public native void putOrderedObject(Object paramObject1, long paramLong, Object paramObject2);
      
      public native void putOrderedInt(Object paramObject, long paramLong, int paramInt);
      
      public native void putOrderedLong(Object paramObject, long paramLong1, long paramLong2);
      
      public native void unpark(Object paramObject);
      
      public native void park(boolean paramBoolean, long paramLong);
      
      public native int getLoadAverage(double[] paramArrayOfDouble, int paramInt);
      
      public native void loadFence();
      
      public native void storeFence();
      
      public native void fullFence();
    }
    View Code

    原文链接:https://www.cnblogs.com/tong-yuan/p/Unsafe.html

  • 相关阅读:
    document
    reg() replace
    BOM和DOM的区别
    注册表
    实现移动端通过下拉菜单栏实现pc端的导航栏
    通过ajax获取api,并且通过jquery获取自定义属性
    git的使用
    当盒子不设置width,而设置max-width遇到的问题
    CSS之position
    JavaScript之数组常用的方法
  • 原文地址:https://www.cnblogs.com/yrjns/p/12209069.html
Copyright © 2011-2022 走看看