Java多线程——Thread类

Java多线程——Thread类

Java 中线程实现方式有两种:

  • 继承Thread类,并重写run方法
  • 实现Runnable接口的run方法

Thread类

使用方法:继承Thread类,并重写run方法

public class Demo {
    public static class MyThread extends Thread {
        public void run() {
            System.out.println("my thread");
        }
    }

    public static void main(String[] args) {
        Thread mythread = new MyThread();
        // 调用start()方法后,该线程才算启动
        mythread.start();
    }
}

Runable 接口

使用方法:实现Runnable接口的run方法

public class Demo1 {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        new Demo.MyThread().start();
        // Java 8 函数式编程,可以省略MyThread类
        new Thread(() -> {
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

Java线程状态

Thread 类里有一个枚举类型 State,定义了线程的几种状态,分别有:

  • NEW:表示当前线程尚未启动,NEW状态表示实例化一个线程之后,但没有开始执行,即 Thread 实例还没调用 start() 方法。
  • RUNNABLE:表示当前线程正在运行中,RUNNABLE 状态包括了操作系统线程状态中的 Running 和 Ready,处于此状态的线程可能正在运行,也可能正在等待系统资源。
  • BLOCKED:表示当前线程处于阻塞状态,处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。
  • WAITING:表示当前线程处于无限期等待状态,处于这种状态的线程不会被分配CPU执行时间,它们要等待显示的被其它线程唤醒。
  • TIMED_WAITING:表示当前线程处于超时等待状态,处于这种状态的线程也不会被分配 CPU 执行时间,不过无需等待被其它线程显示的唤醒,在一定时间之后它们会由系统自动的唤醒。
  • TERMINATED:表示当前线程处于终止状态,已终止线程的线程状态,线程已经结束执行,即 run() 方法执行完成。

Java8 中 state 枚举类代码:

public enum State {
   /**
    * Thread state for a thread which has not yet started.
    */
   NEW,

   /**
    * Thread state for a runnable thread.  A thread in the runnable
    * state is executing in the Java virtual machine but it may
    * be waiting for other resources from the operating system
    * such as processor.
    */
   RUNNABLE,

   /**
    * Thread state for a thread blocked waiting for a monitor lock.
    * A thread in the blocked state is waiting for a monitor lock
    * to enter a synchronized block/method or
    * reenter a synchronized block/method after calling
    * {@link Object#wait() Object.wait}.
    */
   BLOCKED,

   /**
    * Thread state for a waiting thread.
    * A thread is in the waiting state due to calling one of the
    * following methods:
    * <ul>
    *   <li>{@link Object#wait() Object.wait} with no timeout</li>
    *   <li>{@link #join() Thread.join} with no timeout</li>
    *   <li>{@link LockSupport#park() LockSupport.park}</li>
    * </ul>
    *
    * <p>A thread in the waiting state is waiting for another thread to
    * perform a particular action.
    *
    * For example, a thread that has called <tt>Object.wait()</tt>
    * on an object is waiting for another thread to call
    * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
    * that object. A thread that has called <tt>Thread.join()</tt>
    * is waiting for a specified thread to terminate.
    */
   WAITING,

   /**
    * Thread state for a waiting thread with a specified waiting time.
    * A thread is in the timed waiting state due to calling one of
    * the following methods with a specified positive waiting time:
    * <ul>
    *   <li>{@link #sleep Thread.sleep}</li>
    *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
    *   <li>{@link #join(long) Thread.join} with timeout</li>
    *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
    *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
    * </ul>
    */
   TIMED_WAITING,

   /**
    * Thread state for a terminated thread.
    * The thread has completed execution.
    */
   TERMINATED;
}

查看线程当前状态可以调用 getState() 方法,并进入:

/**
  * Returns the state of this thread.
  * This method is designed for use in monitoring of the system state,
  * not for synchronization control.
  *
  * @return this thread's state.
  * @since 1.5
*/
public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
public static State toThreadState(int var0) {
    if ((var0 & 4) != 0) {
        return State.RUNNABLE;
    } else if ((var0 & 1024) != 0) {
        return State.BLOCKED;
    } else if ((var0 & 16) != 0) {
        return State.WAITING;
    } else if ((var0 & 32) != 0) {
        return State.TIMED_WAITING;
    } else if ((var0 & 2) != 0) {
        return State.TERMINATED;
    } else {
        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
    }
}

NEW:

实例化一个线程之后,并且这个线程没有开始执行,这个时候的状态就是 NEW,尚未启动指的是还没调用 Thread 实例的 start() 方法。

关于start() 的两个引申问题:

  1. 反复调用同一个线程的start()方法是否可行?
  2. 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

查看 start() 方法代码:

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

在 start() 内部,这里有一个 threadStatus 的变量。如果它不等于0,调用 start() 是会直接抛出异常的。在调用一次start()之后,threadStatus 的值会改变(threadStatus !=0),此时再次调用 start() 方法会抛出 IllegalThreadStateException 异常。

因此,两个问题的答案都是不可行。

RUNNABLE:

表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。

RUNNABLE 状态也可以理解为存活着正在尝试征用 CPU 的线程(有可能这个瞬间并没有占用CPU,但是它可能正在发送指令等待系统调度)。由于在真正的系统中,并不是开启一个线程后,CPU就只为这一个线程服务,它必须使用许多调度算法来达到某种平衡,不过这个时候线程依然处于RUNNABLE状态。

BLOCKED:

BLOCKED 称为阻塞状态,原因通常是它在等待一个“锁”,当尝试进入一个 synchronized 语句块/方法时,锁已经被其它线程占有,就会被阻塞,直到另一个线程走完临界区或发生了相应锁对象的 wait() 操作后,它才有机会去争夺进入临界区的权利。

处于 BLOCKED 状态的线程,即使对其调用 thread.interrupt() 也无法改变其阻塞状态,因为 interrupt() 方法只是设置线程的中断状态,即做一个标记,不能唤醒处于阻塞状态的线程。

注意:

ReentrantLock.lock() 操作后进入的是 WAITING 状态,其内部调用的是 LockSupport.park() 方法。

WAITING:

处于这种状态的线程不会被分配CPU执行时间,它们要等待显示的被其它线程唤醒。WAITING 状态通常是指一个线程拥有对象锁后进入到相应的代码区域后,调用相应的“锁对象”的 wait() 方法操作后产生的一种结果。变相的实现还有 LockSupport.park()、Thread.join()等,它们也是在等待另一个事件的发生,也就是描述了等待的意思。

以下方法会让线程陷入无限期等待状态:

  • 没有设置timeout参数的Object.wait()

  • 没有设置timeout参数的Thread.join()

  • LockSupport.park()

注意:

LockSupport.park(Object blocker) 会挂起当前线程,参数blocker是用于设置当前线程的“volatile Object parkBlocker 成员变量”

parkBlocker 是用于记录线程是被谁阻塞的,可以通过LockSupport.getBlocker()获取到阻塞的对象,用于监控和分析线程用的。

“阻塞”与“等待”的区别:

  • “阻塞”状态是等待着获取到一个排他锁,进入“阻塞”状态都是被动的,离开“阻塞”状态是因为其它线程释放了锁,不阻塞了;

  • “等待”状态是在等待一段时间 或者 唤醒动作的发生,进入“等待”状态是主动的

如主动调用 Object.wait(),如无法获取到 ReentraantLock,主动调用 LockSupport.park(),如主线程主动调用 subThread.join(),让主线程等待子线程执行完毕再执行。离开“等待”状态是因为其它线程发生了唤醒动作或者到达了等待时间。

TIMED_WAITING:

处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其它线程显示的唤醒,在一定时间之后它们会由系统自动的唤醒。

以下方法会让线程进入TIMED_WAITING限期等待状态:

  • Thread.sleep()方法

  • 设置了timeout参数的Object.wait()方法

  • 设置了timeout参数的Thread.join()方法

  • LockSupport.parkNanos()方法

  • LockSupport.parkUntil()方法

TERMINATED:

已终止线程的线程状态,线程已经结束执行。这个状态仅仅是 Java 语言提供的一个状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。

线程状态的转换

线程状态转换图:

image

BLOCKED状态与RUNNABLE状态的转换

处于BLOCKED状态的线程是因为在等待锁的释放。假如这里有两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状态。

同时,run() 方法的执行是需要时间的,并不是启动 start() 方法后就会立即执行,查看以下例子:

public class Demo2 {

    public void blockedTest() {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        }, "a");
        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                testMethod();
            }
        }, "b");

        a.start();
        b.start();
        System.out.println(a.getName() + ":" + a.getState()); // 输出?
        System.out.println(b.getName() + ":" + b.getState()); // 输出?
    }

    // 同步方法争夺锁
    private synchronized void testMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Demo2 demo2 = new Demo2();
        demo2.blockedTest();
    }
}

