面试-线程(一)

https://blog.csdn.net/qq_21492635/article/details/72900037点击打开链接

https://blog.csdn.net/djd1234567/article/details/46004849点击打开链接

http://www.importnew.com/12773.htmljava线程TOP50

https://blog.csdn.net/qq_34952110/article/details/79307180个人理解的GC

学习多线程的前提:了解JVM之中的JMM(内存模型)

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。Java内存模型规定所有的变量都是存在主存当中(类似于计算机组成原理说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

0.       多线程的三个概念

l  原子性
l  可见性 指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。
l  顺序性  指令重排序,导致代码语句执行步骤和我们直接写的并不一样

进程和线程的区别?

https://blog.csdn.net/Zheng548/article/details/54669908点击打开链接

http://www.cnblogs.com/Wenxu/p/7942757.html点击打开链接

进程是正在进行的程序,每一个进程执行都有一个顺序,该顺序是一个执行路径(或者叫控制单元)

线程就是进程中的一个独立单元,控制着进程执行。

扫描二维码关注公众号,回复: 1925621 查看本文章
  1. 定义方面:进程是程序在某个数据集合上的一次运行活动;线程是进程中的一个执行路径。(进程可以创建多个线程)
  2. 角色方面:在支持线程机制的系统中,进程是系统资源分配和调度的独立单位,线程是CPU调度的单位。
  3. 资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈,栈指针程序计数器等寄存器。
  4. 独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。(进程结束后拥有的线程都将销毁,线程结束不影响               同个进程的其它线程)
  5. 开销方面。进程切换的开销较大。线程相对较小。线程是轻量级的进程(引入线程也出于了开销的考虑。)         
1.    线程的状态转换

1新建(new)----创建后尚未启动

2可运行(Runnable)----可能正在运行(Running),也可能等待CPU时间片(Ready)。

3阻塞(Blocking)----等待获取一个排它锁,如果其线程释放了锁就会结束此状态。线程因为某种原因放弃了cpu 使用权停止运行。                                         具备运行资格但是没有执行权限。

3无限等待(waiting)----等待其它线程显示地唤醒,否则不会被分配CPU时间片。

4限期等待(Timed Waiting)----无需等待其它线程显示地呼唤,在一定时间之后会被系统自动唤醒。

5死亡(Terminated)----可以四=是线程结束任务后自己结束,或者产生异常而结束。


调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。


2.      创建线程的三种方式

l  1.继承Thread类,并且重写run方法,调用start()方法启用线程。
public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
l  2.实现Runnable接口,并且实现run方法,然后把实现Runnable接口的类作为参数放进一个新的new Thread(“参数”)实例中去,然后调用new Thread().start()方法
public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
l  3.创建匿名内部类,直接Thread thread = new Thread(){

public void run(){};

PS:new Thread()的时候,Thread类有一个构造函数,是接收一个Runnable对象作为参数,因此我们可以new一个新的Runnable对象作为参数传入new Thread()里,然后在Runnable的匿名内部类里实现run方法


其实还有一种是:使用Callable和Future创建线程

https://www.cnblogs.com/HigginCui/p/5901713.html点击打开链接

https://www.cnblogs.com/lcngu/p/6863529.html点击打开链接

【创建和启动线程的步骤】

1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法没有返回值,再创建Callable实现类的实例。(从java8开始,可以直接使用Lambda表达式创建Callable对象)。

2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

3.使用FutureTask作为Thread对象的target创建并启动新线程。

4.调用FutureTask对象的get方法来获得子线程执行结束后的返回值。


3线程同步

为何同步?

     java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如对同一个数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性准确性 

     所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法

      Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

l  线程的同步使用关键词synchronized,使用方式如下

1. 同步一个代码块

public void func () {
    synchronized (this) {
        // ...同步函数用的锁是this'}
}它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
     SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于以下代码,两个线程调用了不同对象的同步代码块因此这两个线程就不需要同步。从输出结果可看出,两个线程交叉执行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = 33Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

2. 同步一个方法

public synchronized void func () {
    // ...
}

它和同步代码块一样,只作用于同一个对象。

3. 同步一个类

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. 同步一个静态方法

public synchronized static void fun() {
    // ...
}

作用于整个类。

1.      声明一个Object,让两个线程的运行条件是必须占有这个Object线程才能运行,简单来说,就是一个线程运行的时候,这个对象被这个线程所占有,当第一个线程执行完毕时,释放该对象,然后第二个线程才能继续运行。

2.      可以把object替换为this,指代当前对象,其实任何对象都可以作为一个类似于“锁”的存在。

3.      在要调用的方法体内部声明synchronized

4.      在方法名前声明synchronized,其实效果等同于4,在方法内部声明synchronized(this),只不过锁对象变为调用那个方法的那个类的实例了

5.      在静态方法前声明synchronized,所对象变为类名.class,类的字节码文件在内存中也是一个对象

6.      总的来说,想让两段代码同步,必须让同一个对象当锁


单例设计模式---懒汉式和饿汉式

懒汉式的特点用于延迟加载,实例你得延迟加载,如果多个线程访问会出现安全问题,可以加同步代码块解决。加同步时候使用的锁是该类所属的.class(字节码文件)对象。

延迟加载的单利设计模模式代码:

class Single{
    private static Single s=null;
    private Single(){}
    public static Single getInstance(){
        if(s==null){
             synchronized(Single.class)
             {
                if(s=null)
                s=new Single(); 
             }
        }
         return s;
    }
}

ReentrantLock

public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:

1. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

2. 可实现公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

3. 锁绑定多个条件

一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。

synchronized 和 ReentrantLock 比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。

3. 功能

ReentrantLock 多了一些高级功能。

4. 使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。


4线程通信:多个线程操作同一个资源,但是操作动作不同。

https://www.cnblogs.com/Wenxu/p/7979023.html 点击打开链接

为什么线程通信?

1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务, 并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据

2.当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!

3.所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

什么是线程通信?

就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺

1:等待唤醒机制(wait(),notify())(生产者消费者)

 就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

jion():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。

wait():让当前线程等待,并且释放当前线程占有对象上的锁.。要确保调用wait()方法的时候拥有锁,wait()方法的调用必须放在                               synchronized方法或synchronized块中

notify():  notify方法的意思是唤醒等待的线程,并且让等待的线程重新占有对象锁

notifyAll():唤醒在此对象监视器上等待地所有线程。

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调 用 notify() 或者 notifyAll() 来唤醒挂起的线程。

wait() 和 sleep() 的区别

  1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  2. wait() 会释放锁,sleep() 不会。
5ThreadLocal

http://www.cnblogs.com/dolphin0520/p/3920407.html点击打开链接

https://www.cnblogs.com/yxysuanfa/p/7125761.html点击打开链接

https://blog.csdn.net/xiaohulunb/article/details/19603611这个比较好

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本

ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。 
ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。 
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。 
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

ThreadLocal实现了在一个线程内存储数据,从而让这个线程之内的数据可以共享,而这个数据在线程间是独立的,互斥的。打开Thread的源码,我们可以看到存在一个名为ThreadLocalMap的变量,此变量其实维护了一张哈希表,以键值对的形式存储了多个Entry的,其中的key就是当前ThreadLocal,值就是我们要在线程中共享的值。底层是一个Entry数组,初始长度也是16,扩容为1.5。

     多个线程共享数据

l  上次我们说到使用ThreadLocal在线程内共享数据,接下来我们讨论如何在线程之间共享数据

1.      让一个类实现Runnable接口,然后多个线程把这个实现了Runnable接口的类当做参数,用来实例化一个线程。

众所周知,实例化一个Thread时,我们是可以把一个实现了Runnable接口的类当做参数传递进去的,这时候,启动线程之后调用的是实现了Runnable接口之后的run()方法。只要我们把操作的数据放在run()方法里,我们就可以实现在多个线程之间共享数据了。

2.      把数据操作的方法设置为同步的


6  Volatile关键字的使用(内存可见性)

https://www.cnblogs.com/xll1025/p/6486170.html点击打开链接

http://www.cnblogs.com/dolphin0520/p/3920373.html这个从内存模型到volatitle

volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

volatile是一个特殊的修饰符,只有成员变量才能使用它。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,

public class A8_19 {
    private static int a = 0;
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                a++;
                System.out.println(Thread.currentThread().getName() + "启动");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                a++;
                System.out.println(Thread.currentThread().getName() + "启动");
            }
        }).start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a);
    }

