多线程-基础

什么是线程

我们知道,一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。

线程和进程之间的关系,类似于工厂和工人之间的关系,进程好比是工厂,线程就如同工厂中的工人。一个工厂可以容纳多个工人,工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源,每个工人负责完成一项具体的任务,他们相互配合,共同保证整个工厂的平稳运行。

每个进程执行前,操作系统都会为其分配所需的资源,包括要执行的程序代码、数据、内存空间、文件资源等。一个进程至少包含 1 个线程,可以包含多个线程,所有线程共享进程的资源,各个线程也可以拥有属于自己的私有资源。

进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。

下图描述了进程和线程之间的关系:


图 1 进程和线程的关系


如图所示,所有线程共享的进程资源有:

  • 代码:即应用程序的代码;
  • 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
  • 进程空间:操作系统分配给进程的内存空间;
  • 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。


各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。

什么是多线程

所谓多线程,即一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序。

我们通常将以“多线程”方式编写的程序称为“多线程程序”,将编写多线程程序的过程称为“多线程编程”,将拥有多个线程的进程称为“多线程进程”。

当进程中仅包含 1 个执行程序指令的线程时,该线程又称“主线程”,这样的进程称为“单线程进程”。

如今,很多应用程序(软件)都是多线程程序,例如 QQ 具备同时和多人聊天的能力、迅雷具备同时下载多个资源的能力、很多杀毒软件可以同时开启杀毒、清理垃圾、电脑加速等功能。

了解了什么是线程和多线程之后,我们正式开始学习如何编写多线程程序。

-------------------------

1、继承Thread类

​ 继承Thread必须重写run方法,(具体业务执行方法,需要执行的业务方法,定义在此方法中),注意此方法是线程启动后线程自动调用的;

案例

public class MyThread extends Thread{

    @Override
    public void run() {
        //线程执行的业务方法
        System.out.println("子线程执行");
        for (int i = 0;i < 5;i++){
            System.out.println("--- 线程名---:"+Thread.currentThread().getName()+",序号:"+i);
        }
    }

    public static void main(String[] args) {
        //主线程
        System.out.println("***主线程执行***");
        System.out.println("***线程名***:"+Thread.currentThread().getName());

        //创建一个线程并启动,只能通过主线程创建其他线程
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        //启动线程:start()方法(一旦启动,自动启动子线程,当前线程继续向下执行,不会等子线程)
        thread1.start();

        //启动多线程
        //多线程并发执行:不是正真一样上的并行执行(肉眼感官是并行),而是通过cpu的调度算法,有序cpu执行极快,所以肉眼看起来是并行的;
        thread2.start();

        //调用run方法,不可以启动线程,就是对象的普通方法调用,等run方法执行结束,主线程才能继续执行
        //thread1.run();
        //thread2.run();

        System.out.println("----主线程执行结束----");

    }
}

运行结果

调用start()方法

***主线程执行***
***线程名***:main
----主线程执行结束----
子线程执行
--- 线程名---:Thread-0,序号:0
--- 线程名---:Thread-0,序号:1
--- 线程名---:Thread-0,序号:2
--- 线程名---:Thread-0,序号:3
--- 线程名---:Thread-0,序号:4
子线程执行
--- 线程名---:Thread-1,序号:0
--- 线程名---:Thread-1,序号:1
--- 线程名---:Thread-1,序号:2
--- 线程名---:Thread-1,序号:3
--- 线程名---:Thread-1,序号:4

调用run()方法

***主线程执行***
***线程名***:main
子线程执行
--- 线程名---:main,序号:0
--- 线程名---:main,序号:1
--- 线程名---:main,序号:2
--- 线程名---:main,序号:3
--- 线程名---:main,序号:4
子线程执行
--- 线程名---:main,序号:0
--- 线程名---:main,序号:1
--- 线程名---:main,序号:2
--- 线程名---:main,序号:3
--- 线程名---:main,序号:4
----主线程执行结束----      //必须等子线程完成后才可以继续运行

注意start()方法和run()方法的区别

start():启动线程start()方法(一旦启动,自动启动子线程,当前线程继续向下执行,不会等子线程);
run() :调用run方法,不可以启动线程,只是对象的普通方法调用,等run方法执行结束,主线程才能继续执行;

2、实现Runnable接口

实现Runnable接口,也必须实现run方法;

案例

public class MyRunnable implements Runnable{

    private int num = 5;

    @Override
    public void run() {
        //线程执行的业务方法
        System.out.println("子线程执行");
        for (int i = 0;i < 5 ;i++){
            if(num>0){
                System.out.println("--- 线程名---:"+Thread.currentThread().getName()+",序号:"+num--);
            }
        }
    }