输出结果:此时 a 线程已得到锁,进入 run() 方法,但还没有执行 Thread.sleep(),由于 a 线程已得到锁,b 线程等待锁的释放,进入 BLOCKED 状态。

a:RUNNABLE
b:BLOCKED

Process finished with exit code 0

调换语句顺序,发现结果发生变化:

public void blockedTest() throws InterruptedException {
    ... ...
    a.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出?
    b.start();
    System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

输出结果:原因是测试方法的main线程只保证了a,b两个线程调用start()方法(转化为RUNNABLE状态),还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。

a:RUNNABLE
b:RUNNABLE

Process finished with exit code 0

让 main 线程停一段时间,就可以得到 BLOCKED 状态的结果:

public void blockedTest() throws InterruptedException {
    ... ...
    a.start();
    Thread.sleep(200L);
    System.out.println(a.getName() + ":" + a.getState()); // 输出?
    b.start();
    Thread.sleep(200L);
    System.out.println(b.getName() + ":" + b.getState()); // 输出?
}

输出结果:此时 a 线程已得到锁,正在执行 run() 方法,进入 TIMED_WAITING 状态,b 线程由于得不到锁,进入 BLOCKED 状态

a:TIMED_WAITING
b:BLOCKED

Process finished with exit code 0

WAITING状态与RUNNABLE状态的转换

有3个方法可以使线程从RUNNABLE状态转为WAITING状态。

Thread.join():

public void blockedTest() {
    ······
    a.start();
    a.join();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
    System.out.println(b.getName() + ":" + b.getState());
}

输出结果:调用join()方法不会释放锁,会一直等待当前线程执行完毕(转换为TERMINATED状态)。b线程的状态,有可能打印RUNNABLE(尚未进入同步方法),也有可能打印TIMED_WAITING(进入了同步方法)。

a:TERMINATED
b:TIMED_WAITING

Process finished with exit code 0

需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。

Object.wait():导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

使用示例:

public class WaitNotifyTest {
	public static void main(String[] args) {
		Object lock = new Object();
		
		new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程A等待获取lock锁");
                synchronized (lock) {
                    try {
                        System.out.println("线程A获取了lock锁");
                        Thread.sleep(1000);
                        System.out.println("线程A将要运行lock.wait()方法进行等待");
                        lock.wait();
                        System.out.println("线程A等待结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
		
		new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程B等待获取lock锁");
                synchronized (lock) {
                    System.out.println("线程B获取了lock锁");
                    try {
                    	Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程B将要运行lock.notify()方法进行通知");
                    lock.notify();
                }
            }
        }).start();
	}
}

输出结果:

线程A等待获取lock锁
线程A获取了lock锁
线程B等待获取lock锁
线程A将要运行lock.wait()方法进行等待
线程B获取了lock锁
线程B将要运行lock.notify()方法进行通知
线程A等待结束

Process finished with exit code 0

LockSupport.park():LockSupport.park() 调用的是 Unsafe 中的 native 代码,与Object类的wait/notify机制相比,park 以 thread 为操作对象更符合阻塞线程的直观定义,操作更精准,可以准确地唤醒某一个线程增加了灵活性。

//LockSupport中
public static void park() {
        UNSAFE.park(false, 0L);
    }

TIMED_WAITING状态与RUNNABLE状态转换

TIMED_WAITING 与 WAITING 状态类似,只是 TIMED_WAITING 状态等待的时间是指定的。

Thread.sleep(long):

使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入RUNNABLE状态。

Sleep 方法代码:

public static native void sleep(long millis) throws InterruptedException;

// 纳秒级别控制
public static void sleep(long millis, int nanos) throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    sleep(millis);
}

Object.wait(long):

wait(long) 方法使线程进入 TIMED_WAITING 状态。这里的wait(long)方法与无参方法 wait() 相同的地方是,都可以通过其他线程调用 notify() 或 notifyAll() 方法来唤醒。

不同的地方是,有参方法 wait(long) 就算其他线程不来唤醒它,经过指定时间 long 之后它会自动唤醒,拥有去争夺锁的资格。

Thread.join(long):

join(long) 使当前线程执行指定时间,并且使线程进入 TIMED_WAITING 状态。

join(long) 的使用:调用 a.join(1000L),因为是指定了具体 a 线程执行的时间的,并且执行时间是小于 a 线程 sleep 的时间,所以 a 线程状态输出 TIMED_WAITING,b 线程状态仍然不固定(RUNNABLE 或 BLOCKED)。

public void blockedTest() {
    ······
    a.start();
    a.join(1000L);
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
    System.out.println(b.getName() + ":" + b.getState());
}

Java线程中断

Thread类里提供的关于线程中断的几个方法:

  • Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase);
  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false;
  • Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。

