- 浏览: 1336564 次
- 性别:
- 来自: 成都
文章分类
- 全部博客 (471)
- 原创文章 (4)
- Database (84)
- J2SE (63)
- Web (26)
- Javascript (30)
- Lucene (11)
- os (13)
- 算法 (8)
- Webservice (1)
- Open projects (18)
- Hibernate (18)
- Spring (15)
- Css (2)
- J2ee (2)
- 综合技术 (18)
- 安全管理 (13)
- PatternsInJava (27)
- NIO (5)
- Ibatis (2)
- 书籍收藏 (1)
- quartz (7)
- 并发编程 (15)
- oracle问题 (2)
- ios (60)
- coco2d-iphone (3)
- C++ (6)
- Zookeeper (2)
- golang (4)
- animation (2)
- android (1)
最新评论
-
dandingge123:
【引用】限制UITextField输入长度的方法 -
qja:
...
对List顺序,逆序,随机排列实例代码 -
安静听歌:
现在在搞这个,,,,,哎~头都大了,,,又freemarker ...
通用大型网站页面静态化解决方案(一) -
springdata-jpa:
java quartz定时任务demo教程源代码下载,地址:h ...
Quartz 配置参考 -
马清天:
[b][/b][list][*]引用[u][/u][/list ...
通用大型网站页面静态化解决方案(一)
设计线程安全的类需要考虑哪些因素?
1) 找出哪些变量属于对象的状态
2) 找出哪些不变量属于对象的状态
3) 使用合适的并发策略来管理对状态的访问
考虑线程安全的需求
1) 同步范围多大? 整个方法? 一个大块? 小块?
2) 有哪些限制和先决条件?
java内建的监视器模型, 通过锁定, 即在锁对象添加监视器, 一旦锁定释放, 监视器通知其他等待的线程. Object.wait/notity/notifyAll
"代理"线程安全性
如果一个类合理的使用了一个已经实现线程安全的类, 就是把线程安全性交给内部对象"代理"了.
JDK提供一些线程安全的类, 如Vector, HashTable, ConcurrentHashMap, Collections.concurrentXXX(Obj), BlockingQueue等. 例子:
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null)
throw new IllegalArgumentException(
"invalid vehicle name: " + id);
}
}
对于只有单个需要代理的对象, 可以很方便的应用上述的"代理"方式, 对于多个相互独立的对象而已, 也可以使用
public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
public void add(..);
public void remove(...);
}
但是如果这些对象有相互关系, 就出问题了
如何给线程安全的类增加方法
1) 修改源代码, 最好. 需了解原来的线程安全的策略
2) 继承类, 并增加synchronized方法.
3) 用Decorator模式, 把类包起来, 增加新方法, 然而需要注意锁定的对象. 比如下面是不对的
public class ListHelper<E> {
//锁定对象为返回的list
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
...
public synchronized boolean putIfAbsent(E x) { //锁定对象为ListHelper对象
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
因为两个同步的锁定不是同一个对象, 因此不同的线程可以分别操作putIfAbsent方法和list对象固有的方法. 如果要让这个类正确运行, 需要修正如下
public class ListHelper<E> {
//锁定对象为list
public List<E> list =Collections.synchronizedList(new ArrayList<E>());
...
public boolean putIfAbsent(E x) {
synchronized (list) { //锁定对象为list
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
使用synchronized collection的原子性的问题
比如下面的代码:
另一种变通的方法是, 在遍历集合之前, 复制一份集合的引用. 当然集合复制也有细微的性能代价.
被隐藏的Iterator
JDK5的并发集合
ConcurrentMap(接口)和 ConcurrentHashMap, 并发环境下HashMap的替代品
CopyOnWriteArrayList, 并发环境下List的替代品
Queue: ConcurrentLinkedQueue, PriorityQueue(非并发). 不阻塞, 如果队列为空, get()返回null
BlockingQueue: 它的方法是阻塞的, 当队列为空, get()会阻塞直到队列有元素加入, 当队列满脸, insert()也会阻塞
ConcurrentHashMap使用一种lock striping的机制来实现并发控制, 并且提供了弱一致性的Iterator, 允许在迭代期间被修改而不抛出ConcurrentModificationException(例如,忽略被删除的元素). 然而作为代价, size()返回的值不是精确的元素的数量, 不过size()在并发环境的作用比get, put, containsKey的用途小得多
看一小段程序
ConcurrentHashMap<String, String> m = new ConcurrentHashMap<String, String>();
m.put("1", "one");
m.put("2", "two");
m.put("3", "three");
Iterator keyIt = m.keySet().iterator();
// !! remove some element, remove-if-equals 风格的操作
m.remove("1", "one");
while (keyIt.hasNext()) {
String k = (String)keyIt.next();
System.out.println("key: " + k + ", value: " + m.get(k));
}
输出结果:
key: 1, value: null
key: 3, value: three
key: 2, value: two
很短的代码, 一下执行完了, 没有再报错, 可以发现remove操作没有影响到遍历抛出异常. 然而, key: 1, value: null 这行输出说明了我们还留着对这个key的引用.
ConcurrentHashMap有几种新的风格的方法, put-if-absent, remove-if-equal, replace-if-equal, 比如上面的 m.remove("1", "one"); 就是, 如果改为 m.remove("1", "two"); 那么["1","one"]这一对就被保留下来了
CopyOnWriteArrayList, 顾名思义, 就能猜到每次都这个类做了改动, 就会copy一份原来的数组, 在改动后替换原来的数组, 再返回给调用者
BlockingQueue是基于 生产者-消费者 的模式构建的, 包括LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue,SynchronousQueue 4个实现类. 看一点示例代码:
public static void putAndOffer() throws InterruptedException {
BlockingQueue<String> q = new ArrayBlockingQueue<String>(1);
q.offer("1");
System.out.println("offer A done");
q.offer("2");
System.out.println("offer B done");
q.clear();
q.put("A");
System.out.println("put A done");
q.put("B");
System.out.println("put B done");
}
输出结果:
offer A done
offer B done
put A done
我们指定了BlockingQueue的容量为1, offer方法是不阻塞的, 所以q.offer("2");直接返回false, 而put方法是阻塞的, 所以System.out.println("put B done");一直没有执行.
PriorityBlockingQueue: 对于加入队列的元素是可排序的
SynchronousQueue: 不能算是queue的实现, 但是模仿了queue的行为, 更像是worker模式的实现. 因为它内部维护了一组用于处理元素的线程, 并且直接把加入队列的元素分配给处理该元素的线程. 如果没有可用线程, 就阻塞了.
BlockingQueue实例代码
下面是摘录的代码, 使用BlockingQueue, 分配N个线程执行文件索引的任务, 很经典:
1) 找出哪些变量属于对象的状态
2) 找出哪些不变量属于对象的状态
3) 使用合适的并发策略来管理对状态的访问
考虑线程安全的需求
1) 同步范围多大? 整个方法? 一个大块? 小块?
2) 有哪些限制和先决条件?
java内建的监视器模型, 通过锁定, 即在锁对象添加监视器, 一旦锁定释放, 监视器通知其他等待的线程. Object.wait/notity/notifyAll
"代理"线程安全性
如果一个类合理的使用了一个已经实现线程安全的类, 就是把线程安全性交给内部对象"代理"了.
JDK提供一些线程安全的类, 如Vector, HashTable, ConcurrentHashMap, Collections.concurrentXXX(Obj), BlockingQueue等. 例子:
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null)
throw new IllegalArgumentException(
"invalid vehicle name: " + id);
}
}
对于只有单个需要代理的对象, 可以很方便的应用上述的"代理"方式, 对于多个相互独立的对象而已, 也可以使用
public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
public void add(..);
public void remove(...);
}
但是如果这些对象有相互关系, 就出问题了
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
}
显然setLower, setUpper, isInRange三个方法的调用没有原子性保障, 而且isInRange同时依赖到两个相关的值, 很容易导致结果错误. 需要增加某些锁定来保证. 或者运行的情况下, 把两个对象合并为一个如何给线程安全的类增加方法
1) 修改源代码, 最好. 需了解原来的线程安全的策略
2) 继承类, 并增加synchronized方法.
3) 用Decorator模式, 把类包起来, 增加新方法, 然而需要注意锁定的对象. 比如下面是不对的
public class ListHelper<E> {
//锁定对象为返回的list
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
...
public synchronized boolean putIfAbsent(E x) { //锁定对象为ListHelper对象
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
因为两个同步的锁定不是同一个对象, 因此不同的线程可以分别操作putIfAbsent方法和list对象固有的方法. 如果要让这个类正确运行, 需要修正如下
public class ListHelper<E> {
//锁定对象为list
public List<E> list =Collections.synchronizedList(new ArrayList<E>());
...
public boolean putIfAbsent(E x) {
synchronized (list) { //锁定对象为list
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
使用synchronized collection的原子性的问题
比如下面的代码:
public class VectorHelper{
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
非常简单, 很多代码都是这样写的, 但是, 这段代码如果要正常运行, 必须是在假设Vertor对象是不被多个线程共享的情况下. 因为虽然Vector本身是线程安全的, 但VectorHelper不是, 并且getLast和deleteLast同时依赖于Vector.size()来判断最后一个元素. 很容易造成ArrayIndexOutOfBoundsException. 如果Vector被多个线程共享, 最简单的就是加上同步, 然后对一个集合的同步会带来很大的性能代价, 因为阻止了其他线程对集合的访问, 特别是当集合很大并且处理的任务非常大的情况下. 另一种变通的方法是, 在遍历集合之前, 复制一份集合的引用. 当然集合复制也有细微的性能代价.
被隐藏的Iterator
public class HiddenIterator {
@GuardedBy("this")
private final Set<Integer> set = new HashSet<Integer>();
public synchronized void add(Integer i) { set.add(i); }
public synchronized void remove(Integer i) { set.remove(i); }
public void addTenThings() {
Random r = new Random();
for (int i = 0; i < 10; i++)
add(r.nextInt());
System.out.println("DEBUG: added ten elements to " + set);
}
}
能想象这么简单的代码会在什么地方出错么? 答案是最后一行, System.out.println, 因为用到的set.toString(), 而toString()内部是会用Iterator来访问set的, 一旦此过程中set被修改, 就抛出ConcurrentModificationException.JDK5的并发集合
ConcurrentMap(接口)和 ConcurrentHashMap, 并发环境下HashMap的替代品
CopyOnWriteArrayList, 并发环境下List的替代品
Queue: ConcurrentLinkedQueue, PriorityQueue(非并发). 不阻塞, 如果队列为空, get()返回null
BlockingQueue: 它的方法是阻塞的, 当队列为空, get()会阻塞直到队列有元素加入, 当队列满脸, insert()也会阻塞
ConcurrentHashMap使用一种lock striping的机制来实现并发控制, 并且提供了弱一致性的Iterator, 允许在迭代期间被修改而不抛出ConcurrentModificationException(例如,忽略被删除的元素). 然而作为代价, size()返回的值不是精确的元素的数量, 不过size()在并发环境的作用比get, put, containsKey的用途小得多
看一小段程序
ConcurrentHashMap<String, String> m = new ConcurrentHashMap<String, String>();
m.put("1", "one");
m.put("2", "two");
m.put("3", "three");
Iterator keyIt = m.keySet().iterator();
// !! remove some element, remove-if-equals 风格的操作
m.remove("1", "one");
while (keyIt.hasNext()) {
String k = (String)keyIt.next();
System.out.println("key: " + k + ", value: " + m.get(k));
}
输出结果:
key: 1, value: null
key: 3, value: three
key: 2, value: two
很短的代码, 一下执行完了, 没有再报错, 可以发现remove操作没有影响到遍历抛出异常. 然而, key: 1, value: null 这行输出说明了我们还留着对这个key的引用.
ConcurrentHashMap有几种新的风格的方法, put-if-absent, remove-if-equal, replace-if-equal, 比如上面的 m.remove("1", "one"); 就是, 如果改为 m.remove("1", "two"); 那么["1","one"]这一对就被保留下来了
CopyOnWriteArrayList, 顾名思义, 就能猜到每次都这个类做了改动, 就会copy一份原来的数组, 在改动后替换原来的数组, 再返回给调用者
BlockingQueue是基于 生产者-消费者 的模式构建的, 包括LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue,SynchronousQueue 4个实现类. 看一点示例代码:
public static void putAndOffer() throws InterruptedException {
BlockingQueue<String> q = new ArrayBlockingQueue<String>(1);
q.offer("1");
System.out.println("offer A done");
q.offer("2");
System.out.println("offer B done");
q.clear();
q.put("A");
System.out.println("put A done");
q.put("B");
System.out.println("put B done");
}
输出结果:
offer A done
offer B done
put A done
我们指定了BlockingQueue的容量为1, offer方法是不阻塞的, 所以q.offer("2");直接返回false, 而put方法是阻塞的, 所以System.out.println("put B done");一直没有执行.
PriorityBlockingQueue: 对于加入队列的元素是可排序的
SynchronousQueue: 不能算是queue的实现, 但是模仿了queue的行为, 更像是worker模式的实现. 因为它内部维护了一组用于处理元素的线程, 并且直接把加入队列的元素分配给处理该元素的线程. 如果没有可用线程, 就阻塞了.
BlockingQueue实例代码
下面是摘录的代码, 使用BlockingQueue, 分配N个线程执行文件索引的任务, 很经典:
public class FileCrawler implements Runnable { //这个是producer角色 private final BlockingQueue<File> fileQueue; private final FileFilter fileFilter; private final File root; ... public FileCrawler(File root, BlockingQueue queue, FileFilter filter){ ... } public void run() { try { crawl(root); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /*把该目录下所有的文件夹都放进一个queue, 如果满了, 就阻塞*/ private void crawl(File root) throws InterruptedException { File[] entries = root.listFiles(fileFilter); if (entries != null) { for (File entry : entries) if (entry.isDirectory()) crawl(entry); else if (!alreadyIndexed(entry)) fileQueue.put(entry); //把找到的目录放入队列 } } } public class Indexer implements Runnable { //这个是consumer角色 private final BlockingQueue<File> queue; public Indexer(BlockingQueue<File> queue) { // queue在indexer和FileCrawler之间共享 this.queue = queue; } public void run() { try { while (true) indexFile(queue.take()); //从queue中获取一个File对象进行索引 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
//这个是客户端的方法, 可以假设roots是在界面设置的几个目录 public static void startIndexing(File[] roots) {
BlockingQueue<File> queue = new LinkedBlockingQueue<File>(BOUND); FileFilter filter = new FileFilter() { public boolean accept(File file) { return true; } }; for (File root : roots) new Thread(new FileCrawler(queue, filter, root)).start(); //此处是否可以考虑使用线程池更有效? 而且, 如果有些目录层次非常深, //就会有某些线程运行时间非常长, 相反有些线程非常快就执行完毕. //最恶劣的情况是可能其他线程都完成了, 而退化到只有一个线程中运行, //成为"单线程"程序? for (int i = 0; i < N_CONSUMERS; i++) new Thread(new Indexer(queue)).start(); //此处是否可以考虑使用线程池更有效? } 基于上述这种"单线程"的考虑, 不谋而合的, JDK6引入了一种"Work Stealing"的模式, 即N个线程中运行, 每个线程处理自己的事情, 一旦自己手头的事情处理, 那么就去尝试"偷"来其他线程的任务来运行. 这样一来, 系统就会时刻保持多个线程中处理任务, 而不是出现"一人忙活,大家凉快"的情况
转自:http://hi.baidu.com/iwishyou2/blog/item/552e162adab77f305243c116.html
发表评论
-
通过 Terracotta实现基于Tomcat的Web应用集群教程
2011-09-28 17:08 4167http://forums.terracotta.org/fo ... -
Future接口的应用
2011-04-07 14:16 1208import java.util.concurren ... -
ScheduledThreadPool
2011-04-07 14:16 2484使用延迟线程池可以指定任务在特定的时延之后执行。下面是一个例子 ... -
多线程摘录 009
2011-04-07 14:11 1291Atomic Variable & nonblocki ... -
多线程摘录 008
2011-04-07 14:10 1656* !!! synchronized使得在等待锁定的线程无法被 ... -
多线程摘录 007
2011-04-07 14:09 669* 测试多线程程序的安全性和生存型 - 不要做出& ... -
多线程摘录 006
2011-04-07 14:08 1237生存性的问题* 死锁最常见的情况, 就是一组线程处理任务的时候 ... -
多线程摘录 005
2011-04-07 14:06 1400使用线程池* 什么样的任务需要特殊的执行策略呢?1) 有依赖关 ... -
多线程摘录 004
2011-04-07 14:06 1520* 使用哪种模式的并发?观察一下简单的服务器方式一: ... -
用ReentrantLock模拟宴会的热闹情景
2011-04-07 14:04 2000一个简单的ReentrantLock的例子, 情景是几个朋友吃 ... -
多线程摘录 003
2011-04-07 14:03 1231同步器A synchronizer is any objec ... -
多线程摘录 001
2011-04-07 14:00 1338需要考虑什么?* 正确性. 程序必须能得到正确的结果* 生存性 ... -
java 5.0 多线程编程实践
2011-04-07 13:49 1149Java5增加了新的类库并发集java.util.concu ... -
Java ExecutorService线程
2011-04-07 13:48 5845ExecutorService 建立多线程的步骤: 1。 ...
相关推荐
在Delphi编程环境中,多线程技术是一种强大的工具,它允许程序同时执行多个任务,从而提高应用程序的响应性和效率。本教程将引导你深入理解Delphi中的多线程编程,并通过实例来帮助你掌握相关技能。 一、多线程概念...
《C语言多进程多线程编程》是一本深入探讨C语言在并发编程领域的专业书籍。在计算机科学中,进程和线程是操作系统中并行执行任务的基本单位,理解和掌握它们对于提升程序性能和优化资源利用至关重要。这本书籍针对...
从内容摘录中无法直接得知程序是如何处理线程同步问题的,但可以确定在多线程环境中运行排序算法时,这一点尤为重要。 图形化展示排序过程的核心是ShowData方法。这个方法使用了Windows Forms中的Graphics类来绘制...
摘要:VC/C++源码,系统相关,多线程 与VC++爱好者们分享一个很不错的多线程MultiThread应用例子,你可将之中的模块摘录出来供需要者使用。本多线程模块实例以新建文件写入数据为操作目的,创建多个线程分别创建文件,...
同时,他可能会讨论Java的特性,如多线程、反射、泛型等,以及这些特性和设计模式的结合使用。 通过对《Java与模式 阎宏 摘录》的学习,开发者不仅可以提升自己的编程技巧,还能深入了解如何在实际工作中选择和应用...
A:晕,这最终还是调用了老汉多线程……那和线程也没什么区别吧……你应该再试一试线程池…… B:不完全是,因为纤程要先ConvertThreadToFiber,才能CreateFiber,VB中就一个线程,你把它Convert成纤程,那纤程...
这部分可能涉及到在多线程环境中保护GUI资源的同步机制。在Windows环境下,可能使用标准的互斥对象来实现这一功能,而在其他环境(如单片机)中,可能需要自定义的解决方案。 总的来说,ucgui是一个提供图形用户...
包括了c++经典笔试题,多线程编程,操作系统,数据库,网络相关知识。以及一些经典面经
ACE Reactor框架是其核心部分之一,它是一个事件多路分离器,能够在一个进程或线程中处理多个客户端连接的事件。使用Reactor框架开发用户程序相对简单,主要包括三个步骤:首先,从`ACE_Event_Handler`派生出子类并...
根据提供的文件信息,我们可以推断出这部分内容主要讨论了多线程编程中的关键概念与技术。下面将基于这些有限的信息,展开对多线程编程中的一些核心知识点进行详细阐述。 ### 多线程编程概述 多线程编程是现代软件...
《精通VB.pdf》:这是一本高级教程,旨在帮助读者从熟悉VB到精通,内容可能涉及更复杂的编程概念和技术,如面向对象编程、高级控件应用、多线程处理、网络编程等,有助于提升编程技能。 《VB-函数-速查手册.pdf》:...
2. **控制共享资源的访问**:在多线程环境下,可以确保共享资源被正确地管理,避免资源竞争问题。 3. **简化配置过程**:单例模式下的对象通常作为配置或参数传递给其他对象使用,简化了配置过程。 #### 实现方式 ...
可以通过锁(lock)机制、读写锁、线程静态字段(ThreadStaticAttribute)等方式确保多线程环境下的安全性。 #### 4.3 什么是装箱与拆箱? 装箱是指将值类型转换为引用类型的过程;拆箱则是相反的过程。这在使用泛型...
相反,多线程技术可以在较少核心的情况下提供更好的性能提升,因为它可以在同一核心上同时执行多个线程,增加处理效率,并且在开发难度、功耗和面积上,多线程技术更有优势。作者提出未来CPU发展不应只是在多核上...
本资源是关于Python语言程序设计的教程,总共分为10章,涵盖了Python编程语言的基础知识、数据类型、控制结构、函数、模块、文件处理、异常处理、面向对象编程、多线程编程等方面的内容。 第一章至第四章主要介绍了...
线程.rar可能包含的是关于CvtCNKI的多线程下载技术文档,这通常意味着软件可能利用多线程技术加速从CNKI等网站下载文献,提高效率,尤其对于大量文献的下载来说,这是一个非常实用的功能。 CvtCNKI-v2.0.1是软件的...
并发编程是一个广泛的话题,涉及到线程的创建、管理、数据共享、同步、内存模型、设计无锁数据结构、设计基于锁的数据结构、并发代码设计、高级线程管理和多线程应用程序的测试与调试等多个方面。 在描述中,书籍的...
在IT行业中,电子阅读器是一种专门用于阅读电子书的软件或设备,它们通常具有文本...这个修复过程涉及到了C#中对剪贴板的操作、可能的多线程同步问题以及异常处理策略,这些都是开发高质量软件时必须考虑的关键因素。