当前位置:   article > 正文

JUC Lock 读写锁_读写锁,先获取读锁,在获取写锁

读写锁,先获取读锁,在获取写锁

ReentrantReadWriteLock1.5+ 读写锁

对共享数据的查看、查询就是读,对共享数据的修改就是写,读时不会涉及共享数据的修改,不修改意味着多个线程读取的数据就不会变化,那么在此情况下多个线程在用锁来进行排它读取操作就会影响效率。反过来说,如果此共享数据需要修改(写),那么开始写到结束写这段时间就不能读。

以上总结起来就是,多个线程可以同时读,但一旦有某一个线程开始写,那么其余线程就不能读也不能写。再总结简单点:读读线程可以同时进行,读写不能同时进行,写写也不能同时进行。

ReentrantReadWriteLock 继承关系图

在这里插入图片描述

  • ReentrantReadWriteLock 并不是直接实现 Lock 接口,而是实现 ReadWriteLock 接口
public interface ReadWriteLock {
    /**
     * 返回读锁,就是 ReadLock 对象
     *
     */
    Lock readLock();

    /**
     * 返回写锁,就是 WriteLock 对象
     */
    Lock writeLock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • ReentrantReadWriteLock 内部定义了 5 个内部类,Sync(以及对应的两个实现类:FairSync、NonfairSync)、WriteLock、ReadLock
    • Sync 和 ReentrantLock 一样,继承自 AQS。
    • WriteLock、ReadLock:都实现了 Lock 接口,他们的实例都包含一个 Sync 对象,该 Sync 对象和ReentrantReadWriteLock 实例化时的 Sync 对象是一致的。