    public static void main(String[] args) {
        //主线程
        System.out.println("***主线程执行***");
        System.out.println("***线程名***:"+Thread.currentThread().getName());

        //创建一个子线程,并启动
        MyRunnable runnable1 = new MyRunnable();

        //实现Runnable接口方式创建的线程,不能自己启动,只能通过Thread类,将Runnable作为参数传入Thread类的构造方法中,
        // 构造线程对象,才可以启动
        Thread thread1 = new Thread(runnable1);
        thread1.start();
        //创建多线程 (如果传入的Runnable参数一样,可以共享资源)
        Thread thread2 = new Thread(runnable1);
        thread2.start();

        System.out.println("----主线程执行结束----");

    }

}

运行结果

***主线程执行***
***线程名***:main
----主线程执行结束----
子线程执行
子线程执行
--- 线程名---:Thread-0,序号:5
--- 线程名---:Thread-1,序号:4
--- 线程名---:Thread-0,序号:3
--- 线程名---:Thread-1,序号:2
--- 线程名---:Thread-0,序号:1

注意

如果传入的Runnable参数一样,可以共享资源;

3、比较两种创建线程的方式

继承Thread类

  • 编写简单,可直接操作线程
  • 适用于单继承

实现Runnable接口

  • 避免单继承局限性
  • 便于共享资源

4、实现Callable接口

4.1实现Callable接口调用的call方法

创建线程的方式三,实现Callable接口,线程自动调用的时call方法,不是run方法,jdk1.5后才提供;

4.2 FutureTask 类的继承关系

执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。

//FutureTask<V> 是 RunnableFuture<V> 的实现类
public class FutureTask<V> implements RunnableFuture<V> ;
//RunnableFuture<V> 接口继承了 Runnable, Future<V>          //所以FutureTask最后需要放到Thread参数中,这里跟继承Runnable方法一样;
public interface RunnableFuture<V> extends Runnable, Future<V> ;

类图

img

4.3 案例

public class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("***子线程执行***");
        //执行线程处理方法
        int sun = 0;
        for (int i = 0; i < 5; i++) {
            sun+=i;
        }
        return sun;
    }

    public static void main(String[] args) {
        //主线程执行
        System.out.println("---主线程执行---");

        //创建子线程,不可以自己单独启动,必须借助FutureTask才可以,必须获取子线程执行结果
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);

        //启动线程,只能借助Thread类
        Thread thread = new Thread(futureTask);
        thread.start();

        //获取子线程的执行结果(必须要子线程执行结束,才可以获取结果)
        try {
            Integer resultSun = futureTask.get(); //接收返回值
            System.out.println("五以内的数字之和:"+resultSun);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("---主线程结束---");

    }
}

运行结果

---主线程执行---
***子线程执行***
五以内的数字之和:10  //接收到返回值
---主线程结束---
JAVA 复制 全屏

4.4 Callable和Runnable的区别

1)所在包不同,Callable在java.util.concurrent包下,Runnable在java.lang包;

2)实现的接口方法不同:Callable是重写call方法,Runnable的重写run方法;

3)抛出异常处理不同:Callable是重写call方法,可以抛出异常,但是Runnable是重写run方法不可以抛出异 常,如果抛出线程直接中断;

4)返回值不同:实现Callable接口的线程,可以通过FutureTask获取返回值,但是是实现Runnable接口的线程,无法获取返回值;

1、线程状态关系

2、线程的状态分析

线程的五种状态:创建-就绪-运行-阻塞-死亡

1.创建状态
创建线程对象之后,尚未调用其start方法之前;

2.可运行状态:就绪和运行
1)当调用start()方法启动线程之后,如果cup没有给当前线程分配资源,当前线程就是就绪状态;
2)一旦获到cpu分配的资源,就进入运行状态;

3.运行状态:线程获得cpu资源,开始运行;

4.阻塞状态
一个正在运行的线程因某种原因不能继续运行时,进度阻塞状态。阻塞状态一种“不可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行的状态;

5.死亡状态
一个线程的run()方法执行完毕,stop()方法被调用或在运行过程中出现未捕捉的异常时,线程进入死亡状态,线程就不可以再次执行;

3、案例

案例

public class MyThreadState implements Runnable{
    @Override
    public void run() {
        System.out.println("---3 运行状态---");

        //线程休眠,单位是毫秒
        try {
            System.out.println("---4.1 进入阻塞--");
            Thread.sleep(2000);
            System.out.println("---4.2 恢复运行状态---");
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("---5.1死亡状态---");
        }
        System.out.println("---5.2 死亡状态---");
    }

