Thinking in Java from Chapter 21
From Thinking in Java 4th Edition
并发
线程可以驱动任务,因此你需要一种描述任务的方式,这可由Runnable接口来提供。
要想定义任务,只需要实现Runnable接口,并编写run()方法,使得该任务可以执行你的命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class LiftOff implements Runnable { protected int countDown = 10 ; // Default private static int taskCount = 0 ; private final int id = taskCount++; public LiftOff() {} public LiftOff( int countDown){ this .countDown = countDown; } public String status(){ return "#" + id + "(" + (countDown > 0 ? countDown : "LiftOff!" ) + "). " ; } public void run(){ while (countDown-- > 0 ){ System.out.println(status()); Thread.yield(); } } } |
从Runnable导出一个类时,它必须具有run()方法,但是它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器:
1 2 3 4 5 6 7 8 9 10 | public class BasicThread { public static void main(String[] args){ Thread t = new Thread( new LiftOff()); t.start(); System.out.println( "Waiting for LiftOff" ); } } /* Output: Waiting for LiftOff #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), */ |
Thread构造器只需要一个Runnable对象。
1. 调用Thread对象的start()方法为该线程执行必要的初始化操作。
2. 调用Runnable对象的run()方法,以便在这个新线程中启动该任务。
从输出可以看出start()方法迅速返回了,因为"Waiting for LiftOff"消息在倒计时完成之前就出现了。
实际上你产生的是对LiftOff.run()方法的调用,并且这个方法还没完成,但是因为LiftOff.run()是由不同的线程执行的,因此你仍旧可以执行main()线程中的其他操作。
如果添加更多的线程去驱动更多的任务,就可以看到所有任务彼此之间是如何呼应的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class MoreBasicThreads { public static void main(String[] args){ for ( int i = 0 ; i < 5 ; ++i) new Thread( new LiftOff()).start(); System.out.println( "Waiting for LiftOff" ); } } /* Output: Waiting for LiftOff #3(9), #1(9), #3(8), #1(8), #3(7), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(LiftOff!), #4(9), #4(8), #4(7), #4(6), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(LiftOff!), #0(9), #4(5), #3(6), #4(4), #3(5), #4(3), #3(4), #4(2), #3(3), #4(1), #3(2), #4(LiftOff!), #0(8), #3(1), #0(7), #3(LiftOff!), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), */ |
使用Executor
Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。
Executor用来代替MoreBasicThreads.java中显式创建Thread对象。
ExecutorService知道如何构建上下文来执行Runnable对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import java.util.concurrent.*; public class CachedThreadPool { public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); // No space between new and Cached for ( int i = 0 ; i < 5 ; ++i) exec.execute( new LiftOff()); exec.shutdown(); } } /* Output: #0(9), #2(9), #0(8), #1(9), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), #2(8), #1(8), #3(9), #2(7), #1(7), #3(8), #2(6), #1(6), #3(7), #2(5), #2(4), #4(9), #3(6), #1(5), #3(5), #1(4), #1(3), #1(2), #1(1), #1(LiftOff!), #4(8), #2(3), #4(7), #4(6), #3(4), #4(5), #3(3), #4(4), #3(2), #4(3), #3(1), #4(2), #2(2), #4(1), #4(LiftOff!), #3(LiftOff!), #2(1), #2(LiftOff!), */ |
上例中,CachedThreadPool将为每个任务都创建一个线程。
ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型。
常见的情况是,Executor被用来创建和管理系统中的所有的任务。
对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前任务(本例中为main的线程)将继续运行在shutdown()被调用之前提交的所有任务。
下面的程序展示了CachedThreadPool替换为不同类型的Executor。FixedThreadPool使用了有限的线程集来执行所提交的任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import java.util.concurrent.*; public class FixedThreadPool { public static void main(String[] args){ // Constructor argument is number of threads: ExecutorService exec = Executors.newFixedThreadPool( 5 ); for ( int i = 0 ; i < 5 ; ++i) exec.execute( new LiftOff()); exec.shutdown(); } } /* Output: #0(9), #2(9), #4(9), #0(8), #2(8), #3(9), #1(9), #1(8), #0(7), #4(8), #0(6), #4(7), #0(5), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(LiftOff!), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(LiftOff!), #3(8), #2(7), #0(4), #3(7), #2(6), #0(3), #3(6), #2(5), #0(2), #3(5), #0(1), #3(4), #2(4), #0(LiftOff!), #3(3), #2(3), #3(2), #2(2), #3(1), #2(1), #3(LiftOff!), #2(LiftOff!), */ |
有了FixedThreadPool,你就可以一次性预先执行代价高昂的线程分配。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。
SingleThreadExecutor就像是线程数量为1的FixedThreadPool。如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前结束,所有的任务将使用相同的线程。
下例中可以看到每个任务都是按照它们被提交的顺序、并且是在下一个任务开始之前完成的。因此SingleThreadExecutor会序列化所有提交给它们的任务,并会维护它自己(隐藏)的悬挂任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import java.util.concurrent.*; public class SingleThreadExecutor { public static void main(String[] args){ ExecutorService exec = Executors.newSingleThreadExecutor(); for ( int i = 0 ; i < 5 ; ++i) exec.execute( new LiftOff()); exec.shutdown(); } } /* Output: #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(LiftOff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(LiftOff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(LiftOff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(LiftOff!), */ |
从任务中返回值
Runnable是执行工作的独立任务,但是它不返回任何值。如果希望能够返回值,则必须实现Callable接口而不是Runnable接口。
Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回的值,并且必须使用ExecutorService.submit()方法调用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import java.util.concurrent.*; import java.util.*; class TaskWithResult implements Callable<String> { private int id; public TaskWithResult( int id){ this .id = id; } public String call() { return "result of TaskWithResult " + id; } } public class CallableDemo { public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<Future<String>>(); for ( int i = 0 ; i < 10 ; ++i) results.add(exec.submit( new TaskWithResult(i))); for (Future<String> fs : results) try { // get() blocks until completion: System.out.println(fs.get()); } catch (InterruptedException e){ System.out.println(e); return ; } catch (ExecutionException e) { System.out.println(e); } finally { exec.shutdown(); } } } /* Output: result of TaskWithResult 0 result of TaskWithResult 1 result of TaskWithResult 2 result of TaskWithResult 3 result of TaskWithResult 4 result of TaskWithResult 5 result of TaskWithResult 6 result of TaskWithResult 7 result of TaskWithResult 8 result of TaskWithResult 9 */ |
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化 。
1. 可以用isDone()方法来查看Future是否完成
2. 任务完成时,可以调用get()方法来获取该结果
也可以不用isDone()进行检查就直接调用get(),这种情况下,get()将阻塞直至结果准备就绪。
休眠
影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。
在LiftOff类中,把yield()的调用换成sleep()将得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import java.util.concurrent.*; public class SleepingTask extends LiftOff { public void run(){ try { while (countDown-- > 0 ){ System.out.print(status()); // Old-style // Thread.sleep(100); // Java SE5/6-style: TimeUnit.MILLISECONDS.sleep( 100 ); } } catch (InterruptedException e) { System.err.println( "Interrupted" ); } } public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); for ( int i = 0 ; i < 5 ; ++i) exec.execute( new SleepingTask()); exec.shutdown(); } } /* Output: #0(9), #3(9), #1(9), #4(9), #2(9), #0(8), #3(8), #1(8), #2(8), #4(8), #0(7), #4(7), #2(7), #3(7), #1(7), #4(6), #0(6), #3(6), #1(6), #2(6), #4(5), #3(5), #2(5), #0(5), #1(5), #4(4), #3(4), #2(4), #1(4), #0(4), #4(3), #2(3), #3(3), #0(3), #1(3), #4(2), #2(2), #0(2), #3(2), #1(2), #4(1), #2(1), #0(1), #3(1), #1(1), #4(LiftOff!), #2(LiftOff!), #0(LiftOff!), #3(LiftOff!), #1(LiftOff!), */ |
sleep()调用可以抛出InterruptedException异常,并可以看到,它在run()中捕获。因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部产生的异常。
优先级
调度器将倾向于让优先权最高的线程先执行,然而,这并不意味着优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
绝大多数时间里,所有线程都应该按照默认的优先级运行。试图操纵线程的优先级通常是一种错误。
可以用getPriority()来读取现有线程的优先级,并且在任何时候都可以通过setPriority()来修改它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import java.util.concurrent.*; public class SimplePriorities implements Runnable { private int countDown = 5 ; private volatile double d; // No optimization private int priority; public SimplePriorities( int priority){ this .priority = priority; } public String toString(){ return Thread.currentThread() + ": " + countDown; } public void run(){ Thread.currentThread().setPriority(priority); while ( true ){ // An expensive, interruptable operation: for ( int i = 1 ; i < 100000 ; ++i){ d += (Math.PI + Math.E) / ( double )i; if ( 0 == i % 1000 ) Thread.yield(); } System.out.println( this ); if ( 0 == --countDown) return ; } } public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); for ( int i = 0 ; i < 5 ; ++i) exec.execute( new SimplePriorities(Thread.MIN_PRIORITY)); exec.execute( new SimplePriorities(Thread.MAX_PRIORITY)); exec.shutdown(); } } |
让步
如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用CPU了。这个暗示将通过yield()方法做出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。
当调用yield()时,你也在建议具有相同优先级的其他线程可以运行。
后台线程
后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可缺少的部分。
当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。
反过来,只要有非后台线程还在运行,程序就不会终止:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import java.util.concurrent.*; import static net.mindview.util.Print.*; public class SimpleDaemons implements Runnable { public void run(){ try { while ( true ) { TimeUnit.MILLISECONDS.sleep( 100 ); print(Thread.currentThread() + " " + this ); } } catch (InterruptedException e) { print( "sleep() interrupted" ); } } public static void main(String[] args) throws Exception { for ( int i = 0 ; i < 10 ; ++i){ Thread daemon = new Thread( new SimpleDaemons()); daemon.setDaemon( true ); // Must call before start() daemon.start(); } print( "All daemons started" ); TimeUnit.MILLISECONDS.sleep( 175 ); } } /* Output: All daemons started Thread[Thread-4,5,main] SimpleDaemons@187a0b5 Thread[Thread-2,5,main] SimpleDaemons@153c85c Thread[Thread-3,5,main] SimpleDaemons@23fc9c Thread[Thread-8,5,main] SimpleDaemons@b211be Thread[Thread-0,5,main] SimpleDaemons@11f65e4 Thread[Thread-1,5,main] SimpleDaemons@15332f2 Thread[Thread-9,5,main] SimpleDaemons@1da14a7 Thread[Thread-7,5,main] SimpleDaemons@16b9e5b Thread[Thread-5,5,main] SimpleDaemons@1bbee70 Thread[Thread-6,5,main] SimpleDaemons@1cbca8d */ |
SimpleDaemons.java创建了显示的线程,以便可以设置它们的后台标志。通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(后台、优先级、名称):
1 2 3 4 5 6 7 8 9 10 | package net.mindview.util; import java.util.concurrent.*; public class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon( true ); return t; } } |
现在可以用一个新的DaemonThreadFactory作为参数传递给Executor.newCachedThreadPool():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package net.mindview.util; import java.util.concurrent.*; public class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon( true ); return t; } } // Using a Thread Factory to create daemons. import java.util.concurrent.*; import net.mindview.util.*; import static net.mindview.util.Print.*; public class DaemonFromFactory implements Runnable { public void run(){ try { while ( true ){ TimeUnit.MILLISECONDS.sleep( 100 ); print(Thread.currentThread() + " " + this ); } } catch (InterruptedException e){ print( "Interrupted" ); } } public static void main(String[] args) throws Exception { ExecutorService exec = Executor.newCachedThreadPool( new DaemonThreadFactory()); for ( int i = 0 ; i < 10 ; ++i) exec.execute( new DaemonFromFactory()); print( "All daemons started" ); TimeUnit.MILLISECONDS.sleep( 500 ); // Run for a while } } /* Output All daemons started Thread[Thread-0,5,main] DaemonFromFactory@c01009 Thread[Thread-9,5,main] DaemonFromFactory@1bd343 Thread[Thread-7,5,main] DaemonFromFactory@7c321a Thread[Thread-5,5,main] DaemonFromFactory@c79e29 Thread[Thread-3,5,main] DaemonFromFactory@279922 Thread[Thread-1,5,main] DaemonFromFactory@1fcb7ef Thread[Thread-8,5,main] DaemonFromFactory@7e3dfb Thread[Thread-2,5,main] DaemonFromFactory@1415cb9 Thread[Thread-4,5,main] DaemonFromFactory@334426 Thread[Thread-6,5,main] DaemonFromFactory@15226d3 Thread[Thread-0,5,main] DaemonFromFactory@c01009 Thread[Thread-9,5,main] DaemonFromFactory@1bd343 .... */ |
每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象,而这个对象将被用来创建新的线程:
1 2 3 4 5 6 7 8 | package net.mindview.util; import java.util.concurrent.*; public class DaemonThreadPoolExecutor extends ThreadPoolExecutor { public DaemonThreadPoolExecutor() { super ( 0 , Integer.MAX_VALUE, 60L, TimeUnit, SECONDS, new SynchronousQueue<Runnable>(), new DaemonThradFactory()); } } |
可以通过iDaemon()方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将自动被设置成后台线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // Daemon threads spawn other daemon threads. import java.util.concurrent.*; import static net.mindview.util.Print.*; class Daemon implements Runnable { private Thread[] t = new Thread[ 10 ]; public void run() { for ( int i = 0 ; i < t.length; ++i){ t[i] = new Thread( new DaemonSpawn()); t[i].start(); printnb( "DaemonSpawn " + i + " started, " ); } for ( int i = 0 ; i < t.length; ++i) printnb( "t[" + i + "].isDaemon() = " + t[i].isDaemon() + ", " ); while ( true ) Thread.yield(); } } class DaemonSpawn implements Runnable { public void run(){ while ( true ) Thread.yield(); } } public class Daemons { public static void main(String[] args) throws Exception { Thread d = new Thread( new Daemon()); d.setDaemon( true ); d.start(); printnb( "d.isDaemon() = " + d.isDaemon() + ", " ); // Allow the daemon threads to // finish their startup processes: TimeUnit.SECONDS.sleep( 1 ); } } /* Output: d.isDaemon() = true, DaemonSpawn 0 started, DaemonSpawn 1 started, DaemonSpawn 2 started, DaemonSpawn 3 started, DaemonSpawn 4 started, DaemonSpawn 5 started, DaemonSpawn 6 started, DaemonSpawn 7 started, DaemonSpawn 8 started, DaemonSpawn 9 started, t[0].isDaemon() = true, t[1].isDaemon() = true, t[2].isDaemon() = true, t[3].isDaemon() = true, t[4].isDaemon() = true, t[5].isDaemon() = true, t[6].isDaemon() = true, t[7].isDaemon() = true, t[8].isDaemon() = true, t[9].isDaemon() = true, */ |
应该意识到后台进程在不执行finally子句的情况下就会终止其run()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Daemon threads don't run the finally clause import java.util.concurrent.*; import static net.mindview.util.Print.*; class ADaemon implements Runnable { public void run() { try { print( "Starting ADaemon" ); TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e){ print( "Exiting via InterruptedException" ); } finally { print( "This should always run?" ); } } } public class DaemonsDontRunFinally { public static void main(String[] args) throws Exception { Thread t = new Thread( new ADaemon()); t.setDaemon( true ); t.start(); } } /* Output: Starting ADaemon */ |
目前的示例中,都是实现了Runnable。在非常简单的情况下,你可能会希望使用直接从Thread继承这种可替换的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public class SimpleThread extends Thread { private int countDown = 5 ; private static int threadCount = 0 ; public SimpleThread() { // Store the thread name: super (Integer.toString(++threadCount)); start(); } public String toString() { return "#" + getName() + "(" + countDown + "), " ; } public void run() { while ( true ){ System.out.print( this ); if ( 0 == --countDown) return ; } } public static void main(String[] args){ for ( int i = 0 ; i < 5 ; ++i) new SimpleThread(); } } /* Output: #2(5), #2(4), #2(3), #2(2), #2(1), #4(5), #4(4), #4(3), #4(2), #4(1), #5(5), #5(4), #5(3), #5(2), #5(1), #3(5), #3(4), #3(3), #3(2), #3(1), #1(5), #1(4), #1(3), #1(2), #1(1), */ |
你可以通过适当的Thread构造器为Thread对象赋予具体的名称,这个名称可以通过使用getName()从toString()中获得。
另一种可能会看到的惯用法是自管理的Runnable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // A Runnable containing its own driver Thread. public class SelfManaged implements Runnable { private int countDown = 5 ; private Thread t = new Thread( this ); public SelfManaged() {t.start();} public String toString() { return Thread.currentThread().getName() + "(" + countDown + "), " ; } public void run() { while ( true ){ System.out.print( this ); if ( 0 == --countDown) return ; } } public static void main(String[] args){ for ( int i = 0 ; i < 5 ; ++i) new SelfManaged(); } } /* Output: Thread-3(5), Thread-3(4), Thread-3(3), Thread-3(2), Thread-3(1), Thread-5(5), Thread-5(4), Thread-5(3), Thread-5(2), Thread-5(1), Thread-6(5), Thread-6(4), Thread-6(3), Thread-6(2), Thread-6(1), Thread-4(5), Thread-4(4), Thread-4(3), Thread-4(2), Thread-4(1), Thread-7(5), Thread-7(4), Thread-7(3), Thread-7(2), Thread-7(1), */ |
这与从Thread继承并没有什么特别的差异,只是语法稍微晦涩一些。但是,实现接口使得你可以继承另一个不同的类,而从Thread继承将不行。
注意,这个示例中的start()是在构造器中调用的。应该意识到,在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象。这是优选Executor而不是显式地创建Thread对象的另一个原因。
有时,通过使用内部类来讲线程代码隐藏在类中将会很有用:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)