JavaSE 第二次学习随笔(四)

 

---------------------------------------------------------------------------------------------------------------------------------------------------------
/*
3,需求说明
定义一个线程A,输出1 ~ 10之间的整数,
定义一个线程B,逆序输出1 ~ 10之间的整数,要求线程A和线程B交替输出
*/

public void wu() {
        Runnable r1 = () -> {
            ThreadA();
        };
        Runnable r2 = () -> {
            ThreadB();
        };

        new Thread(r1).start();
        new Thread(r2).start();
    }


    private int numa = 11;
    private int numb = 0;
    public synchronized void ThreadA() {
        while (numa + numb == 11 && numa > 1) {
            numa--;
            try {
                System.out.println("ThreadA:" + numa);
                this.wait(3);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                this.notifyAll();
            }
        }
    }

    public synchronized void ThreadB() {
        while (numa  + numb + 1== 11 && numb < 10) {
            numb++;
            try {
                System.out.println("ThreadB:" + numb);
                this.wait(3);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                this.notifyAll();
            }
        }
    }

  


---------------------------------------------------------------------------------------------------------------------------------------------------------
1.多线程的两种实现方式
多线程由两种实现方法,一种是继承Thread类重写run()方法, 另一种是实现Runnable接口实现run()方法,将Runnable创建的对象当作任务对象在创建Thread对象时将Runnable对象传入都是使用.start()执行线程;
同步的方法有synchronized方法和synchronized代码块以及wait()和notify() notifyAll();
2.线程的基本概念、线程的基本状态以及状态之间的关系
线程是cpu调度的基本单元,进程是系统资源调度的基本单位一个进程中至少有一个线程在负责程序的执行;正常情况下运行程序便有两个线程一个main线程一个gc线程(垃圾回收器);
线程的基本状态有:创建,就绪,执行,阻塞,停止;
创建线程时为线程对象分配内存此时为创建态,
当调用该线程的start()方法此时该线程就进入了就绪态,
当该线程开始运行run()方法时进入运行态,只有就绪状态的线程才能转到运行状态,单CPU的运行环境下在任意时刻都只有一个线程是此状态
阻塞态包括等待阻塞(wait()),同步阻塞(synchronized),以及其他阻塞(sleep()或者io请求等…),当阻塞完毕重新进入就绪状态等待cpu的调度
当执行完run()方法中的代码或者遇到了无法catch的异常退出run()方法此时进入停止态,本线程结束.
3.同步和异步有何异同,在什么情况下分别使用他们?举例说明。
同步和异步的不同在于对资源的操作是否是同时的,同步情况下对于一份共享的资源,同一时间只有获得到同步锁的线程能够对它进行操作,其他没有获得锁的线程进入阻塞态等待当前线程对共享资源操作完毕再进入并执行代码.异步情况下对于共享资源可以任意访问,不需要查询是否有其他线程正在访问或操作该资源.
当多个线程需要对同一份共享资源进行操作的时候需要进行线程同步,当多个线程之间没有对共享资源的操作时不需要同步; 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

4.sleep()和wait() 的区别
Thread.sleep()执行时,线程不会释放锁便会进入指定时长的休眠状态, 在时间期满后,该线程会进入就绪状态, 如果本线程的优先级比正在运行态的线程级别更高则会进入运行态,而那个线程进入阻塞态或者正在运行的线程进入阻塞态, 本线程抢占到cpu运行时间片进入运行态;
Object.wait()执行时, 线程会释放对象锁并进入等待池,只有针对该对象发出notify方法或者noitfyAll()方法后本线程才会重新进入就绪状态;
5.启动一个线程是用run()还是start()?
使用start()方法才能启动一个线程,使此线程进入就绪状态等待cpu调度执行;run()方法并不能使得此线程进入就绪状态,而且run()方法可以产生必须退出的标志来停止一个线程

6.出现不同步问题的原因: 多个线程共用了一份数据
解决方法: 在代码中加入同步代码块,同一时间内只允许一个下次呢和概念执行任务,其他的线程无法进入任务代码区域,只有当前线程完成任务,释放了锁其他线程才能进入此代码块枷锁并执行任务
* 同步代码块,相当于线程之间形成的互斥关系
* 对所谓锁的要求: 锁必须是能被多个线程共享的,必须是对象 ; 比如 : obj this Object.class ,使用线程同步锁时一定要尽量降低锁粒度来提高效率, 尽量使用范围小的对象来充当锁, 范围太大容易造成不必要的麻烦

