lock、synchronized相关

{
"title":"Lock相关",
"date":2020-05-09T16:21:59+08:00,
"draft":true,
"tags":["lock、synchronized"],
"comments":true,
"share":true
}

  1. lock、synchronized异同

【synchronized 和 Lock 的区别? 用Lock有什么好处?举例说明】

1.1原始构成

(1).synchronized是关键字,属于jvm层面(底层是monitorenter和monitorexit(1个monitorenter2个monitorexit,多余的哪一个是防止异常退出),通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块和同步方法中才能使用wait/notify等方法)

(2). Lock(ReentrantLock)是具体类,api层面的锁。

1.2使用方法

(1).synchronized不需要用户手动去释放锁,当synchronized代码执行完成后线程自动的释放对锁的占用

(2).Lock(ReentrantLock)是需要手动释放锁,就有可能出现死锁情况。

1.3等待是否可中断

(1).synchronized不可以中断,除非抛出异常或者运行成功

(2).ReentrantLock可以中断(更灵活),

​ a:设置超时方法:tryLock(long time, TimeUnit unit)

​ b:ReentrantLock中的lockInterruptibly()方法,底层会调用interrupt()方法可中断

1.4加锁是否公平

(1).synchronized是非公平锁

(2).ReentrantLock两种都可以,默认是非公平锁 (true是公平,false是非公平)

1.5锁是否可以绑定condition

(1).synchronized没有

(2).ReentrantLock中有condition,可以通过condition实现精准唤醒,定点通知相应的线程。

(ReentrantLock的优点:精准唤醒,定点通知)

1.6 代码验证Lock的优点

(1).synchronized版本生产者、消费者

要求:两个线程,一个线程对一个初始值为0的变量+1,另一个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。

线程操作资源类 ,判断通知干活, 防止虚假唤醒。

package com.xpf.juc;

