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

多线程下并发同步机制

    博客分类:
  • J2SE
阅读更多
1.  前言

JDK提供的并发包,除了上一篇提到的用于集合外,还有线程的调度、协作、调度等等功能。上篇提到过,线程之间除了竞争关系,还有协作关系。在高并发环境下有效利用Java并发包解决线程之间协作的特殊场景。在并行计算,尤其是多线程计算的结果集合并的时候都需要用到这些并发同步器。还有一种使用场景,就是跨越多台机器(实机)的多线程进行并行运算,需要将多台机器进行结果集的汇总,合并。其原理核心也是使用这些并发协作包。

2.  FutureTask

FutureTask是进行并行结果集合并的类,此类是Future接口的实现。在主线程中启动多个线程进行并发计算,之后再根据各个线程的执行结果进行汇总,归并,得出一个总的结果,这个多线程可以是在一台机器上,充分利用多核CPU硬件,在科研单位可能分布式集群环境一起并发计算一个大任务,每个机器相当于一个线程,执行完毕后将反馈的结果返回来进行合并后才是最终的结果。而主线程可以等待分线程的结果,也可以不等待,全凭具体业务需要而定,不过一般情况下还是要等一等分线程的结果才能往下执行的。如果不等分线程,也可以在主线程中不再理会分线程即可。

举个实例,比如这时候东方不败要想练成《葵花宝典》,那么需要前提条件是2个,第一手中得有《葵花宝典》秘籍,第二就是挥刀自宫。恩,挥刀自宫这个主线程——东方不败可以自己完成,夺取《葵花宝典》可以派别人——兄弟童柏雄去干,2条线并行实施,等另一个人取得《葵花宝典》了,这边主线程也挥刀自宫了,行了,能练了!