7.join()和yield() {http://www.importnew.com/14958.html 我实在是懒得把两份地址都复制过来...}

Java线程调度的一点背景在各种各样的线程中,Java虚拟机必须实现一个有优先权的、基于优先级的调度程序。这意味着Java程序中的每一个线程被分配到一定的优先权,使用定义好的范围内的一个正整数表示。优先级可以被开发者改变。即使线程已经运行了一定时间,Java虚拟机也不会改变其优先级

优先级的值很重要,因为Java虚拟机和下层的操作系统之间的约定是操作系统必须选择有最高优先权的Java线程运行。所以我们说Java实现了一个基于优先权的调度程序。该调度程序使用一种有优先权的方式实现,这意味着当一个有更高优先权的线程到来时,无论低优先级的线程是否在运行,都会中断(抢占)它。这个约定对于操作系统来说并不总是这样,这意味着操作系统有时可能会选择运行一个更低优先级的线程。(我憎恨多线程的这一点,因为这不能保证任何事情)

注意Java并不限定线程是以时间片运行,但是大多数操作系统却有这样的要求。在术语中经常引起混淆:抢占经常与时间片混淆。事实上,抢占意味着只有拥有高优先级的线程可以优先于低优先级的线程执行,但是当线程拥有相同优先级的时候,他们不能相互抢占。它们通常受时间片管制,但这并不是Java的要求。

理解线程的优先权
接下来,理解线程优先级是多线程学习很重要的一步,尤其是了解yield()函数的工作过程。

记住当线程的优先级没有指定时,所有线程都携带普通优先级。
优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
记住优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
由调度程序决定哪一个线程被执行。
t.setPriority()用来设定线程的优先级。
记住在线程开始方法被调用之前,线程的优先级应该被设定。
你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级

yield()方法
理论上,yield意味着放手,放弃,投降。一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。其仅仅是进入就绪态,不会放弃线程同步锁;

join()方法: 也可以写成join(num ms)
定义:线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。
原理:线程一旦调用了join()方法,他的优先级会高于调用他的线程.调用他的线程会等当前的线程执行完后再去执行.
注意点:优先级只比调用他的线程的高.对其他的线程没有影响.join方法必须在线程开始工作后,执行.


/**
* 单例模式懒汉方式下使用双重校验锁尽量减少线程安全的判断次数, 来提高效率
*/

class SingleInstance {
    private static SingleInstance singleInstance2 = null;
    private SingleInstance() {}
    public static SingleInstance getSingleInstance() {
        if(singleInstance2 == null){
            synchronized(SingleInstance.class) {
                if(singleInstance2 == null) {
                    singleInstance2 = new SingleInstance();
                }
            }
        }
        return singleInstance2;
    }
}	

  



1.多线程多消费者多生产者 (消费产品到0或者某个程度开始生产,生产到某个程度开始停止生产)

public class Demo2 {
    public static void main(String[] args) {
        Obj o = new Obj();
        Thread p1 = new Thread(new Producer(o));
        Thread p2 = new Thread(new Producer(o));
        Thread p3 = new Thread(new Producer(o));
        Thread p4 = new Thread(new Producer(o));
        Thread c1 = new Thread(new Customer(o));
        Thread c2 = new Thread(new Customer(o));
        Thread c3 = new Thread(new Customer(o));
        Thread c4 = new Thread(new Customer(o));
        p1.start();
        c1.start();
        p2.start();
        c2.start();
        p3.start();
        c3.start();
        p4.start();
        c4.start();
    }

}

class Obj {
    public int num = 0;

    public Obj() {
    }

    public Obj(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        this.num = num;
    }
}


class Producer implements Runnable {
    volatile Obj o = null;

    public Obj getO() {
        return o;
    }

    public void setO(Obj o) {
        this.o = o;
    }

    public Producer() {
    }

    public Producer(Obj o) {
        this.o = o;
    }

    @Override
    public void run() {
        while (true) {
            if (o.getNum() > 30) {
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + "唤醒消费停止生产");
                    o.notify();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (o.getNum() < 30) {
                synchronized (o) {
                    if (o.getNum() < 30) {
                        o.setNum(o.getNum() + 1);
                        System.out.println(Thread.currentThread().getName() + "生产, 还剩余" + o.getNum());
                    }
                }
            }
        }
    }
}

class Customer implements Runnable {
    volatile Obj o = null;

    public Customer(Obj o) {
        this.o = o;
    }

    public Customer() {
    }

    public Obj getO() {
        return o;
    }

    public void setO(Obj o) {
        this.o = o;
    }

    @Override
    public void run() {
        long a = System.nanoTime();
        while (true) {
            if (o.getNum() <= 0) {
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + "唤醒生产停止消费");
                    o.notify();
                    try {
                        o.wait();
                        a = System.nanoTime();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (o.getNum() > 0) {
                synchronized (o) {
                    if (o.getNum() > 0) {
                        o.setNum(o.getNum() - 1);
                        System.out.println(Thread.currentThread().getName() + "消费, 还剩余" + o.getNum() +" "+ (System.nanoTime() - a));
                    }
                }
            }
        }
    }
}
		
		
	2.多线程多消费者多生产者(无论多少生产者消费者, 生产一次消费一次)
public class Demo4 {
    public static void main(String[] args) {
        Dat d = new Dat();
        Runnable r1 = new P(d);
        Runnable r2 = new C(d);

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        Thread t3 = new Thread(r1);
        Thread t4 = new Thread(r2);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class Dat {
    int num = 2;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();

    public void inc() {
        while (true){
            try {
                lock.lock();
                while (num % 2 == 0) {
                    c1.await();
                }
                num++;
                System.out.println(Thread.currentThread().getName() + "Inc" + num);
                c2.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public void dec() {
        while (true) {
            try {
                lock.lock();
                while(num %2 != 0) {
                    c2.await();
                }
                num--;
                System.out.println(Thread.currentThread().getName() + "Dec" + num);
                c1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

}

class P implements Runnable {
    Dat d = null;

    @Override
    public void run() {
        d.inc();
    }

    public P(Dat d) {
        this.d = d;
    }

}

class C implements Runnable {
    Dat d = null;

    @Override
    public void run() {
        d.dec();
    }

    public C(Dat d) {
        this.d = d;
    }
}
		
	(另一种方式, 加标记方式)	
		try {
			lock.lock();
			while (flag == false) {
				try {
					//wait();
					conditionCon.await();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			System.out.println(Thread.currentThread().getName()+"  消费了:"+name+"  产品的数量:"+count+"  产品的价格:"+price);
			
			flag = !flag;
			
			//唤醒生产线程
			//notify();
			//notifyAll();
			conditionPro.signal();
		} finally {
			lock.unlock();
		}
		
		

  



* 线程的停止:如何结束他的任务
* 1.通过一个标识结束线程
* 2.调用stop方法--有固有的安全性问题,系统不推荐使用.
* 3.调用interrupt()方法
thread.interrupt();//当主线程执行到某个阶段时,掉用子线程的interrupt方法,触发子线程的异常(InterruptedException),让子线程的任务结束,子线程就结束了.

守护线程
* 守护线程:相当于后台线程.依赖于前台线程.正常情况下,当前台线程结束的时候,不管守护线程有没有结束,都会立刻结束.
* 典型的守护线程:垃圾回收线程
/*
* 当程序调用setDaemon方法时,并且将参数设置成true.当前线程就变成了守护线层.
* 注意:这个方法一定要在start方法之前调用
*/
thread.setDaemon(true);
thread.start();
-------------------------------------------------------------------------------------------------------------------------------------------------------------
* 流(IO流):input-outputStream,作用:实现两个设备之间的数据传输
*
* 设备:磁盘(硬盘),内存,键盘,文件,网络,控制台
* 网络:当前主机之外的网上资源
*
* 分类:
* 根据操作的方式:输入流和输出流 全部都是以内存为参考对象,输入或输出
* 根据数据的类型:字节流和字符流
*
* 字节流:传输的是字节,可以操作任意类型的数据 ------音频,视频,文件,图片等
* 字符流:传输的是字节,不同点是在传输过程中加入了编码的操作,让我们的操作更方便------文本
*
* 因内存为参考
* 字节流:
* 字节输入流:InputStream
* 字节输出流:OutputStream
*
* 字符流的两个父类:
* 字符读入流(将数据输入内存):Reader
* 字符写出流(将数据从内存取出):Writer
* 流内也是有指针的~

字符流和字节流的具体一点的区别(https://www.cnblogs.com/wsg25/p/7499227.html)
1.字节流也称为原始数据,需要用户读入后进行相应的编码转换。而字符流的实现是基于自动转换的,读取数据时会把数据按照JVM的默认编码自动转换成字符。
2.字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,而字节流处理单元为1个字节,操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的。
3.字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串,字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以;

文件操作(字符流写操作)
1.创建FileWriter类的对象并关联文件
/*
* 注意点:一:关联文件的特点:如果之前不存在,程序会自动创建一个,如果存在会直接使用,但是会将文件之前的内容覆盖掉
* 注意点:二:如果只写文件的名字,不写具体的路径,默认路径就是当前的工程.
* 注意点:三:我们可以自己指定路径,但是一定要保证路径存在.否则报异常:FileNotFoundException: Q:\temp1.txt (系统找不到指定的路径。)
*/
FileWriter fileWriter = new FileWriter("test");

2.调用将数据从内存写入磁盘的方法
//注意点四:在执行write方法时,数据被临时放到了流对象的内部数组中,这个数组是一个字节数组,会默认去查编码表
fileWriter.write("bingbingbang");

3.刷新--将内存中的内容从内存刷新到磁盘
//fileWriter.flush();

4.关闭流--两个功能:a:关闭流 b:刷新
//第五个注意点:流对象使用完后必须关闭,否则如果后续程序打开流便无法再读取或者写入
fileWriter.close();
//第六个注意点:当流对象关闭之后,不能再进行操作,否则会报异常:Stream closed

5.字符文件续写
FilWriter fileWriter = new FileWirter("d:\\test", append : true);

读操作
1.创建FileReader类的对象并关联文件(文件不存在时抛出异常 FileNotFoundException)
2.调用将数据读入内存的方法

* 两种读取方式
* read()一个一个的读...慢吞吞的 在英文中: 读出指针(光标)后一个字符, 指针(光标)后移一个, 当读出为 -1 时说明读取完毕
* read(byte[])一次可以读多个字符, 将多个读到的字符存储到数组中,其返回值为读取到的字符的个数, -1 时表示读取完毕


字符缓冲流
* 字符缓冲流:(字符缓冲区)
* 定义:为了提高读写的能力,本身没有读写的能力,要想进行读写就必须借助于字符流实现.
*
* 可以将缓冲流类比于催化剂或者高速的小车
* Buffered 流是典型的装饰设计模式
* 字符缓冲流分类:
* 字符缓冲读入流:BufferedReader 没有读的能力
* 字符缓冲写出流:BufferedWriter 没有写的能力
实例:
写操作

        //创建字符写出流
		FileWriter fileWriter = new FileWriter("test");
		//使用字符缓冲流实现写出
		BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
		//写的操作
		bufferedWriter.write("asdf");
		//bufferedWriter.write("\r\n");//windows下的换行
		bufferedWriter.newLine();//换行,可以实现跨平台        

  

读操作

      //a:一次读一个字符
		int num;
				while ((num = bufferedReader.read()) != -1) {
					System.out.print((char)num);
				}
		//b:一次读多个字符
				char[] arr = new char[2];
				while ((num = bufferedReader.read(arr)) != -1) {
					System.out.print(new String(arr,0,num));
				}
		//c:一次读一行  readLine()
		//原理:一个字符一个字符的读,直到读到换行符为止.然后将所有读到的字符返回
		//注意点:不会将当前的换行符返回 ;返回值就是我们读到的内容.如果读完了,返回null
		String data = null;
		while ((data = bufferedReader.readLine()) != null) {
			System.out.print(data);
			System.out.println();//换行
		}

          //小坑
          /*
          *读一行文字。 任何一条线都被视为终止
          *换行符('\n'),回车符('\r'),回车符
          *紧接着换行,或到达文件结尾
          */
          bufferedReader.readLine()

  

字节流

* 字节流:传输的是字节, 可以传输任何类型的数据
* 字节输出流 OutPutStream
* 字节输入流 InPutStream
* 字节缓冲输出流 BufferedOutPutStream
* 字节缓冲输入流 BufferedInPutStream
*

//		字节流的写入
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\fileWrite.txt");
//        因为是使用的字节流不能直接使用字符, 需要进行编码
        fileOutputStream.write("fileWrite".getBytes());
		
//		字节流读入(单个读取)
		FileInputStream fileInputStream = new FileInputStream("d:\\fileWrite.txt");
//        因为是使用的字节流不能直接使用字符, 需要进行编码
        int num =0;
        while ((num = fileInputStream.read())!= -1){
            System.out.print((char) num);
        }

//		字节流读入(以数组读取)
		FileInputStream fileInputStream = new FileInputStream("d:\\fileWrite.txt");
//        因为是使用的字节流不能直接使用字符, 需要进行编码
        byte [] bs = new byte[3];
        while ((fileInputStream.read(bs))!= -1){
            System.out.print(new String(bs));
        }
		
//		InputStream的 available()方法获取文件字节数
		FileInputStream fileInputStream = new FileInputStream("d:\\fileWrite.txt");
//        如果文本字节数大不建议使用
        byte [] bs = new byte[fileInputStream.available()];
        while ((fileInputStream.read(bs))!= -1){
            System.out.println(new String(bs));
        }

  


标准输入输出流
//从键盘(输入源)接收数据到内存中
//可以直接都得到标准输入流对象,已经绑定了输入源,也就是说可以直接从键盘输入数据
//TIP: 标准输入流是一个字节流
InputStream inputStream = System.in;
//InputStream.read()是一个阻塞式的方法,会一直等待用户的输入
int num = inputStream.read();

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

* 转换流: 从字节流到字符流
* 字节->字符 解码
* 字符->字节 编码
* 要让字符缓冲流的readline() newLine()等服务于字节流
* InputStreamReader 输入转换流
* OutputStreamWriter 输出转换流

BufferedReader keyBoardAsInput = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter consoleAsOutput = new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader fileAsInput = new BufferedReader(new FileReader("d:\\test"));
BufferedWriter fileAsOutput = new BufferedWriter(new FileWriter("d:\\testok",true));	//可追加

 


/**
* 更换输入源输出源
* 原来是从键盘输入, 改成从文件输入
* 原来是从控制台输出,改成从文件输出
*/

System.setIn(new FileInputStream("d:\\test"));
System.setOut(new PrintStream("d:\\testok"));
BufferedReader keyBoardAsInput = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter consoleAsOutput = new BufferedWriter(new OutputStreamWriter(System.out));


/**
* 文件 File类. 用来操作文件和路径
*
* 创建文件
* 创建路径
* 创建多路径
*
* 判断是否是文件
* 判断是否是路径
* 判断是否隐藏
*
* 获取路径
* 获取指定目录下的文件或文件夹
* 获取文件List
*
* 创建File对象的两种方式
* 通过完整路径实现
* 通过new File(父目录,子目录)
* 通过new File(new File(父目录), 子目录)
*/

* 字节打印流: PrintStream 除了拥有输出流的特点之外还有打印的功能
* 字符打印流: PrintWriter
*
*
* PrintStream的构造方法可接受参数
* 1,字符串路径
* 2,File对象
* 3,OutputStream
*
* 普通的write方法需要调用flush或者close方法才可以看到数据.
* JDK1.5之后Java对PrintStream进行了扩展,增加了格式化输出方式,可以使用printf()重载方法直接格式化输出。
* 但是在格式化输出的时候需要指定输出的数据类型格式。%d, %s, %f, %lf, %c
*
* 字节打印流支持的设备:
* 1.File类型的文件
* 2.字符串类型的文件
* 3.字节输出流
*
* PrintStream在实际中主要有下面两个用处
* 1、把任意类型的数据自动转换为字符串输出,相当的方便。
* 2、方便收集异常日志。
* System.out就是System内部维护的一PrintStream的对象
*
* 打印异常到文件 PrintStream logPrintStream = new PrintStream(new FileOutputStream(logFile, true));
*
*
* PrintWriter
* 1,字符串路径。
* 2,File对象。
* 对于1,2类型的数据,还可以指定编码表。也就是字符集。
* 3,OutputStream
* 4,Writer
*对于3,4类型的数据,可以指定自动刷新。
* 注意:该自动刷新值为true时,只有三个方法可以用:println,printf,format.
* 如果想要既有自动刷新,又可执行编码。如何完成流对象的包装?
* PrintWrter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream("a.txt"),"utf-8"),true);
* 如果想要提高效率。还要使用打印方法。
* PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a.txt"),"utf-8")),true);

 

编码的转换问题
*常用转换流的编码的方法是使用OutputStreamWriter(OutputStream, charSet), InPutStreamReader(inputStream, charSet). PrintWriter(String fileName, charSet)
*使用new String(byte[], charSet) 对读入的数据进行编码, 写出时String.getBytes(charSet)
*造成编码问题的主要原因: 如 GBK 和 UTF-8 一个是二字节的一个是三字节的编码格式, 其由于两编码格式的不同必然出现乱码问题, 两格式对于缺失字节的补充的详细规则不同
*所以有可能会导致编码转换后无法正确转回...但是 ISO8859-1 和此两种编码的转换不会出现这种问题, 因为 ISO8859-1 是一个字节的编码;

posted @ 2018-08-20 20:39  一根咸鱼干  阅读(280)  评论(0编辑  收藏  举报