“VisualVM线程监控线程状态”与“Java线程状态”对应关系总结:

通过 dump thread stack,并与 VisualVM 监控信息中的线程名称对应,找到的 VisualVM 每种线程状态的线程堆栈如下:

1、运行:RUNNABLE

"http-bio-8080-Acceptor-0" daemon prio=6 tid=0x000000000d7b4800 nid=0xa264 runnable [0x000000001197e000]
      java.lang.Thread.State: RUNNABLE
            at java.net.DualStackPlainSocketImpl.accept0(Native Method)
            at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
            at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:398)
            at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
            - locked <0x00000000c2303850> (a java.net.SocksSocketImpl)
            at java.net.ServerSocket.implAccept(ServerSocket.java:530)
            at java.net.ServerSocket.accept(ServerSocket.java:498)
            at org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFactory.java:60)
            at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
            at java.lang.Thread.run(Thread.java:745)

Locked ownable synchronizers:
        - None


2、休眠:sleeping

"Druid-ConnectionPool-Destory-293325558" daemon prio=6 tid=0x000000000d7ad000 nid=0x9c94 waiting on condition [0x000000000bf0f000]
      java.lang.Thread.State: TIMED_WAITING (sleeping)
           at java.lang.Thread.sleep(Native Method)
            at com.alibaba.druid.pool.DruidDataSource$DestroyConnectionThread.run(DruidDataSource.java:1685)