结果: Thread-1启动

Thread-0启动

1

当我们用volatile修饰符来修饰一个变量的时候,不管在哪个线程之中,只要这个变量被修改,被修改之后的数据会立即更新到主存之中。

把修改之后的数据立即更新到主存之中,就能保证其他线程也能访问到被修改的数据?

1.    使用volatile修饰符修饰的数据被修改后会强制更新在主存之中

2.    此数据在其他线程中的缓存行中的缓存变量无效

3.    由于其他线程读取此数据的时候,发现缓存无效,其他线程就会从主存中重新读取数据,也就能读取到主存中更新的数据了 

可是仍然存在一个问题,我们并不能保证其他线程是什么时候从主存之中拿到数据的。有可能人家别的线程是你更新数据之前(没写到主存之前)就拿到数据的旧值了,这样仍然不能保证可见性。因为数据的读取,修改,并且更新到主存之中,是三个操作,不是原子性的。只有在最后一步把数据成功更新至主存中,才会让其他线程的缓存行无效。因此只有在一个线程操作完一个数据并放回主存之后,另一个线程才有权利从主存中获取这个值才行。这就需要利用synchronized关键字或者lock对象来解决了,我们就可以保证只有一个方法能操作数据,访问主存。 

此外volatile还有一个作用,那就是保证顺序性,禁止指令重排。