/**

  • @Author: Xia
  • @Date: 2020/5/10 23:35
  • @Email:x2358114512@163.com
  • /
    class ShareData{ // 资源类
    private int num = 0;

    public synchronized void prdo(){//生产者
      while(num != 0){
          try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
      }
      num++;
      System.out.println(Thread.currentThread().getName()+"\t:"+num);
      this.notify();
    }

    public synchronized void cust(){//消费者
      while(num != 1){
          try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
      }
      num--;
      System.out.println(Thread.currentThread().getName()+"\t:"+num);
      this.notify();
    }
    }


    public class PrdoCustomer {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.prdo();
          }
      },"AAA").start();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.cust();
          }
      },"BBB").start();

    }
    }
    synchronized版本结果:
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0

(2).Lock版本生产者、消费者

要求:两个线程,一个线程对一个初始值为0的变量+1,另一个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。

package com.xpf.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**

class ShareData{ // 资源类
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void prdo(){//生产者
lock.lock();
try{
while(num != 0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"\t:"+num);
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}

public void cust(){//消费者
lock.lock();
try{
while(num != 1){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"\t:"+num);
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}

public class PrdoCustomer {
public static void main(String[] args) {
ShareData shareData = new ShareData();

new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.prdo();
}
},"AAA").start();

new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.cust();
}
},"BBB").start();

}
}
Lock结果:

    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0
    AAA    :1
    BBB    :0

(3).虚假唤醒代码验证

要求:四个线程,两个线程对一个初始值为0的变量+1,另两个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。

虚假唤醒 : 需要重新判断一次该值【也就是num的状态】,当判断的地方由while()变为if()时,if不能对变量的状态重新判断造成的,虚假唤醒。

解决方法:将代码中的if()判断改为while()。

package com.xpf.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**

  • @Author: Xia
  • @Date: 2020/5/10 23:35
  • @Email:x2358114512@163.com
  • /
    class ShareData{
    private int num = 0;
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void prdo(){//生产者
      lock.lock();
      try{
          if(num != 0){
              condition.await();
          }
          num++;
          System.out.println(Thread.currentThread().getName()+"\t:"+num);
         condition.signal();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }

    public void cust(){
      lock.lock();
      try{
          if(num != 1){
             condition.await();
          }
          num--;
          System.out.println(Thread.currentThread().getName()+"\t:"+num);
          condition.signal();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }
    }
    public class PrdoCustomer {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.prdo();
          }
      },"AAA").start();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.cust();
          }
      },"BBB").start();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.cust();
          }
      },"CCC").start();
      new Thread(() -> {
          for (int i = 0; i < 5; i++) {
              shareData.cust();
          }
      },"DDD").start();

    }
    }
    代码结果:

AAA :1
BBB :0
AAA :1
BBB :0
AAA :1
CCC :0
BBB :-1
AAA :0
AAA :1
CCC :0
BBB :-1
CCC :-2
BBB :-3
CCC :-4
DDD :-5
CCC :-6
DDD :-7
(4).Lock实现定点通知

要求:多线程之间的线程调用顺序:A->B->C,且A打印1次,B打印2次,C打印3次,来2轮。

package com.xpf.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**

  • @Author: Xia
  • @Date: 2020/5/10 23:35
  • @Email:x2358114512@163.com
  • /
    class ShareData{
    private int num = 1; //A->1;B->2;C->3;
    ReentrantLock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void print(int num, int counts){
      for (int i = 0; i < counts; i++) {
          System.out.println(Thread.currentThread().getName()+"\t:"+num);
      }
    }

    public void print1(){
      lock.lock();
      try{
          if(num != 1){
              condition1.await();
          }
          print(num,1);
          num = 2;
         condition2.signalAll();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }

    public void print2(){
      lock.lock();
      try{
          if(num != 2){
              condition2.await();
          }
          print(num,2);
          num = 3;
          condition3.signalAll();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }

    public void print3(){
      lock.lock();
      try{
          if(num != 3){
              condition3.await();
          }
          print(num,3);
          num = 1;
          condition1.signalAll();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }

    }
    public class PrdoCustomer {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          for (int i = 0; i < 2; i++) {
              shareData.print1();
          }
      },"AAA").start();
      new Thread(() -> {
          for (int i = 0; i < 2; i++) {
              shareData.print2();
          }
      },"BBB").start();
      new Thread(() -> {
          for (int i = 0; i < 2; i++) {
              shareData.print3();
          }
      },"CCC").start();

    }
    }
    定点通知结果:

AAA :1
BBB :2
BBB :2
CCC :3
CCC :3
CCC :3


AAA :1
BBB :2
BBB :2
CCC :3
CCC :3
CCC :3
1.7留一个作业:使用阻塞队列如何实现消费者、生产者模式

  1. synchronized中对象锁、全局锁

使用Synchronized关键字处理有两种模式:同步代码块,同步方法 。

同步代码块:如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象。并且synchronized 同步的代码块,在同一时刻只允许一个线程进入代码块处理。

同步方法: 同一时刻内只有一个线程能进入到改方法中的。

对象锁:锁住同一个对象。

全局锁: 锁住这个类对应的Class对象 。

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
对于普通同步方法,锁是当前实例对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
对于静态同步方法,锁是当前类的Class对象。

package com.xpf.juc;

/**

  • @Author: Xia
  • @Date: 2020/4/21 14:56
  • @Email:x2358114512@163.com
  • /
    import java.util.concurrent.TimeUnit;

    class Phone{

    public static synchronized void sendEmail() throws Exception{
      TimeUnit.SECONDS.sleep(3);
      System.out.println("使用Email");
    }

    public synchronized void sendMS(){
      System.out.println("使用MS");
    }

    public void sayHello(){ //普通方法
      System.out.println("hello");
    }
    }

    public class Lock8Demo04 {
    //注意线程的调度和代码的先后没有关系,底层操作系统的调度,谁抢到了谁就先运行。
    public static void main(String[] args) throws Exception {
      Phone phone1 = new Phone();
      Phone phone2 = new Phone();
      new Thread(() -> {
          try {
              phone1.sendEmail();
          } catch (Exception e) {
              e.printStackTrace();
          }
      },"AAA").start();
      Thread.sleep(100);  //让"AAA"线程先执行
      new Thread(() -> {
          phone2.sendMS();
    // phone1.sayHello();
      },"BBB").start();
    }
    }

/**
*

  • (1)一个对象里如果有多个synchronized方法,某一时刻内只能有一个线程去调用其中的一个synchronized方法,其他的线程只能在等待,换句话说,某一个时刻只能有一个线程去访问含有synchronized方法的对象。
    synchronized方法锁的是当前对象this(其实只会锁住当前对象this中所有的synchronized方法,不包含p普通方法),被锁定之后,其他的线程都不能进入当前对象的其他synchronized方法中,但是可以进入其他的普通方法

  • (2)静态synchronized方法,此时synchronized锁的不仅是当前对象this,而锁的是当前对象的类模板,不同锁对象不会影响。

    同1部手机: Phone phone1 = new Phone();

    有2部手机: Phone phone1 = new Phone();

                 Phone phone2 = new Phone();
  • 1.同1部手机,标准访问,先访问sendEmail()还是sendMS()?

  • 结果:先访问sendEmail()后是sendMS()。

  • 注意:synchronized锁的是当前类的对象,而不是当前方法

  • 2.同1部手机,在sendEmail()中先睡眠4秒,先访问sendEmail()还是sendMS()?

  • 结果:先访问sendEmail()后是sendMS()。

  • 注意:一旦进入sendEmail()后,synchronized锁的是当前类的对象,

  • 执行完睡眠时间和相应操作后才释放锁,sendMS()才能才能获得synchronized锁

  • 3.同1部手机,在Phone中新增普通sayHello(),sendEmail()中还是睡眠4秒,先访问sendEmail()还是普通sayHello()?

  • 结果:先访问普通sayHello()后是sendEmail()。

  • 注意:普通方法和synchronized锁没有关系

  • 4.有2部手机,sendEmail()中还是睡眠4秒,先访问手机1中的sendEmail()还是手机2中的sendMS()?

  • 结果:先访问endMS()后是sendEmail()。

  • 注意:访问多个资源,不是同一个资源

  • ————————————————上面4个案例中的synchronized方法不含static静态方法——————————————————

  • 5.同1部手机,Phone中的同步方法sendEmail()和同步方法sendMS()都加了静态方法,先访问sendEmail()还是sendMS()?

  • 结果:先访问静态同步方法sendEmail()后是静态同步方法sendMS()。和第二个锁结果一样

  • 6.有2部手机,Phone中的sendEmail()和sendMS()变成静态方法,先访问sendEmail()还是sendMS()?

  • 结果:先访问静态同步方法sendEmail()后是静态同步方法sendMS()。

  • 注意:staice使得两个同步方法不仅属于该对象,更属于该类。此时synchronized锁的是类对象

  • 7.同1部手机,Phone中的同步方法sendEmail()加了静态方法,先访问sendEmail()还是sendMS()?

  • 结果:先访问同步方法sendMS()后是静态同步方法sendEmail()。

  • 注意:这两把锁是两个不同的对象,同步方法sendMS()中使用的锁对象是this,静态同步方法sendEmail()中使用的锁对象是类对象Class本身,

  • ,所以AAA线程进入静态同步方法sendEmail()后锁住类对象Class本身,而同步方法sendEmail()不需要等待其释放锁,而是自己可以直接执行

  • 所以静态同步方法与非静态同步方法之间是不会有竞态条件的。

  • 8.有2部手机,Phone中的同步方法sendEmail()加了静态方法,先访问sendEmail()还是sendMS()?

  • 结果:先访问phone2的同步方法sendMS()后是phone1的静态同步方法sendEmail()。

  • 注意:同上

  • /

    1. 常见锁集合

3.1 公平锁与非公平锁

公平锁(fair): 是指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到。

非公平锁(unfair):是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下, 有可能造成优先级反转或者饥饿现象(某个线程可能一直都不能抢到锁)。

注意:对于synchronized而言也是一种非公平锁;并发包下的ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 (默认是非公平锁)。

3.2 可重入锁(又名递归锁)

(1).可重入锁(又名递归锁):指的是是同一线程外层函数获得锁之后,内层函数仍然能获取该锁的代码,【在同一个线程外层方法获取锁的时候,在进入内层方***自动获取锁】也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。

(2).注意:ReentrantLock/synchronized就是一个典型的可重入锁(下面代码验证);可重入锁最大的作用就是避免死锁。

(3).代码验证synchronized是可重入锁

package com.xpf.Interview.juc.lock;

/**

  • @Author: Xia
  • @Date: 2020/5/12 10:10
  • @Email:x2358114512@163.com
  • /
    class ShareData{
    public synchronized void write(){
      System.out.println(Thread.currentThread().getName()+"\t write方法");
      read();
    }

    private synchronized void read() {
      System.out.println(Thread.currentThread().getName()+"\t read方法");
    }
    }

    public class LockDemo {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          shareData.write();
      },"AAA").start();
    }
    }
    结果:

AAA write方法
AAA read方法
(4)代码验证ReentrantLock是可重入锁

package com.xpf.Interview.juc.lock;

import java.util.concurrent.locks.ReentrantLock;

/**

  • @Author: Xia
  • @Date: 2020/5/12 10:10
  • @Email:x2358114512@163.com
  • /
    class ShareData{
    ReentrantLock lock = new ReentrantLock();
    public void write(){
      lock.lock();
      try {
          System.out.println(Thread.currentThread().getName()+"\t write方法");
          read();
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }

    private void read() {
      lock.lock();
      try {
          System.out.println(Thread.currentThread().getName()+"\t read方法");
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          lock.unlock();
      }
    }
    }

    public class LockDemo {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          shareData.write();
      },"AAA").start();
    }
    }
    结果:

AAA write方法
AAA read方法
注意:当write()中出现多对lock.lock() 和 lock.unlock()方法时,只要对应匹配(两两配对)则会正确输出,当lock.lock()比lock.unlock()多一个时,第二个线程执行是会卡死,一直等待前一个线程释放lock.unlock()。
3.3 自旋锁(spinlock)

(1).自旋锁(spinlock):其实就是用循环去代替阻塞。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU(下面代码验证)。

(2).代码验证自旋锁的原理

package com.xpf.Interview.juc.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**

  • @Author: Xia
  • @Date: 2020/5/12 10:10
  • @Email:x2358114512@163.com
  • /
    class ShareData{
    private AtomicReference<thread> atomicReference = new AtomicReference<>();

    public void myLock(){
      Thread thread = Thread.currentThread();
      System.out.println(thread.getName()+"\t进入myLock方法");
      while(!atomicReference.compareAndSet(null,thread)){
          System.out.println(thread.getName()+"循环获取锁");
      }
    }

    public void myUnLock(){
      Thread thread = Thread.currentThread();
      System.out.println(thread.getName()+"\t进入myUnLock方法");
      atomicReference.compareAndSet(thread,null);
    }
    }

    public class LockDemo {
    public static void main(String[] args) {
      ShareData shareData = new ShareData();
      new Thread(() -> {
          shareData.myLock();
          try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }  //"AAA"线程睡眠之后,"BBB"循环获取锁
          shareData.myUnLock();
      },"AAA").start();
      try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }  // 让"AAA"线程先执行
      new Thread(() -> {
          shareData.myLock();
          shareData.myUnLock();
      },"BBB").start();
    }
    }
    结果:</thread>

AAA 进入myLock方法
BBB 进入myLock方法
BBB循环获取锁
BBB循环获取锁
BBB循环获取锁
..........
BBB循环获取锁
BBB循环获取锁
AAA 进入myUnLock方法
BBB循环获取锁
BBB 进入myUnLock方法
3.4 读/写锁

(1).独占锁(写锁):指的是该锁只能被一个线程所持有, ReentrantLock和synchronized都是独占锁。
(2).共享锁(读锁):指的是该锁可以被多个线程所持有,ReentrantReadWriterLock其读锁是共享锁,其写锁是独占锁。(下面代码验证)
注意:读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

(3).代码验证ReentrantReadWriterLock的读写锁

package com.xpf.Interview.juc.lock;

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

/**

  • @Author: Xia
  • @Date: 2020/5/12 10:10
  • @Email:x2358114512@163.com
  • /
    class Cache{ // 缓存的读写分离
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    HashMap<String,String> hashMap = new HashMap<>();

    public void read(String key){ //读锁(共享锁)
      readWriteLock.readLock().lock();
      try {
          System.out.println(Thread.currentThread().getName()+"\t 正在读取:"+key);
          TimeUnit.MICROSECONDS.sleep(100);
          String value = hashMap.get(key);
          System.out.println(Thread.currentThread().getName()+"\t 读取完成");
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          readWriteLock.readLock().unlock();
      }
    }

    public void write(String key, String value){ //写锁(独享锁)
      readWriteLock.writeLock().lock();
      try {
          System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key);
          TimeUnit.MICROSECONDS.sleep(100);
          hashMap.put(key,value);
          System.out.println(Thread.currentThread().getName()+"\t 写入完成");
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          readWriteLock.writeLock().unlock();
      }
    }
    }

    public class LockDemo {
    public static void main(String[] args) {
      Cache cache = new Cache();
      for (int i = 0; i < 3; i++) {
          final  int j = i;
          new Thread(() -> {
              cache.write(j+"",j+"");
          },String.valueOf(i)).start();
      }
      for (int i = 0; i < 3; i++) {
          final  int k = i;
          new Thread(() -> {
              cache.read(k+"");
          },String.valueOf(i)).start();
      }
    }
    }
    结果:

0 正在写入:0
0 写入完成
1 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
0 正在读取:0
2 正在读取:2
1 正在读取:1
1 读取完成
2 读取完成
0 读取完成

结果分析:
写操作是原子+独占的过程,整个过程是一个完整的整体,中间没有被分割,没有被打断。
例如:0线程写入0,紧接着执行的是0线程写入完成。
读操作是共享的过程,可保证并发读是非常高效。
例如:0线程正在读取,紧接着执行的2、1线程正在读取,最后才执行的是0线程读取完成
3.5 死锁相关

死锁相关
1.什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去。如果系统资源充足,线程的资源请求都能够得到满足,死锁出现的可能性比较低,否则就会因争夺有限资源而陷入死锁。

2.产生死锁的主要原因?
2.1 系统资源不足
2.2 进程运行推进的顺序不合适
2.3 资源分配不当

3.死锁代码演示。

4.死锁的解决方案?
4.1 jps命令定位进程编号
4.2 jstack找到死锁查看

args) {
String lockA = "lockA";
String lockB = "lockB";
package com.xpf.Interview.juc.ThreadPool;

import java.util.concurrent.TimeUnit;

/**

  • @Author: Xia
  • @Date: 2020/4/25 20:55
  • @Email:x2358114512@163.com
  • /

    class HoldThread implements Runnable {

    private String lockA;
    private String lockB;

    public HoldThread(String lockA, String lockB) {
      this.lockA = lockA;
      this.lockB = lockB;
    }

    @Override
    public void run() {
      synchronized (lockA) {
          System.out.println(Thread.currentThread().getName() + "\t 自己持有锁:" + lockA + ",尝试获得:" + lockB);
          try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
          synchronized (lockB) {
              System.out.println(Thread.currentThread().getName() + "\t 自己持有锁:" + lockB + ",尝试获得:" + lockA);
          }
      }
    }
    }

    public class DeadLockDemo24 {
    public static void main(String[] args) {
      String lockA = "lockA";
      String lockB = "lockB";
      new Thread(new HoldThread(lockA, lockB), "AAA").start();
      new Thread(new HoldThread(lockB, lockA), "BBB").start();
    }
    }

select a language

运行结果:
BBB 自己持有锁:lockB,尝试获得:lockA
AAA 自己持有锁:lockA,尝试获得:lockB
卡死不能结束程序。

全部评论

相关推荐

想去夏威夷的小哥哥在度假:5和6才是重点
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务