java多线程(java多线程方法)
- 时间:
- 5351人关注
这是一篇关于java多线程方法的编程热门专题内容,被865位程序员关注,内容涉及到java多线程、java多线程方法、多线程、java多线程有几种实现方法等,由宋靖琪编辑补充。
java多线程开发之通过对战游戏学习CyclicBarrier
CyclicBarrier是java.util.concurrent包下面的一个工具类,字面意思是可循环使用(Cyclic)的屏障(Barrier),通过它可以实现让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才会继续执行。
这篇文章将介绍CyclicBarrier这个同步工具类的以下几点
- 通过案例分析
- 两种不同构造函数测试
- CyclicBarrier和CountDownLatch的区别
- await方法及源码分析。
需求
继上一篇CountDownLatch模拟游戏加载后,现在用户点击开始按钮后,需要匹配包括自己在内的五个玩家才能开始游戏,匹配玩家成功后进入到选择角色阶段。当5位玩家角色都选择完毕后,开始进入游戏。进入游戏时需要加载相关的数据,待全部玩家都加载完毕后正式开始游戏。
解决方案
从需求中可以知道,想要开始游戏需要经过三个阶段,分别是
匹配玩家
选择角色
加载数据
在这三个阶段中,都需要互相等待对方完成才能继续进入下个阶段。
这时可以采用CyclicBarrier来作为各个阶段的节点,等待其他玩家到达,在进入下个阶段。
定义继承Runnable的类
这里名称就叫做StartGame,包含两个属性
private String player; private CyclicBarrier barrier;
通过构造函数初始化两个属性
public StartGame(String player, CyclicBarrier barrier) { this.player = player; this.barrier = barrier; }
run方法如下
public void run() { try { System.out.println(this.getPlayer()+" 开始匹配玩家..."); findOtherPlayer(); barrier.await(); System.out.println(this.getPlayer()+" 进行选择角色..."); choiceRole(); System.out.println(this.getPlayer()+" 角色选择完毕等待其他玩家..."); barrier.await(); System.out.println(this.getPlayer()+" 开始游戏,进行游戏加载..."); loading(); System.out.println(this.getPlayer()+" 游戏加载完毕等待其他玩家加载完成..."); barrier.await(); start(); } catch (Exception e){ e.printStackTrace(); } }
其他的方法findOtherPlayer()、choiceRole()等待使用
Thread.sleep()
来模拟花费时间
编写测试代码
CyclicBarrier有两个构造函数,如下
public CyclicBarrier(int parties) {} public CyclicBarrier(int parties, Runnable barrierAction) {}
先来看看一个参数的构造函数
CyclicBarrier(int parties)
public static void main(String[] args) throws IOException { CyclicBarrier barrier = new CyclicBarrier(5); Thread player1 = new Thread(new StartGame("1",barrier)); Thread player2 = new Thread(new StartGame("2",barrier)); Thread player3 = new Thread(new StartGame("3",barrier)); Thread player4 = new Thread(new StartGame("4",barrier)); Thread player5 = new Thread(new StartGame("5",barrier)); player1.start(); player2.start(); player3.start(); player4.start(); player5.start(); System.in.read(); }
测试结果如下
CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier barrier = new CyclicBarrier(5);
替换为
CyclicBarrier barrier = new CyclicBarrier(5, () -> { try { System.out.println("阶段完成,等待2秒..."); Thread.sleep(2000); System.out.println("进入下个阶段..."); } catch (InterruptedException e) { e.printStackTrace(); } });
再来看看效果
可以看到在到达某个节点时,会执行实例化CyclicBarrier时传入的Runnable对象。而且每一次到达都会执行一次。
CyclicBarrier和CountDownLatch的区别
CountDownLatch | CyclicBarrier |
---|---|
计数为0时,无法重置 | 计数达到0时,计数置为传入的值重新开始 |
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 | 调用await()方法计数减一,若减一后的值不等于0,则线程阻塞 |
不可重复使用 | 可重复使用 |
await方法
public int await(){} public int await(long timeout, TimeUnit unit){}
无参的await方法这里就不做介绍了,主要介绍下有参的await方法。
有参的await方法传入两个参数,一个是时间、另一个是时间单位
当调用有参的await方法时会出现下方两个异常
java.util.concurrent.TimeoutException java.util.concurrent.BrokenBarrierException
TimeoutException异常是指调用await方法后等待时间超过传入的时间,此时会将CyclicBarrier的状态变成broken,其他调用await方法将会抛出BrokenBarrierException异常,这时的CyclicBarrier将变得不可用,需要调用reset()方法重置CyclicBarrier的状态。
为什么这么说?
源码分析一波就可以看出来了
不管是有参还是无参的await方法都是调用CyclicBarrier的dowait(boolean timed, long nanos)方法,这个方法代码太长了,截取部分贴出来
private int dowait(boolean timed, long nanos){ //加锁、try catch代码 final Generation g = generation; //判断栅栏的状态 if (g.broken) throw new BrokenBarrierException(); //...省略 int index = --count; //(index == 0) 时的代码,省略 for (;;) { try { if (!timed) trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) {} //判断栅栏的状态 if (g.broken) throw new BrokenBarrierException(); if (g != generation) return index; //判断是否是定时的,且已经超时了 if (timed && nanos <= 0L) { //打破栅栏的状态 breakBarrier(); throw new TimeoutException(); } } //解锁 }
在代码的尾部进行判断当前等待是否已经超时,如果是会调用breakBarrier()方法,且抛出TimeoutException异常,下面是breakBarrier()的代码
private void breakBarrier() { generation.broken = true; count = parties; trip.signalAll(); }
代码中将broken状态置为true,表示当前栅栏移除损坏状态,且重置栅栏数量,然后唤醒其他等待的线程。此时被唤醒的线程或者其他线程进入dowait方法时,都会抛出BrokenBarrierException异常
案例源代码地址:
https://github.com/rainbowda/learnWay/tree/master/learnConcurrency/src/main/java/com/learnConcurrency/utils/cyclicBarrier
JAVA多线程实现生产者消费者的实例详解
JAVA多线程实现生产者消费者的实例详解
下面的代码实现了生产者消费者的问题
Product.Java
package consumerProducer; public class Product { private String id; public String getId() { return id; } public void setId(String id) { this.id = id; } public Product(String id) { this.id=id; } public String toString() { return "product "+id; } }
Pool.java
package consumerProducer; import java.util.*; public class Pool { private int number=0; private List<Product>products=new LinkedList<Product>(); public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } public synchronized Product consumeProduct(){ //可以去掉synchronized关键字 if(products.size()>0) { Product p= products.get(0); products.remove(0); number--; return p; } else return null; } public synchronized void addProduct(Product p){ //可以去掉synchronized关键字 products.add(p); number++; } }
Consumer.java
package consumerProducer; public class Consumer implements Runnable { private String id; Pool pool; public Consumer(String id,Pool pool) { this.id=id; this.pool=pool; } @Override public void run() { while(!Thread.currentThread().interrupted()) { Product product=null; synchronized(pool){ while(pool.getNumber()<=0)//生产不足 { try { pool.wait();//生产者等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } product=pool.consumeProduct(); } System.out.println("consuming "+id+product.toString()); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
Producer.java
package consumerProducer; public class Producer implements Runnable{ private int i_p=0; private String id; Pool pool; int i=0; public Producer(String id ,Pool pool) { this.id=id; this.pool=pool; } public Product createProduct() { return new Product(String.valueOf(++i_p)); } @Override public void run() { // TODO Auto-generated method stub while(!Thread.currentThread().interrupted()) { Product p=new Product(String.valueOf(++i_p)); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(pool) { pool.addProduct(p); System.out.println("producer "+id+" adding product...."+p.toString()); pool.notifyAll(); } } } }
Main.java
package consumerProducer; public class Main { public static void main(String[] args) { // TODO Auto-generated method stub Pool pool=new Pool(); for(int i=0;i<5;i++) { Thread consumer=new Thread(new Consumer("consumer "+i,pool)); Thread producer=new Thread(new Producer("producer "+i,pool)); consumer.start(); producer.start(); } } }
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
java多线程CountDownLatch与线程池ThreadPoolExecutor/ExecutorService案例
1、CountDownLatch:
一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
2、ThreadPoolExecutor/ExecutorService:
线程池,使用线程池可以复用线程,降低频繁创建线程造成的性能消耗,同时对线程的创建、启动、停止、销毁等操作更简便。
3、使用场景举例:
年末公司组织团建,要求每一位员工周六上午8点到公司门口集合,统一乘坐公司所租大巴前往目的地。
在这个案例中,公司作为主线程,员工作为子线程。
4、代码示例:
package com.test.thread; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author javaloveiphone * @date 创建时间:2017年1月25日 上午10:59:11 * @Description: */ public class Company { public static void main(String[] args) throws InterruptedException { //员工数量 int count = 5; //创建计数器 //构造参数传入的数量值代表的是latch.countDown()调用的次数 CountDownLatch latch = new CountDownLatch(count); //创建线程池,可以通过以下方式创建 //ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1,1,60,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(count)); ExecutorService threadPool = Executors.newFixedThreadPool(count); System.out.println("公司发送通知,每一位员工在周六早上8点到公司大门口集合"); for(int i =0;i<count ;i++){ //将子线程添加进线程池执行 Thread.sleep(10); threadPool.execute(new Employee(latch,i+1)); } try { //阻塞当前线程,直到所有员工到达公司大门口之后才执行 latch.await(); // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。 //latch.await(long timeout, TimeUnit unit) System.out.println("所有员工已经到达公司大门口,大巴车发动,前往活动目的地。"); } catch (InterruptedException e) { e.printStackTrace(); }finally{ //最后关闭线程池,但执行以前提交的任务,不接受新任务 threadPool.shutdown(); //关闭线程池,停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。 //threadPool.shutdownNow(); } } } //分布式工作线程 class Employee implements Runnable{ private CountDownLatch latch; private int employeeIndex; public Employee(CountDownLatch latch,int employeeIndex){ this.latch = latch; this.employeeIndex = employeeIndex; } @Override public void run() { try { System.out.println("员工:"+employeeIndex+",正在前往公司大门口集合..."); Thread.sleep(10); System.out.println("员工:"+employeeIndex+",已到达。"); } catch (Exception e) { e.printStackTrace(); }finally{ //当前计算工作已结束,计数器减一 latch.countDown(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //执行coutDown()之后,继续执行自己的工作,不受主线程的影响 System.out.println("员工:"+employeeIndex+",吃饭、喝水、拍照。"); } } }
打印输出结果如下:
公司发送通知,每一位员工在周六早上8点到公司大门口集合 员工:1,正在前往公司大门口集合... 员工:1,已到达。 员工:2,正在前往公司大门口集合... 员工:2,已到达。 员工:1,吃饭、喝水、拍照。 员工:3,正在前往公司大门口集合... 员工:2,吃饭、喝水、拍照。 员工:3,已到达。 员工:4,正在前往公司大门口集合... 员工:3,吃饭、喝水、拍照。 员工:4,已到达。 员工:5,正在前往公司大门口集合... 员工:4,吃饭、喝水、拍照。 员工:5,已到达。 所有员工已经到达公司大门口,大巴车发动,前往活动目的地。 员工:5,吃饭、喝水、拍照。
注意:
每一个员工到达之后,执行countDown()方法,直到所有员工到达之后,计数器为0,主线程才会继续执行。
但子线程执行了countDown()方法,之后会继续自己的工作,比如上面的【吃饭、喝水、拍照】,是不受主线程是否阻塞、其它线程是否已经执行countDown()方法的影响的。
5、CountDownLatch与CyclicBarrier的对比可以看:
java多线程CyclicBarrier使用示例,让线程起步走
补充:CountDownLatch踩过的坑
线上生产环境dubbo报线程池满了,经过一天排查锁定在开三个线程计算最后合并数据的步骤中。简单描述下该步骤线程开三个 调用三个不同的方法 使用countdownlatch 计数器等待三个方法全部执行完成 合并数据。
但是由于其中一个方法调用第三方接口,接口返回异常导致转换数据报错。导致其中一个方法未正常完成。
举例demo:
public static void main(String[] args) { ExecutorService executorService =Executors.newFixedThreadPool(3); CountDownLatch cdl = new CountDownLatch(3); executorService.execute(new Runnable() { @Override public void run() { /*try { function1(); } catch (Exception e) { //异常处理 e.printStackTrace(); } finally { cdl.countDown(); }*/ function1(); } }); executorService.execute(new Runnable() { @Override public void run() { function2(); cdl.countDown(); } }); executorService.execute(new Runnable() { @Override public void run() { function3(); cdl.countDown(); } }); try { cdl.await(); //cdl.await(20,TimeUnit.SECONDS); System.out.println("三个执行线程结束"); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("执行线程异常"); } finally { executorService.shutdown(); System.out.println("执行线程关闭"); } } private static void function1(){ int i = 10/0; System.out.println("方法一"); } private static void function2(){ System.out.println("方法二"); } private static void function3(){ System.out.println("方法三"); }
方法一抛出异常,但是没有做异常处理导致不会执行线程关闭步骤,是不是和想象中不一样,一开始我也是懵,看了一下CountDownLatch原理就很好理解了,
“CountDownLatch是通过一个计数器来实现的,计数器的初始化值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已完成任务,然后在闭锁上等待的线程就可以恢复执行任务。”【1】
举一个现实中例子就是:CountDownLatch 就像跑步比赛中的裁判,三个方法就是就是三位运动员,运动员2,3都已经到达终点,但是运动员1摔倒了,动不了。裁判员只看到两位运动员到达终点不能宣布比赛结束,所以一直等。。。
就像这样的场景导致线上service执行线程阻塞,接口调用次数累计导致dubbo线程满了(跟dubbo线程模型有关,有时间具体谈谈这一点)
知道原因了,就要考虑怎么修改
比赛不能无限期等,所以比赛必须在有限时间内结束,所以使用
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); }
线程内部也许要增加异常处理
executorService.execute(new Runnable() { @Override public void run() { try { function1(); } catch (Exception e) { //异常处理 e.printStackTrace(); } finally { cdl.countDown(); } // function1(); } });
修改后demo
public static void main(String[] args) { ExecutorService executorService =Executors.newFixedThreadPool(3); CountDownLatch cdl = new CountDownLatch(3); executorService.execute(new Runnable() { @Override public void run() { try { function1(); } catch (Exception e) { //异常处理 e.printStackTrace(); } finally { cdl.countDown(); } // function1(); } }); executorService.execute(new Runnable() { @Override public void run() { function2(); cdl.countDown(); } }); executorService.execute(new Runnable() { @Override public void run() { function3(); cdl.countDown(); } }); try { // cdl.await(); cdl.await(20,TimeUnit.SECONDS); System.out.println("三个执行线程结束"); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("执行线程异常"); } finally { executorService.shutdown(); System.out.println("执行线程关闭"); } } private static void function1(){ int i = 10/0; System.out.println("方法一"); } private static void function2(){ System.out.println("方法二"); } private static void function3(){ System.out.println("方法三"); }
执行结果
大家结合自己的现实使用修改,爬过了使用坑,记录下分享下 ,希望能对别人有用
以上为个人经验,希望能给大家一个参考,也希望大家多多支持码农之家。如有错误或未考虑完全的地方,望不吝赐教。
浅析java 多线程的同步
java 多线程的同步几种方法
一、引言
前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊。闲话不多说,进入正题。
二、为什么要线程同步
因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。
三、不同步时的代码
Bank.java
package threadTest; /** * @author ww * */ public class Bank { private int count =0;//账户余额 //存钱 public void addMoney(int money){ count +=money; System.out.println(System.currentTimeMillis()+"存进:"+money); } //取钱 public void subMoney(int money){ if(count-money < 0){ System.out.println("余额不足"); return; } count -=money; System.out.println(+System.currentTimeMillis()+"取出:"+money); } //查询 public void lookMoney(){ System.out.println("账户余额:"+count); } }
SyncThreadTest.java
package threadTest; /** * Java学习交流QQ群:589809992 我们一起学Java! */ public class SyncThreadTest { public static void main(String args[]){ final Bank bank=new Bank(); Thread tadd=new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub while(true){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } bank.addMoney(100); bank.lookMoney(); System.out.println("\n"); } } }); Thread tsub = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub while(true){ bank.subMoney(100); bank.lookMoney(); System.out.println("\n"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }); tsub.start(); tadd.start(); } }
代码很简单,我就不解释了,看看运行结果怎样呢?截取了其中的一部分,是不是很乱,有写看不懂。
余额不足 账户余额:0 余额不足 账户余额:100 1441790503354存进:100 账户余额:100 1441790504354存进:100 账户余额:100 1441790504354取出:100 账户余额:100 1441790505355存进:100 账户余额:100 1441790505355取出:100 账户余额:100
四、使用同步时的代码
(1)同步方法:
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
修改后的Bank.java
package threadTest; /** * @author ww * */ public class Bank { private int count =0;//账户余额 //存钱 public synchronized void addMoney(int money){ count +=money; System.out.println(System.currentTimeMillis()+"存进:"+money); } //取钱 public synchronized void subMoney(int money){ if(count-money < 0){ System.out.println("余额不足"); return; } count -=money; System.out.println(+System.currentTimeMillis()+"取出:"+money); } //查询 public void lookMoney(){ System.out.println("账户余额:"+count); } }
再看看运行结果:
余额不足 账户余额:0 余额不足 账户余额:0 1441790837380存进:100 账户余额:100 1441790838380取出:100 账户余额:0 1441790838380存进:100 账户余额:100 1441790839381取出:100 账户余额:0
瞬间感觉可以理解了吧。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
(2)同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
Bank.java代码如下:
package threadTest; /** * @author ww * */ public class Bank { private int count =0;//账户余额 //存钱 public void addMoney(int money){ synchronized (this) { count +=money; } System.out.println(System.currentTimeMillis()+"存进:"+money); } //取钱 public void subMoney(int money){ synchronized (this) { if(count-money < 0){ System.out.println("余额不足"); return; } count -=money; } System.out.println(+System.currentTimeMillis()+"取出:"+money); } //查询 public void lookMoney(){ System.out.println("账户余额:"+count); } }
运行结果如下:
余额不足 账户余额:0 1441791806699存进:100 账户余额:100 1441791806700取出:100 账户余额:0 1441791807699存进:100 账户余额:100
效果和方法一差不多。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
(3)使用特殊域变量(Volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新c.因此每次使用该域就要重新计算,而不是使用寄存器中的值d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
Bank.java代码如下:
package threadTest; /** * @author ww * */ public class Bank { private volatile int count = 0;// 账户余额 // 存钱 public void addMoney(int money) { count += money; System.out.println(System.currentTimeMillis() + "存进:" + money); } // 取钱 public void subMoney(int money) { if (count - money < 0) { System.out.println("余额不足"); return; } count -= money; System.out.println(+System.currentTimeMillis() + "取出:" + money); } // 查询 public void lookMoney() { System.out.println("账户余额:" + count); } }
运行效果怎样呢?
余额不足 账户余额:0 余额不足 账户余额:100 1441792010959存进:100 账户余额:100 1441792011960取出:100 账户余额:0 1441792011961存进:100 账户余额:100
是不是又看不懂了,又乱了。这是为什么呢?就是因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
(4)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:ReentrantLock() : 创建一个ReentrantLock实例lock() : 获得锁unlock() : 释放锁注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用Bank.java代码修改如下:
package threadTest; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author ww * */ public class Bank { private int count = 0;// 账户余额 //需要声明这个锁 private Lock lock = new ReentrantLock(); // 存钱 public void addMoney(int money) { lock.lock();//上锁 try{ count += money; System.out.println(System.currentTimeMillis() + "存进:" + money); }finally{ lock.unlock();//解锁 } } // 取钱 public void subMoney(int money) { lock.lock(); try{ if (count - money < 0) { System.out.println("余额不足"); return; } count -= money; System.out.println(+System.currentTimeMillis() + "取出:" + money); }finally{ lock.unlock(); } } // 查询 public void lookMoney() { System.out.println("账户余额:" + count); } }
运行效果怎么样呢?
余额不足 账户余额:0 余额不足 账户余额:0 1441792891934存进:100 账户余额:100 1441792892935存进:100 账户余额:200 1441792892954取出:100 账户余额:100
效果和前两种方法差不多。
如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
(5)使用局部变量实现线程同步
Bank.java代码如下:
package threadTest; /** * @author ww * */ public class Bank { private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { // TODO Auto-generated method stub return 0; } }; // 存钱 public void addMoney(int money) { count.set(count.get()+money); System.out.println(System.currentTimeMillis() + "存进:" + money); } // 取钱 public void subMoney(int money) { if (count.get() - money < 0) { System.out.println("余额不足"); return; } count.set(count.get()- money); System.out.println(+System.currentTimeMillis() + "取出:" + money); } // 查询 public void lookMoney() { System.out.println("账户余额:" + count.get()); } }
运行效果:
余额不足 账户余额:0 余额不足 账户余额:0 1441794247939存进:100 账户余额:100 余额不足 1441794248940存进:100 账户余额:0 账户余额:200 余额不足 账户余额:0 1441794249941存进:100 账户余额:300
看了运行效果,一开始一头雾水,怎么只让存,不让取啊?看看ThreadLocal 的原理:
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。
ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式
如有疑问请留言或者到本站社区交流讨论,感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
java多线程Thread的实现方法代码详解
之前有简单介绍过java多线程的使用,已经Thread类和Runnable类,为了更好地理解多线程,本文就Thread进行详细的分析。
start()
我们先来看看API中对于该方法的介绍:
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
start方法是开启线程的方法,使用后java会创建一个新的线程执行run里的方法。这是一个小demo:
for(int i=0;i<3;i++){ Thread t= new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+" start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" end"); } }); t.start(); } System.out.println("it is over");
执行结果:
it is over
Thread-1 start
Thread-0 start
Thread-2 start
Thread-0 end
Thread-1 end
Thread-2 end
由于多线程是有随机性的,所以每次的结果可能都不一样,这一点也是我们需要注意的,线程的执行顺序和调用顺序并不一致。
run()
run方法就是调用Thread设置的Runnable的run方法,将上面的demo进行修改:
for(int i=0;i<3;i++){ Thread t= new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+" start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" end"); } }); t.run(); } System.out.println("it is over");
执行结果:
main start
main end
main start
main end
main start
main end
it is over
run方法的直接结果和start有很大的差别,完全是按顺序执行,并没有开启新线程。
stop()
stop方法是强制停止线程的执行,是非安全的,不要使用此方法。在调用stop时, 会对锁定的资源进行释放,但这种释放是非一致的,容易引起程序问题。如果想要控制线程的停止,可以使用自定义变量来判断或者isInterrupted()方法:
class Thread1 extends Thread { @Override public void run() { //判断线程体是否运行 while (!isInterrupted()) { // Do Something } } }
interrupt()
interrupt的作用是通知线程,你已经被中断的,但具体的中断执行需要在线程自定义处理,甚至你可以不理会继续执行。具体的中孤单是会线程执行join、wait、sleep方法时,抛出InterruptedException。
Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+" start"); try { for(int i=0;i<100000;i++){ System.out.println(i+""); Thread.sleep(1); } } catch (InterruptedException e) { System.out.println("the thread is interrupted");//可以在这里做资源释放,日志记录等 e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" end"); } }); t1.start(); Thread.sleep(100); t1.interrupt();
执行结果:
65 66 67 68 the thread is interrupted java.lang.InterruptedException: sleep interrupted Thread-0 end at java.lang.Thread.sleep(Native Method) at com.wk.aqi.act.Test$1.run(Test.java:23) at java.lang.Thread.run(Thread.java:745)
isInterrupted()
判断线程是否中断,在执行上面的interrupt方法后,会return true。
setPriority(int newPriority)和getPriority()
设置线程的优先级和获取线程的优先级,cpu分配的资源给侧重给priority高的线程。
Thread t1 = new Thread(new Runnable() { @Override public void run() { long t = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+" start"); for(int i=0;i<1000;i++){ try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" t1 end "+(System.currentTimeMillis()-t)); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { long t = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+" start"); for(int i=0;i<1000;i++){ try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" t2 end "+(System.currentTimeMillis()-t)); } }); t1.setPriority(10); t2.setPriority(1); t2.start(); t1.start();
执行结果:
Thread-0 start Thread-1 start Thread-0 t1 end 1357 Thread-1 t2 end 1371
在优先级一样的情况下,t1和t2是几乎同时完成的,在优先级不一样的情况,有明显的差别。
getName()
比较简单,获取线程的名称。
join()和join(long millis)
jion方法的作用是等待线程执行完成,join(long millis)可以设置最长等待时间。比如主线程需要等待子线程完成,获取子线程的结果后才能继续往下执行,这时候就可以使用join方法
Thread t1 = new Thread(new Runnable() { @Override public void run() { long t = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+" start"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" t1 end "+(System.currentTimeMillis()-t)); } }); t1.start(); t1.join(); System.out.println("等待t1执行完,再执行");
执行结果:
Thread-0 start Thread-0 t1 end 1001 等待t1执行完,再执行
总结
以上就是本文关于java多线程Thread的实现方法代码详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。
Java多线程之线程池七个参数详解
ThreadPoolExecutor是JDK中的线程池实现,这个类实现了一个线程池需要的各个方法,它提供了任务提交、线程管理、监控等方法。
下面是ThreadPoolExecutor类的构造方法源码,其他创建线程池的方法最终都会导向这个构造方法,共有7个参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
这些参数都通过volatile修饰:
public class ThreadPoolExecutor extends AbstractExecutorService { private final BlockingQueue<Runnable> workQueue; private volatile ThreadFactory threadFactory; private volatile RejectedExecutionHandler handler; private volatile long keepAliveTime; // 是否允许核心线程被回收 private volatile boolean allowCoreThreadTimeOut; private volatile int corePoolSize; private volatile int maximumPoolSize; }
corePoolSize:核心线程数
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。
maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。
keepAliveTime:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。
可被回收的线程:
设置allowCoreThreadTimeout=true的核心线程。大于核心线程数的线程(非核心线程)。
unit:时间单位
keepAliveTime的时间单位:
TimeUnit.NANOSECONDS TimeUnit.MICROSECONDS TimeUnit.MILLISECONDS // 毫秒 TimeUnit.SECONDS TimeUnit.MINUTES TimeUnit.HOURS TimeUnit.DAYS
workQueue:工作队列
新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。
JDK默认的工作队列有五种:
1.ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
2。LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。
3.SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
4.PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
5.DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。
threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等。
默认线程工厂:
/** * The default thread factory */ static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
handler:拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。
JDK默认的拒绝策略有四种:
1.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
2.DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
3.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
4.CallerRunsPolicy:由调用线程处理该任务。
默认拒绝策略:
/** * The default rejected execution handler */ private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); public static class AbortPolicy implements RejectedExecutionHandler { /** * Creates an {@code AbortPolicy}. */ public AbortPolicy() { } /** * Always throws RejectedExecutionException. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task * @throws RejectedExecutionException always */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
自定义线程池工具
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** * 线程池工厂工具 * * @author 向振华 * @date 2021/04/11 10:24 */ public class ThreadPoolFactory { /** * 生成固定大小的线程池 * * @param threadName 线程名称 * @return 线程池 */ public static ExecutorService createFixedThreadPool(String threadName) { AtomicInteger threadNumber = new AtomicInteger(0); return new ThreadPoolExecutor( // 核心线程数 desiredThreadNum(), // 最大线程数 desiredThreadNum() * 2, // 空闲线程存活时间 60L, // 空闲线程存活时间单位 TimeUnit.SECONDS, // 工作队列 new ArrayBlockingQueue<>(1024), // 线程工厂 new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, threadName + "-" + threadNumber.getAndIncrement()); } }, // 拒绝策略 new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (!executor.isShutdown()) { try { //尝试阻塞式加入任务队列 executor.getQueue().put(r); } catch (Exception e) { //保持线程的中断状态 Thread.currentThread().interrupt(); } } } }); } /** * 理想的线程数,使用 2倍cpu核心数 */ public static int desiredThreadNum() { return Runtime.getRuntime().availableProcessors() * 2; } }
到此这篇关于Java多线程之线程池七个参数详解的文章就介绍到这了,更多相关java线程池详解内容请搜索码农之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持码农之家!
Java多线程中线程的两种创建方式及比较代码示例
1.线程的概念:线程(thread)是指一个任务从头至尾的执行流,线程提供一个运行任务的机制,对于java而言,一个程序中可以并发的执行多个线程,这些线程可以在多处理器系统上同时运行。当程序作为一个应用程序运行时,java解释器为main()方法启动一个线程。
2.并行与并发:
(1)并发:在单处理器系统中,多个线程共享CPU时间,而操作系统负责调度及分配资源给它们。
(2)并行:在多处理器系统中,多个处理器可以同时运行多个线程,这些线程在同一时间可以同时运行,而不同于并发,只能多个线程共享CPU时间,同一时间只能运行一个线程。
3.线程的创建:
(1)基础概念:java中每个任务就是一个可运行对象,为了创建任务,必须首先定义任务类,任务类必须实现Runnable接口。而线程本质上讲就是便于任务执行的对象。一个线程的执行过程就是一个任务类中run()方法的执行到结束。
(2)通过Runnable接口创建线程:
a.定义一个任务类实现Runnable接口,实现Runnable接口中的run()方法(run()方法告知系统线程该如何运行),run()方法中定义具体的任务代码或处理逻辑。
b.定义了任务类后,为任务类创建一个任务对象。
c.任务必须在线程中执行,创建一个Tread类的对象,将前面创建的实现了Runnable接口的任务类对象作为参数传递给Tread类的构造方法。
d.调用Tread类对象的start()方法,启动一个线程。它会导致任务的run()方法被执行,当run()方法执行完毕,则线程就终止。
实例代码:
package com.muzeet.mutithread; //每个任务都是Runable接口的一个实例,任务是可运行对象,线程是便于任务执行的对象。必须创建任务类,重写run方法定义任务 public class ThreadDemo1 implements Runnable { private int countDown = 10; @Override //重写run方法,定义任务 public void run() { while(countDown-- >0) { System.out.println("$" + Thread.currentThread().getName() + "(" + countDown + ")"); } } //调用start方法会启动一个线程,导致任务中的run方法被调用,run方法执行完毕则线程终止 public static void main(String[] args) { Runnable demo1 = new ThreadDemo1(); Thread thread1 = new Thread(demo1); Thread thread2 = new Thread(demo1); thread1.start(); thread2.start(); System.out.println("火箭发射倒计时:"); } }
程序运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-0(5) $Thread-0(4) $Thread-0(3) $Thread-0(2) $Thread-0(1) $Thread-0(0)
同时运行两个任务对象:
public static void main(String[] args) { Runnable demo1 = new ThreadDemo1(); Runnable demo2 = new ThreadDemo1(); Thread thread1 = new Thread(demo1); Thread thread2 = new Thread(demo2); thread1.start(); thread2.start(); System.out.println("火箭发射倒计时:"); }
运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-1(9) $Thread-0(5) $Thread-1(8) $Thread-0(4) $Thread-1(7) $Thread-0(3) $Thread-1(6) $Thread-1(5) $Thread-0(2) $Thread-1(4) $Thread-1(3) $Thread-1(2) $Thread-1(1) $Thread-1(0) $Thread-0(1) $Thread-0(0)
(3)继承Thread类来创建线程:
a.首先创建一个任务类extends Thread类,因为Thread类实现了Runnable接口,所以自定义的任务类也实现了Runnable接口,重新run()方法,其中定义具体的任务代码或处理逻辑。
b.创建一个任务类对象,可以用Thread或者Runnable作为自定义的变量类型。
c.调用自定义对象的start()方法,启动一个线程。
示例代码:
package com.muzeet.mutithread; //每个任务都是Runable接口的一个实例,任务是可运行对象,线程即可运行对象。必须创建任务类,重写run方法定义任务 public class ExtendFromThread extends Thread { private int countDown = 10; @Override //重写run方法,定义任务 public void run() { while(countDown-- >0) { System.out.println("$" + this.getName() + "(" + countDown + ")"); } } //调用start方法会启动一个线程,导致任务中的run方法被调用,run方法执行完毕则线程终止 public static void main(String[] args) { ExtendFromThread thread1 = new ExtendFromThread(); ExtendFromThread thread2 = new ExtendFromThread(); thread1.start(); thread2.start(); System.out.println("火箭发射倒计时:"); } }
运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-0(5) $Thread-0(4) $Thread-0(3) $Thread-0(2) $Thread-0(1) $Thread-0(0) $Thread-1(9) $Thread-1(8) $Thread-1(7) $Thread-1(6) $Thread-1(5) $Thread-1(4) $Thread-1(3) $Thread-1(2) $Thread-1(1) $Thread-1(0)
一个线程等待另一个线程结束后再执行:当执行PrintNum这个任务时,打印到数字50时,转而去执行打印字符C这个任务,知道线程thread4执行完才继续执行打印数字任务。
package com.muzeet.testThread; public class PrintNum implements Runnable { private int lastNum; public PrintNum(int n) { lastNum = n; } @Override public void run() { // TODO Auto-generated method stub Thread thread4 = new Thread(new PrintChar('c', 40)); thread4.start(); try { for(int i=1;i<=lastNum;i++) { System.out.println(" " + i); if(i == 50) { thread4.join(); } } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
4.两种方法的比较(转载)
首先分析两种方式的输出结果,同样是创建了两个线程,为什么结果不一样呢?
使用实现Runnable接口方式创建线程可以共享同一个目标对象(TreadDemo1tt=newTreadDemo1();),实现了多个相同线程处理同一份资源。当第一个线程执行完任务后,countDown已经为0,所以第二个线程就不会输出。而继承Thread创建线程的方式,new出了两个任务类对象,有各自的成员变量,相互之间不干扰。
然后再看一段来自JDK的解释:
Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。
设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread类实现了Runnable。激活的意思是说某个线程已启动并且尚未停止。
此外,Runnable为非Thread子类的类提供了一种激活方式。通过实例化某个Thread实例并将自身作为运行目标,就可以运行实现Runnable的类。大多数情况下,如果只想重写run()方法,而不重写其他Thread方法,那么应使用Runnable接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。(推荐使用创建任务类,并实现Runnable接口,而不是继承Thread类)
采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
总结
以上就是本文关于Java多线程中线程的两种创建方式及比较代码示例的全部内容,希望对大家有所帮助。如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!
参考资料
-
Java高并发编程详解:多线程与架构设计
《Java高并发编程详解》立足于线程源码剖析,从基础入手,逐层渐进深入,环环紧扣,针对技术细节进行了细致归纳,在解读源码的时候又引入了优秀的架构设计。
大小:120.77 MBJava
立即下载 -
深入浅出Java多线程
RedSpider社区介绍 RedSpider技术社区始于2018年秋的成都。 在本书发布时,社区共五位活跃成员,均参与了本书的撰写及审校⼯作。他们的代 号分别是(排名按拼⾳排序,不分先后):灵鹤,⽑⽑⾍,潘帕斯雄鹰,萤⽕⾍, 啄⽊⻦。 活跃 RedSpider是⼀个⾮常活跃的技术社区,虽然现在成员⼈数不多,但是对于社区的 各种活动都⾮常积极和活跃。⽬前社区内部⼤多都是Java开发⼯程师,且都对技术 始终保持极⾼的热情。 RedSpider是⼀个⾃我驱动的技术学习社区
大小:3.02 MBJava
立即下载 -
图解Java多线程设计模式
本书通过具体的Java 程序,以浅显易懂的语言逐一说明了多线程和并发处理中常用的12 种设计模式。内容涉及线程的基础知识、线程的启动与终止、线程间的互斥处理与协作、线程的有效应用、线程的数量管理以及性能优化的注意事项等。
大小:86.3 MBJava
立即下载 -
深入浅出Java多线程(阿里Java大牛)
大小:3.02 MBJava
立即下载 -
Java多线程编程实战指南:核心篇
Java多线程编程实战指南以基本概念、原理与方法为主线,辅以丰富的实战案例和生活化实例,从Java虚拟机、操作系统和硬件多个层次与角度出发,循序渐进介绍Java平台下的多线程编程核心技术及相关工具
大小:172.6 MBJava编程
立即下载
1小时12分钟前整理