以上我们可以发现:volatile并不能取代synchronized,虽然synchronized的效率并不高,但是synchronized可以保证同一段时间只有一个线程在执行一段代码,从而保证原子性,而volatile不可以保证原子性。

7:happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

8阻塞队列:

https://www.cnblogs.com/dolphin0520/p/3932906.html点击打开链接

http://ifeve.com/java-blocking-queue/点击打开链接

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:

在队列为空时,获取元素的线程会等待队列变为非空。 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。但是有了阻塞队列就不一样了,它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。


9.线程池 ThreadPoolExecutor

http://ifeve.com/java-threadpool/线程池

https://www.cnblogs.com/exe19/p/5359885.html点击打开链接

https://www.jianshu.com/p/210eab345423点击打开链接

合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。



10死锁:两个线程都在等待彼此先完成,造成该的停滞状态。

        多个线程访问同一资源时要考虑到同步的问题,过多的同步还要考虑带来的死锁问题。

11.锁读写锁ReadWriteLock,互斥锁ReentrantLock

   ?????

12CAS:

https://blog.csdn.net/u010412719/article/details/52053390点击打开链接

https://www.jianshu.com/p/8c94e1a41e7e点击打开链接

        CAS全称是compareAndSwap,“比较并交换”,是乐观锁的一种实现方式,涉及有三个操作数:内存值(V)、预期值(A)和新值(B)。实际上CAS操作是一个原子操作,是基于CPU提供的原子操作指令实现的。

CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”.

当且仅当期望值A和内存值V相同时,处理器用新值B更新V的值,否则什么都不做。

解决的问题当多个线程操作时,它解决了悲观锁使用了独占锁,一次只能有一个线程进入临界区的问题,在竞争状态比较低的情况下提高了并发性能。



14.并发集合:ConcurrentHashMap
15.锁的源码/Lock是如何实现的

 
 


猜你喜欢

转载自blog.csdn.net/lettyisme/article/details/80632475
今日推荐