zoukankan      html  css  js  c++  java
  • 某大厂技术面试之一(一)

    题目列表:

    1、编程题:13个人围成一圈,从第一个人开始1、2、3顺序报数,凡数到3的人退出圈子,找到最后留到圈子中的人原来的序号。

        public static void main(String[] args){
            int n=13;int i;
            int [] arr = new int[n];
            //初始将数组中的值都置为1,用来最后判断最后的结果
            for(i=0;i<n;i++){
                arr[i] = 1;
            }
            int count = 0;int count2 = 0;
            for( i = 0;i<n;i++) {
                //如果不等于,继续加1
                if (arr[i] != 0) {
                    count++;
                if (count % 3 == 0 & count / 3 >= 1) {
                    arr[i] = 0;
                    count2++;
                }
                if (count2 == n - 1) {
                    break;
                }
            }
                //判断如果是最后一个数则置为-1
                if(i==n-1){
                    i=-1;
                }
            }
            //遍历得出数据中还是1的值,输出则是最后剩下。
            for(i=0;i<n;i++){
                if(arr[i]==1){
                    System.out.println(i+1);
                    break;
                }
            }
        }

    2、自我介绍

    3、jdk和jre

    4、SQL优化,举例,如何优化SQL

    5、如何查找慢SQL

    6、多线程 用那种方式实现,用的那个

    7、#{} 和 ${}的区别

    8、equals和”==“的区别

    9、MQ的一对一,一对多怎么实现的 代码或者配置

    10、&和&&的区别

    11、redis的数据类型 

    12、多线程IO密集型和CPU密集型的区别

    13、left join和right join、inner join。

    14、线程池参数:核心线程数和最大线程数的区别

    15、mybatis的标签有那些

    16、mybatis分页实现

    17、Java8中lamda表达式的写法

    18、表锁、行锁、列锁、页锁 的区别

    19、Redis的淘汰策略

    ========================================================

    1、编程题

    2、先介绍个人基本信息,求职岗位,再介绍技术,以及项目经验。

    3、JRE和JDK的区别与联系

    参考:https://blog.csdn.net/Tybyqi/article/details/84346304

    3.1、JRE(Java Runtime Enviroment)是Java的运行环境。面向Java程序的使用者,而不是开发者。如果你仅下载并安装了JRE,那么你的系统只能运行Java程序。

    JRE是运行Java程序所运行的环境,包含JVM标准实现及 Java核心类库。它包括Java虚拟机、Java平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

    3.2、JDK(Java Development Kit)又称J2SDK(Java2 Software Development Kit),是Java开发工具包,是整个JAVA的核心,JDK包括Java运行时环境(JRE),

    一个加载器/解释器,一个名为(javac)的编译器,一个文档生成器(Javadoc),一个归档器(jar)以及Java基础的类库(即Java API 包括rt.jar)。

    如果你下载并安装了JDK,那么你不仅可以开发Java程序,也同时拥有了运行Java程序的平台。JDK是整个Java的核心,包括了Java运行环境(JRE),一堆Java工具tools.jar和Java标准类库 (rt.jar)。

    在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre。

    3.3、区别

    JRE主要包含:java类库的class文件(都在lib目录下打包成了jar)和虚拟机(jvm.dll);

    JDK主要包含:java类库的 class文件(都在lib目录下打包成了jar)并自带一个JRE。

    JVM的主要任务是:

    • 加载代码
    • 验证代码
    • 执行代码
    • 提供运行时环境

    那么为什么JDK要自带一个JRE呢?而且jdk/jre/bin下的client 和server两个文件夹下都包含jvm.dll(说明JDK自带的JRE有两个虚拟机)。

    记得在环境变量path中设置jdk/bin路径麽?如果大家不设置的话javac和java是用不了的。确实jdk/bin目录下包含了所有的命令。

    可是有没有人想过我们用的java命令并不是jdk/bin目录下的而是jre/bin目录下的呢?

    不信可以做一个实验,大家可以把jdk /bin目录下的java.exe剪切到别的地方再运行java程序,发现了什么?

    一切OK!(JRE中没有javac命令,原因很简单,它不是开发环境)那么有人会问了?我明明没有设置jre/bin目录到环境变量中啊?试想一下如果java为了提供给大多数人使用,他们是不需要jdk做开发的,只需要jre能让java程序跑起来就可以了,那么每个客户还需要手动去设置环境变量多麻烦啊?

    具备开发功能的jdk自己的jre下才会同时有client性质的jvm和server性质的 jvm, 而仅仅作为运行环境的jre下只需要client性质的jvm.dll就够了。

    所以安装jre的时候安装程序自动帮你把jre的 java.exe添加到了系统变量中,验证的方法很简单,去Windows/system32下面去看看吧,发现了什么?有一个java.exe。

    JDK包含JRE,JRE又包含JVM的关系。

     

     4、SQL优化,举例,如何优化SQL

    优化手段:
    1、 SQL优化
    1.1、 避免 SELECT *,只查询需要的字段。
    1.2.、小表驱动大表,即小的数据集驱动大的数据集:
    当B表的数据集比A表小时,用in优化 exist两表执行顺序是先查B表再查A表查询语句:SELECT * FROM tb_dept WHERE id in (SELECT id FROM tb_dept) ;
    当A表的数据集比B表小时,用exist优化in ,两表执行顺序是先查A表,再查B表,查询语句:SELECT * FROM A WHERE EXISTS (SELECT id FROM B WHERE A.id = B.ID) ;
    1.3.、尽量使用连接代替子查询,因为使用 join 时,MySQL 不会在内存中创建临时表。

    2、优化索引的使用

    2.1、尽量使用主键查询,而非其他索引,因为主键查询不会触发回表查询。
    2.2、不做列运算,把计算都放入各个业务系统实现
    2.3、查询语句尽可能简单,大语句拆小语句,减少锁时间
    2.3、or 查询改写成 union 查询
    2.4、不用函数和触发器
    2.5、避免 %xx 查询,可以使用:select * from t where reverse(f) like reverse('%abc');
    2.6、少用 join 查询
    2.7、使用同类型比较,比如 '123' 和 '123'、123 和 123
    2.8、尽量避免在 where 子句中使用 != 或者 <> 操作符,查询引用会放弃索引而进行全表扫描
    2.9、列表数据使用分页查询,每页数据量不要太大
    2.10、避免在索引列上使用 is null 和 is not null

    3、 表结构设计优化
    3.1、使用可以存下数据最小的数据类型。
    3.2、尽量使用 tinyint、smallint、mediumint 作为整数类型而非 int。
    3.3、尽可能使用 not null 定义字段,因为 null 占用 4 字节空间。数字可以默认 0 ,字符串默认 “”
    3.4、尽量少用 text 类型,非用不可时最好独立出一张表。
    3.5、尽量使用 timestamp,而非 datetime。
    3.6、单表不要有太多字段,建议在 20 个字段以内。

    5、如何查找慢SQL

    如何查找MySQL中查询慢的SQL语句
    5.1、MySQL数据库有几个配置选项可以帮助我们及时捕获低效SQL语句

    1),slow_query_log
    这个参数设置为ON,可以捕获执行时间超过一定数值的SQL语句。(OFF默认禁用)
    -- 设置启用慢查询记录日志,设置成功后,会在目录(我本地的C:/ProgramData/MySQL/MySQL Server 5.5/Data/)新增一个日志文件DESKTOP-KD9H54U-slow.log


    2),long_query_time
    当SQL语句执行时间超过此数值时,就会被记录到日志中,建议设置为1或者更短。

    3),slow_query_log_file
    记录日志的文件名。

    4),log_queries_not_using_indexes
    这个参数设置为ON,可以捕获到所有未使用索引的SQL语句,尽管这个SQL语句有可能执行得挺快。

    5.2、检测mysql中sql语句的效率的方法

    1)、通过查询日志
    (1)、Windows下开启MySQL慢查询
    MySQL在Windows系统中的配置文件一般是是my.ini找到[mysqld]下面加上
    代码如下
    log-slow-queries = F:/MySQL/log/mysqlslowquery。log
    long_query_time = 2

    (2)、Linux下启用MySQL慢查询
    MySQL在Windows系统中的配置文件一般是是my.cnf找到[mysqld]下面加上
    代码如下
    log-slow-queries=/data/mysqldata/slowquery。log
    long_query_time=2

    另外可以通过Explain执行分析计划和show profile。

    6、多线程 用那种方式实现,用的那个

    方式一:继承Thread类的方式

    1. 创建一个继承于Thread类的子类
    2. 重写Thread类中的run():将此线程要执行的操作声明在run()
    3. 创建Thread的子类的对象
    4. 调用此对象的start():①启动线程 ②调用当前线程的run()方法

    方式二:实现Runnable接口的方式

    1. 创建一个实现Runnable接口的类
    2. 实现Runnable接口中的抽象方法:run():将创建的线程要执行的操作声明在此方法中
    3. 创建Runnable接口实现类的对象
    4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    5. 调用Thread类中的start():① 启动线程  ② 调用线程的run() --->调用Runnable接口实现类的run()

    以下两种方式是jdk1.5新增的!

    方式三:实现Callable接口

    说明:

    1. 与使用Runnable相比, Callable功能更强大些
    2. 实现的call()方法相比run()方法,可以返回值
    3. 方法可以抛出异常
    4. 支持泛型的返回值
    5. 需要借助FutureTask类,比如获取返回结果
    • Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Futrue接口的唯一的实现类
    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
    Java实现多线程的四种方式

    方式一:继承Thread类的方式

    1. 创建一个继承于Thread类的子类
    2. 重写Thread类中的run():将此线程要执行的操作声明在run()
    3. 创建Thread的子类的对象
    4. 调用此对象的start():①启动线程 ②调用当前线程的run()方法

    方式二:实现Runnable接口的方式

    1. 创建一个实现Runnable接口的类
    2. 实现Runnable接口中的抽象方法:run():将创建的线程要执行的操作声明在此方法中
    3. 创建Runnable接口实现类的对象
    4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    5. 调用Thread类中的start():① 启动线程  ② 调用线程的run() --->调用Runnable接口实现类的run()

    以下两种方式是jdk1.5新增的!

    方式三:实现Callable接口

    说明:

    1. 与使用Runnable相比, Callable功能更强大些
    2. 实现的call()方法相比run()方法,可以返回值
    3. 方法可以抛出异常
    4. 支持泛型的返回值
    5. 需要借助FutureTask类,比如获取返回结果
    • Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Futrue接口的唯一的实现类
    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

    方式四:使用线程池

    说明:

    • 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

    好处:

    1.  提高响应速度(减少了创建新线程的时间)
    2.  降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    3.  便于线程管理

    使用线程池的接口:

    ExecutorService pool = Executors.newFixedThreadPool(10);
    ThreadPoolExecutor executor = (ThreadPoolExecutor) pool;
    /*
         * 可以做一些操作:
         * corePoolSize:核心池的大小
         * maximumPoolSize:最大线程数
         * keepAliveTime:线程没任务时最多保持多长时间后会终止
    */

    转载自:https://andyoung.blog.csdn.net/article/details/113131611

    JDK 1.5 推出了三大API用来创建线程:

    Executors.newCachedThreadPool():无限线程池(最大21亿)
    Executors.newFixedThreadPool(nThreads):固定大小的线程池
    Executors.newSingleThreadExecutor():单个线程的线程池

    public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
    }
    线程池的创建
    
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);
    
    ============================
    参数说明:
    corePoolSize(线程池的基本线程数): the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set.
    当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
    maximumPoolSize(线程池最大线程数): the maximum number of threads to allow in the pool.
    线程池允许创建的最大线程数。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
    keepAliveTime(线程活动保持时间):when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
    线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
    TimeUnit(线程活动保持时间的单位):the time unit for the keepAliveTime argument.
    可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
    workQueue(任务队列):the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
    用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
    ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
    RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    AbortPolicy:直接抛出异常。
    使用场景:ThreadPoolExecutor中默认的策略就是AbortPolicy,由于ExecutorService接口的系列ThreadPoolExecutor都没有显示的设置拒绝策略,所以默认的都是这个。
    CallerRunsPolicy:只用调用者所在线程来运行任务。
    使用场景:一般在不允许失败、对性能要求不高、并发量较小的场景下使用。
    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    **使用场景:**提交的任务无关紧要,一般用的少。
    DiscardPolicy:不处理,丢弃掉。
      当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
    **使用场景:**发布消息、修改消息类似场景。当老消息还未执行,此时新的消息又来了,这时未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。
    由此可见,创建一个线程所需的参数很多,线程池为我们提供了类Executors的静态工厂方法以创建不同类型的线程池。
    - newFixedThreadPool可以生成固定大小的线程池;
    - newCachedThreadPool可以生成一个无界、可以自动回收的线程池;
    - newSingleThreadScheduledExecutor可以生成一个单个线程的线程池;
    - newScheduledThreadPool还可以生成支持周期任务的线程池。

    向线程池提交任务:

    有两种方式提交任务:
    1.使用void execute(Runnable command)方法提交任务
    execute方法返回类型为void,所以没有办法判断任务是否被线程池执行成功。

    Runnable task = new Runnable() {
        @Override
        public void run() {
            System.out.println("Task is running by " + Thread.currentThread().getName());
            System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());
        }
    };
    threadPool.execute(task);

    2.使用submit方法提交任务
    Future<?> submit(Runnable task);、<T> Future<T> submit(Runnable task, T result);和 Future<T> submit(Callable<T> task);会返回一个Future,

    可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

    Future<?> future = threadPool.submit(task);
    try {
        Object result = future.get();
        System.out.println("任务是否完成:" + future.isDone());
        System.out.println("返回的结果为:" + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        // 关闭线程池
        threadPool.shutdown();
    }

    线程池关闭
    shutdown()方法:这个方法会平滑地关闭ExecutorService,当我们调用这个方法时,ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。
    awaitTermination(long timeout, TimeUnit unit)方法:这个方法有两个参数,一个是timeout即超时时间,另一个是unit即时间单位。这个方法会使当前关闭线程池的线程等待timeout时长,当超过timeout时间后,则去监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
    shutdownNow()方法:这个方法会强制关闭ExecutorService,它将取消所有运行中的任务和在工作队列中等待的任务,这个方法返回一个List列表,列表中返回的是等待在工作队列中的任务。

    package com.markliu.concurrent.threadpool;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class ThreadPoolDemo1 {
    
        public static void main(String[] args) {
            /*
             * 1. 创建线程池
             * 
             * 创建一个固定线程数目的线程池。corePoolSize = maximumPoolSize = 5
             * 即线程池的基本线程数和最大线程数相等。
             * 相当于:
             * new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
             */
            ExecutorService threadPool = Executors.newFixedThreadPool(5);
            /*
             *  2. 封装任务并提交给线程池
             */
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) threadPool;
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println("Task is running by " + Thread.currentThread().getName());
                    System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            /*
             * Starts all core threads, causing them to idly wait for work. 
             * This overrides the default policy of starting core threads 
             * only when new tasks are executed.
             */
            int count = threadPoolExecutor.prestartAllCoreThreads();
            System.out.println("开启的所有core线程数:" + count);
            System.out.println("线程池当前线程数:" + threadPoolExecutor.getPoolSize());
            System.out.println("线程池的core number of threads:" + threadPoolExecutor.getCorePoolSize());
            System.out.println("线程池中的最大线程数:" + threadPoolExecutor.getLargestPoolSize());
            // 3. 执行,获取返回结果
            /**
             * execute方式提交任务
             */
            // threadPool.execute(task);
            /**
             * submit方式提交任务
             */
            Future<?> future = threadPool.submit(task);
            try {
                // 阻塞,等待线程执行完成,并获得结果
                Object result = future.get();
                System.out.println("任务是否完成:" + future.isDone());
                System.out.println("返回的结果为:" + result);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } finally {
                System.out.println("线程池中已经执行完的任务数:" + threadPoolExecutor.getCompletedTaskCount());
                // 4. 关闭线程池
                /*
                 * shutdown方法平滑地关闭线程池,将线程池的状态设为:SHUTDOWN
                 * 停止接受任何新的任务且等待已经提交的任务执行完成,当所有已经
                 * 提交的任务执行完毕后将会关闭线程池
                 */
                threadPool.shutdown();
                /*
                 *  shutdownNow方法强制关闭线程池,将线程池状态设置为:STOP
                 *  取消所有运行中的任务和在工作队列中等待的任务,并返回所有未执行的任务List
                 */
                // hreadPool.shutdownNow();
                System.out.println("线程池是否关闭:" + threadPool.isShutdown());
                try {
                    //当前线程阻塞10ms后,去检测线程池是否终止,终止则返回true
                    while(!threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {
                        System.out.println("检测线程池是否终止:" + threadPool.isTerminated());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程池是否终止:" + threadPool.isTerminated());
            }
        }
    }

    线程池的执行流程分析:

    线程池的主要工作流程如下图:

         

    当提交一个新任务到线程池时,线程池的处理流程如下:

    首先线程池判断“基本线程池”(corePoolSize)是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。

    其次线程池判断工作队列(workQueue)是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。

    最后线程池判断整个线程池的线程数是否已超过maximumPoolSize?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。

    提交任务—>如果线程数未达到corePoolSize,则创建线程执行任务—>如果达到corePoolSize,仍让提交了任务,则会有任务等待,所以将任务保存在任务队列中,直到任务队列workQueue已满—>如果workQueue已满,仍然有任务提交,但未达到最大线程数,则继续创建线程执行任务,直到线程数达到maximumPoolSize,如果达到了maximumPoolSize,则根据饱和策略拒绝该任务。这也就解释了为什么有了corePoolSize还有maximumPoolSize的原因。

     下篇:某大厂技术面试之一(二) 

    不忘初心,相信自己,坚持下去,付诸实施。
  • 相关阅读:
    关于mui的主页面、子页面、页面跳转
    设计图片反复闪,点击后停止(设计定时器)
    购物车+支付宝
    登陆判读,并跳转到指定页面(window.location.href='http://localhost/index.html')
    template.js遍历对象的写法
    jqurey的跨域使用getjson(http://www.jb51.net/Special/894.htm)
    安装sass并ruby更改淘宝镜像
    Y君面试记
    MapReduce 阅读笔记
    安全感
  • 原文地址:https://www.cnblogs.com/controller666/p/14567592.html
Copyright © 2011-2022 走看看