咱先看代码行吧

    package threadConcurrent.test;  
      
    import java.util.concurrent.Callable;  
    import java.util.concurrent.ExecutionException;  
    import java.util.concurrent.FutureTask;  
      
    /** 
     * 分线程汇总 
     * @author liuyan 
     */  
    public class FutureTaskDemo {  
      
        @SuppressWarnings("unchecked")  
        public static void main(String[] args) {  
      
            // 初始化一个Callable对象和FutureTask对象  
            Callable otherPerson = new OtherPerson();  
      
            // 由此任务去执行  
            FutureTask futureTask = new FutureTask(otherPerson);  
      
            // 使用futureTask创建一个线程  
            Thread newhread = new Thread(futureTask);  
              
            System.out.println("newhread线程现在开始启动,启动时间为:" + System.nanoTime()  
                    + " 纳秒");  
              
            newhread.start();  
              
            System.out.println("主线程——东方不败,开始执行其他任务");  
              
            System.out.println("东方不败开始准备小刀,消毒...");  
      
            //兄弟线程在后台的计算线程是否完成,如果未完成则等待  
            //阻塞  
            while (!futureTask.isDone()) {  
                  
                try {  
                    Thread.sleep(500);  
                    System.out.println("东方不败:“等兄弟回来了,我就和小弟弟告别……颤抖……”");  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
              
            System.out.println("newhread线程执行完毕,此时时间为" + System.nanoTime());  
            String result = null;  
            try {  
                result = (String) futureTask.get();  
      
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } catch (ExecutionException e) {  
                e.printStackTrace();  
            }  
              
            if("OtherPerson:::经过一番厮杀取得《葵花宝典》".equals(result)){  
                System.out.println("兄弟,干得好,我挥刀自宫了啊!");  
            }else{  
                System.out.println("还好我没自宫!否则白白牺牲了……");  
            }  
              
        }  
    }  
      
    @SuppressWarnings("all")  
    class OtherPerson implements Callable {  
      
        @Override  
        public Object call() throws Exception {  
      
            // 先休息休息再拼命去!  
            Thread.sleep(5000);  
            String result = "OtherPerson:::经过一番厮杀取得《葵花宝典》";  
            System.out.println(result);  
            return result;  
        }  
      
    }


在这个例子中主线程代表东方不败,分线程代表兄弟——童柏雄,主线程派出FutureTask,把它放置于一个线程对象中,之后线程开始启动,分支线程开始工作。主线程也没闲着,继续做自己的事情,消毒,做着心理斗争等等,通过一个阻塞的死循环,等待分线程的状态,调用分线程的futureTask.isDone()方法进行判断,看看兄弟是否执行结束了,结束了通过futureTask.get()将分线程的执行结果取出来,结果出来了,主线程根据分线程的执行结果再做决定。执行后,结果大家都明了。有一点需要说明的是,有可能分线程与主线程的执行不在一台物理机器上,分线程可以使用jms、webservic、rmi甚至socket技术请求远程的类为其服务。分线程根据远程返回的结果再返回给本机器的主线程,之后再做决策。分布式计算的核心原理也是如此,当然分布式计算比这个复杂得多,笔者只是说其核心的实现原理。

3.  Semaphore

Semaphore是限制多线程共享资源的一个东东,多线程并发访问一个资源的时候,可以限制线程最大使用个数,其他多出来的线程,没办法,耐心等着吧。这个例子在生活中比比皆是,在火车站售票处一共开设了5个窗口,也就表示在同一时间内,火车站的工作人员最多只能为5个人服务,那么高峰时其他人呢,理想的情况下是排队等着,不理想的情况下是,等待的队列没有秩序,有的只是拳头和权势,没有办法,人家的爸爸是李刚,撞人都没事何况是排队买票了,人家说的就是王法。当然了,这个咱们看具体程序。
    package threadConcurrent.test;  
      
    import java.util.Random;  
    import java.util.concurrent.Semaphore;  
      
    /** 
     * 使用Semaphore,限制可以执行的线程数,空闲资源放到队列中等待 
     *  
     * @author liuyan 
     */  
    public class SemaphoreDemo {  
      
        public static void main(String[] args) {  
            Runnable limitedCall = new Runnable() {  
      
                // 随机生成数  
                final Random rand = new Random();  
      
                // 限制只有3个资源是活动的,第二个参数为true则是按照标准“队列”结构先进先出  
                final Semaphore available = new Semaphore(5, true);  
                int count = 0;  
      
                public void run() {  
                    int time = rand.nextInt(10);  
                    int num = count++;  
      
                    try {  
      
                        // 请求资源  
                        available.acquire();  
      
                        int needTime = time * 2000;  
      
                        System.out.println("乘客" + num + "买票需要[ " + needTime  
                                + " 秒]... #");  
      
                        Thread.sleep(needTime);  
      
                        System.out.println("乘客" + num + "买完了 # !");  
      
                        // 运行完了就释放  
                        available.release();  
                    } catch (InterruptedException intEx) {  
                        intEx.printStackTrace();  
                    }  
                }  
            };  
      
            for (int i = 0; i < 25; i++)  
                new Thread(limitedCall).start();  
        }  
    }



注释已经写得比较明确了,构建Semaphore的时候,第一个参数代表线程的执行的最大数目,第二个参数是按照队列的形式将未执行的线程放到队列中,当有线程执行完了后,按照先进先出的原则,进行线程的唤醒,执行。即便是main启动了25个线程,那么其余的线程要向执行也要等前面的线程执行完毕后才能有资格执行。要想让线程按规矩执行,首先应该先向资源池申请资源,available.acquire();就是请求资源池给个资源,如果资源池当前有空闲资源,那么线程就可以正常运行了,如果没有,没办法,排队吧啊。线程运行完毕了,要记得归还资源available.release();如果构造Semaphore的时候没指定第二个参数,或者第二个参数为false,估计您有幸能见到我之前说的李刚的儿子的现象!在此不再赘述。

4.  ScheduledFuture

提到Quartz,大家都知道他是一个负责任务调度的开源工具,使用它可以轻易地在某一时段,某一频率执行相关业务功能。如果仅仅是简单的根据某些时间频率执行某些任务,其实到不必屠龙刀杀小鸡,使用ScheduledFuture可以轻松解决此类频率的问题,启动另一个线程来,在某一个时间频率执行代码。这个还是举个例子吧,战争年代巡视城防,赵云带一个小兵去巡视城防,赵云是将军,每5秒钟巡视一次士兵,看看士兵有没有偷懒,士兵比较累,每1秒巡视一次城防,不能睡觉。如下程序

    package threadConcurrent.test;  
      
    import static java.util.concurrent.TimeUnit.SECONDS;  
      
    import java.util.Date;  
    import java.util.concurrent.Executors;  
    import java.util.concurrent.ScheduledExecutorService;  
    import java.util.concurrent.ScheduledFuture;  
      
    /** 
     * 时间频率调度 
     * @author liuyan 
     */  
    public class ScheduledFutureDemo {  
      
        @SuppressWarnings("unchecked")  
        public static void main(String[] args) {  
      
            // 线程池开辟2个线程  
            final ScheduledExecutorService scheduler = Executors  
                    .newScheduledThreadPool(2);  
      
            // 将军  
            final Runnable general = new Runnable() {  
                int count = 0;  
      
                public void run() {  
                    System.out.println(Thread.currentThread().getName() + ":"  
                            + new Date() + "赵云巡视来了 " + (++count));  
                }  
            };  
              
            // 士兵  
            final Runnable soldier = new Runnable() {  
                int count = 0;  
      
                public void run() {  
                    System.out.println(Thread.currentThread().getName() + ":"  
                            + new Date() + "士兵巡视来了 " + (++count));  
                }  
            };  
      
            // 1秒钟后运行,并每隔2秒运行一次  
            final ScheduledFuture beeperHandle1 = scheduler.scheduleAtFixedRate(  
                    soldier, 1, 1, SECONDS);  
      
            // 5秒钟后运行,并每隔2秒运行一次  
            final ScheduledFuture beeperHandle2 = scheduler.scheduleWithFixedDelay(  
                    general, 5, 5, SECONDS);  
      
            // 30秒后结束关闭任务,并且关闭Scheduler  
            scheduler.schedule(new Runnable() {  
                public void run() {  
                    beeperHandle1.cancel(true);  
                    beeperHandle2.cancel(true);  
                    scheduler.shutdown();  
                }  
            }, 60, SECONDS);  
        }  
      
    }  

    
5.  CountDownLatch

很多资料上都说CountDownLatch是倒数计数器,我觉得这种说法太过专业,其实它就是一个数数的人员。利用它,可以在多线程执行任务完毕后完成进行多线程的等待,便于等待所有的线程之后在干别的事情,这个有点类似于FutureTask,使用上不太一样。这个场景就是一个线程必须要等到其他线程执行完毕后才能往下执行,注意,这里这个线程没必要需要其他线程的执行结果,而是只有一个条件就是其他线程必须执行完毕。咱们依然做个比喻,韦小宝需要8部《四十二章经》才能去鹿鼎山找寻宝藏,怎么办,他有7个老婆,不能资源浪费啊,这个任务同时进行吧。咱们设计8个线程同时进行,等所有老婆都执行完毕了,每个老婆找齐了《四十二章经》了,好了,他可以自己去找宝藏了。等等,咱们小宝哥有7个老婆,何来8个线程,这个问题,读者不必较真,举个例子罢了,咱们给他加个老婆不就行了!

代码如下
    package threadConcurrent.test;  
      
    import java.util.concurrent.CountDownLatch;  
      
    /** 
     * 分部执行任务 
     *  
     * @author liuyan 
     *  
     */  
    public class CountDownLatchDemo implements Runnable {  
        private int id;  
      
        // 线程之间不影响,到了终点后继续做自己的事情  
        private CountDownLatch countDownLatch;  
      
        public CountDownLatchDemo(int id, CountDownLatch countDownLatch) {  
            this.id = id;  
            this.countDownLatch = countDownLatch;  
        }  
      
        @SuppressWarnings("static-access")  
        @Override  
        public void run() {  
            try {  
      
                System.out.println("第" + (id + 1) + "小老婆开始查找《四十二章经》...");  
                Thread.currentThread().sleep(id * 1000);  
                System.out.println("第" + (id + 1) + "本《四十二章经》找到");  
      
                //计数器将等待数字-1  
                countDownLatch.countDown();  
      
                System.out.println("第" + (id + 1) + "小老婆继续干其他事情");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
      
        public static void main(String[] args) {  
            CountDownLatch countDownLatch = new CountDownLatch(8);  
      
            for (int i = 0; i < 8; i++) {  
                new Thread(new CountDownLatchDemo(i, countDownLatch)).start();  
            }  
      
            try {  
      
                System.out.println("韦小宝等着等着8本四十二章……");  
      
                // 韦小宝等着等着  
                countDownLatch.await();  
      
                // 等待运动员到达终点  
                System.out.println("8本四十二章经找寻完成,可以寻宝了!");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }


主线程就当做是韦小宝吧,主线程首先开辟了线程计数器对象,之后就开辟了8个线程,派出了8个小老婆去办事,之后主线程调用countDownLatch.await()阻塞,在家里一直喝着小茶,听着小曲等着8个线程的执行完毕。咱们来看小老婆们,小老婆们就辛苦了,开始找寻经书,之后调用countDownLatch.countDown()方法通知线程计数器减去1,让等待的线程减去1。就好比说有个小老婆找到了《四十二章经》,用iphone发个彩信将经书的夹缝地图发给韦小宝,韦小宝收到了,恩,这个老婆能干,任务完成,我的等待目标减去1。等到等待线程为0的时候,小宝开始行动了,将手机里的地图通过游戏——拼图游戏,一拼凑,大事可成,自己寻宝去!这里大家也看到了CountDownLatch与FutureTask的区别。CountDownLatch侧重的是分线程的完成个数,每次完成一个分线程,等待数目减少一个,等待线程为0的时候,主线程的就不阻塞了,开始往下走。而分线程一旦调用countDownLatch.countDown()方法,就代表分线程任务搞定,主线程就不会因为你的其他事情而不能往下走,完成任务了,小老婆们也可以去旅旅游,休息休息!而FutureTask则是注重执行结果的,主线程需要它的确切结果。所以futureTask执行的call()有返回值。

6.  CyclicBarrier

CyclicBarrier相对于CountDownLatch来说,最大的不同是,分线程具体的执行过程受其他分线程的影响,必须每个分线程都执行完毕了,主线程才继续往下走,而分线程如果在所有分线程执行完毕后还有其他动作,ok,还你自由,不必阻塞了,往下走你的路吧。这个例子是网上的游戏玩家的例子,4个小孩玩游戏,游戏要求必须是4个小孩都得通过第一关,才能开启第二关的关口!否则其他完成第一关的人都得等着其他人完成。这个有点像我们的项目开发,分模块开发,到一定阶段将模块汇总,联调,测试,如果这时候有一个模块没完成,大家等着吧,大家都在那里静静地、盯着你、等着你。
    package threadConcurrent.test;  
      
    import java.util.concurrent.BrokenBarrierException;  
    import java.util.concurrent.CyclicBarrier;  
      
    public class GameBarrier {  
        public static void main(String[] args) {  
            CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() {  
      
                @Override  
                public void run() {  
                    System.out.println("所有玩家进入第二关!");  
                }  
            });  
      
            for (int i = 0; i < 4; i++) {  
                new Thread(new Player(i, cyclicBarrier)).start();  
            }  
        }  
      
    }  
      
    class Player implements Runnable {  
          
        /** 
         * 线程之间需要交互,到一定的条件下,所有线程才能往下走 
         */  
        private CyclicBarrier cyclicBarrier;  
        private int id;  
      
        public Player(int id, CyclicBarrier cyclicBarrier) {  
            this.cyclicBarrier = cyclicBarrier;  
            this.id = id;  
        }  
      
        @Override  
        public void run() {  
            try {  
                System.out.println("玩家" + id + "正在玩第一关...");  
                cyclicBarrier.await();  
                System.out.println("玩家" + id + "进入第二关...");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } catch (BrokenBarrierException e) {  
                e.printStackTrace();  
            }  
        }  
    }


使用cyclicBarrier.await();方法进行等待、阻塞,当所有分线程执行完毕了,主线程开始执行,分线程的自由也解脱了,继续往下走,开始第二关。

7.  Exchanger

Exchanger是线程资源交换器,线程A与线程B在某个运行阶段需要互换资源才能完成任务。这就好比2个公司职员——叶小钗和一页书。分别在不同的项目组——组A和组B,两个组开发者不同的项目,在正常时候叶小钗在组A上班开发者BOSS项目,一页书在项目组B开发ESB中间件产品。而在特殊时期项目组B不需要一页书了,需要叶小钗提供技术支持,就和项目组A要叶小钗,项目组A的leader也不是吃素的,你要叶小钗,没问题,把一页书也得接我们项目组剥削几天!就这样项目组B与项目组A做了这种“交易”(交换),用完了之后,恩~看程序吧
    
    package threadConcurrent.test;  
      
    import java.util.concurrent.Exchanger;  
      
    /** 
     * 资源交换 
     * @author liuyan 
     */  
    public class ExgrDemo {  
        public static void main(String args[]) {  
              
            //交换器  
            Exchanger<String> exgr = new Exchanger<String>();  
      
            new TeamB(exgr);  
            new TeamA(exgr);  
        }  
    }  
      
    /** 
     * 项目组A 
     * @author liuyan 
     */  
    class TeamA implements Runnable {  
        Exchanger<String> ex;  
      
        String str;  
      
        TeamA(Exchanger<String> c) {  
            ex = c;  
            str = new String();  
      
            new Thread(this).start();  
        }  
      
        public void run() {  
            char ch = 'A';  
            for (int i = 0; i < 3; i++) {  
                for (int j = 0; j < 5; j++)  
                    str += (char) ch++;  
      
                try {  
                    str = ex.exchange(str);  
                } catch (InterruptedException exc) {  
                    System.out.println(exc);  
                }  
            }  
        }  
    }  
      
    /** 
     * 项目组B 
     * @author liuyan 
     */  
    class TeamB implements Runnable {  
        Exchanger<String> ex;  
      
        String str;  
      
        TeamB(Exchanger<String> c) {  
            ex = c;  
            new Thread(this).start();  
        }  
      
        public void run() {  
      
            for (int i = 0; i < 3; i++) {  
                try {  
                    str = ex.exchange(new String());  
                    System.out.println("Got: " + str);  
                } catch (InterruptedException exc) {  
                    System.out.println(exc);  
                }  
            }  
        }  
    }  
  


需要说明的就是这种交换一定是成对儿的,就是交换的线程数目一定是偶数。否则奇数个线程,剩下那一个和谁交换去?这也是“等价交易”的一种体现。

8.  总结

这次主要介绍了并发环境下常常使用的并发包,用于控制多线程的并发调度、同步、交互、交换、协作等等。使用这些协作同步器,可以更灵活的处理线程之间的关系。也能更好地使用硬件资源为我们的并发系统提供高效率的运行能力。当然这次总结仅仅限于使用的层次,底层的实现源码分析有时间再做总结。

转载:http://suhuanzheng7784877.iteye.com/blog/1145286
分享到:
评论

相关推荐

    基于Java多线程的并发机制的研究和实现

    研究了程序并发过程中的同步机制和交互通信机制,比较了基于操作系统级和基于Java多线程级并发机制的实现结构,总结了并发程序中死锁预防的一些编程规则和策略。所构造的一个具有完全意义上的并发同步的框架实例有...

    Java多线程编程 线程同步机制.docx

    线程安全问题的产生是因为多个线程并发访问共享数据造成的,如果能将多个线程对共享数据的并发访问改为串行访问,即一个共享数据同一时刻只能被一个线程访问,就可以避免线程安全问题。锁正是基于这种思路实现的一种...

    使用Java多线程的同步机制编写应用程序.docx

    3.掌握多线程的同步机制。 实验内容 根据要求,编写应用程序。要求如下: 1.模拟银行账户,两个以上的用户同时进行存、取操作。 2.银行有一个账户,有两个用户分别向同一个账户存3000元,每次存1000,存三次。 3....

    多线程及并发性

    多线程处理机制,并发性实现方式,同步处理

    操作形同实验——进程同步和互斥

    操作形同实验——进程同步和互斥 (1) 通过编写程序实现进程同步和...(2) 了解Windows2000/XP中多线程的并发执行机制,线程间的同步和互斥。 (3) 学习使用Windows2000/XP中基本的同步对象,掌握相应的API函数。

    基于线程同步与妥协处理机制的多线程技术

    线程同步与妥协处理机制可以较好的解决多线程使用过程中产生的问题。实验中采用了这两种方法后数据混乱、死锁等问题的出现几率大大降低。实验结论表明上面两种方法的使用可以很好的控制死锁、数据混乱的出现,具有...

    Java分布式应用学习笔记03JVM对线程的资源同步和交互机制

    Java分布式应用学习笔记03JVM对线程的资源同步和交互机制

    Java多线程和并发知识整理

    1.1为什么需要多线程 1.2不安全示例 1.3并发问题的根源 1.4JMM 1.5线程安全的分类 1.6线程安全的方法 二、线程基础 2.1状态 2.2使用方式 2.3基础机制 2.4中断 2.5互斥同步 2.6线程合作 三、...

    92道Java多线程与并发面试题含答案(很全)

    Java并发编程的核心概念包括: 线程(Thread):线程是程序执行流的最小单元。...原子操作(Atomic Operations):原子操作是不可中断的操作,即在多线程环境中,这些操作要么完全执行,要么完全不执行。

    C#解决SQlite并发异常问题的方法(使用读写锁)

    使用C#访问sqlite时,常会遇到多线程并发导致SQLITE数据库损坏的问题。 SQLite是文件级别的数据库,其锁也是文件级别的:多个线程可以同时读,但是同时只能有一个线程写。Android提供了SqliteOpenHelper类,加入Java...

    JUC多线程学习个人笔记

    并发集合:JUC提供了一些线程安全的集合类,如ConcurrentHashMap、ConcurrentLinkedQueue等,可以在多线程环境下安全地访问和修改集合。 原子操作:JUC提供了一些原子操作类,如AtomicInteger、AtomicLong等,可以...

    Java有两种实现多线程的方式:通过Runnable接口、通过Thread直接实现,请掌握这两种实现方式,并编写示例程序。

    一、实验目的 掌握多线程程序设计 二、实验环境 1、微型计算机一台 2、WINDOWS操作系统,Java SDK,Eclipse开发环境 三、实验内容 ...3、采用线程同步方法机制来解决多线程共享冲突问题,编写示例程序。

    concurrent 多线程 教材

    24 实现 Java 多线程并发控制框架.mht 25 多线程、多平台环境中的跟踪.mht 26 使用 ConTest 进行多线程单元测试.mht 27 实现非阻塞套接字的一种简单方法.mht 28 基于事件的NIO多线程服务器.mht 29 驯服 Tiger ...

    Java多线程编程的优点和缺点

    加快响应用户的时间:多线程允许并发执行多个任务,可以充分利用多核处理器,从而提高程序的性能和响应速度。比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,...

    详解java多线程的同步控制

    同步控制是并发程序必不可少的重要手段,本文我们将通过重入锁、读写锁、信号量、倒计数器和循环栅栏以及他们的实例来介绍Java并发程序中的同步控制。 目录线程安全 Thread Safety重入锁 ReentrantLock读写锁 ...

    linux之线程同步一.doc

    2. 信号量(Semaphore):信号量是一种用于控制并发访问的同步机制,它通常用于限制同时访问共享资源的线程数量。信号量可以是一个非负整数,用于表示可用的资源数量。当一个线程请求资源时,如果可用资源数量大于零...

    多线程的简单描述

    java多线程简单的描述以及线程的安全、并发,同步机制

    Python编写的开源、多线程的网站爬虫

    需要注意的是,多线程爬虫也面临一些挑战和限制,比如多线程之间的数据共享和同步问题,以及要合理控制并发请求数量,避免过度请求导致服务器拒绝访问或其他问题。此外,对于某些网站可能存在反爬虫机制,需要采取...

    多进程(线程)并发执行

    1、模拟公司会计与出纳行为,会记收...编程实现会计与出纳行为。 2、不采取同步措施情况下,记录会计与出纳分别对账户进行一次存取操作的结果,分析问题。 3、解决2中的问题。 ...用线程及条件变量机制编程实现这一问题。

    Java多线程与线程安全实践-基于Http协议的断点续传的实现.rar

    线程安全性:由于涉及多线程并发处理,需要考虑线程安全性。可以使用Java中的同步机制,如使用synchronized关键字或者使用线程安全的集合类来保证多线程操作的安全性。 实现步骤: 创建一个下载管理器类,用于管理...

Global site tag (gtag.js) - Google Analytics