Locked ownable synchronizers:
        - None


3、等待:wait

"Finalizer" daemon prio=8 tid=0x0000000009349000 nid=0xa470 in Object.wait() [0x000000000a82f000]
      java.lang.Thread.State: WAITING (on object monitor)
            at java.lang.Object.wait(Native Method)
            - waiting on <0x00000000c22a0108> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)

      - locked <0x00000000c22a0108> (a java.lang.ref.ReferenceQueue.Lock)
            at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
            at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

   Locked ownable synchronizers:
        - None

 

"JMX server connection timeout 45" daemon prio=6 tid=0x000000000e846000 nid=0xab10 in Object.wait() [0x00000000137df000]
      java.lang.Thread.State: TIMED_WAITING (on object monitor)
            at java.lang.Object.wait(Native Method)
           - waiting on <0x00000000c55da3f0> (a [I)
            at com.sun.jmx.remote.internal.ServerCommunicatorAdmin$Timeout.run(ServerCommunicatorAdmin.java:168)
            - locked <0x00000000c55da3f0> (a [I)
            at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
         - None


4、驻留:park

"http-bio-8080-exec-2" daemon prio=6 tid=0x000000000d7b8000 nid=0x9264 waiting on condition [0x000000000ee4e000]
      java.lang.Thread.State: WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
           - parking to wait for  <0x00000000c5629bc8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject)
            at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
            at java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
            at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
            at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
            at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
            at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
            at java.util.concurrent.ThreadPoolExecutor.Worker.run(ThreadPoolExecutor.java:615)
            at org.apache.tomcat.util.threads.TaskThread.WrappingRunnable.run(TaskThread.java:61)
            at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - None


"pool-9-thread-1" prio=6 tid=0x000000000d7b2000 nid=0xd5fc waiting on condition [0x000000001187e000]
       java.lang.Thread.State: TIMED_WAITING (parking)
             at sun.misc.Unsafe.park(Native Method)
             - parking to wait for  <0x00000000c563b9e0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject)
             at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
             at java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
             at java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090)
             at java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807)
             at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
             at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
             at java.util.concurrent.ThreadPoolExecutor.Worker.run(ThreadPoolExecutor.java:615)
             at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
       - None


5、监视:monitor

"Thread-1" prio=6 tid=0x000000000a8a1800 nid=0xfdb4 waiting for monitor entry [0x000000000b4de000]
      java.lang.Thread.State: BLOCKED (on object monitor)
            at com.Test2$T.run(Test2.java:58)
           - waiting to lock <0x00000000eab757e0> (a java.lang.Object)

Locked ownable synchronizers:
      - None


汇总如下图所示:

image

posted @ 2021-10-21 20:40  起床睡觉  阅读(1843)  评论(0编辑  收藏  举报