    public static void main(String[] args) {
        //线程的状态演示
        MyThreadState myThreadState = new MyThreadState();
        Thread thread = new Thread(myThreadState);
        System.out.println("---1 创建状态---");

        thread.start();
        System.out.println("---2 就绪状态---");


    }
}

运行结果

---1 创建状态---
---2 就绪状态---
---3 运行状态---
---4.1 进入阻塞--
---4.2 恢复运行状态---
---5.2 死亡状态---
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis) 让当前正在执行的线程在指定的毫秒数内休眠
join() 等待该线程终止(插队)
static void yield() 暂停当前正在执行的线程对象,并执行其他线程(礼让
interript() 中断线程
isAlive() 测试线程是否处于活动那个状态

1、setPriority(int newPriority)

更改线程的优先级;

优先级

取值范围[1,10] 值越小,优先级越小
MAX_PRIORITY = 10; 最大优先级10
NORM_PRIORITY = 5; 默认优先级5
MIN_PRIORITY = 1; 最小优先级1

注意

优先级高的线程并不一定就比优先级低的先获得cpu资源,只是获得cpu资源的概率比较大,具体还要看cpu的调度算法;

设置优先级案例

public class MyThreadPriority implements Runnable{
    @Override
    public void run() {
        //线程执行的业务方法
        System.out.println("子线程执行");
        for (int i = 0;i < 5;i++){
            System.out.println("--- 线程名---:"+Thread.currentThread().getName()+",序号:"+i);
        }
    }

    public static void main(String[] args) {
        //创建线程并设置线程名和优先级
        Thread thread1 = new Thread(new MyThreadPriority(),"线程A");
        Thread thread2 = new Thread(new MyThreadPriority(),"线程B");

        //线程优先级的取值范围:[1,10],默认是5,值越小,优先级越小
        //设置线程优先级,只能代表优先级高的线程获取cpu资源的概率较大,单不是绝对优先,它取决于cpu的调度算法
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);

        //启动线程
        thread1.start();
        thread2.start();

    }

运行结果

子线程执行
--- 线程名---:线程A,序号:0
--- 线程名---:线程B,序号:0
--- 线程名---:线程A,序号:1
--- 线程名---:线程A,序号:2
--- 线程名---:线程A,序号:3
--- 线程名---:线程A,序号:4
--- 线程名---:线程B,序号:1
--- 线程名---:线程B,序号:2
--- 线程名---:线程B,序号:3
--- 线程名---:线程B,序号:4

2、 sleep(long millis)

线程休眠

//休眠一秒
Thread.sleep(1000); //单位毫秒
//TimeUnit.MILLISECONDS.sleep(1000); //单位毫秒
//TimeUnit.SECONDS.sleep(1); //单位毫秒

3、join()

强制加入子线程,谁调用join的方法,谁加入,当前线程会暂停,等待加入的子线程运行结束才可以继续执行;

join案例

//强制加入执行线程:必须要等调用了join方法的线程执行结束,必然发生
public class MyThreadJoin  implements Runnable{
    @Override
    public void run() {
        //线程执行的业务方法
        System.out.println("子线程执行");
        for (int i = 0;i < 3;i++){
            System.out.println("--- 线程名---:"+Thread.currentThread().getName()+",序号:"+i);
            try {
                //休眠一秒
//                Thread.sleep(1000);
//                TimeUnit.MILLISECONDS.sleep(1000);
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        //创建一个子线程对象
        Thread thread = new Thread(new MyThreadJoin(), "强制Join线程");

        //启动子线程
        thread.start();

        //主线程
        System.out.println("***主线程执行***");
        System.out.println("***线程名***:"+Thread.currentThread().getName());

        try {

            System.out.println("---主线程中强制加入子线程,继续执行---");
            //强制加入子线程,谁调用join的方法,谁加入,当前线程会暂停,等待加入的子线程运行结束才可以继续执行;
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("---主线程执行结束---");

    }

}

运行结果

***主线程执行***
子线程执行
***线程名***:main
---主线程中强制加入子线程,继续执行--- //子线程join后主线程要等子线程执行完成才可以继续执行
--- 线程名---:强制Join线程,序号:0
--- 线程名---:强制Join线程,序号:1
--- 线程名---:强制Join线程,序号:2
---主线程执行结束---

4、yield()

线程礼让:提供一种礼让的可能,但是不能保证绝对礼让,是一个概率事件(可能让,可能不让);

yield案例

//线程礼让:提供一种礼让的可能,但是不能保证绝对礼让,是一个概率事件
public class MyThreadYield implements Runnable{
    @Override
    public void run() {
        //线程执行的业务方法
        System.out.println("子线程执行");
        for (int i = 0;i < 10;i++){
            System.out.println("--- 线程名---:"+Thread.currentThread().getName()+",序号:"+i);

            //当执行到第6次,执行礼让
            if(i ==5){
                System.out.println("==="+Thread.currentThread().getName()+"礼让===");
                Thread.yield();
                //静态方法,通过线程对象调用
            }

        }
    }

    public static void main(String[] args) {
        MyThreadYield myThreadYield = new MyThreadYield();

        //创建子线程并启动
        new Thread(myThreadYield,"线程1").start();
        new Thread(myThreadYield,"线程2").start();
        new Thread(myThreadYield,"线程3").start();

    }
}

运行结果

子线程执行
子线程执行
子线程执行
--- 线程名---:线程1,序号:0
--- 线程名---:线程2,序号:0
--- 线程名---:线程1,序号:1
--- 线程名---:线程3,序号:0
--- 线程名---:线程1,序号:2
--- 线程名---:线程2,序号:1
--- 线程名---:线程1,序号:3
--- 线程名---:线程3,序号:1
===线程1礼让===
--- 线程名---:线程2,序号:2 //线程3变成了线程2,礼让了
--- 线程名---:线程1,序号:4
--- 线程名---:线程3,序号:2
--- 线程名---:线程1,序号:5
--- 线程名---:线程2,序号:3
--- 线程名---:线程1,序号:6
--- 线程名---:线程3,序号:3
--- 线程名---:线程1,序号:7
===线程2礼让===
--- 线程名---:线程1,序号:8 //还是线程1,没有发生礼让
===线程3礼让===
--- 线程名---:线程1,序号:9 //还是线程1,没有发生礼让
--- 线程名---:线程2,序号:4
--- 线程名---:线程3,序号:4
--- 线程名---:线程2,序号:5
--- 线程名---:线程3,序号:5
--- 线程名---:线程2,序号:6
--- 线程名---:线程3,序号:6
--- 线程名---:线程2,序号:7
--- 线程名---:线程3,序号:7
--- 线程名---:线程2,序号:8
--- 线程名---:线程3,序号:8
--- 线程名---:线程2,序号:9
--- 线程名---:线程3,序号:9
JAVA 复制 全屏

所以线程礼让是一种概率事件;

5、interript()

线程中断;

6、isAlive()

测试线程是否处于活动那个状态;

1、问题引入

买票问题

1.1 通过继承Thread买票

继承Thread买票案例

/*
    模拟网络购票,多线程资源共享问题,继承Thread方式;
    结论:此种方式,不存在资源共享,通过创建对象启动的线程,每个对象都有各自的属性值
 */
public class MyThreadTicket extends Thread{

    //总票数
    private int remainSite = 100;

    //抢到的座位号
    private int buySite = 0;

    @Override
    public void run() {
        //模拟循环抢票
        while(true){
            //判断余票是否充足,如果不足,结束
            if(remainSite <= 0){
                break;
            }

            //更改强股票数据
            buySite++;
            remainSite--;

            //模拟网络延迟
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName()+"买到第"+buySite+"张票,剩余"+remainSite+"张票");
        }
    }

    public static void main(String[] args) {
        //模拟三人同事抢票
        MyThreadTicket threadTicket1 = new MyThreadTicket();
        threadTicket1.setName("猪八戒");
        MyThreadTicket threadTicket2 = new MyThreadTicket();
        threadTicket2.setName("沙和尚");
        MyThreadTicket threadTicket3 = new MyThreadTicket();
        threadTicket3.setName("孙猴子");

        System.out.println("---抢票开始---");
        threadTicket1.start();
        threadTicket2.start();
        threadTicket3.start();

    }
}

运行结果

---抢票开始---
猪八戒买到第1张票,剩余99张票
孙猴子买到第1张票,剩余99张票
沙和尚买到第1张票,剩余99张票
孙猴子买到第2张票,剩余98张票
猪八戒买到第2张票,剩余98张票
沙和尚买到第2张票,剩余98张票
孙猴子买到第3张票,剩余97张票
沙和尚买到第3张票,剩余97张票
猪八戒买到第3张票,剩余97张票
猪八戒买到第4张票,剩余96张票
沙和尚买到第4张票,剩余96张票
孙猴子买到第4张票,剩余96张票
孙猴子买到第5张票,剩余95张票
......
孙猴子买到第99张票,剩余1张票
猪八戒买到第99张票,剩余1张票
沙和尚买到第99张票,剩余1张票
孙猴子买到第100张票,剩余0张票
猪八戒买到第100张票,剩余0张票
沙和尚买到第100张票,剩余0张票

出现的问题

每个人都买了100张票,没有共享数据;

1.2 通过实现Runnable接口买票

实现Runnable接口案例

/*
    模拟网络购票,实现Runnable方法
 */
public class MyRunnableTicket0 implements Runnable{
    //总票数
    private int remainSite = 100;

    //抢到的座位号
    private int buySite = 0;

    @Override
    public void run() {
        //模拟循环抢票
        while(true){
            //判断余票是否充足,如果不足,结束
            if (remainSite <= 0) {
                break;
            }

            //更改强股票数据
            buySite++;
            remainSite--;

            //模拟网络延迟
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "买到第" + buySite + "张票,剩余" + remainSite + "张票");
        }

    }

    public static void main(String[] args) {
        //创建三个子线程
        MyRunnableTicket0 runnableTicket = new MyRunnableTicket0();
        Thread thread1 = new Thread(runnableTicket,"哪吒");
        Thread thread2 = new Thread(runnableTicket,"金吒");
        Thread thread3 = new Thread(runnableTicket,"木吒");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果

木吒买到第96张票,剩余4张票
哪吒买到第96张票,剩余4张票
金吒买到第96张票,剩余4张票
木吒买到第99张票,剩余1张票
哪吒买到第99张票,剩余1张票
金吒买到第99张票,剩余1张票
木吒买到第100张票,剩余0张票

出现的问题

共享了数据,但是出现了漏票,和几个人买同一张票的情况;

2、解决方法

通过synchronized同步锁来进行同步,使同一时间只有一个人在买票;

2.1 同步代码块

同步代码块案例

/*
    模拟网络购票,实现Runnable方法
    同步代码块方法
 */
public class MyRunnableTicket implements Runnable{
    //总票数
    private int remainSite = 100;

    //抢到的座位号
    private int buySite = 0;
    //同步代码块
    @Override
    public void run() {
        //模拟循环抢票
        while(true){
            //同步代码快
            synchronized (this) {
                //判断余票是否充足,如果不足,结束
                if (remainSite <= 0) {
                    break;
                }

                //更改强股票数据
                buySite++;
                remainSite--;

                //模拟网络延迟
                try {
                    TimeUnit.MILLISECONDS.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "买到第" + buySite + "张票,剩余" + remainSite + "张票");
            }
        }
    }

    public static void main(String[] args) {
        //创建三个子线程
        MyRunnableTicket runnableTicket = new MyRunnableTicket();
        Thread thread1 = new Thread(runnableTicket,"哪吒");
        Thread thread2 = new Thread(runnableTicket,"金吒");
        Thread thread3 = new Thread(runnableTicket,"木吒");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果

哪吒买到第1张票,剩余99张票
哪吒买到第2张票,剩余98张票
哪吒买到第3张票,剩余97张票
哪吒买到第4张票,剩余96张票
哪吒买到第5张票,剩余95张票
......
金吒买到第96张票,剩余4张票
金吒买到第97张票,剩余3张票
金吒买到第98张票,剩余2张票
金吒买到第99张票,剩余1张票
金吒买到第100张票,剩余0张票

可以正常买票,问题解决;

2.2 同步方法

同步方法案例

/*
    模拟网络购票,实现Runnable方法
    同步方法
 */
public class MyRunnableTicket implements Runnable{
    //总票数
    private int remainSite = 100;

    //抢到的座位号
    private int buySite = 0;

    @Override
    public void run() {
        //模拟循环抢票
        while(remainSite > 0){

            //调用同步购买方法
            buyTicket();

            //模拟网络延迟
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    /*
        同步方法
        增加同步锁,限制多线程场景下,只允许一个线程执行当前方法,确保票数修改正确
     */
    public synchronized void buyTicket(){
        //判断余票是否充足,如果不足,结束
        if(remainSite <= 0){
            return;
        }

        //更改强股票数据
        buySite++;
        remainSite--;

        System.out.println(Thread.currentThread().getName()+"买到第"+buySite+"张票,剩余"+remainSite+"张票");
    }

运行结果

哪吒买到第1张票,剩余99张票
哪吒买到第2张票,剩余98张票
哪吒买到第3张票,剩余97张票
哪吒买到第4张票,剩余96张票
哪吒买到第5张票,剩余95张票
......
金吒买到第96张票,剩余4张票
金吒买到第97张票,剩余3张票
金吒买到第98张票,剩余2张票
金吒买到第99张票,剩余1张票
JAVA 复制 全屏

可以正常买票,问题解决;

posted @ 2022-12-01 15:46  hanease  阅读(45)  评论(0编辑  收藏  举报