`
jiahh
  • 浏览: 38590 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Java多线程编程的常见陷阱

阅读更多
1、在构造函数中启动线程
我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:
<!--[if !supportLists]-->1.  <!--[endif]-->public class A{ 
<!--[if !supportLists]-->2.  <!--[endif]-->   public A(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->      this.x=1; 
<!--[if !supportLists]-->4.  <!--[endif]-->      this.y=2; 
<!--[if !supportLists]-->5.  <!--[endif]-->      this.thread=new MyThread(); 
<!--[if !supportLists]-->6.  <!--[endif]-->      this.thread.start(); 
<!--[if !supportLists]-->7.  <!--[endif]-->   } 
<!--[if !supportLists]-->8.  <!--[endif]-->    
<!--[if !supportLists]-->9.  <!--[endif]-->}   
这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thread线程也将在B被完全初始化之前启动,当thread运行时使用到了类A中的某些变量,那么就可能使用的不是你预期中的值,因为在B的构造函数中你可能赋给这些变量新的值。也就是说此时将有两个线程在使用这些变量,而这些变量却没有同步。
解决这个问题有两个办法:将A设置为final,不可继承;或者提供单独的start方法用来启动线程,而不是放在构造函数中。
2、不完全的同步
都知道对一个变量同步的有效方式是用synchronized保护起来,synchronized可能是对象锁,也可能是类锁,看你是类方法还是实例方法。但是,当你将某个变量在A方法中同步,那么在变量出现的其他地方,你也需要同步,除非你允许弱可见性甚至产生错误值。类似这样的代码:
<!--[if !supportLists]-->1.  <!--[endif]-->class A{ 
<!--[if !supportLists]-->2.  <!--[endif]-->  int x; 
<!--[if !supportLists]-->3.  <!--[endif]-->  public int getX(){ 
<!--[if !supportLists]-->4.  <!--[endif]-->     return x; 
<!--[if !supportLists]-->5.  <!--[endif]-->  } 
<!--[if !supportLists]-->6.  <!--[endif]-->  public synchronized void setX(int x) 
<!--[if !supportLists]-->7.  <!--[endif]-->  { 
<!--[if !supportLists]-->8.  <!--[endif]-->     this.x=x; 
<!--[if !supportLists]-->9.  <!--[endif]-->  } 
<!--[if !supportLists]-->10. <!--[endif]-->}    
x的setter方法有同步,然而getter方法却没有,那么就无法保证其他线程通过getX得到的x是最新的值。事实上,这里的setX的同步是没有必要的,因为对int的写入是原子的,这一点JVM规范已经保证,多个同步没有任何意义;当然,如果这里不是int,而是double或者long,那么getX和setX都将需要同步,因为double和long都是64位,写入和读取都是分成两个32位来进行(这一点取决于jvm的实现,有的jvm实现可能保证对long和double的read、write是原子的),没有保证原子性。类似上面这样的代码,其实都可以通过声明变量为volatile来解决。

3、在使用某个对象当锁时,改变了对象的引用,导致同步失效。
这也是很常见的错误,类似下面的代码:
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(array[0]) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   ...... 
<!--[if !supportLists]-->4.  <!--[endif]-->   array[0]=new A(); 
<!--[if !supportLists]-->5.  <!--[endif]-->   ...... 
<!--[if !supportLists]-->6.  <!--[endif]-->}   
同步块使用array[0]作为锁,然而在同步块中却改变了array[0]指向的引用。分析下这个场景,第一个线程获取了array[0]的锁,第二个线程因为无法获取array[0]而等待,在改变了array[0]的引用后,第三个线程获取了新的array[0]的锁,第一和第三两个线程持有的锁是不一样的,同步互斥的目的就完全没有达到了。这样代码的修改,通常是将锁声明为final变量或者引入业务无关的锁对象,保证在同步块内不会被修改引用。
4、没有在循环中调用wait()。
wait和notify用于实现条件变量,你可能知道需要在同步块中调用wait和notify,为了保证条件的改变能做到原子性和可见性。常常看见很多代码做到了同步,却没有在循环中调用wait,而是使用if甚至没有条件判断:
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(lock) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   if(isEmpty() 
<!--[if !supportLists]-->4.  <!--[endif]-->     lock.wait(); 
<!--[if !supportLists]-->5.  <!--[endif]-->    
<!--[if !supportLists]-->6.  <!--[endif]-->} 
<!--[if !supportLists]-->7.  <!--[endif]-->    
对条件的判断是使用if,这会造成什么问题呢?在判断条件之前可能调用notify或者notifyAll,那么条件已经满足,不会等待,这没什么问题。在条件没有满足,调用了wait()方法,释放lock锁并进入等待休眠状态。如果线程是在正常情况下,也就是条件被改变之后被唤醒,那么没有任何问题,条件满足继续执行下面的逻辑操作。问题在于线程可能被意外甚至恶意唤醒,由于没有再次进行条件判断,在条件没有被满足的情况下,线程执行了后续的操作。意外唤醒的情况,可能是调用了notifyAll,可能是有人恶意唤醒,也可能是很少情况下的自动苏醒(称为“伪唤醒”)。因此为了防止这种条件没有满足就执行后续操作的情况,需要在被唤醒后再次判断条件,如果条件不满足,继续进入等待状态,条件满足,才进行后续操作。
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(lock) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   while(isEmpty() 
<!--[if !supportLists]-->4.  <!--[endif]-->     lock.wait(); 
<!--[if !supportLists]-->5.  <!--[endif]-->    
<!--[if !supportLists]-->6.  <!--[endif]-->}    
没有进行条件判断就调用wait的情况更严重,因为在等待之前可能notify已经被调用,那么在调用了wait之后进入等待休眠状态后就无法保证线程苏醒过来。
5、同步的范围过小或者过大。
同步的范围过小,可能完全没有达到同步的目的;同步的范围过大,可能会影响性能。同步范围过小的一个常见例子是误认为两个同步的方法一起调用也是将同步的,需要记住的是Atomic+Atomic!=Atomic。
<!--[if !supportLists]-->1.  <!--[endif]-->Map map=Collections.synchronizedMap(new HashMap()); 
<!--[if !supportLists]-->2.  <!--[endif]-->if(!map.containsKey("a")){ 
<!--[if !supportLists]-->3.  <!--[endif]-->         map.put("a", value); 
<!--[if !supportLists]-->4.  <!--[endif]-->}   
这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。
<!--[if !supportLists]-->1.  <!--[endif]-->Map map = Collections.synchronizedMap(new HashMap()); 
<!--[if !supportLists]-->2.  <!--[endif]-->synchronized (map) { 
<!--[if !supportLists]-->3.  <!--[endif]-->     if (!map.containsKey("a")) { 
<!--[if !supportLists]-->4.  <!--[endif]-->         map.put("a", value); 
<!--[if !supportLists]-->5.  <!--[endif]-->     } 
<!--[if !supportLists]-->6.  <!--[endif]--> }   
注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 Collections.synchronizedMap(new HashMap())使用的锁是map本身,因此没有问题。当然,上面的情况现在更推荐使用ConcurrentHashMap,它有putIfAbsent方法来达到同样的目的并且满足线程安全性。
同步范围过大的例子也很多,比如在同步块中new大对象,或者调用费时的IO操作(操作数据库,webservice等)。不得不调用费时操作的时候,一定要指定超时时间,例如通过URLConnection去invoke某个URL时就要设置connect timeout和read timeout,防止锁被独占不释放。同步范围过大的情况下,要在保证线程安全的前提下,将不必要同步的操作从同步块中移出。
6、正确使用volatile
在jdk5修正了volatile的语义后,volatile作为一种轻量级的同步策略就得到了大量的使用。volatile的严格定义参考jvm spec,这里只从volatile能做什么,和不能用来做什么出发做个探讨。
volatile可以用来做什么?
1)状态标志,模拟控制机制。常见用途如控制线程是否停止:
<!--[if !supportLists]-->1.  <!--[endif]-->private volatile boolean stopped; 
<!--[if !supportLists]-->2.  <!--[endif]-->public void close(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->   stopped=true; 
<!--[if !supportLists]-->4.  <!--[endif]-->} 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->public void run(){ 
<!--[if !supportLists]-->7.  <!--[endif]-->
<!--[if !supportLists]-->8.  <!--[endif]-->   while(!stopped){ 
<!--[if !supportLists]-->9.  <!--[endif]-->      //do something 
<!--[if !supportLists]-->10. <!--[endif]-->   } 
<!--[if !supportLists]-->11. <!--[endif]-->    
<!--[if !supportLists]-->12. <!--[endif]-->}
前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的最新值。
2)安全发布,如修复DLC问题。
<!--[if !supportLists]-->1.  <!--[endif]-->private volatile IoBufferAllocator instance; 
<!--[if !supportLists]-->2.  <!--[endif]-->public IoBufferAllocator getInsntace(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->    if(instance==null){ 
<!--[if !supportLists]-->4.  <!--[endif]-->        synchronized (IoBufferAllocator.class) { 
<!--[if !supportLists]-->5.  <!--[endif]-->            if(instance==null) 
<!--[if !supportLists]-->6.  <!--[endif]-->                instance=new IoBufferAllocator(); 
<!--[if !supportLists]-->7.  <!--[endif]-->        } 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->    return instance; 
<!--[if !supportLists]-->10. <!--[endif]-->}
3)开销较低的读写锁
<!--[if !supportLists]-->1.  <!--[endif]-->public class CheesyCounter { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int value; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getValue() { return value; } 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->    public synchronized int increment() { 
<!--[if !supportLists]-->7.  <!--[endif]-->        return value++; 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->} 
synchronized保证更新的原子性,volatile保证线程间的可见性。
volatile不能用于做什么?
1)不能用于做计数器
<!--[if !supportLists]-->1.  <!--[endif]-->public class CheesyCounter { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int value; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getValue() { return value; } 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->    public int increment() { 
<!--[if !supportLists]-->7.  <!--[endif]-->        return value++; 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->}
因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的最新值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。
2)与其他变量构成不变式
一个典型的例子是定义一个数据范围,需要保证约束lower< upper。
<!--[if !supportLists]-->1.  <!--[endif]-->public class NumberRange { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int lower, upper; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getLower() { return lower; } 
<!--[if !supportLists]-->5.  <!--[endif]-->    public int getUpper() { return upper; } 
<!--[if !supportLists]-->6.  <!--[endif]-->
<!--[if !supportLists]-->7.  <!--[endif]-->    public void setLower(int value) {  
<!--[if !supportLists]-->8.  <!--[endif]-->        if (value > upper)  
<!--[if !supportLists]-->9.  <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->10. <!--[endif]-->        lower = value; 
<!--[if !supportLists]-->11. <!--[endif]-->    } 
<!--[if !supportLists]-->12. <!--[endif]-->
<!--[if !supportLists]-->13. <!--[endif]-->    public void setUpper(int value) {  
<!--[if !supportLists]-->14. <!--[endif]-->        if (value < lower)  
<!--[if !supportLists]-->15. <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->16. <!--[endif]-->        upper = value; 
<!--[if !supportLists]-->17. <!--[endif]-->    } 
<!--[if !supportLists]-->18. <!--[endif]-->} 
尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用setLower(4)和setUpper(3),两个线程交叉进行,最后结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将setLower和setUpper同步:
<!--[if !supportLists]-->1.  <!--[endif]-->public class NumberRange { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int lower, upper; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getLower() { return lower; } 
<!--[if !supportLists]-->5.  <!--[endif]-->    public int getUpper() { return upper; } 
<!--[if !supportLists]-->6.  <!--[endif]-->
<!--[if !supportLists]-->7.  <!--[endif]-->    public synchronized void setLower(int value) {  
<!--[if !supportLists]-->8.  <!--[endif]-->        if (value > upper)  
<!--[if !supportLists]-->9.  <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->10. <!--[endif]-->        lower = value; 
<!--[if !supportLists]-->11. <!--[endif]-->    } 
<!--[if !supportLists]-->12. <!--[endif]-->
<!--[if !supportLists]-->13. <!--[endif]-->    public synchronized void setUpper(int value) {  
<!--[if !supportLists]-->14. <!--[endif]-->        if (value < lower)  
<!--[if !supportLists]-->15. <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->16. <!--[endif]-->        upper = value; 
<!--[if !supportLists]-->17. <!--[endif]-->    } 
<!--[if !supportLists]-->18. <!--[endif]-->}


总结
1、实现Runnable接口比继承Thread类所具有的优势:1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。


2、多线程、多进程的优缺点:多线程的优点:
<!--[if !supportLists]-->·         <!--[endif]-->无需跨进程边界;
<!--[if !supportLists]-->·         <!--[endif]-->程序逻辑和控制方式简单;
<!--[if !supportLists]-->·         <!--[endif]-->所有线程可以直接共享内存和变量等;
<!--[if !supportLists]-->·         <!--[endif]-->线程方式消耗的总资源比进程方式好;
多线程缺点:
<!--[if !supportLists]-->·         <!--[endif]-->每个线程与主程序共用地址空间,受限于2GB地址空间;
<!--[if !supportLists]-->·         <!--[endif]-->线程之间的同步和加锁控制比较麻烦;
<!--[if !supportLists]-->·         <!--[endif]-->一个线程的崩溃可能影响到整个程序的稳定性;
<!--[if !supportLists]-->·         <!--[endif]-->到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
<!--[if !supportLists]-->·         <!--[endif]-->线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
多进程优点:
<!--[if !supportLists]-->·         <!--[endif]-->每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
<!--[if !supportLists]-->·         <!--[endif]-->通过增加CPU,就可以容易扩充性能;
<!--[if !supportLists]-->·         <!--[endif]-->可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
<!--[if !supportLists]-->·         <!--[endif]-->每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多线程缺点:
<!--[if !supportLists]-->·         <!--[endif]-->逻辑控制复杂,需要和主程序交互;
<!--[if !supportLists]-->·         <!--[endif]-->需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
<!--[if !supportLists]-->·         <!--[endif]-->多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。
分享到:
评论

相关推荐

    汪文君JAVA多线程编程实战(完整不加密)

    学习《汪文君JAVA多线程编程实战》不仅能够提高读者对Java多线程编程的理解,还有助于培养良好的并发编程习惯,避免常见的并发陷阱。对于想要提升自己在并发编程领域技能的Java开发者来说,这本书无疑是一份宝贵的...

    Java多线程编程详解:核心概念与高级技术应用

    内容概要:本文详细介绍了 Java多线程编程的基础知识和高级技术。内容涵盖线程的概念与重要性、创建线程的方式、线程的生命周期与基本控制方法、线程同步与死锁、线程间通信、线程池与 Executor框架、并发集合与原子...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    JAVA多线程编程详解-详细操作例子

    本主题将深入探讨“JAVA多线程编程详解-详细操作例子”,结合提供的资源,我们可以从以下几个方面进行学习: 1. **线程的基本概念**: 线程是程序执行的最小单位,一个进程可以有多个线程。在Java中,可以通过实现...

    java多线程资料

    这两本书籍都是深入理解Java多线程编程的宝贵资料。 《Java并发实战》(Addison-Wesley Java Concurrency in Practice)是由Brian Goetz、Tim Peierls、Joshua Bloch、David Holmes和Doug Lea合著的一本经典书籍。...

    Java多线程知识,龙果学院

    "Java多线程知识,龙果学院"这一课程显然是针对这部分内容进行深入讲解的资源,旨在帮助开发者提升在多任务环境下的编程能力。 一、Java多线程基础 1. **线程的概念**:线程是程序执行的最小单元,一个进程可以有...

    java多线程设计模式(pdf)

    《Java多线程设计模式》是一本专注于Java并发编程的权威指南,对于深入理解Java并发机制和提升多线程编程技能具有极高的价值。这本书详细介绍了如何在Java环境中有效地使用多线程,以及如何设计出高效、可维护的并发...

    Java多线程设计模式

    首先,多线程设计模式是指在多线程编程中,通过复用已经验证过的、经过优化的设计方案来提高代码的可读性、可维护性和性能。这些模式通常来自于实际项目经验的总结,能够帮助开发者避免常见的并发陷阱,提高软件质量...

    Java 并发编程实战.pdf

    根据提供的信息,“Java 并发编程实战.pdf”这本书聚焦于Java并发编程的实践与应用,旨在帮助读者深入了解并掌握Java中的多线程技术及其在实际项目中的应用技巧。虽然部分内容未能提供具体章节或实例,但从标题及...

    Java 编程 :常见问题汇总

    ### Java 编程:常见问题汇总 #### 一、字符串连接误用 在Java编程中,经常需要处理字符串连接的问题。...通过遵循这些最佳实践,开发者可以在编写Java程序时避免许多常见的陷阱,提高代码的质量和可维护性。

    java陷阱常见面试题

    本文将深入探讨Java基础陷阱、Java客户端陷阱以及Java服务器陷阱,并提供一些常见的面试题,帮助你更好地理解和应对这些问题。 一、Java基础陷阱 1. 内存管理:Java使用垃圾回收机制管理内存,但过度依赖可能导致...

    Java Thread Programming

    Java线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,提高了应用程序的效率和响应性。...通过学习和实践其中的代码示例,开发者可以提升自己在多线程编程领域的技能,更好地应对并发环境下的挑战。

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    java 并发编程的艺术pdf清晰完整版 源码

    这本书全面地介绍了Java平台上的并发和多线程编程技术,旨在帮助开发者解决在实际工作中遇到的并发问题,提高程序的性能和可伸缩性。 并发编程是现代计算机系统中不可或缺的一部分,尤其是在多核处理器成为主流的...

    2011 Intel 多线程编程大赛 Maze Of Life

    《2011 Intel 多线程编程大赛 Maze Of Life》是英特尔公司在2011年举办的一场技术竞赛,其主要目标是鼓励开发者利用多线程技术来提高程序的性能和效率。在这个项目中,参赛者使用Java语言进行开发,并且特别关注了...

    java并发编程

    Java并发编程是Java开发者必须掌握的关键技能之一,它涉及到如何在多线程环境中高效、安全地执行程序。并发编程能够充分利用多核处理器的计算能力,提高应用程序的响应速度和整体性能。《Java编程并发实战》这本书是...

    JAVA 的多线程浅析.pdf

    多线程编程中常见的问题包括但不限于死锁、资源竞争和线程同步问题。程序员必须对线程之间的通信和资源共享有深刻的理解,以避免这些潜在的陷阱。Java通过提供synchronized关键字、wait()和notify()方法等工具,帮助...

    java并发编程实战(英文版)

    为了帮助开发者避免常见的并发陷阱,《Java并发编程实战》还提供了一系列的最佳实践建议,比如: - **正确使用同步机制**:强调了在使用`synchronized`关键字或`ReentrantLock`时需要注意的问题。 - **异常处理**:...

    电子书《java线程》

    本电子书详细介绍了如何在Java环境中有效地使用多线程,优化程序性能,并避免并发编程中的常见陷阱。 在Java中,线程是程序执行的独立路径,它们可以同时运行,使程序能够处理多个任务。Java提供了一个丰富的线程...

Global site tag (gtag.js) - Google Analytics