    public ReentrantReadWriteLock() {
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);// 传入当前的 ReentrantReadWriteLock 对象
        writerLock = new WriteLock(this);// 传入当前的 ReentrantReadWriteLock 对象
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
  • 1
  • 2
  • 3
protected WriteLock(ReentrantReadWriteLock lock) {
     sync = lock.sync;
 }
  • 1
  • 2
  • 3

示例 1

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest1 {


    private  static  int x = 0;

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        for(int i = 0; i < 10; i++){
            new Thread(()->{
                rwLock.readLock().lock();
                try {
                    log.debug("开始读取数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("{}", x);
                    log.debug("数据读取完毕");
                }finally {
                    rwLock.readLock().unlock();
                }
            },"read-" + i).start();
        }

        for(int i = 0; i < 10; i++){
            new Thread(()->{
                rwLock.writeLock().lock();
                try {
                    log.debug("开始写数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    x++;
                    log.debug("数据写入完毕");
                }finally {
                    rwLock.writeLock().unlock();
                }
            },"write-" + i).start();
        }

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

结果:

16:19:12.658 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:13.663 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.665 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:14.672 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:14.672 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:15.677 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:15.677 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:16.682 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:16.682 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:17.691 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:17.691 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:18.697 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:18.697 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:19.705 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:19.705 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:21.725 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:21.725 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:22.729 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:22.729 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:23.734 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:23.735 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:24.744 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

从结果可以看出,不同的读线程可以同时进行(结果顺序不是以“1.开始读取数据;2. x的值;3. 数据读取完毕”这样的顺序完成),但读写、写写线程是互斥的(打印结果顺序一定是“1. 开始写数据;2. 数据写入完毕”)。

示例 2

示例 1 是读锁和写锁在不同的线程中,如果读锁和写锁都在同一个线程中,有什么要求呢?

在同一个线程中,同时使用读锁和写锁的情况有如下几种:
获取读锁 -> 释放读锁 -> 获取写锁 -> 释放写锁
获取写锁 -> 释放写锁 -> 获取读锁 -> 释放读锁
以上两种方式不存在嵌套,不会出现问题

获取读锁 -> 获取写锁 -> 释放读锁 -> 释放写锁 》》》先获取读锁再获取写锁
获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁 》》》先获取写锁再获取读锁
其他 。。。。。。

先获取读锁再获取写锁

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        rwLock.readLock().lock();
        log.debug("获取读锁成功");

        rwLock.writeLock().lock();
        log.debug("获取写锁成功");

        rwLock.readLock().unlock();
        log.debug("释放读锁成功");
        
        rwLock.writeLock().unlock();
        log.debug("释放写锁成功");

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

结果:在获取写锁的地方阻塞了。因为写锁在开始写的时候,是不能读取的,与读锁互斥,这样的写法将导致写锁阻塞。

先获取写锁再获取读锁

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        rwLock.writeLock().lock();
        log.debug("获取写锁成功");

        rwLock.readLock().lock();
        log.debug("获取读锁成功");

        rwLock.readLock().unlock();
        log.debug("释放读锁成功");

		// 这里不论是线解锁写还是先解锁读,都可以正常执行结束
        rwLock.writeLock().unlock();
        log.debug("释放写锁成功");

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

先获取写锁,再嵌套获取读锁,可以正常执行完毕。按照我们前面的说法,只要有写操作,就会互斥,但是这里怎么能执行完成呢?

锁降级

导致上面示例可以正常执行完成的机制,称作锁降级。就是写锁降级为了读锁。

  • 因为我们在线程内先获取的是写锁,获取成功后,其他线程的读和写锁都互斥,进行阻塞等待该线程是释放锁。
  • 当前线程再次获取读锁,因为其他线程不会获取到锁,所以,这里再次获取读锁是可以的(此时的读锁和写锁都是同一个线程,至于什么时候修改数据,什么时候对写操作对读操作可见,就是在该线程中自己定义了,这变成了一个单线程的问题,跟多线程无关)
  • 最后释放锁的时候,如果写锁先释放,那么其他线程的读锁就可以访问了,当前线程读写锁都释放后,其他线程的写锁就可以尝试获取锁了。

总结

  • ReentrantReadWriteLock 适合线程读多写少的情况(缓存)
  • ReentrantReadWriteLock 容易导致写线程的饥饿情况(因为写线程少,在极端情况下可能一直争抢不到 CPU 资源)

StampedLock1.8+ (邮戳锁)

基本使用

    public static void main(String[] args) {
        // 只有一个无参构造
        StampedLock sl = new StampedLock();
        // 获取读锁
        long stamp = sl.readLock();
        try{
            log.debug("获取读锁,{}",stamp);
        }finally {
            // 释放读锁
            sl.unlockRead(stamp);
        }
        
        // 获取写锁
        stamp = sl.writeLock();
        try {
            log.debug("获取写锁,{}",stamp);
        }finally {
            // 释放写锁
            sl.unlockWrite(stamp);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

示例1

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);

        stamp = sl.readLock();
        log.debug("第二次次获取读锁,{}",stamp);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

结果:

21:39:57.127 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第一次获取读锁,257
21:39:57.132 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第二次次获取读锁,258
  • 1
  • 2

此处两次的 stamp 值是不一样的。

示例2

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);
        sl.unlockRead(stamp);

        stamp = sl.readLock();
        log.debug("第二次次获取读锁,{}",stamp);
        sl.unlockRead(stamp);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

示例2 ,两次打印 stamp 的值是一样的。说明我们每次 readLock() 上锁时,都会打个戳(邮戳),在解锁时使用相同的戳才能正常解锁。如果解锁时的 stamp 与上锁时不一样,则会抛出 IllegalMonitorStateException 异常。

示例3

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);

        stamp = sl.writeLock();
        log.debug("第二次次获取写锁,{}",stamp);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

示例3 在获取读锁后,再次获取写锁就阻塞了(这里无论是先获取写锁还是读锁,都会阻塞),其表现和 ReentrantReadWriteLock 的读写锁一致,读读可重复获取(只是戳不一样),读写、写读、写写都互斥。如果和 ReentrantReadWriteLock 一样,那么 JDK1.8 就没必要再提供 StampedLock 了

StampedLock 提供了如下两种方式来实现乐观锁机制

  • tryOptimisticRead 尝试获取读戳,通常和 validate 方法一起使用,validate 方法来验证是否同时存在写,如果存在就可以通过 readLock 升级为读锁的方式来互斥
  • tryConvertToWriteLock 尝试转换为写戳,其返回 0 表示获取失败,在此之前需要先获取读锁或读戳。只要不是 0 就可以进行写操作,表示没有并发的写操作,如果返回0获取写戳失败,则进行锁升级,将当前的读锁升级为写锁来互斥
  • StampedLock 就是对 ReentrantReadWriteLock 的优化,就是通过以上两个方法及其相关方法来实现乐观锁方式的写操作,以提高效率,减少写线程饥饿问题。

这里的描述不是特别清楚,请结合下面的示例来理解

StampedLock 源码文档上的示例

根据源码文档上的示例进行修改的示例

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

@Slf4j
public class StampedLockTest3 {

    public static void main(String[] args) throws InterruptedException {
        Point p = new Point();
        Random r = new Random();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                try {
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}",p.distanceFromOrigin());
            },"read-"+i).start();
        }


        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                p.moveIfAtOrigin(r.nextInt(100),r.nextInt(100));
            },"write-"+i).start();
        }

        TimeUnit.SECONDS.sleep(10);
    }


    static class Point{
        // x和y轴
        private double x, y;
        private final StampedLock sl = new StampedLock();

        // writeLock 方式(会阻塞)
        void move(double deltaX, double deltaY) { // an exclusively locked method
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }

        // 乐观锁读
        double distanceFromOrigin() { // A read-only method
            // 尝试获取读戳
            long stamp = sl.tryOptimisticRead();
            double currentX = x, currentY = y;
            // 用 validate 方法检查当前戳是否可直接使用(没有写)
            if (!sl.validate(stamp)) {
                // 直接获取读锁
                stamp = sl.readLock();
                try {
                    currentX = x;
                    currentY = y;
                } finally {
                    sl.unlockRead(stamp);
                }
            }

            // 如果当前没有写,那么就可以直接计算,否则就得重新独占式的重新读取(if 里面的逻辑)
            return currentX * currentX + currentY * currentY;
        }

        // 乐观锁进行写
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            // 先获取读锁(示例上提示可以使用 tryOptimisticRead 来获取)
            long stamp = sl.readLock();
            try {
                // 尝试获取写戳
                // tryConvertToWriteLock 方法如果返回 0,则表示获取写戳失败
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {// ws != 0 表示写戳获取成功
                    // 设置 stamp 为写戳
                    stamp = ws;
                    // 修改数据
                    x = newX;
                    y = newY;
                    log.debug("使用写戳修改数据");
                }
                else {
                    // 获取写戳失败,则释放读锁
                    sl.unlockRead(stamp);
                    // 并切换为写锁(也可以说升级为写锁)
                    stamp = sl.writeLock();
                    // 修改数据
                    x = newX;
                    y = newY;
                    log.debug("升级为写锁修改数据");
                }
            } finally {
                // 释放锁
                sl.unlock(stamp);
            }
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

执行后会发现大多数都是使用的写戳(如果将写操作的线程的sleep 去掉,会有写戳和写锁两种方式出现)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/956200
推荐阅读
相关标签
  

闽ICP备14008679号