pdf下载地址:Java面试宝典
第一章内容介绍 20
第二章JavaSE基础 21
一、Java面向对象 21
1. 面向对象都有哪些特性以及你对这些特性的理解 21
2. 访问权限修饰符public、private、protected, 以及不写(默认)时的区别(2017-11-12) 22
3. 如何理解clone对象 22
二、JavaSE语法(2017-11-12-wl) 26
1. Java有没有goto语句?(2017-11-12-wl) 26
2. & 和 && 的区别(2017-11-12-wl) 27
3. 在Java中,如何跳出当前的多重嵌套循环(2017-11-14-wl) 27
4. 两个对象值相同(x.equals(y) == true),但却可有不同的hashCode,这句话对不对?(2017-11-14-wl) 27
5. 是否可以继承String (2017-11-14-wl) 28
6. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?(2017-11-14-wl) 29
7. 重载(overload)和重写(override)的区别?重载的方法能否根据返回类型进行区分?(2017-11-15-wl) 29
8. 为什么函数不能根据返回类型来区分重载?(2017-11-15-wl) 30
9. char 型变量中能不能存储一个中文汉字,为什么?(2017-11-16-wl) 31
10. 抽象类(abstract class)和接口(interface)有什么异同?(2017-11-16-wl) 31
11. 抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被synchronized(2017-11-16-wl) 32
12. 阐述静态变量和实例变量的区别?(2017-11-16-wl) 32
13. ==和equals的区别?(2017-11-22-wzz) 33
14. break和continue的区别?(2017-11-23-wzz) 33
15. String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?(2017-12-1-lyq) 33
三、Java中的多态 35
1. Java中实现多态的机制是什么? 35
四、Java的异常处理 35
1. Java中异常分为哪些种类 35
2. 调用下面的方法,得到的返回值是什么? 35
3. error和exception的区别?(2017-2-23) 36
4. java异常处理机制(2017-2-23) 37
5. 请写出你最常见的5个RuntimeException(2017-11-22-wzz) 37
6. throw和throws的区别(2017-11-22-wzz) 38
7. final、finally、finalize的区别?(2017-11-23-wzz) 38
五、JavaSE常用API 39
1. Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少?(2017-11-14-wl) 39
2. switch是否能作用在byte上,是否能作用在long上,是否能作用在String上?(2017-11-14-wl) 39
3. 数组有没有length() 方法?String有没有length() 方法?(2017-11-14-wl) 39
4. String 、StringBuilder 、StringBuffer的区别?(2017-11-14-wl) 39
5. 什么情况下用“+”运算符进行字符串连接比调用StringBuffer/StringBuilder 对象的append方法连接字符串性能更好?(2017-11-14-wl) 40
6. 请说出下面程序的输出(2017-11-14-wl) 47
7. Java中的日期和时间(2017-11-19-wl) 48
六、Java的数据类型 70
1. Java的基本数据类型都有哪些各占几个字节 70
2. String是基本数据类型吗?(2017-11-12-wl) 71
3. short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1有错吗;(2017-11-12-wl) 71
4. int 和 和 Integer有什么区别?(2017-11-12-wl) 71
5. 下面Integer类型的数值比较输出的结果为?(2017-11-12-wl) 72
6. String类常用方法(2017-11-15-lyq) 74
7. String、StringBuffer、StringBuilder的区别?(2017-11-23-wzz) 74
8. 数据类型之间的转换(2017-11-23-wzz) 75
七、Java的IO 75
1. Java中有几种类型的流(2017-11-23-wzz) 75
2. 字节流如何转为字符流 76
3. 如何将一个java对象序列化到文件里 76
4. 字节流和字符流的区别(2017-11-23-wzz) 77
5. 如何实现对象克隆?(2017-11-12-wl) 77
6. 什么是java序列化,如何实现java序列化?(2017-12-7-lyq) 80
八、Java的集合 81
1. HashMap排序题,上机题。(本人主要靠这道题入职的第一家公司) 81
2. 集合的安全性问题 83
3. ArrayList内部用什么实现的?(2015-11-24) 83
4. 并发集合和普通集合如何区别?(2015-11-24) 89
5. List的三个子类的特点(2017-2-23) 91
6. List和Map、Set的区别(2017-11-22-wzz) 91
7. HashMap 和HashTable有什么区别?(2017-2-23) 92
8. 数组和链表分别比较适合用于什么场景,为什么?(2017-2-23) 93
9. Java中ArrayList和Linkedlist区别?(2017-2-23) 96
10. List a=new ArrayList()和ArrayList a =new ArrayList()的区别?(2017-2-24) 97
11. 要对集合更新操作时,ArrayList和LinkedList哪个更适合?(2017-2-24) 97
12. 请用两个队列模拟堆栈结构(2017-2-24) 101
13. Collection和Map的集成体系(2017-11-14-lyq) 102
14. Map中的key和value可以为null么?(2017-11-21-gxb) 103
九、Java的多线程和并发库 104
(一)多线程基础知识--传统线程机制的回顾(2017-12-11-wl) 104
(二)多线程基础知识--线程并发库(2017-12-11-wl) 118
(三)多线程面试题 246
十、Java内部类 272
1. 静态嵌套类(Static Nested Class) 和内部类(Inner Class)的不同?(2017-11-16-wl) 272
2. 下面的代码哪些地方会产生编译错误?(2017-11-16-wl) 272
第三章JavaSE高级 273
一、Java中的反射 273
1. 说说你对Java中反射的理解 273
二、Java中的动态代理 273
1. 写一个ArrayList的动态代理类(笔试题) 273
2. 动静态代理的区别,什么场景使用?(2015-11-25) 274
三、Java中的设计模式&回收机制 274
1. 你所知道的设计模式有哪些 274
2. 单例设计模式 274
3. 工厂设计模式 276
4. 建造者模式(Builder) 279
5. 适配器设计模式 280
6. 装饰模式(Decorator) 282
7. 策略模式(strategy) 283
8. 观察者模式(Observer) 284
9. JVM垃圾回收机制和常见算法 286
10. 谈谈JVM的内存结构和内存分配 290
11. Java中引用类型都有哪些?(重要) 291
12. heap和stack有什么区别(2017-2-23) 294
13. 解释内存中的栈 (stack) 、堆 (heap) 和方法区 (method area) 的用法(2017-11-12-wl) 301
四、Java的类加载器(2015-12-2) 301
1. Java的类加载器的种类都有哪些? 301
2. 类什么时候被初始化? 302
3. Java类加载体系之ClassLoader双亲委托机制 (2017-2-24) 302
4. 描述一下JVM加载class (2017-11-15-wl) 306
5. 获得一个类对象有哪些方式?(2017-11-23-wzz) 307
五、JVM基础知识(2017-11-16-wl) 307
1. 既然有GC机制,为什么还会有内存泄露的情况(2017-11-16-wl) 307
六、GC基础知识(2017-11-16-wl) 309
1. Java中为什么会有GC机制呢?(2017-11-16-wl) 309
2. 对于Java的GC哪些内存需要回收(2017-11-16-wl) 309
3. Java的GC什么时候回收垃圾(2017-11-16-wl) 309
七、Java8的新特性以及使用(2017-12-02-wl) 311
1. 通过10个示例来初步认识Java8中的lambda表达式(2017-12-02-wl) 311
2. Java8中的lambda表达式要点(2017-12-02-wl) 319
3. Java8中的Optional类的解析(2017-12-02-wl) 321
八、在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?(2017-11-23-gxb) 328
第四章JavaWEB 基础 329
一、JDBC技术 329
1. 说下原生jdbc操作数据库流程?(2017-11-25-wzz) 329
2. 什么要使用PreparedStatement?(2017-11-25-wzz) 330
3. 关系数据库中连接池的机制是什么?(2017-12-6-lyq) 330
三、Http协议 331
1. http的长连接和短连接(2017-11-14-lyq) 331
2. HTTP/1.1与HTTP/1.0的区别(2017-11-21-wzy) 332
3. http常见的状态码有哪些?(2017-11-23-wzz) 335
4. GET和POST的区别?(2017-11-23-wzz) 335
5. http中重定向和请求转发的区别?(2017-11-23-wzz) 337
四、Cookie和Session 337
1. Cookie和Session的区别(2017-11-15-lyq) 337
2. session共享怎么做的(分布式如何实现session共享)? 337
3. 在单点登录中,如果cookie被禁用了怎么办?(2017-11-23-gxb) 340
五、jsp技术 341
1. 什么是jsp,什么是Servlet?jsp和Servlet有什么区别?(2017-11-23-wzz) 341
2. jsp有哪些域对象和内置对象及他们的作用?(2017-11-25-wzz) 342
六、XML技术 343
1. 什么是xml,使用xml的优缺点,xml的解析器有哪几种,分别有什么区别?(2017-11-25-wzz) 343
第五章JavaWEB高级 344
一、Filter和Listener 344
二、AJAX 344
1. 谈谈你对ajax的认识?(2017-11-23-wzz) 344
2. jsonp原理(2017-11-21-gxb) 345
三、Linux 346
1. 说一下常用的Linux命令 346
2. Linux中如何查看日志?(2017-11-21-gxb) 347
3. Linux怎么关闭进程(2017-11-21-gxb) 348
四、常见的前端框架有哪些 349
1. EasyUI(2017-11-23-lyq) 349
2. MiniUI(2017-11-23-lyq) 351
1. jQueryUI(2017-11-23-lyq) 352
2. Vue.js(2017-11-23-lyq) 353
3. AngularJS (2017-11-23-lyq) 355
第六章数据库 359
一、Mysql 359
1. SQL的select语句完整的执行顺序(2017-11-15-lyq) 359
2. SQL之聚合函数(2017-11-15-lyq) 361
3. SQL之连接查询(左连接和右连接的区别)(2017-11-15-lyq) 361
4. SQL之sql注入(2017-11-15-lyq) 362
5. Mysql性能优化(2017-11-15-lyq) 362
6. 必看sql面试题(学生表_课程表_成绩表_教师表)(2017-11-25-wzz) 363
7. Mysql数据库架构图(2017-11-25-wzz) 364
8. Mysql架构器中各个模块都是什么?(2017-11-25-wzz) 365
9. Mysql存储引擎有哪些?(2017-11-25-wzz) 366
10. MySQL事务介绍(2017-11-25-wzz) 367
11. MySQL怎么创建存储过程(2017-11-25-wzz) 369
12. MySQL触发器怎么写?(2017-11-25-wzz) 370
13. MySQL语句优化(2017-11-26-wzz) 371
14. MySQL中文乱码问题完美解决方案(2017-12-07-lwl) 372
15. 如何提高MySQL的安全性(2017-12-8-lwl) 374
二、Oracle 376
1. 什么是存储过程,使用存储过程的好处?(2017-11-25-wzz) 376
2. Oracle存储过程怎么创建?(2017-11-25-wzz) 377
3. 如何使用Oracle的游标?(2017-11-25-wzz) 378
4. Oracle中字符串用什么连接?(2017-11-25-wzz) 378
5. Oracle中是如何进行分页查询的?(2017-11-25-wzz) 379
6. 存储过程和存储函数的特点和区别?(2017-11-25-wzz) 379
7. 存储过程与SQL的对比?(2017-11-21-gxb) 379
8. 你觉得存储过程和SQL语句该使用哪个?(2017-11-21-gxb) 380
9. 触发器的作用有哪些?(2017-11-21-gxb) 381
10. 在千万级的数据库查询中,如何提高效率?(2017-11-23-gxb) 381
第七章框架 385
一、SpringMVC 385
1. SpringMVC的工作原理(2017-11-13-lyq) 385
2. SpringMVC常用注解都有哪些?(2017-11-24-gxb) 386
3. 如何开启注解处理器和适配器?(2017-11-24-gxb) 386
4. 如何解决get和post乱码问题?(2017-11-24-gxb) 386
二、Spring 387
1. 谈谈你对Spring的理解(2017-11-13-lyq) 387
2. Spring中的设计模式(2017-11-13-lyq) 387
3. Spring的常用注解(2017-11-13-lyq) 388
4. 简单介绍一下Spring bean的生命周期(2017-11-21-gxb) 389
5. Spring结构图(2017-11-22-lyq) 390
6. Spring能帮我们做什么?(2017-11-22-lyq) 392
7. 请描述一下Spring的事务(2017-11-22-lyq) 393
8. BeanFactory 常用的实现类有哪些?(2017-12-03-gxb) 396
9. 解释Spring JDBC、Spring DAO和Spring ORM(2017-12-03-gxb) 397
10. 简单介绍一下Spring WEB 模块。(2017-12-03-gxb) 397
11. Spring配置文件有什么作用?(2017-12-03-gxb) 398
12. 什么是Spring IOC 容器?(2017-12-03-gxb) 398
13. IOC的优点是什么? 398
14. ApplicationContext的实现类有哪些?(2017-12-03-gxb) 398
15. BeanFactory与AppliacationContext有什么区别(2017-12-03-gxb) 399
16. 什么是Spring的依赖注入?(2017-12-04-gxb) 399
17. 有哪些不同类型的IOC(依赖注入)方式?(2017-12-04-gxb) 399
18. 什么是Spring beans?(2017-12-04-gxb) 400
19. 一个Spring Beans的定义需要包含什么?(2017-12-04-gxb) 400
20. 你怎样定义类的作用域?(2017-12-04-gxb) 401
21. Spring支持的几种bean的作用域。(2017-12-04-gxb) 401
22. Spring框架中的单例bean是线程安全的吗?(2017-12-04-gxb) 401
23. 什么是Spring的内部bean?(2017-12-04-gxb) 401
24. 在Spring中如何注入一个java集合?(2017-12-04-gxb) 402
25. 什么是bean的自动装配?(2017-12-04-gxb) 402
26. 解释不同方式的自动装配。(2017-12-04-gxb) 402
27. 什么是基于Java的Spring注解配置? 给一些注解的例子(2017-12-05-gxb) 403
28. 什么是基于注解的容器配置?(2017-12-05-gxb) 403
29. 怎样开启注解装配?(2017-12-05-gxb) 403
30. 在Spring框架中如何更有效地使用JDBC?(2017-12-05-gxb) 403
31. 使用Spring通过什么方式访问Hibernate?(2017-12-05-gxb) 404
32. Spring支持的ORM框架有哪些?(2017-12-05-gxb) 404
33. 简单解释一下spring的AOP(2017-12-05-gxb) 404
34. 在Spring AOP 中,关注点和横切关注的区别是什么?(2017-12-05-gxb) 405
35. 什么是连接点?(2017-12-05-gxb) 405
36. Spring的通知是什么?有哪几种类型?(2017-12-05-gxb) 405
37. 什么是切点?(2017-12-05-gxb) 406
38. 什么是目标对象?(2017-12-05-gxb) 406
39. 什么是代理?(2017-12-05-gxb) 406
40. 什么是织入?什么是织入应用的不同点?(2017-12-05-gxb) 406
三、Shiro 406
1. 简单介绍一下Shiro框架(2017-11-23-gxb) 406
2. Shiro主要的四个组件(2017-12-2-wzz) 407
3. Shiro运行原理(2017-12-2-wzz) 408
4. Shiro的四种权限控制方式(2017-12-2-wzz) 408
5. 授权实现的流程(2017-12-2-wzz) 409
四、Mybatis 410
1. Mybatis中#和$的区别?(2017-11-23-gxb) 410
2. Mybatis的编程步骤是什么样的?(2017-12-2-wzz) 411
3. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的?(2017-12-2-wzz) 411
4. 使用MyBatis的mapper接口调用时有哪些要求?(2017-12-2-wzz) 411
5. Mybatis中一级缓存与二级缓存?(2017-12-4-lyq) 412
6. MyBatis在insert插入操作时返回主键ID(2017-12-4-lyq) 412
五、Struts2 413
1. 简单介绍一下Struts2(2017-11-24-gxb) 413
2. Struts2的执行流程了解么?(2017-11-24-gxb) 414
3. Struts2中Action配置的注意事项有哪些?(2017-11-24-gxb) 416
4. 拦截器和过滤器有哪些区别?(2017-11-24-gxb) 417
5. Struts2的封装方式有哪些?(2017-11-24-gxb) 417
6. 简单介绍一下Struts2的值栈。(2017-11-24-gxb) 419
7. SpringMVC和Struts2的区别?(2017-11-23-gxb) 420
8. Struts2中的 # 和 % 分别是做什么的?(2017-11-30-wzz) 421
9. Struts2中有哪些常用结果类型?(2017-12-1-lyq) 422
六、Hibernate 422
1. 简述一下hibernate的开发流程(2017-11-24-gxb) 422
2. hibernate中对象的三种状态(2017-11-24-gxb) 423
3. hibernate的缓存机制。(2017-11-24-gxb) 423
4. Hibernate的查询方式有哪些?(2017-11-24-gxb) 424
5. Hibernate和Mybatis的区别?(2017-11-23-gxb) 424
6. Hibernate和JDBC优缺点对比(2017-11-29-wzz) 425
7. 关于Hibernate的orm思想你了解多少?(2017-11-29-wzz) 426
8. get和load的区别?(2017-11-30-wzz) 426
9. 如何进行Hibernate的优化?(2017-11-30-wzz) 427
10. 什么是Hibernate 延迟加载?(2017-12-1-lyq) 428
11. No Session问题原理及解决方法?(2017-12-4-lyq) 428
12. Spring的两种代理JDK和CGLIB的区别浅谈(2017-12-4-lyq) 429
13. 叙述Session的缓存的作用(2017-12-9-lwl) 430
14. Session的清理和清空有什么区别?(2017-12-10-lwl) 430
15. 请简述Session的特点有哪些?(2017-12-10-lwl) 430
16. 比较Hibernate三种检索策略的优缺点(2017-12-10-lwl) 431
七、Quartz 定时任务 432
1. 什么是Quartz 框架(2017-12-2-wzz) 432
2.配置文件 applicationContext_job.xml各个属性作用(2017-12-2-wzz) 432
3.Cron表达式详解(2017-12-2-wzz) 432
4. 如何监控 Quartz 的 job 执行状态:运行中,暂停中,等待中? (2017-12-2-wzz) 433
第八章最新技术 433
一、Redis 433
1. Redis的特点?(2017-11-25-wzz) 433
2. 为什么redis需要把所有数据放到内存中?(2017-11-25-wzz) 434
3. Redis常见的性能问题都有哪些?如何解决?(2017-11-25-wzz) 434
4. Redis最适合的场景有哪些?(2017-11-25-wzz) 435
5. Memcache与Redis的区别都有哪些?(2017-11-25-wzz) 435
6. Redis用过RedisNX吗?Redis有哪几种数据结构?(2017-11-14-lyq) 435
7. Redis的优缺点(2017-11-22-lyq) 436
8. Redis的持久化(2017-11-23-lyq) 437
二、消息队列ActiveMQ 439
1. 如何使用ActiveMQ解决分布式事务?(2017-11-21-gxb) 439
2. 了解哪些消息队列?(2017-11-24-gxb) 440
3. ActiveMQ如果消息发送失败怎么办?(2017-11-24-gxb) 442
三、Dubbo 443
1. Dubbo的容错机制有哪些。(2017-11-23-gxb) 443
2. 使用dubbo遇到过哪些问题?(2017-11-23-gxb) 444
3. Dubbo的连接方式有哪些?(2017-12-1-lyq) 445
四、并发相关 448
1. 如何测试并发量?(2017-11-23-gxb) 448
五、Nginx 448
1. Nginx反向代理为什么能够提升服务器性能?(2017-11-24-gxb) 448
2. Nginx 和 Apache 各有什么优缺点? (2017-11-24-gxb) 449
3. Nginx 多进程模型是如何实现高并发的?(2017-12-5-lyq) 449
六、Zookeeper 450
1. 简单介绍一下zookeeper以及zookeeper的原理。(2017-11-24-gxb) 450
七、solr 451
1. 简单介绍一下solr(2017-11-24-gxb) 451
2. solr怎么设置搜索结果排名靠前?(2017-11-24-gxb) 452
3. solr中IK分词器原理是什么?(2017-11-24-gxb) 452
八、webService 452
1. 什么是webService?(2017-11-24-lyq) 452
2. 常见的远程调用技术(2017-11-24-lyq) 452
九、Restful 453
1. 谈谈你对restful的理解以及在项目中的使用?(2017-11-30-wzz) 453
第九章企业实战面试题 454
一、智慧星(2017-11-25-wmm) 454
1. 选择题 454
2. 编程题 457
二、中讯志远科技(2017-11-26-wmm) 460
1. 问答题 460
三、腾讯(2016年校招面试题2017-11-29-wzy) 464
1. 选择题 464
四、北京宝蓝德股份科技有限公司(2017-12-03-wmm) 477
1.选择题 477
2.问答题 479
五、智慧流(2017-12-04-wmm) 481
1.选择题 481
2. 问答题 486
3. 逻辑思维题 487
六、某公司 (2017-12-05-wmm) 491
1. 选择题 491
2. 问答题 500
七、华胜天成(2017-12-11-wzy) 516
1. 不定项选择题 516
2. 简答题 527
八、诚迈(2017-12-7-lyq) 527
1. 选择题 527
2. 判断题 529
3. 简答题 529
4. 编程题 533
5. linux试题 537
6. 数据库试题 539
7. 应用服务器试题 540
九、科大讯飞(2017-12-11-lyq) 542
十、泰瑞(2017-12-16-wmm) 547
1. 笔试题 547
2. 上机题 548
十一、文思创新(2017-12-17-wmm) 551
1. 什么叫对象?什么叫类?什么面向对象(OOP)? 551
2. 相对于JDK1.4,JDK1.5 有哪些新特性? 552
3. JAVA中使用final修饰符,对程序有哪些影响? 552
4. Java环境变量Unix/Linux下如何配置? 553
5. 写出5个你在JAVA开发中常用的包含(全名),并简述其作用。 554
6. 写出5个常见的运行时异常(RuntimeException)。 555
7. 方法重载(overload)需要满足什么条件,方法覆盖/方法重写(override)需要满足什么条件?(二选一) 555
8. 继承(inheritance)的优缺点是什么? 556
9. 为什么要使用接口和抽象类? 556
10. 什么是自定义异常?如何自定义异常? 558
11. Set,List,Map有什么区别? 558
12. 什么叫对象持久化(OBJect PERSIstence),为什么要进行对象持久化? 558
13. JavaScript 有哪些优缺点? 559
14. Jsp有什么特点? 560
15. 什么叫脏数据,什么叫脏读(Dirty Read) 561
第十章项目业务逻辑问题 561
一、传统项目(2017-12-5-lyq) 561
1. 什么是BOS? 561
2. Activity 工作流 562
[if !supportLists]第一章 [endif]内容介绍
该宝典是一份知识点全面又能不断更新,与时俱进的学习手册,不仅收录了作者亲身面试遇到的问题,还收录了近上万名黑马学子面试时遇到的问题。我们会一直不断地更新和充实该宝典,同时也希望读者朋友能够多多提供优质的面试题,也许下一个版本就有你提供的面试题哦。
本人的面试实战记录发布在黑马论坛:http://bbs.itheima.com/thread-196394-1-1.html
大家可以访问上面的网址,通过阳哥的实战记录略微感知一下真实面试的情况,从中学习一些面试技巧以便让自己在未来的面试中能够得心应手,顺利拿到自己喜欢的offer。
注意:该面试宝典仅供参考,由于作者本人的知识水平有限加之编写时间仓促因此难免有bug的存在,希望大家见谅。
该宝典的一个明确目标是能够让90%以上的Java技术面试题都落到该宝典中,如果您有不错的知识或者面试题,您可以发送到[email protected],本人将不胜感激。让天下没有难学的知识,希望你我的努力能帮到更多的莘莘学子。
世间事,很多都可投机取巧,但技术却必须靠日积月累的努力来提高。本宝典更加注重的是知识的掌握,而不仅仅是对面试题的应付。在展示常见的面试问题以及回答技巧的同时还详细讲解了每一道题所包含的知识点,让读者不仅知其然,更知其所以然。
[if !supportLists]第二章 [endif]JavaSE基础
[if !supportLists]一、[endif]Java面向对象
[if !supportLists]1. [endif]面向对象都有哪些特性以及你对这些特性的理解
1)继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
2)封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
3)多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
4)抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
注意:默认情况下面向对象有3大特性,封装、继承、多态,如果面试官问让说出4大特性,那么我们就把抽象加上去。
[if !supportLists]2. [endif]访问权限修饰符public、private、protected, 以及不写(默认)时的区别(2017-11-12)
该题目比较简单,不同的权限修饰符的区别见下表。
修饰符当前类同 包子 类其他包
public √√√√
protected√√√×
default√√××
private√×××
[if !supportLists]3. [endif]如何理解clone对象
3.1为什么要用clone?
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B 任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
3.2 new一个对象的过程和clone一个对象的过程区别
new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone在第一步是和new相似的,都是分配内存,调用clone方法时,分配的内存和原对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
3.3 clone对象的使用
3.3.1复制对象和复制引用的区别
[if !supportLists]1. [endif]Person p = new Person(23, "zhang");
[if !supportLists]2. [endif]Person p1 = p;
[if !supportLists]3. [endif]System.out.println(p);
[if !supportLists]4. [endif]System.out.println(p1);
当Person p1 = p;执行之后, 是创建了一个新的对象吗? 首先看打印结果:
[if !supportLists]1.[endif]com.itheima.Person@2f9ee1ac
[if !supportLists]2.[endif]com.itheima.Person@2f9ee1ac
可以看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p和p1只是引用而已,他们都指向了一个相同的对象Person(23, “zhang”) 。 可以把这种现象叫做引用的复制。上面代码执行完成之后, 内存中的情景如下图所示:
而下面的代码是真真正正的克隆了一个对象。
[if !supportLists]1.[endif]Person p = new Person(23, "zhang");
[if !supportLists]2.[endif]Person p1 = (Person) p.clone();
[if !supportLists]3.[endif]System.out.println(p);
[if !supportLists]4.[endif]System.out.println(p1);
从打印结果可以看出,两个对象的地址是不同的,也就是说创建了新的对象,而不是把原对象的地址赋给了一个新的引用变量:
[if !supportLists]1. [endif]com.itheima.Person@2f9ee1ac
[if !supportLists]2. [endif]com.itheima.Person@67f1fba0
以上代码执行完成后,内存中的情景如下图所示:
3.3.2 深拷贝和浅拷贝
上面的示例代码中,Person中有两个成员变量,分别是name和age, name是String类型, age是int类型。代码非常简单,如下所示:
[if !supportLists]1.[endif]public class Person implements Cloneable{
[if !supportLists]2.[endif]privatint age ;
[if !supportLists]3.[endif] private String name;
[if !supportLists]4.[endif] public Person(int age, String name) {
[if !supportLists]5.[endif] this.age = age;
[if !supportLists]6.[endif] this.name = name;
[if !supportLists]7.[endif] }
[if !supportLists]8.[endif] public Person() {}
[if !supportLists]9.[endif] public int getAge() {
[if !supportLists]10.[endif] return age;
[if !supportLists]11.[endif] }
[if !supportLists]12.[endif] public String getName() {
[if !supportLists]13.[endif] return name;
[if !supportLists]14.[endif] }
[if !supportLists]15.[endif] @Override
[if !supportLists]16.[endif] protected Object clone() throws CloneNotSupportedException {
[if !supportLists]17.[endif] return (Person)super.clone();
[if !supportLists]18.[endif] }
[if !supportLists]19.[endif]}
由于age是基本数据类型, 那么对它的拷贝没有什么疑议,直接将一个4字节的整数值拷贝过来就行。但是name是String类型的, 它只是一个引用, 指向一个真正的String对象,那么对它的拷贝有两种方式: 直接将原对象中的name的引用值拷贝给新对象的name字段, 或者是根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段。这两种拷贝方式分别叫做浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下图所示:
下面通过代码进行验证。如果两个Person对象的name的地址值相同, 说明两个对象的name都指向同一个String对象,也就是浅拷贝, 而如果两个对象的name的地址值不同, 那么就说明指向不同的String对象, 也就是在拷贝Person对象的时候, 同时拷贝了name引用的String对象, 也就是深拷贝。验证代码如下:
[if !supportLists]1. [endif]Person p = new Person(23, "zhang");
[if !supportLists]2. [endif]Person p1 = (Person) p.clone();
[if !supportLists]3. [endif]String result = p.getName() == p1.getName()
[if !supportLists]4. [endif]? "clone是浅拷贝的" : "clone是深拷贝的";
[if !supportLists]5. [endif]System.out.println(result);
打印结果为:
[if !supportLists]6. [endif]clone是浅拷贝的
所以,clone方法执行的是浅拷贝, 在编写程序时要注意这个细节。
如何进行深拷贝:
由上一节的内容可以得出如下结论:如果想要深拷贝一个对象,这个对象必须要实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象引用的其他对象也要clone一份,这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。那么,按照上面的结论,实现以下代码 Body类组合了Head类,要想深拷贝Body类,必须在Body类的clone方法中将Head类也要拷贝一份。代码如下:
[if !supportLists]1.[endif]static class Body implements Cloneable{
[if !supportLists]2.[endif] public Head head;
[if !supportLists]3.[endif] public Body() {}
[if !supportLists]4.[endif] public Body(Head head) {this.head = head;}
[if !supportLists]5.[endif] @Override
[if !supportLists]6.[endif] protected Object clone() throws CloneNotSupportedException {
[if !supportLists]7.[endif] Body newBody = (Body) super.clone();
[if !supportLists]8.[endif] newBody.head = (Head) head.clone();
[if !supportLists]9.[endif] return newBody;
[if !supportLists]10.[endif] }
[if !supportLists]11.[endif]}
[if !supportLists]12.[endif]static class Head implements Cloneable{
[if !supportLists]13.[endif] public Face face;
[if !supportLists]14.[endif] public Head() {}
[if !supportLists]15.[endif] @Override
[if !supportLists]16.[endif] protected Object clone() throws CloneNotSupportedException {
[if !supportLists]17.[endif] return super.clone();
[if !supportLists]18.[endif] } }
[if !supportLists]19.[endif]public static void main(String[] args) throws CloneNotSupportedException {
[if !supportLists]20.[endif] Body body = new Body(new Head(new Face()));
[if !supportLists]21.[endif] Body body1 = (Body) body.clone();
[if !supportLists]22.[endif] System.out.println("body == body1 : " + (body == body1) );
[if !supportLists]23.[endif] System.out.println("body.head == body1.head : " + (body.head == body1.head));
[if !supportLists]24.[endif]}
打印结果为:
[if !supportLists]1. [endif]body == body1 : false
[if !supportLists]2. [endif]body.head == body1.head : false
[if !supportLists]二、[endif]JavaSE语法(2017-11-12-wl)
[if !supportLists]1. [endif]Java有没有goto语句?(2017-11-12-wl)
goto是Java 中的保留字,在目前版本的Java中没有使用。根据 James Gosling(Java 之父)编写的《The Java Programming Language》一书的附录中给出了一个 Java 关键字列表,其中有 goto 和 const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉 C 语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字。
[if !supportLists]2. [endif]& 和 && 的区别(2017-11-12-wl)
&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
[if !supportLists]3. [endif]在Java中,如何跳出当前的多重嵌套循环(2017-11-14-wl)
在最外层循环前加一个标记如A,然后用 break A;可以跳出多重循环。(Java 中支持带标签的break和continue 语句,作用有点类似于 C 和 C++中的 goto 语句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用)。
[if !supportLists]4. [endif]两个对象值相同(x.equals(y) == true),但却可有不同的hashCode,这句话对不对?(2017-11-14-wl)
不对,如果两个对象x 和 y 满足 x.equals(y) == true,它们的哈希码(hashCode)应当相同。
Java 对于eqauls 方法和 hashCode 方法是这样规定的:(1)如果两个对象相同(equals 方法返回 true),那么它们的hashCode 值一定要相同;(2)如果两个对象的 hashCode 相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
关于equals 和 hashCode方法,很多Java程序员都知道,但很多人也就是仅仅知道而已,在Joshua Bloch
的大作《Effective Java》(很多软件公司,《Effective Java》、《Java 编程思想》以及《重构:改善既有代码质量》是 Java 程序员必看书籍,如果你还没看过,那就赶紧去买一本吧)中是这样介绍 equals 方法的。
首先equals 方法必须满足自反性(x.equals(x)必须返回 true)、对称性(x.equals(y)返回 true 时,y.equals(x)也必须返回 true)、传递性(x.equals(y)和 y.equals(z)都返回 true 时,x.equals(z)也必须返回 true)和一致性(当x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y)应该得到同样的返回值),而且对于任何非 null值的引用 x,x.equals(null)必须返回false。实现高质量的equals方法的诀窍包括:1. 使用==操作符检查"参数是否为这个对象的引用";2. 使用 instanceof 操作符检查"参数是否为正确的类型";3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;5. 重写 equals 时总是要重写 hashCode;6. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解。
[if !supportLists]5. [endif]是否可以继承String (2017-11-14-wl)
String类是final类,不可以被继承。
继承String 本身就是一个错误的行为,对 String 类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。
[if !supportLists]6. [endif]当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?(2017-11-14-wl)
是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和 C#中可以通过传引用或传输出参数来改变传入的参数的值。说明:Java中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类(将需要通过方法调用修改的引用置于一个 Wrapper 类中,再将Wrapper 对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从 C 和 C++转型为 Java 程序员的开发者无法容忍。
[if !supportLists]7. [endif]重载(overload)和重写(override)的区别?重载的方法能否根据返回类型进行区分?(2017-11-15-wl)
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
方法重载的规则:
[if !supportLists]1.[endif]方法名一致,参数列表中参数的顺序,类型,个数不同。
[if !supportLists]2.[endif]重载与方法的返回值无关,存在于父类和子类,同类中。
[if !supportLists]3.[endif]可以抛出不同的异常,可以有不同修饰符。
方法重写的规则:
[if !supportLists]1.[endif]参数列表必须完全与被重写方法的一致,返回类型必须完全与被重写方法的返回类型一致。
[if !supportLists]2.[endif]构造方法不能被重写,声明为final的方法不能被重写,声明为static的方法不能被重写,但是能够被再次声明。
[if !supportLists]3.[endif]访问权限不能比父类中被重写的方法的访问权限更低。
[if !supportLists]4.[endif]重写的方法能够抛出任何非强制异常(UncheckedException,也叫非运行时异常),无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
[if !supportLists]8. [endif]为什么函数不能根据返回类型来区分重载?(2017-11-15-wl)
该道题来自华为面试题。
因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。例如:
[if !supportLists]1.[endif]float max(int a, int b);
[if !supportLists]2.[endif]int max(int a, int b);
当调用max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
再比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:
[if !supportLists]1.[endif]void f() {}
[if !supportLists]2.[endif]int f() {}
若编译器可根据上下文(语境)明确判断出含义,比如在int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我们关心的不是返回值,而是方法调用的其他效果。所以假如我们像下面这样调用方法:f(); Java 怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能。
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。
[if !supportLists]9. [endif]char 型变量中能不能存储一个中文汉字,为什么?(2017-11-16-wl)
char 类型可以存储一个中文汉字,因为Java中使用的编码是 Unicode(不选择任何特定的编码,直接
使用字符在字符集中的编号,这是统一的唯一方法),一个char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是 Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader 和 OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内存的特征来实现了。
[if !supportLists]10. [endif]抽象类(abstract class)和接口(interface)有什么异同?(2017-11-16-wl)
不同:
抽象类:
1.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.接口中的成员全都是public的
4.抽象类中可以定义成员变量
5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
6.抽象类中可以包含静态方法
7.一个类只能继承一个抽象类
接口:
1.接口中不能定义构造器
2.方法全部都是抽象方法
3.抽象类中的成员可以是 private、默认、protected、public
4.接口中定义的成员变量实际上都是常量
5.接口中不能有静态方法
6.一个类可以实现多个接口
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
[if !supportLists]11. [endif]抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被synchronized(2017-11-16-wl)
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由
本地代码(如C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
[if !supportLists]12. [endif]阐述静态变量和实例变量的区别?(2017-11-16-wl)
静态变量: 是被static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
[if !supportLists]13. [endif]==和equals的区别?(2017-11-22-wzz)
equals和== 最大的区别是一个是方法一个是运算符。
==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
equals():用来比较方法两个对象的内容是否相等。
注意:equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
[if !supportLists]14. [endif]break和continue的区别?(2017-11-23-wzz)
break和continue都是用来控制循环的语句。
break用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue用于跳过本次循环,执行下次循环。
[if !supportLists]15. [endif]String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?(2017-12-1-lyq)
没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s原先指向一个String对象,内容是 "Hello",然后我们对s进行了“+”操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个 String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为 String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:
[if !supportLists]1.[endif]public class Demo {
[if !supportLists]2.[endif] private String s;
[if !supportLists]3.[endif] ...
[if !supportLists]4.[endif] s = "Initial Value";
[if !supportLists]5.[endif] ...
[if !supportLists]6.[endif]}
而非
[if !supportLists]1.[endif]s = new String("Initial Value");
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。 至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即 StringBuffer。
[if !supportLists]三、[endif]Java中的多态
[if !supportLists]1. [endif]Java中实现多态的机制是什么?
靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
[if !supportLists]四、[endif]Java的异常处理
[if !supportLists]1. [endif]Java中异常分为哪些种类
[if !supportLists]1)[endif]按照异常需要处理的时机分为编译时异常(也叫强制性异常)也叫CheckedException和运行时异常(也叫非强制性异常)也叫RuntimeException。只有java语言提供了Checked异常,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。这体现了Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种:
1 当前方法知道如何处理该异常,则用try...catch块来处理该异常。
2 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
运行时异常只有当代码在运行时才发行的异常,编译时不需要try catch。Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
[if !supportLists]2. [endif]调用下面的方法,得到的返回值是什么?
[if !supportLists]1. [endif]public int getNum(){
[if !supportLists]2. [endif] try {
[if !supportLists]3. [endif] int a = 1/0;
[if !supportLists]4. [endif] return 1;
[if !supportLists]5. [endif] } catch (Exception e) {
[if !supportLists]6. [endif] return 2;
[if !supportLists]7. [endif] }finally{
[if !supportLists]8. [endif] return 3;
[if !supportLists]9. [endif] }
代码在走到第3行的时候遇到了一个MathException,这时第四行的代码就不会执行了,代码直接跳转到catch语句中,走到第6行的时候,异常机制有这么一个原则如果在catch中遇到了return或者异常等能使该函数终止的话那么有finally就必须先执行完finally代码块里面的代码然后再返回值。因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。如果finally仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3。
[if !supportLists]3. [endif]error和exception的区别?(2017-2-23)
Error类和Exception类的父类都是Throwable类,他们的区别如下。
Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。
[if !supportLists]4. [endif]java异常处理机制(2017-2-23)
Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception,Error表示应用程序本身无法克服和恢复的一种严重问题。Exception表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常,系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException);普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。
java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch处理或用throws声明,所以系统异常也称为unchecked异常。
[if !supportLists]5. [endif]请写出你最常见的5个RuntimeException(2017-11-22-wzz)
下面列举几个常见的RuntimeException。
1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
2)java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
3)java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
4)java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
5)java.lang.IllegalArgumentException 方法传递参数错误。
6)java.lang.ClassCastException 数据类型转换异常。
7)java.lang.NoClassDefFoundException 未找到类定义错误。
8)SQLException SQL异常,常见于操作数据库时的SQL语句错误。
9)java.lang.InstantiationException实例化异常。
10)java.lang.NoSuchMethodException 方法不存在异常。
[if !supportLists]6. [endif]throw和throws的区别(2017-11-22-wzz)
throw:
1)throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
2)throw是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行throw一定是抛出了某种异常。
throws:
1)throws语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
2)throws主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
3)throws表示出现异常的一种可能性,并不一定会发生这种异常。
[if !supportLists]7. [endif]final、finally、finalize的区别?(2017-11-23-wzz)
1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
2)finally:异常处理语句结构的一部分,表示总是执行。
3)finalize:Object类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。
[if !supportLists]五、[endif]JavaSE常用API
[if !supportLists]1. [endif]Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少?(2017-11-14-wl)
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5然后进行取整。
[if !supportLists]2. [endif]switch是否能作用在byte上,是否能作用在long上,是否能作用在String上?(2017-11-14-wl)
Java5以前switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型。
从Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
[if !supportLists]3. [endif]数组有没有length() 方法?String有没有length() 方法?(2017-11-14-wl)
数组没有length()方法,而是有length 的属性。String有length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。
[if !supportLists]4. [endif]String 、StringBuilder 、StringBuffer的区别?(2017-11-14-wl)
Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们都可以储存和操作字符串,区别如下。
[if !supportLists]1)[endif]String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。初学者可能会有这样的误解:
[if !supportLists]1. [endif]String str = “abc”;
[if !supportLists]2. [endif]str = “bcd”;
如上,字符串str明明是可以改变的呀!其实不然,str仅仅是一个引用对象,它指向一个字符串对象“abc”。第二行代码的含义是让str重新指向了一个新的字符串“bcd”对象,而“abc”对象并没有任何改变,只不过该对象已经成为一个不可及对象罢了。
2)StringBuffer/StringBuilder表示的字符串对象可以直接进行修改。
3)StringBuilder是Java5中引入的,它和 StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被synchronized修饰,因此它的效率理论上也比StringBuffer要高。
[if !supportLists]5. [endif]什么情况下用“+”运算符进行字符串连接比调用StringBuffer/StringBuilder 对象的append方法连接字符串性能更好?(2017-11-14-wl)
该题来自华为。
字符串是Java程序中最常用的数据结构之一。在Java中String类已经重载了"+"。也就是说,字符串可以直接使用"+"进行连接,如下面代码所示:
[if !supportLists]1.[endif]String s = "abc" + "ddd";
但这样做真的好吗?当然,这个问题不能简单地回答yes or no。要根据具体情况来定。在Java中提供了一个StringBuilder类(这个类只在J2SE5及以上版本提供,以前的版本使用StringBuffer类),这个类也可以起到"+"的作用。那么我们应该用哪个呢?
下面让我们先看看如下的代码:
[if !supportLists]1. [endif]package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] public class TestSimplePlus
[if !supportLists]4. [endif] {
[if !supportLists]5. [endif] public static void main(String[] args)
[if !supportLists]6. [endif] {
[if !supportLists]7. [endif] String s = "abc";
[if !supportLists]8. [endif] String ss = "ok" + s + "xyz" + 5;
[if !supportLists]9. [endif] System.out.println(ss);
[if !supportLists]10. [endif] }
[if !supportLists]11. [endif] }
上面的代码将会输出正确的结果。从表面上看,对字符串和整型使用"+"号并没有什么区别,但事实真的如此吗?下面让我们来看看这段代码的本质。
我们首先使用反编译工具(如jdk带的javap、或jad)将TestSimplePlus反编译成Java Byte Code,其中的奥秘就一目了然了。在本文将使用jad来反编译,命令如下:jad -o -a -s d.java TestSimplePlus.class
反编译后的代码如下:
[if !supportLists]1. [endif]package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]import java.io.PrintStream;
[if !supportLists]4. [endif]
[if !supportLists]5. [endif]public class TestSimplePlus
[if !supportLists]6. [endif]{
[if !supportLists]7. [endif] public TestSimplePlus()
[if !supportLists]8. [endif] {
[if !supportLists]9. [endif] // 0 0:aload_0
[if !supportLists]10. [endif] // 1 1:invokespecial #8
[if !supportLists]11. [endif] // 2 4:return
[if !supportLists]12. [endif] }
[if !supportLists]13. [endif]
[if !supportLists]14. [endif] public static void main(String args[])
[if !supportLists]15. [endif] {
[if !supportLists]16. [endif] String s = "abc";
[if !supportLists]17. [endif] // 0 0:ldc1 #16
[if !supportLists]18. [endif] // 1 2:astore_1
[if !supportLists]19. [endif] String ss = (new StringBuilder("ok")).append(s).append("xyz").append(5).toString();
[if !supportLists]20. [endif] // 2 3:new #18
[if !supportLists]21. [endif] // 3 6:dup
[if !supportLists]22. [endif] // 4 7:ldc1 #20
[if !supportLists]23. [endif] // 5 9:invokespecial #22
[if !supportLists]24. [endif] // 6 12:aload_1
[if !supportLists]25. [endif] // 7 13:invokevirtual #25
[if !supportLists]26. [endif] // 8 16:ldc1 #29
[if !supportLists]27. [endif] // 9 18:invokevirtual #25
[if !supportLists]28. [endif] // 10 21:iconst_5
[if !supportLists]29. [endif] // 11 22:invokevirtual #31
[if !supportLists]30. [endif] // 12 25:invokevirtual #34
[if !supportLists]31. [endif] // 13 28:astore_2
[if !supportLists]32. [endif] System.out.println(ss);
[if !supportLists]33. [endif] // 14 29:getstatic #38
[if !supportLists]34. [endif] // 15 32:aload_2
[if !supportLists]35. [endif] // 16 33:invokevirtual #44
[if !supportLists]36. [endif] // 17 36:return
[if !supportLists]37. [endif] }
[if !supportLists]38. [endif]}
读者可能看到上面的Java字节码感到迷糊,不过大家不必担心。本文的目的并不是讲解Java Byte Code,因此,并不用了解具体的字节码的含义。
使用jad反编译的好处之一就是可以同时生成字节码和源代码。这样可以进行对照研究。从上面的代码很容易看出,虽然在源程序中使用了"+",但在编译时仍然将"+"转换成StringBuilder。因此,我们可以得出结论,在Java中无论使用何种方式进行字符串连接,实际上都使用的是StringBuilder。
那么是不是可以根据这个结论推出使用"+"和StringBuilder的效果是一样的呢?这个要从两个方面的解释。如果从运行结果来解释,那么"+"和StringBuilder是完全等效的。但如果从运行效率和资源消耗方面看,那它们将存在很大的区别。
当然,如果连接字符串行表达式很简单(如上面的顺序结构),那么"+"和StringBuilder基本是一样的,但如果结构比较复杂,如使用循环来连接字符串,那么产生的Java Byte Code就会有很大的区别。先让我们看看如下的代码:
[if !supportLists]1. [endif]package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] import java.util.*;
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] public class TestComplexPlus
[if !supportLists]6. [endif] {
[if !supportLists]7. [endif] public static void main(String[] args)
[if !supportLists]8. [endif] {
[if !supportLists]9. [endif] String s = "";
[if !supportLists]10. [endif] Random rand = new Random();
[if !supportLists]11. [endif] for (int i = 0; i < 10; i++)
[if !supportLists]12. [endif] {
[if !supportLists]13. [endif] s = s + rand.nextInt(1000) + " ";
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif] System.out.println(s);
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif] }
上面的代码返编译后的Java Byte Code如下:
[if !supportLists]1. [endif]package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]import java.io.PrintStream;
[if !supportLists]4. [endif]import java.util.Random;
[if !supportLists]5. [endif]
[if !supportLists]6. [endif]public class TestComplexPlus
[if !supportLists]7. [endif]{
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] public TestComplexPlus()
[if !supportLists]10. [endif] {
[if !supportLists]11. [endif] // 0 0:aload_0
[if !supportLists]12. [endif] // 1 1:invokespecial #8
[if !supportLists]13. [endif] // 2 4:return
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] public static void main(String args[])
[if !supportLists]17. [endif] {
[if !supportLists]18. [endif] String s = "";
[if !supportLists]19. [endif] // 0 0:ldc1 #16
[if !supportLists]20. [endif] // 1 2:astore_1
[if !supportLists]21. [endif] Random rand = new Random();
[if !supportLists]22. [endif] // 2 3:new #18
[if !supportLists]23. [endif] // 3 6:dup
[if !supportLists]24. [endif] // 4 7:invokespecial #20
[if !supportLists]25. [endif] // 5 10:astore_2
[if !supportLists]26. [endif] for(int i = 0; i < 10; i++)
[if !supportLists]27. [endif] //* 6 11:iconst_0
[if !supportLists]28. [endif] //* 7 12:istore_3
[if !supportLists]29. [endif] //* 8 13:goto 49
[if !supportLists]30. [endif] s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").toString();
[if !supportLists]31. [endif] // 9 16:new #21
[if !supportLists]32. [endif] // 10 19:dup
[if !supportLists]33. [endif] // 11 20:aload_1
[if !supportLists]34. [endif] // 12 21:invokestatic #23
[if !supportLists]35. [endif] // 13 24:invokespecial #29
[if !supportLists]36. [endif] // 14 27:aload_2
[if !supportLists]37. [endif] // 15 28:sipush 1000
[if !supportLists]38. [endif] // 16 31:invokevirtual #32
[if !supportLists]39. [endif] // 17 34:invokevirtual #36
[if !supportLists]40. [endif] // 18 37:ldc1 #40
[if !supportLists]41. [endif] // 19 39:invokevirtual #42
[if !supportLists]42. [endif] // 20 42:invokevirtual #45
[if !supportLists]43. [endif] // 21 45:astore_1
[if !supportLists]44. [endif]
[if !supportLists]45. [endif] // 22 46:iinc 3 1
[if !supportLists]46. [endif] // 23 49:iload_3
[if !supportLists]47. [endif] // 24 50:bipush 10
[if !supportLists]48. [endif] // 25 52:icmplt 16
[if !supportLists]49. [endif] System.out.println(s);
[if !supportLists]50. [endif] // 26 55:getstatic #49
[if !supportLists]51. [endif] // 27 58:aload_1
[if !supportLists]52. [endif] // 28 59:invokevirtual #55
[if !supportLists]53. [endif] // 29 62:return
[if !supportLists]54. [endif] }
[if !supportLists]55. [endif]}
大家可以看到,虽然编译器将"+"转换成了StringBuilder,但创建StringBuilder对象的位置却在for语句内部。这就意味着每执行一次循环,就会创建一个StringBuilder对象(对于本例来说,是创建了10个StringBuilder对象),虽然Java有垃圾回收器,但这个回收器的工作时间是不定的。如果不断产生这样的垃圾,那么仍然会占用大量的资源。解决这个问题的方法就是在程序中直接使用StringBuilder来连接字符串,代码如下:
[if !supportLists]1. [endif]package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]import java.util.*;
[if !supportLists]4. [endif]
[if !supportLists]5. [endif]public class TestStringBuilder
[if !supportLists]6. [endif]{
[if !supportLists]7. [endif] public static void main(String[] args)
[if !supportLists]8. [endif] {
[if !supportLists]9. [endif] String s = "";
[if !supportLists]10. [endif] Random rand = new Random();
[if !supportLists]11. [endif] StringBuilder result = new StringBuilder();
[if !supportLists]12. [endif] for (int i = 0; i < 10; i++)
[if !supportLists]13. [endif] {
[if !supportLists]14. [endif] result.append(rand.nextInt(1000));
[if !supportLists]15. [endif] result.append(" ");
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif] System.out.println(result.toString());
[if !supportLists]18. [endif] }
[if !supportLists]19. [endif]}
上面代码反编译后的结果如下:
[if !supportLists]1. [endif]20.package string;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]import java.io.PrintStream;
[if !supportLists]4. [endif]import java.util.Random;
[if !supportLists]5. [endif]
[if !supportLists]6. [endif]public class TestStringBuilder
[if !supportLists]7. [endif]{
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] public TestStringBuilder()
[if !supportLists]10. [endif] {
[if !supportLists]11. [endif] // 0 0:aload_0
[if !supportLists]12. [endif] // 1 1:invokespecial #8
[if !supportLists]13. [endif] // 2 4:return
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] public static void main(String args[])
[if !supportLists]17. [endif] {
[if !supportLists]18. [endif] String s = "";
[if !supportLists]19. [endif] // 0 0:ldc1 #16
[if !supportLists]20. [endif] // 1 2:astore_1
[if !supportLists]21. [endif] Random rand = new Random();
[if !supportLists]22. [endif] // 2 3:new #18
[if !supportLists]23. [endif] // 3 6:dup
[if !supportLists]24. [endif] // 4 7:invokespecial #20
[if !supportLists]25. [endif] // 5 10:astore_2
[if !supportLists]26. [endif] StringBuilder result = new StringBuilder();
[if !supportLists]27. [endif] // 6 11:new #21
[if !supportLists]28. [endif] // 7 14:dup
[if !supportLists]29. [endif] // 8 15:invokespecial #23
[if !supportLists]30. [endif] // 9 18:astore_3
[if !supportLists]31. [endif] for(int i = 0; i < 10; i++)
[if !supportLists]32. [endif] //* 10 19:iconst_0
[if !supportLists]33. [endif] //* 11 20:istore 4
[if !supportLists]34. [endif] //* 12 22:goto 47
[if !supportLists]35. [endif] {
[if !supportLists]36. [endif] result.append(rand.nextInt(1000));
[if !supportLists]37. [endif] // 13 25:aload_3
[if !supportLists]38. [endif] // 14 26:aload_2
[if !supportLists]39. [endif] // 15 27:sipush 1000
[if !supportLists]40. [endif] // 16 30:invokevirtual #24
[if !supportLists]41. [endif] // 17 33:invokevirtual #28
[if !supportLists]42. [endif] // 18 36:pop
[if !supportLists]43. [endif] result.append(" ");
[if !supportLists]44. [endif] // 19 37:aload_3
[if !supportLists]45. [endif] // 20 38:ldc1 #32
[if !supportLists]46. [endif] // 21 40:invokevirtual #34
[if !supportLists]47. [endif] // 22 43:pop
[if !supportLists]48. [endif] }
[if !supportLists]49. [endif]
[if !supportLists]50. [endif] // 23 44:iinc 4 1
[if !supportLists]51. [endif] // 24 47:iload 4
[if !supportLists]52. [endif] // 25 49:bipush 10
[if !supportLists]53. [endif] // 26 51:icmplt 25
[if !supportLists]54. [endif] System.out.println(result.toString());
[if !supportLists]55. [endif] // 27 54:getstatic #37
[if !supportLists]56. [endif] // 28 57:aload_3
[if !supportLists]57. [endif] // 29 58:invokevirtual #43
[if !supportLists]58. [endif] // 30 61:invokevirtual #47
[if !supportLists]59. [endif] // 31 64:return
[if !supportLists]60. [endif] }
[if !supportLists]61. [endif]}
从上面的反编译结果可以看出,创建StringBuilder的代码被放在了for语句外。虽然这样处理在源程序中看起来复杂,但却换来了更高的效率,同时消耗的资源也更少了。
在使用StringBuilder时要注意,尽量不要"+"和StringBuilder混着用,否则会创建更多的StringBuilder对象,如下面代码所:
for (int i = 0; i < 10; i++) { result.append(rand.nextInt(1000)); result.append(" "); }
改成如下形式:
for (int i = 0; i < 10; i++){ result.append(rand.nextInt(1000) + " ");}
则反编译后的结果如下:
for(int i = 0; i < 10; i++) //* 10 19:iconst_0 //* 11 20:istore 4 //* 12 22:goto 65 { result.append((new StringBuilder(String.valueOf(rand.nextInt(1000)))).append(" ").toString()); // 13 25:aload_3 // 14 26:new #21 // 15 29:dup
从上面的代码可以看出,Java编译器将"+"编译成了StringBuilder,这样for语句每循环一次,又创建了一个StringBuilder对象。 如果将上面的代码在JDK1.4下编译,必须将StringBuilder改为StringBuffer,而JDK1.4将"+"转换为StringBuffer(因为JDK1.4并没有提供StringBuilder类)。StringBuffer和StringBuilder的功能基本一样,只是StringBuffer是线程安全的,而StringBuilder不是线程安全的。因此,StringBuilder的效率会更高。
[if !supportLists]6. [endif]请说出下面程序的输出(2017-11-14-wl)
[if !supportLists]1. [endif]class StringEqualTest {
[if !supportLists]2. [endif] public static void main(String[] args) {
[if !supportLists]3. [endif] String s1 = "Programming";
[if !supportLists]4. [endif] String s2 = new String("Programming");
[if !supportLists]5. [endif] String s3 = "Program";
[if !supportLists]6. [endif] String s4 = "ming";
[if !supportLists]7. [endif] String s5 = "Program" + "ming";
[if !supportLists]8. [endif] String s6 = s3 + s4;
[if !supportLists]9. [endif] System.out.println(s1 == s2); //false
[if !supportLists]10. [endif] System.out.println(s1 == s5); //true
[if !supportLists]11. [endif] System.out.println(s1 == s6); //false
[if !supportLists]12. [endif] System.out.println(s1 == s6.intern()); //true
[if !supportLists]13. [endif] System.out.println(s2 == s2.intern()); //false
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
补充:解答上面的面试题需要知道如下两个知识点:
1. String对象的intern()方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String 对象的 equals结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
2. 字符串的+操作其本质是创建了StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象,这一点可以用 javap -c StringEqualTest.class 命令获得 class 文件对应的 JVM 字节码指令就可以看出来。
[if !supportLists]7. [endif]Java中的日期和时间(2017-11-19-wl)
7.1如何取得年月日、小时分钟秒?(2017-11-19-wl)
[if !supportLists]1. [endif]public class DateTimeTest {
[if !supportLists]2. [endif] public static void main(String[] args) {
[if !supportLists]3. [endif] Calendar cal = Calendar.getInstance();
[if !supportLists]4. [endif] System.out.println(cal.get(Calendar.YEAR));
[if !supportLists]5. [endif] System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
[if !supportLists]6. [endif] System.out.println(cal.get(Calendar.DATE));
[if !supportLists]7. [endif] System.out.println(cal.get(Calendar.HOUR_OF_DAY));
[if !supportLists]8. [endif] System.out.println(cal.get(Calendar.MINUTE));
[if !supportLists]9. [endif] System.out.println(cal.get(Calendar.SECOND));
[if !supportLists]10. [endif] // Java 8
[if !supportLists]11. [endif] LocalDateTime dt = LocalDateTime.now();
[if !supportLists]12. [endif] System.out.println(dt.getYear());
[if !supportLists]13. [endif] System.out.println(dt.getMonthValue()); // 1 - 12
[if !supportLists]14. [endif] System.out.println(dt.getDayOfMonth());
[if !supportLists]15. [endif] System.out.println(dt.getHour());
[if !supportLists]16. [endif] System.out.println(dt.getMinute());
[if !supportLists]17. [endif] System.out.println(dt.getSecond());
[if !supportLists]18. [endif] }
[if !supportLists]19. [endif]}
7.2如何取得从1970年1月1日0时0分0 秒到现在的毫秒数?(2017-11-19-wl)
[if !supportLists]1. [endif]Calendar.getInstance().getTimeInMillis(); //第一种方式
[if !supportLists]2. [endif]System.currentTimeMillis(); //第二种方式
[if !supportLists]3. [endif]// Java 8
[if !supportLists]4. [endif]Clock.systemDefaultZone().millis();
7.3如何取得某月的最后一天?(2017-11-19-wl)
[if !supportLists]1. [endif]//获取当前月第一天:
[if !supportLists]2. [endif]Calendar c = Calendar.getInstance();
[if !supportLists]3. [endif]c.add(Calendar.MONTH, 0);
[if !supportLists]4. [endif]c.set(Calendar.DAY_OF_MONTH,1);//设置为1号,当前日期既为本月第一天
[if !supportLists]5. [endif]String first = format.format(c.getTime());
[if !supportLists]6. [endif]System.out.println("===============first:"+first);
[if !supportLists]7. [endif]
[if !supportLists]8. [endif]//获取当前月最后一天
[if !supportLists]9. [endif] Calendar ca = Calendar.getInstance();
[if !supportLists]10. [endif]ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
[if !supportLists]11. [endif]String last = format.format(ca.getTime());
[if !supportLists]12. [endif]System.out.println("===============last:"+last);
[if !supportLists]13. [endif]
[if !supportLists]14. [endif]//Java 8
[if !supportLists]15. [endif]LocalDate today = LocalDate.now();
[if !supportLists]16. [endif]//本月的第一天
[if !supportLists]17. [endif]LocalDate firstday = LocalDate.of(today.getYear(),today.getMonth(),1);
[if !supportLists]18. [endif]//本月的最后一天
[if !supportLists]19. [endif]LocalDate lastDay =today.with(TemporalAdjusters.lastDayOfMonth());
[if !supportLists]20. [endif]System.out.println("本月的第一天"+firstday);
[if !supportLists]21. [endif]System.out.println("本月的最后一天"+lastDay);
7.4如何格式化日期?(2017-11-19-wl)
1)Java.text.DataFormat 的子类(如 SimpleDateFormat 类)中的 format(Date)方法可将日期格式化。
2)Java 8 中可以用 java.time.format.DateTimeFormatter来格式化时间日期,代码如下所示:
[if !supportLists]1. [endif]import java.text.SimpleDateFormat;
[if !supportLists]2. [endif]import java.time.LocalDate;
[if !supportLists]3. [endif]import java.time.format.DateTimeFormatter;
[if !supportLists]4. [endif]import java.util.Date;
[if !supportLists]5. [endif]class DateFormatTest {
[if !supportLists]6. [endif]
[if !supportLists]7. [endif] public static void main(String[] args) {
[if !supportLists]8. [endif] SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
[if !supportLists]9. [endif] Date date1 = new Date();
[if !supportLists]10. [endif] System.out.println(oldFormatter.format(date1));
[if !supportLists]11. [endif]
[if !supportLists]12. [endif] // Java 8
[if !supportLists]13. [endif] DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
[if !supportLists]14. [endif] LocalDate date2 = LocalDate.now();
[if !supportLists]15. [endif] System.out.println(date2.format(newFormatter));
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]}
补充:Java的时间日期API一直以来都是被诟病的东西,为了解决这一问题,Java 8中引入了新的时间日期API,其中包括 LocalDate、LocalTime、LocalDateTime、Clock、Instant 等类,这些的类的设计都使用了不变模式,因此是线程安全的设计。
7.5打印昨天的当前时刻? (2017-11-19-wl)
[if !supportLists]1. [endif]import java.util.Calendar;
[if !supportLists]2. [endif]class YesterdayCurrent {
[if !supportLists]3. [endif] public static void main(String[] args){
[if !supportLists]4. [endif] Calendar cal = Calendar.getInstance();
[if !supportLists]5. [endif] cal.add(Calendar.DATE, -1);
[if !supportLists]6. [endif] System.out.println(cal.getTime());
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif]}
[if !supportLists]9. [endif]
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]//java-8
[if !supportLists]12. [endif]import java.time.LocalDateTime;
[if !supportLists]13. [endif]class YesterdayCurrent {
[if !supportLists]14. [endif] public static void main(String[] args) {
[if !supportLists]15. [endif] LocalDateTime today = LocalDateTime.now();
[if !supportLists]16. [endif] LocalDateTime yesterday = today.minusDays(1);
[if !supportLists]17. [endif] System.out.println(yesterday);
[if !supportLists]18. [endif] }
[if !supportLists]19. [endif]}
7.6 Java8的日期特性? (2017-12-3-wl)
Java 8日期/时间特性
Java 8日期/时间API是JSR-310的实现,它的实现目标是克服旧的日期时间实现中所有的缺陷,新的日期/时间API的一些设计原则是:
[if !supportLists]l [endif]不变性:新的日期/时间API中,所有的类都是不可变的,这对多线程环境有好处。
[if !supportLists]l [endif]关注点分离:新的API将人可读的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区定义了不同的类。
[if !supportLists]l [endif]清晰:在所有的类中,方法都被明确定义用以完成相同的行为。举个例子,要拿到当前实例我们可以使用now()方法,在所有的类中都定义了format()和parse()方法,而不是像以前那样专门有一个独立的类。为了更好的处理问题,所有的类都使用了工厂模式和策略模式,一旦你使用了其中某个类的方法,与其他类协同工作并不困难。
[if !supportLists]l [endif]实用操作:所有新的日期/时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。
[if !supportLists]l [endif]可扩展性:新的日期/时间API是工作在ISO-8601日历系统上的,但我们也可以将其应用在非ISO的日历上。
Java 8日期/时间API包解释
[if !supportLists]l [endif]java.time包:这是新的Java日期/时间API的基础包,所有的主要基础类都是这个包的一部分,如:LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration等等。所有这些类都是不可变的和线程安全的,在绝大多数情况下,这些类能够有效地处理一些公共的需求。
[if !supportLists]l [endif]java.time.chrono包:这个包为非ISO的日历系统定义了一些泛化的API,我们可以扩展AbstractChronology类来创建自己的日历系统。
[if !supportLists]l [endif]java.time.format包:这个包包含能够格式化和解析日期时间对象的类,在绝大多数情况下,我们不应该直接使用它们,因为java.time包中相应的类已经提供了格式化和解析的方法。
[if !supportLists]l [endif]java.time.temporal包:这个包包含一些时态对象,我们可以用其找出关于日期/时间对象的某个特定日期或时间,比如说,可以找到某月的第一天或最后一天。你可以非常容易地认出这些方法,因为它们都具有“withXXX”的格式。
[if !supportLists]l [endif]java.time.zone包:这个包包含支持不同时区以及相关规则的类。
Java 8日期/时间常用API
1.java.time.LocalDate
LocalDate是一个不可变的类,它表示默认格式(yyyy-MM-dd)的日期,我们可以使用now()方法得到当前时间,也可以提供输入年份、月份和日期的输入参数来创建一个LocalDate实例。该类为now()方法提供了重载方法,我们可以传入ZoneId来获得指定时区的日期。该类提供与java.sql.Date相同的功能,对于如何使用该类,我们来看一个简单的例子。
package com.journaldev.java8.time;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
/**
* LocalDate Examples
* @author pankaj
*
*/
public class LocalDateExample {
public static void main(String[] args) {
//Current Date
LocalDate today = LocalDate.now();
System.out.println("Current Date="+today);
//Creating LocalDate by providing input arguments
LocalDate firstDay_2014 = LocalDate.of(2014, Month.JANUARY, 1);
System.out.println("Specific Date="+firstDay_2014);
//Try creating date by providing invalid inputs
//LocalDate feb29_2014 = LocalDate.of(2014, Month.FEBRUARY, 29);
//Exception in thread "main" java.time.DateTimeException:
//Invalid date 'February 29' as '2014' is not a leap year
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalDate todayKolkata = LocalDate.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Date in IST="+todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDate todayIST = LocalDate.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalDate dateFromBase = LocalDate.ofEpochDay(365);
System.out.println("365th day from base date= "+dateFromBase);
LocalDate hundredDay2014 = LocalDate.ofYearDay(2014, 100);
System.out.println("100th day of 2014="+hundredDay2014);
}
}
输出:
Current Date=2014-04-28
Specific Date=2014-01-01
Current Date in IST=2014-04-29
365th day from base date= 1971-01-01
100th day of 2014=2014-04-10
2.java.time.LocalTime
LocalTime是一个不可变的类,它的实例代表一个符合人类可读格式的时间,默认格式是hh:mm:ss.zzz。像LocalDate一样,该类也提供了时区支持,同时也可以传入小时、分钟和秒等输入参数创建实例,我们来看一个简单的程序,演示该类的使用方法。
package com.journaldev.java8.time;
import java.time.LocalTime;
import java.time.ZoneId;
/**
* LocalTime Examples
*/
public class LocalTimeExample {
public static void main(String[] args) {
//Current Time
LocalTime time = LocalTime.now();
System.out.println("Current Time="+time);
//Creating LocalTime by providing input arguments
LocalTime specificTime = LocalTime.of(12,20,25,40);
System.out.println("Specific Time of Day="+specificTime);
//Try creating time by providing invalid inputs
//LocalTime invalidTime = LocalTime.of(25,20);
//Exception in thread "main" java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalTime timeKolkata = LocalTime.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Time in IST="+timeKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalTime todayIST = LocalTime.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalTime specificSecondTime = LocalTime.ofSecondOfDay(10000);
System.out.println("10000th second time= "+specificSecondTime);
}
}
输出:
Current Time=15:51:45.240
Specific Time of Day=12:20:25.000000040
Current Time in IST=04:21:45.276
10000th second time= 02:46:40
3. java.time.LocalDateTime
LocalDateTime是一个不可变的日期-时间对象,它表示一组日期-时间,默认格式是yyyy-MM-dd-HH-mm-ss.zzz。它提供了一个工厂方法,接收LocalDate和LocalTime输入参数,创建LocalDateTime实例。我们来看一个简单的例子。
package com.journaldev.java8.time;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;
public class LocalDateTimeExample {
public static void main(String[] args) {
//Current Date
LocalDateTime today = LocalDateTime.now();
System.out.println("Current DateTime="+today);
//Current Date using LocalDate and LocalTime
today = LocalDateTime.of(LocalDate.now(), LocalTime.now());
System.out.println("Current DateTime="+today);
//Creating LocalDateTime by providing input arguments
LocalDateTime specificDate = LocalDateTime.of(2014, Month.JANUARY, 1, 10, 10, 30);
System.out.println("Specific Date="+specificDate);
//Try creating date by providing invalid inputs
//LocalDateTime feb29_2014 = LocalDateTime.of(2014, Month.FEBRUARY, 28, 25,1,1);
//Exception in thread "main" java.time.DateTimeException:
//Invalid value for HourOfDay (valid values 0 - 23): 25
//Current date in "Asia/Kolkata", you can get it from ZoneId javadoc
LocalDateTime todayKolkata = LocalDateTime.now(ZoneId.of("Asia/Kolkata"));
System.out.println("Current Date in IST="+todayKolkata);
//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
//LocalDateTime todayIST = LocalDateTime.now(ZoneId.of("IST"));
//Getting date from the base date i.e 01/01/1970
LocalDateTime dateFromBase = LocalDateTime.ofEpochSecond(10000, 0, ZoneOffset.UTC);
System.out.println("10000th second time from 01/01/1970= "+dateFromBase);
}
}
输出:
Current DateTime=2014-04-28T16:00:49.455
Current DateTime=2014-04-28T16:00:49.493
Specific Date=2014-01-01T10:10:30
Current Date in IST=2014-04-29T04:30:49.493
10000th second time from 01/01/1970= 1970-01-01T02:46:40
在所有这三个例子中,我们已经看到如果我们提供了无效的参数去创建日期/时间,那么系统会抛出java.time.DateTimeException,这是一种运行时异常,我们并不需要显式地捕获它。
同时我们也看到,能够通过传入ZoneId得到日期/时间数据,你可以从它的Javadoc中得到支持的Zoneid的列表,当运行以上类时,可以得到以上输出。
4. java.time.Instant
Instant类是用在机器可读的时间格式上的,它以Unix时间戳的形式存储日期时间,我们来看一个简单的程序
package com.journaldev.java8.time;
import java.time.Duration;
import java.time.Instant;
public class InstantExample {
public static void main(String[] args) {
//Current timestamp
Instant timestamp = Instant.now();
System.out.println("Current Timestamp = "+timestamp);
//Instant from timestamp
Instant specificTime = Instant.ofEpochMilli(timestamp.toEpochMilli());
System.out.println("Specific Time = "+specificTime);
//Duration example
Duration thirtyDay = Duration.ofDays(30);
System.out.println(thirtyDay);
}
}
输出:
Current Timestamp = 2014-04-28T23:20:08.489Z
Specific Time = 2014-04-28T23:20:08.489Z
PT720H
5.日期API工具
我们早些时候提到过,大多数日期/时间API类都实现了一系列工具方法,如:加/减天数、周数、月份数,等等。还有其他的工具方法能够使用TemporalAdjuster调整日期,并计算两个日期间的周期。
package com.journaldev.java8.time;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;
public class DateAPIUtilities {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
//Get the Year, check if it's leap year
System.out.println("Year "+today.getYear()+" is Leap Year? "+today.isLeapYear());
//Compare two LocalDate for before and after
System.out.println("Today is before 01/01/2015? "+today.isBefore(LocalDate.of(2015,1,1)));
//Create LocalDateTime from LocalDate
System.out.println("Current Time="+today.atTime(LocalTime.now()));
//plus and minus operations
System.out.println("10 days after today will be "+today.plusDays(10));
System.out.println("3 weeks after today will be "+today.plusWeeks(3));
System.out.println("20 months after today will be "+today.plusMonths(20));
System.out.println("10 days before today will be "+today.minusDays(10));
System.out.println("3 weeks before today will be "+today.minusWeeks(3));
System.out.println("20 months before today will be "+today.minusMonths(20));
//Temporal adjusters for adjusting the dates
System.out.println("First date of this month= "+today.
with(TemporalAdjusters.firstDayOfMonth()));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
System.out.println("Last date of this year= "+lastDayOfYear);
Period period = today.until(lastDayOfYear);
System.out.println("Period Format= "+period);
System.out.println("Months remaining in the year= "+period.getMonths());
}
}
输出:
Year 2014 is Leap Year? false
Today is before 01/01/2015? true
Current Time=2014-04-28T16:23:53.154
10 days after today will be 2014-05-08
3 weeks after today will be 2014-05-19
20 months after today will be 2015-12-28
10 days before today will be 2014-04-18
3 weeks before today will be 2014-04-07
20 months before today will be 2012-08-28
First date of this month= 2014-04-01
Last date of this year= 2014-12-31
Period Format= P8M3D
Months remaining in the year= 8
6.解析和格式化
将一个日期格式转换为不同的格式,之后再解析一个字符串,得到日期时间对象,这些都是很常见的。我们来看一下简单的例子。
package com.journaldev.java8.time;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateParseFormatExample {
public static void main(String[] args) {
//Format examples
LocalDate date = LocalDate.now();
//default format
System.out.println("Default format of LocalDate="+date);
//specific format
System.out.println(date.format(DateTimeFormatter.ofPattern("d::MMM::uuuu")));
System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
LocalDateTime dateTime = LocalDateTime.now();
//default format
System.out.println("Default format of LocalDateTime="+dateTime);
//specific format
System.out.println(dateTime.format(DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss")));
System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
Instant timestamp = Instant.now();
//default format
System.out.println("Default format of Instant="+timestamp);
//Parse examples
LocalDateTime dt = LocalDateTime.parse("27::Apr::2014 21::39::48",
DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss"));
System.out.println("Default format after parsing = "+dt);
}
}
输出:
Default format of LocalDate=2014-04-28
28::Apr::2014
20140428
Default format of LocalDateTime=2014-04-28T16:25:49.341
28::Apr::2014 16::25::49
20140428
Default format of Instant=2014-04-28T23:25:49.342Z
Default format after parsing = 2014-04-27T21:39:48
7.旧的日期时间支持
旧的日期/时间类已经在几乎所有的应用程序中使用,因此做到向下兼容是必须的。这也是为什么会有若干工具方法帮助我们将旧的类转换为新的类,反之亦然。我们来看一下简单的例子。
package com.journaldev.java8.time;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
public class DateAPILegacySupport {
public static void main(String[] args) {
//Date to Instant
Instant timestamp = new Date().toInstant();
//Now we can convert Instant to LocalDateTime or other similar classes
LocalDateTime date = LocalDateTime.ofInstant(timestamp,
ZoneId.of(ZoneId.SHORT_IDS.get("PST")));
System.out.println("Date = "+date);
//Calendar to Instant
Instant time = Calendar.getInstance().toInstant();
System.out.println(time);
//TimeZone to ZoneId
ZoneId defaultZone = TimeZone.getDefault().toZoneId();
System.out.println(defaultZone);
//ZonedDateTime from specific Calendar
ZonedDateTime gregorianCalendarDateTime = new GregorianCalendar().toZonedDateTime();
System.out.println(gregorianCalendarDateTime);
//Date API to Legacy classes
Date dt = Date.from(Instant.now());
System.out.println(dt);
TimeZone tz = TimeZone.getTimeZone(defaultZone);
System.out.println(tz);
GregorianCalendar gc = GregorianCalendar.from(gregorianCalendarDateTime);
System.out.println(gc);
}
}
输出:
Date = 2014-04-28T16:28:54.340
2014-04-28T23:28:54.395Z
America/Los_Angeles
2014-04-28T16:28:54.404-07:00[America/Los_Angeles]
Mon Apr 28 16:28:54 PDT 2014
sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]
java.util.GregorianCalendar[time=1398727734404,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2014,MONTH=3,WEEK_OF_YEAR=18,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=118,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=28,SECOND=54,MILLISECOND=404,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
补充:我们可以看到,旧的TimeZone和GregorianCalendar类的toString()方法太啰嗦了,一点都不友好。
7.7 Java8之前的日期和时间使用的槽点 (2017-11-19-wl)
Tiago Fernandez 做过一次投票,选举最烂的 JAVA API,排第一的 EJB2.X,第二的就是日期 API(Date和Calender)
[if !supportLists]1. [endif]槽点一
最开始的时候,Date 既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂(不懂单一职责,你妈妈知道吗?纯属恶搞~哈哈)
后来从JDK 1.1 开始,这三项职责分开了:
· 1)使用Calendar 类实现日期和时间字段之间转换;
· 2)使用DateFormat 类来格式化和分析日期字符串;
· 3)而Date 只用来承载日期和时间信息。
原有Date 中的相应方法已废弃。不过,无论是 Date,还是 Calendar,都用着太不方便了,这是 API 没有设计好的地方。
[if !supportLists]2. [endif]槽点二
坑爹的year和month。
我们看下面的代码:
[if !supportLists]1. [endif]Date date = new Date(2012,1,1);
[if !supportLists]2. [endif]System.out.println(date);
输出Thu Feb 01 00:00:00 CST 3912
观察输出结果,year 是 2012+1900,而 month,月份参数我不是给了 1 吗?怎么输出二月(Feb)了?
应该曾有人告诉你,如果你要设置日期,应该使用java.util.Calendar,像这样...
[if !supportLists]1. [endif]Calendar calendar = Calendar.getInstance();
[if !supportLists]2. [endif]calendar.set(2013, 8, 2);
这样写又不对了,calendar 的 month 也是从 0 开始的,表达 8 月份应该用 7 这个数字,要么就干脆用枚举
[if !supportLists]1. [endif]calendar.set(2013, Calendar.AUGUST, 2);
注意上面的代码,Calendar 年份的传值不需要减去 1900(当然月份的定义和 Date 还是一样),这种不一致真是让人抓狂!有些人可能知道,Calendar 相关的 API 是 IBM 捐出去的,所以才导致不一致。
[if !supportLists]3. [endif]槽点三
java.util.Date 与 java.util.Calendar 中的所有属性都是可变的
下面的代码,计算两个日期之间的天数....
[if !supportLists]1. [endif]public static void main(String[] args) {
[if !supportLists]2. [endif] Calendar birth = Calendar.getInstance();
[if !supportLists]3. [endif] birth.set(1975, Calendar.MAY, 26);
[if !supportLists]4. [endif] Calendar now = Calendar.getInstance();
[if !supportLists]5. [endif] System.out.println(daysBetween(birth, now));
[if !supportLists]6. [endif]System.out.println(daysBetween(birth, now)); //显示 0?
[if !supportLists]7. [endif]}
[if !supportLists]8. [endif]
[if !supportLists]9. [endif]public static long daysBetween(Calendar begin, Calendar end) {
[if !supportLists]10. [endif] long daysBetween = 0;
[if !supportLists]11. [endif] while(begin.before(end)) {
[if !supportLists]12. [endif] begin.add(Calendar.DAY_OF_MONTH, 1);
[if !supportLists]13. [endif] daysBetween++;
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif] return daysBetween;
[if !supportLists]16. [endif]}
daysBetween 有点问题,如果连续计算两个 Date 实例的话,第二次会取得 0,因为 Calendar 状态是可变的,考虑到重复计算的场合,最好复制一个新的 Calendar
[if !supportLists]1. [endif]public static long daysBetween(Calendar begin, Calendar end) {
[if !supportLists]2. [endif]Calendar calendar = (Calendar) begin.clone(); //复制
[if !supportLists]3. [endif] long daysBetween = 0;
[if !supportLists]4. [endif] while(calendar.before(end)) {
[if !supportLists]5. [endif] calendar.add(Calendar.DAY_OF_MONTH, 1);
[if !supportLists]6. [endif] daysBetween++;
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif] return daysBetween;
[if !supportLists]9. [endif]}
以上种种,导致目前有些第三方的java 日期库诞生,比如广泛使用的 JODA-TIME,还有 Date4j 等,虽然第三方库已经足3 / 8够强大,好用,但还是有兼容问题的,比如标准的 JSF 日期转换器与 joda-time API 就不兼容,你需要编写自己的转换器,所以标准的 API 还是必须的,于是就有了JSR310。
7.8 Java8日期实现JSR310规范 (2017-11-23-wl)
[if !supportLists]1. [endif]JSR310介绍
JSR 310 实际上有两个日期概念。第一个是 Instant,它大致对应于 java.util.Date 类,因为它代表了一个确定的时间点,即相对于标准 Java 纪元(1970 年 1 月 1 日)的偏移量;但与 java.util.Date 类不同的是其精确到了纳秒级别。
第二个对应于人类自身的观念,比如LocalDate 和 LocalTime。他们代表了一般的时区概念,要么是日期(不包含时间),要么是时间(不包含日期),类似于 java.sql 的表示方式。此外,还有一个 MonthDay,它可以存储某人的生日(不包含年份)。每个类都在内部存储正确的数据而不是像 java.util.Date 那样利用午夜 12 点来区分日期,利用 1970-01-01 来表示时间。
目前Java8 已经实现了 JSR310 的全部内容。新增了 java.time 包定义的类表示了日期-时间概念的规则,包括 instants,durations, dates, times, time-zones and periods。这些都是基于 ISO 日历系统,它又是遵循 Gregorian 规则的。最重要的一点是值不可变,且线程安全,通过下面一张图,我们快速看下 java.time 包下的一些主要的类的值的格式,方便理解。
[if !supportLists]2. [endif]Java8方法概览
java.time包下的方法概览
方法名说明
Of静态工厂方法
parse静态工厂方法,关注于解析
get获取某些东西的值
is检查某些东西的是否是 true
with不可变的 setter 等价物
plus加一些量到某个对象
minus从某个对象减去一些量
to转换到另一个类型
at把这个对象与另一个对象组合起来
与旧的API相比
[if !supportLists]3. [endif]简单实用java.time的API实用
[if !supportLists]1. [endif]public class TimeIntroduction {
[if !supportLists]2. [endif] public static void testClock() throws InterruptedException {
[if !supportLists]3. [endif]//时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。
[if !supportLists]4. [endif]Clock c1 = Clock.systemUTC(); //系统默认 UTC 时钟(当前瞬时时间 System.currentTimeMillis())
[if !supportLists]5. [endif]System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)
[if !supportLists]6. [endif]Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)
[if !supportLists]7. [endif]Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎时区
[if !supportLists]8. [endif]System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)
[if !supportLists]9. [endif]Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海时区
[if !supportLists]10. [endif]System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)
[if !supportLists]11. [endif]Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));//固定上海时区时钟
[if !supportLists]12. [endif] System.out.println(c4.millis());
[if !supportLists]13. [endif] Thread.sleep(1000);
[if !supportLists]14. [endif]
[if !supportLists]15. [endif]System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动
[if !supportLists]16. [endif]Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟
[if !supportLists]17. [endif] System.out.println(c1.millis());
[if !supportLists]18. [endif] System.out.println(c5.millis());
[if !supportLists]19. [endif] }
[if !supportLists]20. [endif] public static void testInstant() {
[if !supportLists]21. [endif]//瞬时时间 相当于以前的 System.currentTimeMillis()
[if !supportLists]22. [endif] Instant instant1 = Instant.now();
[if !supportLists]23. [endif]System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于 1970-01-01 00:00:00
[if !supportLists]24. [endif]UTC的一个时间
[if !supportLists]25. [endif]System.out.println(instant1.toEpochMilli()); //精确到毫秒
[if !supportLists]26. [endif]Clock clock1 = Clock.systemUTC(); //获取系统 UTC 默认时钟
[if !supportLists]27. [endif]Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间
[if !supportLists]28. [endif] System.out.println(instant2.toEpochMilli());
[if !supportLists]29. [endif]Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟
[if !supportLists]30. [endif]Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间
[if !supportLists]31. [endif] System.out.println(instant3.toEpochMilli());//equals instant1
[if !supportLists]32. [endif] }
[if !supportLists]33. [endif] public static void testLocalDateTime() {
[if !supportLists]34. [endif]//使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()
[if !supportLists]35. [endif]默认时区
[if !supportLists]36. [endif] LocalDateTime now = LocalDateTime.now();
[if !supportLists]37. [endif] System.out.println(now);
[if !supportLists]38. [endif]//自定义时区
[if !supportLists]39. [endif] LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Paris"));
[if !supportLists]40. [endif]System.out.println(now2);//会以相应的时区显示日期
[if !supportLists]41. [endif]//自定义时钟
[if !supportLists]42. [endif] Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
[if !supportLists]43. [endif] LocalDateTime now3 = LocalDateTime.now(clock);
[if !supportLists]44. [endif]System.out.println(now3);//会以相应的时区显示日期
[if !supportLists]45. [endif]//不需要写什么相对时间 如 java.util.Date 年是相对于 1900 月是从 0 开始
[if !supportLists]46. [endif] //2013-12-31 23:59
[if !supportLists]47. [endif] LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
[if !supportLists]48. [endif]//年月日 时分秒 纳秒
[if !supportLists]49. [endif] LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);
[if !supportLists]50. [endif]//使用瞬时时间 + 时区
[if !supportLists]51. [endif] Instant instant = Instant.now();
[if !supportLists]52. [endif] LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
[if !supportLists]53. [endif] System.out.println(d3);
[if !supportLists]54. [endif]//解析 String--->LocalDateTime
[if !supportLists]55. [endif] LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
[if !supportLists]56. [endif] System.out.println(d4);
[if !supportLists]57. [endif]LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于
[if !supportLists]58. [endif]999000000纳秒
[if !supportLists]59. [endif]
[if !supportLists]60. [endif] System.out.println(d5);
[if !supportLists]61. [endif]//使用 DateTimeFormatter API 解析 和 格式化
[if !supportLists]62. [endif] DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
[if !supportLists]63. [endif] LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
[if !supportLists]64. [endif] System.out.println(formatter.format(d6));
[if !supportLists]65. [endif]//时间获取
[if !supportLists]66. [endif] System.out.println(d6.getYear());
[if !supportLists]67. [endif] System.out.println(d6.getMonth());
[if !supportLists]68. [endif] System.out.println(d6.getDayOfYear());
[if !supportLists]69. [endif] System.out.println(d6.getDayOfMonth());
[if !supportLists]70. [endif] System.out.println(d6.getDayOfWeek());
[if !supportLists]71. [endif] System.out.println(d6.getHour());
[if !supportLists]72. [endif] System.out.println(d6.getMinute());
[if !supportLists]73. [endif] System.out.println(d6.getSecond());
[if !supportLists]74. [endif] System.out.println(d6.getNano());
[if !supportLists]75. [endif]//时间增减
[if !supportLists]76. [endif] LocalDateTime d7 = d6.minusDays(1);
[if !supportLists]77. [endif] LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
[if !supportLists]78. [endif]//LocalDate即年月日 无时分秒
[if !supportLists]79. [endif]//LocalTime即时分秒 无年月日
[if !supportLists]80. [endif]//API和 LocalDateTime 类似就不演示了
[if !supportLists]81. [endif] }
[if !supportLists]82. [endif] public static void testZonedDateTime() {
[if !supportLists]83. [endif]//即带有时区的 date-time 存储纳秒、时区和时差(避免与本地 date-time 歧义)。
[if !supportLists]84. [endif]//API和 LocalDateTime 类似,只是多了时差(如 2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
[if !supportLists]85. [endif] ZonedDateTime now = ZonedDateTime.now();
[if !supportLists]86. [endif] System.out.println(now);
[if !supportLists]87. [endif] ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
[if !supportLists]88. [endif] System.out.println(now2);
[if !supportLists]89. [endif]//其他的用法也是类似的 就不介绍了
[if !supportLists]90. [endif] ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
[if !supportLists]91. [endif] System.out.println(z1);
[if !supportLists]92. [endif]}
[if !supportLists]93. [endif] public static void testDuration() {
[if !supportLists]94. [endif]//表示两个瞬时时间的时间段
[if !supportLists]95. [endif] Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123),
[if !supportLists]96. [endif] Instant.now())
[if !supportLists]97. [endif];
[if !supportLists]98. [endif]//得到相应的时差
[if !supportLists]99. [endif] System.out.println(d1.toDays());
[if !supportLists]100. [endif] System.out.println(d1.toHours());
[if !supportLists]101. [endif] System.out.println(d1.toMinutes());
[if !supportLists]102. [endif]
[if !supportLists]103. [endif] System.out.println(d1.toMillis());
[if !supportLists]104. [endif] System.out.println(d1.toNanos());
[if !supportLists]105. [endif]//1天时差 类似的还有如 ofHours()
[if !supportLists]106. [endif] Duration d2 = Duration.ofDays(1);
[if !supportLists]107. [endif] System.out.println(d2.toDays());
[if !supportLists]108. [endif]}
[if !supportLists]109. [endif] public static void testChronology() {
[if !supportLists]110. [endif]//提供对 java.util.Calendar 的替换,提供对年历系统的支持
[if !supportLists]111. [endif] Chronology c = HijrahChronology.INSTANCE;
[if !supportLists]112. [endif] ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
[if !supportLists]113. [endif] System.out.println(d);
[if !supportLists]114. [endif] }
[if !supportLists]115. [endif]/**
[if !supportLists]116. [endif]*新旧日期转换
[if !supportLists]117. [endif]*/
[if !supportLists]118. [endif] public static void testNewOldDateConversion(){
[if !supportLists]119. [endif] Instant instant=new Date().toInstant();
[if !supportLists]120. [endif] Date date=Date.from(instant);
[if !supportLists]121. [endif] System.out.println(instant);
[if !supportLists]122. [endif] System.out.println(date);
[if !supportLists]123. [endif] }
[if !supportLists]124. [endif] public static void main(String[] args) throws InterruptedException {
[if !supportLists]125. [endif] testClock();
[if !supportLists]126. [endif] testInstant();
[if !supportLists]127. [endif] testLocalDateTime();
[if !supportLists]128. [endif] testZonedDateTime();
[if !supportLists]129. [endif] testDuration();
[if !supportLists]130. [endif] testChronology();
[if !supportLists]131. [endif] testNewOldDateConversion();
[if !supportLists]132. [endif] }
[if !supportLists]133. [endif]}
7.9 JSR310规范Joda-Time的区别 (2017-11-23-wl)
其实JSR310 的规范领导者 Stephen Colebourne,同时也是 Joda-Time 的创建者,JSR310 是在 Joda-Time 的基础上建立的,参考了绝大部分的 API,但并不是说 JSR310=JODA-Time,下面几个比较明显的区别是:
1. 最明显的变化就是包名(从 org.joda.time 以及 java.time)
2. JSR310 不接受 NULL 值,Joda-Time 视 NULL 值为 0
3. JSR310 的计算机相关的时间(Instant)和与人类相关的时间(DateTime)之间的差别变得更明显
4. JSR310 所有抛出的异常都是 DateTimeException 的子类。虽然 DateTimeException 是一个 RuntimeException
7.10 总结 (2017-11-23-wl)
Java.timejava.util.Calendar 以及 Date
流畅的 API不流畅的 API
实例不可变实例可变
线程安全非线程安全
[if !supportLists]六、[endif]Java的数据类型
[if !supportLists]1. [endif]Java的基本数据类型都有哪些各占几个字节
如下表所示:
四类八种字节数数据表示范围
整型
byte1-128~127
short2-32768~32767
int4-2147483648~2147483647
long8-263~263-1
浮点型float4-3.403E38~3.403E38
double8-1.798E308~1.798E308
字符型char2表示一个字符,如('a','A','0','家')
布尔型boolean1只有两个值true与false
[if !supportLists]2. [endif]String是基本数据类型吗?(2017-11-12-wl)
String是引用类型,底层用char数组实现的。
[if !supportLists]3. [endif]short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1有错吗;(2017-11-12-wl)
前者不正确,后者正确。对于short s1 = 1; s1 = s1 + 1;由于1是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给short型。而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
[if !supportLists]4. [endif]int 和 和 Integer有什么区别?(2017-11-12-wl)
Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java为每个原始类型提供了包装类型:
- 原始类型: boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
[if !supportLists]5. [endif]下面Integer类型的数值比较输出的结果为?(2017-11-12-wl)
如果不明就里很容易认为两个输出要么都是true 要么都是 false。首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看 valueOf 的源代码就知道发生了什么。
源码:
IntegerCache 是 Integer 的内部类,其代码如下所示:
简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1==f2 的结果是 true,而 f3==f4 的结果是 false。
提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。
[if !supportLists]6. [endif]String类常用方法(2017-11-15-lyq)
[if !supportLists]7. [endif]String、StringBuffer、StringBuilder的区别?(2017-11-23-wzz)
(1)、可变不可变
String:字符串常量,在修改时不会改变自身;若修改,等于重新生成新的字符串对象。
StringBuffer:在修改时会改变对象自身,每次操作都是对StringBuffer对象本身进行修改,不是生成新的对象;使用场景:对字符串经常改变情况下,主要方法:append(),insert()等。
[if !supportLists](2)[endif]、线程是否安全
String:对象定义后不可变,线程安全。
StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区大量数据。
StringBuilder:是线程不安全的,适用于单线程下操作字符串缓冲区大量数据。
[if !supportLists](3)[endif]、共同点
StringBuilder与StringBuffer有公共父类AbstractStringBuilder(抽象类)。
StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(...)。只是StringBuffer会在方法上加synchronized关键字,进行同步。最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。
[if !supportLists]8. [endif]数据类型之间的转换(2017-11-23-wzz)
(1)、字符串如何转基本数据类型?
调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型。
[if !supportLists](2)[endif]、基本数据类型如何转字符串?
一种方法是将基本数据类型与空字符串(“”)连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf()方法返回相应字符串。
[if !supportLists]七、[endif]Java的IO
[if !supportLists]1. [endif]Java中有几种类型的流(2017-11-23-wzz)
按照流的方向:输入流(inputStream)和输出流(outputStream)。
按照实现功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。)
按照处理数据的单位:字节流和字符流。字节流继承于InputStream和OutputStream,字符流继承于InputStreamReader 和OutputStreamWriter。
[if !supportLists]2. [endif]字节流如何转为字符流
字节输入流转字符输入流通过InputStreamReader实现,该类的构造函数可以传入InputStream对象。
字节输出流转字符输出流通过OutputStreamWriter实现,该类的构造函数可以传入OutputStream对象。
[if !supportLists]3. [endif]如何将一个java对象序列化到文件里
在java中能够被序列化的类必须先实现Serializable接口,该接口没有任何抽象方法只是起到一个标记作用。
[if !supportLists]1. [endif]//对象输出流
[if !supportLists]2. [endif] ObjectOutputStream objectOutputStream =
[if !supportLists]3. [endif]new ObjectOutputStream(new FileOutputStream(new File("D://obj")));
[if !supportLists]4. [endif] objectOutputStream.writeObject(new User("zhangsan", 100));
[if !supportLists]5. [endif] objectOutputStream.close();
[if !supportLists]6. [endif] //对象输入流
[if !supportLists]7. [endif] ObjectInputStream objectInputStream =
[if !supportLists]8. [endif]new ObjectInputStream(new FileInputStream(new File("D://obj")));
[if !supportLists]9. [endif] User user = (User)objectInputStream.readObject();
[if !supportLists]10. [endif] System.out.println(user);
[if !supportLists]11. [endif] objectInputStream.close();
[if !supportLists]4. [endif]字节流和字符流的区别(2017-11-23-wzz)
字节流读取的时候,读到一个字节就返回一个字节;字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回。 字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。字节流主要是操作byte类型数据,以byte数组为准,主要操作类就是OutputStream、InputStream
字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,而字节流处理单元为1个字节,操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点。在程序中一个字符等于两个字节,java提供了Reader、Writer两个专门操作字符流的类。
[if !supportLists]5. [endif]如何实现对象克隆?(2017-11-12-wl)
有两种方式。
1). 实现 Cloneable 接口并重写 Object 类中的 clone()方法;
2). 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
[if !supportLists]12. [endif]import java.io.ByteArrayInputStream;
[if !supportLists]13. [endif]import java.io.ByteArrayOutputStream;
[if !supportLists]14. [endif]import java.io.ObjectInputStream;
[if !supportLists]15. [endif]import java.io.ObjectOutputStream;
[if !supportLists]16. [endif]import java.io.Serializable;
[if !supportLists]17. [endif]public class MyUtil {
[if !supportLists]18. [endif]private MyUtil() {
[if !supportLists]19. [endif]throw new AssertionError();
[if !supportLists]20. [endif]}
[if !supportLists]21. [endif]@SuppressWarnings("unchecked")
[if !supportLists]22. [endif]public static T clone(T obj) throws Exception {
[if !supportLists]23. [endif]ByteArrayOutputStream bout = new ByteArrayOutputStream();
[if !supportLists]24. [endif]ObjectOutputStream oos = new ObjectOutputStream(bout);
[if !supportLists]25. [endif]oos.writeObject(obj);
[if !supportLists]26. [endif]ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
[if !supportLists]27. [endif]ObjectInputStream ois = new ObjectInputStream(bin);
[if !supportLists]28. [endif]return (T) ois.readObject();
[if !supportLists]29. [endif]//说明:调用 ByteArrayInputStream 或 ByteArrayOutputStream 对象的 close 方法没有任何意义
[if !supportLists]30. [endif]//这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
[if !supportLists]31. [endif]}
[if !supportLists]32. [endif]}
测试代码:
[if !supportLists]1. [endif]import java.io.Serializable;
[if !supportLists]2. [endif]/**
[if !supportLists]3. [endif]*人类
[if !supportLists]4. [endif] */
[if !supportLists]5. [endif]class Person implements Serializable {
[if !supportLists]6. [endif]private static final long serialVersionUID = -9102017020286042305L;
[if !supportLists]7. [endif]private String name; //姓名
[if !supportLists]8. [endif]private int age; //年龄
[if !supportLists]9. [endif]private Car car; //座驾
[if !supportLists]10. [endif]public Person(String name, int age, Car car) {
[if !supportLists]11. [endif]this.name = name;
[if !supportLists]12. [endif]this.age = age;
[if !supportLists]13. [endif]this.car = car;
[if !supportLists]14. [endif]}
[if !supportLists]15. [endif]public String getName() {
[if !supportLists]16. [endif]return name;
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif]public void setName(String name) {
[if !supportLists]19. [endif]this.name = name;
[if !supportLists]20. [endif]}
[if !supportLists]21. [endif]public int getAge() {
[if !supportLists]22. [endif]return age;
[if !supportLists]23. [endif]}
[if !supportLists]24. [endif]public void setAge(int age) {
[if !supportLists]25. [endif]this.age = age;
[if !supportLists]26. [endif]}
[if !supportLists]27. [endif]public Car getCar() {
[if !supportLists]28. [endif]return car;
[if !supportLists]29. [endif]}
[if !supportLists]30. [endif]public void setCar(Car car) {
[if !supportLists]31. [endif]this.car = car;
[if !supportLists]32. [endif]}
[if !supportLists]33. [endif]@Override
[if !supportLists]34. [endif]public String toString() {
[if !supportLists]35. [endif]return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
[if !supportLists]36. [endif]}
[if !supportLists]37. [endif]}
[if !supportLists]1. [endif]/**
[if !supportLists]2. [endif]*小汽车类
[if !supportLists]3. [endif]*/
[if !supportLists]4. [endif]class Car implements Serializable {
[if !supportLists]5. [endif]private static final long serialVersionUID = -5713945027627603702L;
[if !supportLists]6. [endif]private String brand; //品牌
[if !supportLists]7. [endif]private int maxSpeed; //最高时速
[if !supportLists]8. [endif]public Car(String brand, int maxSpeed) {
[if !supportLists]9. [endif]this.brand = brand;
[if !supportLists]10. [endif]this.maxSpeed = maxSpeed;
[if !supportLists]11. [endif]}
[if !supportLists]12. [endif]public String getBrand() {
[if !supportLists]13. [endif]return brand;
[if !supportLists]14. [endif]}
[if !supportLists]15. [endif]public void setBrand(String brand) {
[if !supportLists]16. [endif]this.brand = brand;
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif]public int getMaxSpeed() {
[if !supportLists]19. [endif]return maxSpeed;
[if !supportLists]20. [endif]}
[if !supportLists]21. [endif]public void setMaxSpeed(int maxSpeed) {
[if !supportLists]22. [endif]this.maxSpeed = maxSpeed;
[if !supportLists]23. [endif]}
[if !supportLists]24. [endif]@Override
[if !supportLists]25. [endif]public String toString() {
[if !supportLists]26. [endif]return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
[if !supportLists]27. [endif]}
[if !supportLists]28. [endif]}
[if !supportLists]1. [endif]class CloneTest {
[if !supportLists]2. [endif]public static void main(String[] args) {
[if !supportLists]3. [endif]try {
[if !supportLists]4. [endif]Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
[if !supportLists]5. [endif]Person p2 = MyUtil.clone(p1); //深度克隆
[if !supportLists]6. [endif]p2.getCar().setBrand("BYD");
[if !supportLists]7. [endif]//修改克隆的 Person 对象 p2 关联的汽车对象的品牌属性
[if !supportLists]8. [endif]//原来的 Person 对象 p1 关联的汽车不会受到任何影响
[if !supportLists]9. [endif]//因为在克隆 Person 对象时其关联的汽车对象也被克隆了
[if !supportLists]10. [endif]System.out.println(p1);
[if !supportLists]11. [endif]} catch (Exception e) {
[if !supportLists]12. [endif]e.printStackTrace();
[if !supportLists]13. [endif]}
[if !supportLists]14. [endif]}
[if !supportLists]15. [endif]}
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
[if !supportLists]6. [endif]什么是java序列化,如何实现java序列化?(2017-12-7-lyq)
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
原文链接:https://www.cnblogs.com/yangchunze/p/6728086.html
[if !supportLists]八、[endif]Java的集合
[if !supportLists]1. [endif]HashMap排序题,上机题。(本人主要靠这道题入职的第一家公司)
已知一个HashMap集合, User有name(String)和age(int)属性。请写一个方法实现对HashMap的排序功能,该方法接收HashMap为形参,返回类型为HashMap,要求对HashMap中的User的age倒序进行排序。排序时key=value键值对不得拆散。
注意:要做出这道题必须对集合的体系结构非常的熟悉。HashMap本身就是不可排序的,但是该道题偏偏让给HashMap排序,那我们就得想在API中有没有这样的Map结构是有序的,LinkedHashMap,对的,就是他,他是Map结构,也是链表结构,有序的,更可喜的是他是HashMap的子类,我们返回LinkedHashMap即可,还符合面向接口(父类编程的思想)。
但凡是对集合的操作,我们应该保持一个原则就是能用JDK中的API就有JDK中的API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用Collections集合工具类。
[if !supportLists]1. [endif]public class HashMapTest {
[if !supportLists]2. [endif] public static void main(String[] args) {
[if !supportLists]3. [endif] HashMap users = new HashMap<>();
[if !supportLists]4. [endif] users.put(1, new User("张三", 25));
[if !supportLists]5. [endif] users.put(3, new User("李四", 22));
[if !supportLists]6. [endif] users.put(2, new User("王五", 28));
[if !supportLists]7. [endif] System.out.println(users);
[if !supportLists]8. [endif] HashMap sortHashMap = sortHashMap(users);
[if !supportLists]9. [endif] System.out.println(sortHashMap);
[if !supportLists]10. [endif] /**
[if !supportLists]11. [endif] *控制台输出内容
[if !supportLists]12. [endif] * {1=User [name=张三, age=25], 2=User [name=王五, age=28], 3=User [name=李四, age=22]}
[if !supportLists]13. [endif] {2=User [name=王五, age=28], 1=User [name=张三, age=25], 3=User [name=李四, age=22]}
[if !supportLists]14. [endif] */
[if !supportLists]15. [endif] }
[if !supportLists]16. [endif]
[if !supportLists]17. [endif] public static HashMap sortHashMap(HashMap map) {
[if !supportLists]18. [endif] //首先拿到map的键值对集合
[if !supportLists]19. [endif] Set> entrySet = map.entrySet();
[if !supportLists]20. [endif]
[if !supportLists]21. [endif] //将set集合转为List集合,为什么,为了使用工具类的排序方法
[if !supportLists]22. [endif] List> list = new ArrayList>(entrySet);
[if !supportLists]23. [endif] //使用Collections集合工具类对list进行排序,排序规则使用匿名内部类来实现
[if !supportLists]24. [endif] Collections.sort(list, new Comparator>() {
[if !supportLists]25. [endif]
[if !supportLists]26. [endif] @Override
[if !supportLists]27. [endif] public int compare(Entry o1, Entry o2) {
[if !supportLists]28. [endif] //按照要求根据User的age的倒序进行排
[if !supportLists]29. [endif] return o2.getValue().getAge()-o1.getValue().getAge();
[if !supportLists]30. [endif] }
[if !supportLists]31. [endif] });
[if !supportLists]32. [endif] //创建一个新的有序的HashMap子类的集合
[if !supportLists]33. [endif] LinkedHashMap linkedHashMap = new LinkedHashMap();
[if !supportLists]34. [endif] //将List中的数据存储在LinkedHashMap中
[if !supportLists]35. [endif] for(Entry entry : list){
[if !supportLists]36. [endif] linkedHashMap.put(entry.getKey(), entry.getValue());
[if !supportLists]37. [endif] }
[if !supportLists]38. [endif] //返回结果
[if !supportLists]39. [endif] return linkedHashMap;
[if !supportLists]40. [endif] }
[if !supportLists]41. [endif]}
[if !supportLists]42. [endif]
[if !supportLists]2. [endif]集合的安全性问题
请问ArrayList、HashSet、HashMap是线程安全的吗?如果不是我想要线程安全的集合怎么办?
我们都看过上面那些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。话又说过来如果他们安全了也就没第二问了。
在集合中Vector和HashTable倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了synchronized关键字。
Collections工具类提供了相关的API,可以让上面那3个不安全的集合变为安全的。
[if !supportLists]1. [endif]// Collections.synchronizedCollection(c)
[if !supportLists]2. [endif]// Collections.synchronizedList(list)
[if !supportLists]3. [endif]// Collections.synchronizedMap(m)
[if !supportLists]4. [endif]// Collections.synchronizedSet(s)
上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集合的核心方法添加上了synchronized关键字。
[if !supportLists]3. [endif]ArrayList内部用什么实现的?(2015-11-24)
(回答这样的问题,不要只回答个皮毛,可以再介绍一下ArrayList内部是如何实现数组的增加和删除的,因为数组在创建的时候长度是固定的,那么就有个问题我们往ArrayList中不断的添加对象,它是如何管理这些数组呢?)
ArrayList内部是用Object[]实现的。接下来我们分别分析ArrayList的构造、add、remove、clear方法的实现原理。
[if !supportLists]一、[endif]构造函数
1)空参构造
/**
* Constructs a new {@code ArrayList} instance with zero initial capacity.
*/
public ArrayList() {
array = EmptyArray.OBJECT;
}
array是一个Object[]类型。当我们new一个空参构造时系统调用了EmptyArray.OBJECT属性,EmptyArray仅仅是一个系统的类库,该类源码如下:
public final class EmptyArray {
private EmptyArray() {}
public static final boolean[] BOOLEAN = new boolean[0];
public static final byte[] BYTE = new byte[0];
public static final char[] CHAR = new char[0];
public static final double[] DOUBLE = new double[0];
public static final int[] INT = new int[0];
public static final Class[] CLASS = new Class[0];
public static final Object[] OBJECT = new Object[0];
public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}
也就是说当我们new 一个空参ArrayList的时候,系统内部使用了一个new Object[0]数组。
[if !supportLists]2)[endif]带参构造1
/**
* Constructs a new instance of {@code ArrayList} with the specified
* initial capacity.
*
* @param capacity
* the initial capacity of this {@code ArrayList}.
*/
public ArrayList(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}
该构造函数传入一个int值,该值作为数组的长度值。如果该值小于0,则抛出一个运行时异常。如果等于0,则使用一个空数组,如果大于0,则创建一个长度为该值的新数组。
3)带参构造2
/**
* Constructs a new instance of {@code ArrayList} containing the elements of
* the specified collection.
*
* @param collection
* the collection of elements to add.
*/
public ArrayList(Collection collection) {
if (collection == null) {
throw new NullPointerException("collection == null");
}
Object[] a = collection.toArray();
if (a.getClass() != Object[].class) {
Object[] newArray = new Object[a.length];
System.arraycopy(a, 0, newArray, 0, a.length);
a = newArray;
}
array = a;
size = a.length;
}
如果调用构造函数的时候传入了一个Collection的子类,那么先判断该集合是否为null,为null则抛出空指针异常。如果不是则将该集合转换为数组a,然后将该数组赋值为成员变量array,将该数组的长度作为成员变量size。这里面它先判断a.getClass是否等于Object[].class,其实一般都是相等的,我也暂时没想明白为什么多加了这个判断,toArray方法是Collection接口定义的,因此其所有的子类都有这样的方法,list集合的toArray和Set集合的toArray返回的都是Object[]数组。
这里讲些题外话,其实在看Java源码的时候,作者的很多意图都很费人心思,我能知道他的目标是啥,但是不知道他为何这样写。比如对于ArrayList, array是他的成员变量,但是每次在方法中使用该成员变量的时候作者都会重新在方法中开辟一个局部变量,然后给局部变量赋值为array,然后再使用,有人可能说这是为了防止并发修改array,毕竟array是成员变量,大家都可以使用因此需要将array变为局部变量,然后再使用,这样的说法并不是都成立的,也许有时候就是老外们写代码的一个习惯而已。
[if !supportLists]二、[endif]add方法
add方法有两个重载,这里只研究最简单的那个。
/**
* Adds the specified object at the end of this {@code ArrayList}.
*
* @param object
* the object to add.
* @return always true
*/
@Override public boolean add(E object) {
Object[] a = array;
int s = size;
if (s == a.length) {
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
}
a[s] = object;
size = s + 1;
modCount++;
return true;
}
[if !supportLists]1、[endif]首先将成员变量array赋值给局部变量a,将成员变量size赋值给局部变量s。
[if !supportLists]2、[endif]判断集合的长度s是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新分配新数组了),重新分配数组的时候需要计算新分配内存的空间大小,如果当前的长度小于MIN_CAPACITY_INCREMENT/2(这个常量值是12,除以2就是6,也就是如果当前集合长度小于6)则分配12个长度,如果集合长度大于6则分配当前长度s的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将s往右移1位,相当于s=s/2,只不过位运算是效率最高的运算。
[if !supportLists]3、[endif]将新添加的object对象作为数组的a[s]个元素。
[if !supportLists]4、[endif]修改集合长度size为s+1
[if !supportLists]5、[endif]modCotun++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。
[if !supportLists]6、[endif]return true,这个返回值意义不大,因为一直返回true,除非报了一个运行时异常。
[if !supportLists]三、[endif]remove方法
remove方法有两个重载,我们只研究remove(int index)方法。
/**
* Removes the object at the specified location from this list.
*
* @param index
* the index of the object to remove.
* @return the removed object.
* @throws IndexOutOfBoundsException
* when {@code location < 0 || location >= size()}
*/
@Override public E remove(int index) {
Object[] a = array;
int s = size;
if (index >= s) {
throwIndexOutOfBoundsException(index, s);
}
@SuppressWarnings("unchecked")
E result = (E) a[index];
System.arraycopy(a, index + 1, a, index, --s - index);
a[s] = null; // Prevent memory leak
size = s;
modCount++;
return result;
}
[if !supportLists]1、[endif]先将成员变量array和size赋值给局部变量a和s。
[if !supportLists]2、[endif]判断形参index是否大于等于集合的长度,如果成了则抛出运行时异常
[if !supportLists]3、[endif]获取数组中脚标为index的对象result,该对象作为方法的返回值
[if !supportLists]4、[endif]调用System的arraycopy函数,拷贝原理如下图所示。
[if !supportLists]5、[endif]接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一个元素设置为null,否则就可能内存泄露。
[if !supportLists]6、[endif]重新给成员变量array和size赋值
[if !supportLists]7、[endif]记录修改次数
[if !supportLists]8、[endif]返回删除的元素(让用户再看最后一眼)
[if !supportLists]四、[endif]clear方法
/**
* Removes all elements from this {@code ArrayList}, leaving it empty.
*
* @see #isEmpty
* @see #size
*/
@Override public void clear() {
if (size != 0) {
Arrays.fill(array, 0, size, null);
size = 0;
modCount++;
}
}
如果集合长度不等于0,则将所有数组的值都设置为null,然后将成员变量size设置为0即可,最后让修改记录加1。
[if !supportLists]4. [endif]并发集合和普通集合如何区别?(2015-11-24)
并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是jdk1.5之后才有的,主要作者是Doug Lea(http://baike.baidu.com/view/3141057.htm)完成的。
在java中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了synchronized同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。
参考阅读:
ConcurrentHashMap是线程安全的HashMap的实现,默认构造同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel属性,三属性默认值分别为16、0.75及16。其内部使用锁分段技术,维持这锁Segment的数组,在Segment数组中又存放着Entity[]数组,内部hash算法将数据较均匀分布在不同锁中。
put操作:并没有在此方法上加上synchronized,首先对key.hashcode进行hash操作,得到key的hash值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。
ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key)
首先对key.hashCode进行hash操作,基于其值找到对应的Segment对象,调用其get方法完成当前操作。而Segment的get操作首先通过hash值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry产生不一致性,那么ConcurrentHashMap是如何保证的?
对象数组大小的改变只有在put操作时有可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如HashEntry对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了HashEntry对象后,怎么能保证它及其next属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap采用了一个简单的方式,即HashEntry对象中的hash、key、next属性都是final的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于next属性构建的链表是不会发生变化的。
ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于put和remove等改变集合对象的操作,基于volatile及HashEntry链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的Map而言,而它采用的这些方法也可谓是对于Java内存模型、并发机制深刻掌握的体现。
推荐博客地址:http://m.oschina.net/blog/269037
[if !supportLists]5. [endif]List的三个子类的特点(2017-2-23)
ArrayList 底层结构是数组,底层查询快,增删慢。
LinkedList 底层结构是链表型的,增删快,查询慢。
voctor 底层结构是数组线程安全的,增删慢,查询慢。
[if !supportLists]6. [endif]List和Map、Set的区别(2017-11-22-wzz)
6.1结构特点
List和Set是存储单列数据的集合,Map是存储键和值这样的双列数据的集合;List中存储的数据是有顺序,并且允许重复;Map中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的hashcode决定,位置是固定的(Set集合根据hashcode来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的);
6.2实现类
List接口有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。
Map接口有三个实现类(HashMap:基于hash表的Map接口实现,非线程安全,高效,支持null值和null键;HashTable:线程安全,低效,不支持null值和null键;LinkedHashMap:是HashMap的一个子类,保存了记录的插入顺序;SortMap接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set接口有两个实现类(HashSet:底层是由HashMap实现,不允许集合中有重复的值,使用该方式时需要重写equals()和hashCode()方法;LinkedHashSet:继承与HashSet,同时又基于LinkedHashMap来进行实现,底层使用的是LinkedHashMp)。
6.3区别
List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如TreeSet类,可以按照默认顺序,也可以通过实现Java.util.Comparator接口来自定义排序方式。
[if !supportLists]7. [endif]HashMap 和HashTable有什么区别?(2017-2-23)
HashMap是线程不安全的,HashMap是一个接口,是Map的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全,HashMap的效率要较HashTable的效率高一些.
HashTable 是线程安全的一个集合,不允许null值作为一个key值或者Value值;
HashTable是sychronize,多个线程访问时不需要自己为它的方法实现同步,而HashMap在被多个线程访问的时候需要自己为它的方法实现同步;
[if !supportLists]8. [endif]数组和链表分别比较适合用于什么场景,为什么?(2017-2-23)
8.1 数组和链表简介
在计算机中要对给定的数据集进行若干处理,首要任务是把数据集的一部分(当数据量非常大时,可能只能一部分一部分地读取数据到内存中来处理)或全部存储到内存中,然后再对内存中的数据进行各种处理。
例如,对于数据集S{1,2,3,4,5,6},要求S中元素的和,首先要把数据存储到内存中,然后再将内存中的数据相加。
当内存空间中有足够大的连续空间时,可以把数据连续的存放在内存中,各种编程语言中的数组一般都是按这种方式存储的(也可能有例外),如图1(b);当内存中只有一些离散的可用空间时,想连续存储数据就非常困难了,这时能想到的一种解决方式是移动内存中的数据,把离散的空间聚集成连续的一块大空间,如图1(c)所示,这样做当然也可以,但是这种情况因为可能要移动别人的数据,所以会存在一些困难,移动的过程中也有可能会把一些别人的重要数据给丢失。另外一种,不影响别人的数据存储方式是把数据集中的数据分开离散地存储到这些不连续空间中,如图(d)。这时为了能把数据集中的所有数据联系起来,需要在前一块数据的存储空间中记录下一块数据的地址,这样只要知道第一块内存空间的地址就能环环相扣地把数据集整体联系在一起了。C/C++中用指针实现的链表就是这种存储形式。
图内存分配
由上可知,内存中的存储形式可以分为连续存储和离散存储两种。因此,数据的物理存储结构就有连续存储和离散存储两种,它们对应了我们通常所说的数组和链表,
8.2 数组和链表的区别
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低
链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
8.3 链表和数组使用场景
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
参考博客:http://blog.csdn.net/u011277123/article/details/53908387
8.4 跟数组相关的面试题
用面向对象的方法求出数组中重复value的个数,按如下个数输出:
1出现:1次
3出现:2次
8出现:3次
2出现:4次
int[] arr = {1,4,1,4,2,5,4,5,8,7,8,77,88,5,4,9,6,2,4,1,5};
[if !supportLists]9. [endif]Java中ArrayList和Linkedlist区别?(2017-2-23)
ArrayList和Vector使用了数组的实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
LinkedList使用了循环双向链表数据结构。与基于数组的ArrayList相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。
LinkedList链表由一系列表项连接而成。一个表项总是包含3个部分:元素内容,前驱表和后驱表,如图所示:
在下图展示了一个包含3个元素的LinkedList的各个表项间的连接关系。在JDK的实现中,无论LikedList是否为空,链表内部都有一个header表项,它既表示链表的开始,也表示链表的结尾。表项header的后驱表项便是链表中第一个元素,表项header的前驱表项便是链表中最后一个元素。
[if !supportLists]10. [endif]List a=new ArrayList()和ArrayList a =new ArrayList()的区别?(2017-2-24)
List list = new ArrayList();这句创建了一个ArrayList的对象后把上溯到了List。此时它是一个List对象了,有些ArrayList有但是List没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了ArrayList的所有属性。 所以需要用到ArrayList独有的方法的时候不能用前者。实例代码如下:
[if !supportLists]1.[endif]List list = new ArrayList();
[if !supportLists]2.[endif]ArrayList arrayList = new ArrayList();
[if !supportLists]3.[endif]list.trimToSize(); //错误,没有该方法。
[if !supportLists]4.[endif]arrayList.trimToSize(); //ArrayList里有该方法。
[if !supportLists]11. [endif]要对集合更新操作时,ArrayList和LinkedList哪个更适合?(2017-2-24)
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 2.如果集合数据是对于集合随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。 3.如果集合数据是对于集合新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
ArrayList和LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用ArrayList来存储一系列的String或者Integer。那 么ArrayList和LinkedList在性能上有什么差别呢?什么时候应该用ArrayList什么时候又该用LinkedList呢?
[if !supportLists]一.[endif]时间复杂度 首先一点关键的是,ArrayList的内部实现是基于基础的对象数组的,因此,它使用get方法访问列表中的任意一个元素时(random access),它的速度要比LinkedList快。LinkedList中的get方法是按照顺序从列表的一端开始检查,直到另外一端。对LinkedList而言,访问列表中的某个指定元素没有更快的方法了。 假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是ArrayList类型的也可能是LinkedList类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是ArrayList和LinkedList时的查询速度,看下面的程序:
[if !supportLists]1.[endif]public class TestList{
[if !supportLists]2.[endif] public static final int N=50000; //50000个数
[if !supportLists]3.[endif] public static List values; //要查找的集合
[if !supportLists]4.[endif]//放入50000个数给value;
[if !supportLists]5.[endif] static{
[if !supportLists]6.[endif] Integer vals[]=new Integer[N];
[if !supportLists]7.[endif] Random r=new Random();
[if !supportLists]8.[endif] for(int i=0,currval=0;i
[if !supportLists]9.[endif] vals=new Integer(currval);
[if !supportLists]10.[endif] currval+=r.nextInt(100)+1;
[if !supportLists]11.[endif] }
[if !supportLists]12.[endif] values=Arrays.asList(vals);
[if !supportLists]13.[endif] }
[if !supportLists]14.[endif]//通过二分查找法查找
[if !supportLists]15.[endif] static long timeList(List lst){
[if !supportLists]16.[endif] long start=System.currentTimeMillis();
[if !supportLists]17.[endif] for(int i=0;i
[if !supportLists]18.[endif] int index=Collections.binarySearch(lst, values.get(i));
[if !supportLists]19.[endif] if(index!=i)
[if !supportLists]20.[endif]System.out.println("***错误***");
[if !supportLists]21.[endif] }
[if !supportLists]22.[endif] return System.currentTimeMillis()-start;
[if !supportLists]23.[endif] }
[if !supportLists]24.[endif] public static void main(String args[])...{
[if !supportLists]25.[endif]System.out.println("ArrayList消耗时间:"+timeList(new ArrayList(values)));
[if !supportLists]26.[endif]System.out.println("LinkedList消耗时间:"+timeList(new LinkedList(values)));
[if !supportLists]27.[endif] }
[if !supportLists]28.[endif]}
得到的输出是:
[if !supportLists]1. [endif]ArrayList消耗时间:15
[if !supportLists]2. [endif]LinkedList消耗时间:2596
这个结果不是固定的,但是基本上ArrayList的时间要明显小于LinkedList的时间。因此在这种情况下不宜用LinkedList。二分查找法使用的随机访问(random access)策略,而LinkedList是不支持快速的随机访问的。对一个LinkedList做随机访问所消耗的时间与这个list的大小是成比例的。而相应的,在ArrayList中进行随机访问所消耗的时间是固定的。 这是否表明ArrayList总是比 LinkedList性能要好呢?这并不一定,在某些情况下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现 时效率更高。比方说,利用Collections.reverse方法对列表进行反转时,其性能就要好些。看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下LinkedList就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素:
[if !supportLists]1.[endif]import java.util.*;
[if !supportLists]2.[endif]public class ListDemo {
[if !supportLists]3.[endif] static final int N=50000;
[if !supportLists]4.[endif] static long timeList(List list){
[if !supportLists]5.[endif] long start=System.currentTimeMillis();
[if !supportLists]6.[endif] Object o = new Object();
[if !supportLists]7.[endif] for(int i=0;i
[if !supportLists]8.[endif] list.add(0, o);
[if !supportLists]9.[endif] return System.currentTimeMillis()-start;
[if !supportLists]10.[endif] }
[if !supportLists]11.[endif] public static void main(String[] args) {
[if !supportLists]12.[endif]System.out.println("ArrayList耗时:"+timeList(new ArrayList()));
[if !supportLists]13.[endif]System.out.println("LinkedList耗时:"+timeList(new LinkedList()));
[if !supportLists]14.[endif] }
[if !supportLists]15.[endif]}
这时我的输出结果是
[if !supportLists]1. [endif]ArrayList耗时:2463
[if !supportLists]2. [endif]LinkedList耗时:15
[if !supportLists]二.[endif]空间复杂度在LinkedList中有一个私有的内部类,定义如下:
[if !supportLists]1.[endif]private static class Entry {
[if !supportLists]2.[endif] Object element;
[if !supportLists]3.[endif] Entry next;
[if !supportLists]4.[endif] Entry previous;
[if !supportLists]5.[endif] }
每个Entry对象reference列表 中的一个元素,同时还有在LinkedList中它的上一个元素和下一个元素。一个有1000个元素的LinkedList对象将有1000个链接在一起 的Entry对象,每个对象都对应于列表中的一个元素。这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为它要存储这1000个 Entity对象的相关信息。 ArrayList使用一个内置的数组来存 储元素,这个数组的起始容量是10.当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。 这就意味着,如果你有一个包含大量元素的ArrayList对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成 的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个 ArrayList将会有多少个元素,我们可以通过构造方法来指定容量。我们还可以通过trimToSize方法在ArrayList分配完毕之后去掉浪 费掉的空间。
三.总结 ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下: 1.对ArrayList和 LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶 尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。 2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。 3.LinkedList不支持高效的随机元素访问。 4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间 可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
[if !supportLists]12. [endif]请用两个队列模拟堆栈结构(2017-2-24)
两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下
队列a和b
[if !supportLists](1)[endif]入栈:a队列为空,b为空。例:则将”a,b,c,d,e”需要入栈的元素先放a中,a进栈为”a,b,c,d,e”
[if !supportLists](2)[endif]出栈:a队列目前的元素为”a,b,c,,d,e”。将a队列依次加入Arraylist集合a中。以倒序的方法,将a中的集合取出,放入b队列中,再将b队列出列。代码如下:
[if !supportLists]1.[endif] public static void main(String[] args) {
[if !supportLists]2.[endif] Queue queue = new LinkedList(); //a队列
[if !supportLists]3.[endif] Queue queue2=new LinkedList(); //b队列
[if !supportLists]4.[endif] ArrayList a=new ArrayList(); //arrylist集合是中间参数
[if !supportLists]5.[endif] //往a队列添加元素
[if !supportLists]6.[endif] queue.offer("a");
[if !supportLists]7.[endif] queue.offer("b");
[if !supportLists]8.[endif] queue.offer("c");
[if !supportLists]9.[endif] queue.offer("d");
[if !supportLists]10.[endif] queue.offer("e");
[if !supportLists]11.[endif] System.out.print("进栈:");
[if !supportLists]12.[endif]//a队列依次加入list集合之中
[if !supportLists]13.[endif] for(String q : queue){
[if !supportLists]14.[endif] a.add(q);
[if !supportLists]15.[endif] System.out.print(q);
[if !supportLists]16.[endif] }
[if !supportLists]17.[endif]//以倒序的方法取出(a队列依次加入list集合)之中的值,加入b对列
[if !supportLists]18.[endif] for(int i=a.size()-1;i>=0;i--){
[if !supportLists]19.[endif] queue2.offer(a.get(i));
[if !supportLists]20.[endif] }
[if !supportLists]21.[endif]//打印出栈队列
[if !supportLists]22.[endif] System.out.println("");
[if !supportLists]23.[endif] System.out.print("出栈:");
[if !supportLists]24.[endif] for(String q : queue2){
[if !supportLists]25.[endif] System.out.print(q);
[if !supportLists]26.[endif] }
[if !supportLists]27.[endif] }
打印结果为(遵循栈模式先进后出):
进栈:a b c d e
出栈:e d c b a
[if !supportLists]13. [endif]Collection和Map的集成体系(2017-11-14-lyq)
Collection:
Map:
[if !supportLists]14. [endif]Map中的key和value可以为null么?(2017-11-21-gxb)
HashMap对象的key、value值均可为null。
HahTable对象的key、value值均不可为null。
且两者的的key值均不能重复,若添加key相同的键值对,后面的value会自动覆盖前面的value,但不会报错。测试代码如下:
[if !supportLists]1. [endif]public class Test {
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] public static void main(String[] args) {
[if !supportLists]4. [endif]Map map = new HashMap();//HashMap对象
[if !supportLists]5. [endif]Map tableMap = new Hashtable();//HashTable对象
[if !supportLists]6. [endif]
[if !supportLists]7. [endif] map.put(null, null);
[if !supportLists]8. [endif]System.out.println("hashMap的[key]和[value]均可以为null:" + map.get(null));
[if !supportLists]9. [endif]
[if !supportLists]10. [endif] try {
[if !supportLists]11. [endif] tableMap.put(null, "3");
[if !supportLists]12. [endif] System.out.println(tableMap.get(null));
[if !supportLists]13. [endif] } catch (Exception e) {
[if !supportLists]14. [endif]System.out.println("【ERROR】:hashTable的[key]不能为null");
[if !supportLists]15. [endif] }
[if !supportLists]16. [endif]
[if !supportLists]17. [endif] try {
[if !supportLists]18. [endif] tableMap.put("3", null);
[if !supportLists]19. [endif] System.out.println(tableMap.get("3"));
[if !supportLists]20. [endif] } catch (Exception e) {
[if !supportLists]21. [endif]System.out.println("【ERROR】:hashTable的[value]不能为null");
[if !supportLists]22. [endif] }
[if !supportLists]23. [endif] }
[if !supportLists]24. [endif]
[if !supportLists]25. [endif]}
运行结果:
hashMap的[key]和[value]均可以为null:null 【ERROR】:hashTable的[key]不能为null 【ERROR】:hashTable的[value]不能为null
[if !supportLists]九、[endif]Java的多线程和并发库
对于Java程序员来说,多线程在工作中的使用场景还是比较常见的,而仅仅掌握了Java中的传统多线程机制,还是不够的。在JDK5.0之后,Java增加的并发库中提供了很多优秀的API,在实际开发中用的比较多。因此在看具体的面试题之前我们有必要对这部分知识做一个全面的了解。
(一)多线程基础知识--传统线程机制的回顾(2017-12-11-wl)
[if !supportLists]( 1 ) [endif]传统使用类Thread和接口Runnable实现
[if !supportLists]1. [endif]在Thread子类覆盖的run方法中编写运行代码
方式一
new Thread(){
@Override
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
[if !supportLists]2. [endif]在传递给Thread对象的Runnable对象的run方法中编写代码
new Thread(new Runnable(){
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}).start();
[if !supportLists]3. [endif]总结
查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法
/**
* If this thread was constructed using a separate
* Runnable
run object, then that
* Runnable
object's run
method is called;
* otherwise, this method does nothing and returns.
*
* Subclasses of Thread
should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
[if !supportLists]( 2 ) [endif]定实现时器Timer和TimerTask
Timer在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。下面我们就针对一道面试题来使用Timer定时类。
[if !supportLists]1. [endif]请模拟写出双重定时器(面试题)
要求:使用定时器,间隔4秒执行一次,再间隔2秒执行一次,以此类推执行。
class TimerTastCus extends TimerTask{
@Override
public void run() {
count = (count +1)%2;
System.err.println("Boob boom ");
new Timer().schedule(new TimerTastCus(), 2000+2000*count);
}
}
Timer timer = new Timer();
timer.schedule(new TimerTastCus(), 2000+2000*count);
while (true) {
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//PS:下面的代码中的count变量中
//此参数要使用在你匿名内部类中,使用final修饰就无法对其值进行修改,
//只能改为静态变量
private static volatile int count = 0;
[if !supportLists]( 3 ) [endif]线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
[if !supportLists]1. [endif]间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
[if !supportLists]2. [endif]直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
下面我们通过一道面试题来体会线程的交互。
要求:子线程运行执行10次后,主线程再运行5次。这样交替执行三遍
public static void main(String[] args) {
final Bussiness bussiness = new Bussiness();
//子线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
bussiness.subMethod();
}
}
}).start();
//主线程
for (int i = 0; i < 3; i++) {
bussiness.mainMethod();
}
}
}
class Bussiness {
private boolean subFlag = true;
public synchronized void mainMethod() {
while (subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ " : main thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
subFlag = true;
notify();
}
public synchronized void subMethod() {
while (!subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.err.println(Thread.currentThread().getName()
+ " : sub thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
subFlag = false;
notify();
}
}
[if !supportLists]( 4 ) [endif]线程局部变量ThreadLocal
[if !supportLists]l [endif]ThreadLocal的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
[if !supportLists]l [endif]每个线程调用全局ThreadLocal对象的set方法,在set方法中,首先根据当前线程获取当前线程的ThreadLocalMap对象,然后往这个map中插入一条记录,key其实是ThreadLocal对象,value是各自的set方法传进去的值。也就是每个线程其实都有一份自己独享的ThreadLocalMap对象,该对象的Key是ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
[if !supportLists]l [endif]ThreadLocal的应用场景:
[if !supportLists]Ø [endif]订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
[if !supportLists]Ø [endif]银行转账包含一系列操作:把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
[if !supportLists]Ø [endif]例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
[if !supportLists]1. [endif]ThreadLocal的使用方式
[if !supportLists](1) [endif]在关联数据类中创建private static ThreadLocal
在下面的类中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态SerialNum.get() 方法的每个线程维护了一个“序列号”,该方法将返回当前线程的序列号。(线程的序列号是在第一次调用SerialNum.get() 时分配的,并在后续调用中不会更改。)
public class SerialNum {
// The next serial number to be assigned
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
另一个例子,也是私有静态 ThreadLocal 实例:
public class ThreadContext { private String userId; private Long transactionId; private static ThreadLocal threadLocal = new ThreadLocal(){ @Override protected ThreadContext initialValue() { return new ThreadContext(); } }; public static ThreadContext get() { return threadLocal.get(); }
public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public Long getTransactionId() { return transactionId; } public void setTransactionId(Long transactionId) { this.transactionId = transactionId; }}
补充:在JDK的API对ThreadLocal私有化的说明。并举例‘线程唯一标识符’UniqueThreadIdGenerator ,大家学习是可以结合官方API来学习。
[if !supportLists]2. [endif]在Util类中创建ThreadLocal
这是上面用法的扩展,即把ThreadLocal的创建放到工具类中。
public class HibernateUtil { private static Log log = LogFactory.getLog(HibernateUtil.class);private static final SessionFactory sessionFactory; //定义SessionFactory static { try {//通过默认配置文件hibernate.cfg.xml创建SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) {log.error("初始化SessionFactory失败!", ex); throw new ExceptionInInitializerError(ex); } }//创建线程局部变量session,用来保存Hibernate的Session public static final ThreadLocal session = new ThreadLocal(); /***获取当前线程中的Session * @return Session * @throws HibernateException */ public static Session currentSession() throws HibernateException { Session s = (Session) session.get();//如果Session还没有打开,则新开一个Session if (s == null) { s = sessionFactory.openSession();session.set(s); //将新开的Session保存到线程局部变量中 } return s; } public static void closeSession() throws HibernateException {//获取线程局部变量,并强制转换为Session类型 Session s = (Session) session.get(); session.set(null); if (s != null) s.close(); }}
[if !supportLists]3. [endif]在Runnable中创建ThreadLocal
在线程类内部创建ThreadLocal,基本步骤如下:
[if !supportLists]①、[endif]在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
[if !supportLists]②、[endif]在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型
[if !supportLists]③、[endif]在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable{ ThreadLocal studenThreadLocal = new ThreadLocal(); @Override public void run() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running..."); Random random = new Random(); int age = random.nextInt(100); System.out.println(currentThreadName + " is set age: " + age); Studen studen = getStudent(); //通过这个方法,为每个线程都独立的new一个student对象,每个线程的的student对象都可以设置不同的值 studen.setAge(age); System.out.println(currentThreadName + " is first get age: " + studen.getAge()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( currentThreadName + " is second get age: " + studen.getAge()); } private Studen getStudent() { Studen studen = studenThreadLocal.get(); if (null == studen) { studen = new Studen(); studenThreadLocal.set(studen); } return studen; } public static void main(String[] args) { ThreadLocalTest t = new ThreadLocalTest(); Thread t1 = new Thread(t,"Thread A"); Thread t2 = new Thread(t,"Thread B"); t1.start(); t2.start(); } }class Studen{ int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
[if !supportLists]( 5 ) [endif]多线程共享数据
在Java传统线程机制中的共享数据方式,大致可以简单分两种情况:
[if !supportLists]Ø [endif]多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
[if !supportLists]Ø [endif]多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的Runnable对象。例如,银行存取款。
下面我们通过两个示例代码来分别说明这两种方式。
[if !supportLists]1. [endif]多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
/**
*共享数据类
**/
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
*多线程类
**/
class RunnableCusToInc implements Runnable{
private ShareData shareData;
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
/**
*测试方法
**/
public static void main(String[] args) {
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();
}
}
}
[if !supportLists]2. [endif]多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
[if !supportLists]1) [endif]将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
public static void main(String[] args) {
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
if(i%2 == 0){
new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();
}else{
new Thread(new RunnableCusToDec(shareData),"Thread "+ i).start();
}
}
}
//封装共享数据类
class RunnableCusToInc implements Runnable{
//封装共享数据
private ShareData shareData;
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
//封装共享数据类
class RunnableCusToDec implements Runnable{
//封装共享数据
private ShareData shareData;
public RunnableCusToDec(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.dec();
}
}
}
/**
*共享数据类
**/
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[if !supportLists]2) [endif]将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
public static void main(String[] args) {
//公共数据
final ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
if(i%2 == 0){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
},"Thread "+ i).start();
}else{
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.dec();
}
}
},"Thread "+ i).start();
}
}
}
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void dec() {
num--;
System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
(二)多线程基础知识--线程并发库(2017-12-11-wl)
Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。下面带你认识下java.util.concurrent包里的这些类,然后你可以尝试着如何在项目中使用它们。本文中将使用Java 6 版本,我不确定这和 Java 5 版本里的是否有一些差异。我不会去解释关于 Java 并发的核心问题 – 其背后的原理,也就是说,如果你对那些东西感兴趣,参考《Java 并发指南》。
[if !supportLists]( 1 ) [endif]Java的线程并发库介绍
Java5的多线程并有两个大发库在java.util.concurrent包及子包中,子包主要的包有一下两个
[if !supportLists]1) [endif]java.util.concurrent包 (多线程并发库)
[if !supportLists]Ø [endif] java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建 java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性,后面、我们会做介绍。
[if !supportLists]Ø [endif]如果一些类名看起来相似,可能是因为java.util.concurrent 中的许多概念源自 Doug Lea 的 util.concurrent 库。
[if !supportLists]2) [endif]java.util.concurrent.atomic包 (多线程的原子性操作提供的工具类)
[if !supportLists]Ø [endif]查看atomic包文档页下面的介绍,它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据进行多线程的操作(AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpDater…)
[if !supportLists]Ø [endif]通过如下两个方法快速理解atomic包的意义:
[if !supportLists]n [endif]AtomicInteger类的boolean compareAndSet(expectedValue, updateValue);
[if !supportLists]n [endif]AtomicIntegerArray类的int addAndGet(int i, int delta);
[if !supportLists]Ø [endif]顺带解释volatile类型的作用,需要查看java语言规范。
[if !supportLists]n [endif]volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)
[if !supportLists]n [endif]volatile没有原子性。
[if !supportLists]3) [endif]java.util.concurrent.lock包 (多线程的锁机制)
为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。本包下有三大接口,下面简单介绍下:
[if !supportLists]Ø [endif]Lock接口:支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是ReentrantLock。
[if !supportLists]Ø [endif]ReadWriteLock接口:以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
[if !supportLists]Ø [endif]Condition接口:描述了可能会与锁有关联的条件变量。这些变量在用法上与使用Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。
[if !supportLists]( 2 ) [endif]Java的并发库入门
下面我们将分别介绍java.util.concurrent包下的常用类的使用。
[if !supportLists]1) [endif]java.util.concurrent包
java.util.concurrent包描述:
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能的类。此包下有一些组件,其中包括:
[if !supportLists]l [endif]执行程序(线程池)
[if !supportLists]l [endif]并发队列
[if !supportLists]l [endif]同步器
[if !supportLists]l [endif]并发Collocation
下面我们将java.util.concurrent包下的组件逐一简单介绍:
[if !supportLists]A. [endif]执行程序
[if !supportLists]Ø [endif]Executors线程池工厂类
首次我们来说下线程池的作用:
线程池作用就是限制系统中执行线程的数量。 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
[if !supportLists]l [endif]减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
[if !supportLists]l [endif]可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
Executors详解:
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。ThreadPoolExecutor是Executors类的底层实现。我们先介绍下Executors。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
Java5中并发库中,线程池创建线程大致可以分为下面三种:
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();
下面我们通过简单示例来分别说明:
[if !supportLists]l [endif]固定大小连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
/**
* Java线程:线程池-
*
* @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
运行结果:
pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-2正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-2正在执行。。。
从上面的运行来看,我们Thread类都是在线程池中运行的,线程池在执行execute方法来执行Thread类中的run方法。不管execute执行几次,线程池始终都会使用2个线程来处理。不会再去创建出其他线程来处理run方法执行。这就是固定大小线程池。
[if !supportLists]l [endif]单任务连接池
我们将上面的代码
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newSingleThreadExecutor();
运行结果:
pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。
运行结果看出,单任务线程池在执行execute方法来执行Thread类中的run方法。不管execute执行几次,线程池始终都会使用单个线程来处理。
补充:在java的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现异常信息:Exception in thread "main" java.lang.IllegalThreadStateException。那么如何解决这个问题呢?
我们这里就可以使用Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)
[if !supportLists]l [endif]可变连接池
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newCachedThreadPool();
运行结果:
pool-1-thread-5正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-4正在执行。。。 pool-1-thread-3正在执行。。。 pool-1-thread-2正在执行。。。
运行结果看出,可变任务线程池在执行execute方法来执行Thread类中的run方法。这里execute执行多次,线程池就会创建出多个线程来处理Thread类中run方法。所有我们看到连接池会根据执行的情况,在程序运行时创建多个线程来处理,这里就是可变连接池的特点。
那么在上面的三种创建方式,Executors还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示下。
[if !supportLists]l [endif]延迟连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Java线程:线程池-
*
* @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
//使用定时执行风格的方法
pool.schedule(t4, 10, TimeUnit.MILLISECONDS);//t4和t5在10秒后执行
pool.schedule(t5, 10, TimeUnit.MILLISECONDS);
//关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
运行结果:
pool-1-thread-1正在执行。。。 pool-1-thread-2正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-2正在执行。。。
[if !supportLists]Ø [endif]ExecutorService执行器服务
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
ExecutorService例子:
以下是一个简单的ExecutorService 例子:
//线程工厂类创建出线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行一个线程任务
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
//线程池关闭
executorService.shutdown();
上面代码首先使用newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务的线程池。然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某个线程执行该 Runnable。这里可以看成一个任务分派,示例代码中的任务分派我们可以理解为:
一个线程将一个任务委派给一个ExecutorService 去异步执行。
一旦该线程将任务委派给ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。
如下图:
ExecutorService实现:
既然ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。
java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
[if !supportLists]l [endif]ThreadPoolExecutor
[if !supportLists]l [endif]ScheduledThreadPoolExecutor
ExecutorService创建:
ExecutorService 的创建依赖于你使用的具体实现。但是你也可以使用 Executors 工厂类来创建 ExecutorService 实例。代码示例:
ExecutorService executorService1 = Executors.newSingleThreadExecutor(); //之前Executors已介绍
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService使用:
有几种不同的方式来将任务委托给ExecutorService 去执行:
[if !supportLists]l [endif]execute(Runnable)
[if !supportLists]l [endif]submit(Runnable)
[if !supportLists]l [endif]submit(Callable)
[if !supportLists]l [endif]invokeAny(…)
[if !supportLists]l [endif]invokeAll(…)
接下来我们挨个看一下这些方法。
[if !supportLists]ü [endif]execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用 ExecutorService 执行一个 Runnable 的示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
//执行 ExecutorService 中的方法
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
//线程池关闭
executorService.shutdown();
特点:没有办法得知被执行的Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介绍)。
[if !supportLists]ü [endif]submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕。以下是 ExecutorService submit() 示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //获得执行完run方法后的返回值,这里使用的Runnable,所以这里没有返回值,返回的是null。
executorService.shutdown();
[if !supportLists]ü [endif]submit(Runnable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。
以下是一个ExecutorService Callable 示例:
//从Executors中获得ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
executorService.shutdown();
输出:
Asynchronous Callable
future.get() = Callable Result
[if !supportLists]ü [endif]invokeAny()
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set> callables = new HashSet>();
callables.add(new Callable() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
上述代码将会打印出给定Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有时是 “Task 1″,有时是 “Task 2″ 等等。
[if !supportLists]ü [endif]invokeAll()
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。记住,一个任务可能会由于一个异常而结束,因此它可能没有"成功"。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。
以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set> callables = new HashSet>();
callables.add(new Callable() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable() {
public String call() throws Exception {
return "Task 3";
}
});
List> futures = executorService.invokeAll(callables);
for(Future future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();
输出结果:
future.get = Task 3
future.get = Task 1
future.get = Task 2
Executors关闭:
使用shutdown和shutdownNow可以关闭线程池
两者的区别:
shutdown只是将空闲的线程interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是interrupt所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为interrupt()方法能力有限。
[if !supportLists]Ø [endif]ThreadPoolExecutor线程池执行者
java.util.concurrent.ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。
ThreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
[if !supportLists]l [endif]corePoolSize
[if !supportLists]l [endif]maximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于corePoolSize,一个新的线程将被创建,即使池中可能尚有空闲线程。如果内部任务队列已满,而且有至少 corePoolSize 正在运行,但是运行线程的数量低于 maximumPoolSize,一个新的线程将被创建去执行该任务。
ThreadPoolExecutor 图解:
创建ThreadPoolExecutor:
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 5000;
ExecutorService threadPoolExecutor =
new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()
);
构造方法参数列表解释:
corePoolSize -池中所保存的线程数,包括空闲线程。
maximumPoolSize -池中允许的最大线程数。
keepAliveTime -当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime参数的时间单位。
workQueue -执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
[if !supportLists]Ø [endif]ScheduledPoolExecutor定时线程池执行者
java.util.concurrent.ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线程执行。
ScheduledPoolExecutor例子:
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},
5,
TimeUnit.SECONDS);//5秒后执行
首先一个内置5 个线程的 ScheduledExecutorService 被创建。之后一个 Callable 接口的匿名类示例被创建然后传递给 schedule() 方法。后边的俩参数定义了 Callable 将在 5 秒钟之后被执行。
ScheduledExecutorService的实现:
ScheduledExecutorService 是一个接口,你要用它的话就得使用 java.util.concurrent 包里对它的某个实现类。ScheduledExecutorService 具有以下实现类:ScheduledThreadPoolExecutor
创建一个ScheduledExecutorService:
如何创建一个ScheduledExecutorService 取决于你采用的它的实现类。但是你也可以使用 Executors 工厂类来创建一个 ScheduledExecutorService 实例。比如:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledExecutorService的使用:
一旦你创建了一个ScheduledExecutorService,你可以通过调用它的以下方法:
[if !supportLists]l [endif]schedule (Callable task, long delay, TimeUnit timeunit)
[if !supportLists]l [endif]schedule (Runnable task, long delay, TimeUnit timeunit)
[if !supportLists]l [endif]scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
[if !supportLists]l [endif]scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
下面我们就简单看一下这些方法。
[if !supportLists]ü [endif]schedule (Callable task, long delay, TimeUnit timeunit)
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},
5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
输出结果:
Executed! result = Called!
[if !supportLists]ü [endif]schedule (Runnable task, long delay, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
[if !supportLists]ü [endif]scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
[if !supportLists]ü [endif]scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
除了period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。scheduleAtFixedRate() 方法中,period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。
ScheduledExecutorService的关闭:
正如ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM 继续运行,即使所有其他线程已经全被关闭。
你可以使用从ExecutorService 接口继承来的 shutdown() 或 shutdownNow() 方法将 ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。
[if !supportLists]Ø [endif]ForkJoinPool合并和分叉(线程池)
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小的子任务,只要它还能分割。可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任务分割是如何进行的。
合并和分叉的解释:
在我们开始看ForkJoinPool 之前我们先来简要解释一下分叉和合并的原理。分叉和合并原理包含两个递归进行的步骤。两个步骤分别是分叉步骤和合并步骤。
分叉:
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,这些子任务可以被并发执行。如下图所示:
通过把自己分割成多个子任务,每个子任务可以由不同的CPU 并行执行,或者被同一个 CPU 上的不同线程执行。只有当给的任务过大,把它分割成几个子任务才有意义。把任务分割成子任务有一定开销,因此对于小型任务,这个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。很大程度上取决于它要做的工作的种类。
合并:
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。一旦子任务执行结束,该任务可以把所有结果合并到同一个结果。图示如下:
当然,并非所有类型的任务都会返回一个结果。如果这个任务并不返回一个结果,它只需等待所有子任务执行完毕。也就不需要结果的合并啦。
所以我们可以将ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
创建一个ForkJoinPool:
你可以通过其构造子创建一个ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool 示例:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
提交任务到ForkJoinPool:
就像提交任务到ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和 RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
RecursiveAction:
RecursiveAction 是一种没有任何返回值的任务。它只是做一些工作,比如写数据到磁盘,然后就退出了。一个 RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。你可以通过继承来实现一个 RecursiveAction。示例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List subtasks =
new ArrayList();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List createSubtasks() {
List subtasks =
new ArrayList();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。如果 workLoad 高于一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。如果 workLoad 低于特定阀值,该工作将由 MyRecursiveAction 自己执行。你可以这样规划一个 MyRecursiveAction 的执行:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个没有返回值的任务
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool执行任务
forkJoinPool.invoke(myRecursiveAction);
运行结果:
Splitting workLoad : 24
Doing workLoad myself: 12
RecursiveTask:
RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List subtasks =
new ArrayList();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List createSubtasks() {
List subtasks =
new ArrayList();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
除了有一个结果返回之外,这个示例和RecursiveAction 的例子很像。MyRecursiveTask 类继承自 RecursiveTask,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。此外,本示例还通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返回。对于不同级别的递归,这种子任务的结果合并可能会发生递归。
你可以这样规划一个RecursiveTask:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个有返回值的任务
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
//线程池执行并返回结果
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult);
注意: ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
[if !supportLists]B. [endif]并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现。
PS:至于非阻塞队列是靠CAS非阻塞算法,在这里不再介绍,大家只用知道,Java非阻塞队列是使用CAS算法来实现的就可以。感兴趣的童鞋可以维基网上自行学习.
下面我们先介绍阻塞队列。
阻塞队列:
阻塞队列(BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
[if !supportLists]Ø [endif]BlockingQueue阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:
一个线程往里边放,另外一个线程从里边取的一个BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue的方法:
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
方法\处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。
无法向一个BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个 NullPointerException.
BlockingQueue的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用BlockingQueue,Java.util.concurrent包下具有以下 BlockingQueue 接口的实现类:
[if !supportLists]l [endif]ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
[if !supportLists]l [endif]DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
[if !supportLists]l [endif]LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
[if !supportLists]l [endif]PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
[if !supportLists]l [endif]SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
[if !supportLists]Ø [endif]ArrayBlockingQueue阻塞队列
ArrayBlockingQueue类图
如上图ArrayBlockingQueue内部有个数组items用来存放队列元素,putindex下标标示入队元素下标,takeIndex是出队下标,count统计队列元素个数,从定义可知道并没有使用volatile修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁lock用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外notEmpty,notFull条件变量用来进行出入队的同步。
另外构造函数必须传入队列大小参数,所以为有界队列,默认是Lock为非公平锁。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ps:
所谓公平锁:就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
ArrayBlockingQueue方法
[if !supportLists]ü [endif]offer方法
在队尾插入元素,如果队列满则返回false,否者入队返回true。
public boolean offer(E e) {
//e为null,则抛出NullPointerException异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列满则返回false
if (count == items.length)
return false;
else {
//否者插入元素
insert(e);
return true;
}
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
//元素入队
items[putIndex] = x;
//计算下一个元素应该存放的下标
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是在CPU缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。
另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。另外insert后调用 notEmpty.signal();是为了激活调用notEmpty.await()阻塞后放入notEmpty条件队列中的线程。
[if !supportLists]ü [endif]Put操作
在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁
lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入notFull管理的条件队列
while (count == items.length)
notFull.await();
//插入元素
insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了notFull.signal方法激活该线程。代码逻辑很简单,但是这里需要思考一个问题为啥调用lockInterruptibly方法而不是Lock方法。我的理解是因为调用了条件变量的await()方法,而await()方法会在中断标志设置后抛出InterruptedException异常后退出,所以还不如在加锁时候先看中断标志是不是被设置了,如果设置了直接抛出InterruptedException异常,就不用再去获取锁了。然后看了其他并发类里面凡是调用了await的方法获取锁时候都是使用的lockInterruptibly方法而不是Lock也验证了这个想法。
[if !supportLists]ü [endif]Poll操作
从队头获取并移除元素,队列为空,则返回null。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//当前队列为空则返回null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.cast(items[takeIndex]);
//数组中值值为null;
items[takeIndex] = null;
//队头指针计算,队列元素个数减一
takeIndex = inc(takeIndex);
--count;
//发送信号激活notFull条件队列里面的线程
notFull.signal();
return x;
}
[if !supportLists]ü [endif]Take操作
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空,当前线程会被挂起放到notEmpty的条件队列里面,直到入队操作执行调用notEmpty.signal后当前线程才会被激活,await才会返回。
[if !supportLists]ü [endif]Peek操作
返回队列头元素但不移除该元素,队列为空,返回null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列为空返回null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.cast(items[i]);
}
[if !supportLists]ü [endif]Size操作
获取队列元素个数,非常精确因为计算size时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
[if !supportLists]ü [endif]ArrayBlockingQueue小结
ArrayBlockingQueue通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加synchronized的意味。其中offer,poll操作通过简单的加锁进行入队出队操作,而put,take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比LinkedBlockingQueue,ArrayBlockingQueue的size操作的结果是精确的,因为计算前加了全局锁。
ArrayBlockingQueue示例
需求:在多线程操作下,一个数组中最多只能存入3个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用java的多线程来实现。(面试)
代码实现:
public class BlockingQueueTest {
public static void main(String[] args) {
final BlockingQueue queue = new ArrayBlockingQueue(3);
for(int i=0;i<2;i++){
new Thread(){
public void run(){
while(true){
try {
Thread.sleep((long)(Math.random()*1000));
System.out.println(Thread.currentThread().getName() + "准备放数据!");
queue.put(1);
System.out.println(Thread.currentThread().getName() + "已经放了数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
new Thread(){
public void run(){
while(true){
try {
//将此处的睡眠时间分别改为100和1000,观察运行结果
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "准备取数据!");
System.err.println(queue.take());
System.out.println(Thread.currentThread().getName() + "已经取走数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
输出结果:
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有3个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有3个数据
Thread-0准备放数据!
Thread-1准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有3个数据
…………………
[if !supportLists]Ø [endif]LinkedBlockingQueue阻塞队列
LinkedBlockingQueue类图
LinkedBlockingQueue中也有两个Node分别用来存放首尾节点,并且里面有个初始值为0的原子变量count用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁,其中takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外notEmpty和notFull用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型,如下类图:
/**通过take取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/**等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/*通过put放置进行加锁、放置*/
private final ReentrantLock putLock = new ReentrantLock();
/**等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/*记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
队列的容量:
//队列初始容量,Integer最大值
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//初始化首尾节点
last = head = new Node(null);
}
如图默认队列容量为0x7fffffff;用户也可以自己指定容量。
LinkedBlockingQueue方法
ps:下面介绍LinkedBlockingQueue用到很多Lock对象。详细可以查找Lock对象的介绍
[if !supportLists]ü [endif]带时间的Offer操作-生产者
在ArrayBlockingQueue中已经简单介绍了Offer()方法,LinkedBlocking的Offer方法类似,在此就不过多去介绍。这次我们从介绍下带时间的Offer方法
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//空元素抛空指针异常
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取可被中断锁,只有一个线程克获取
putLock.lockInterruptibly();
try {
//如果队列满则进入循环
while (count.get() == capacity) {
//nanos<=0直接返回
if (nanos <= 0)
return false;
//否者调用await进行等待,超时则返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await在超时时间内返回则添加元素(2)
enqueue(new Node(e));
c = count.getAndIncrement();
//队列不满则激活其他等待入队线程(3)
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//c==0说明队列里面有一个元素,这时候唤醒出队线程(4)
if (c == 0)
signalNotEmpty();
return true;
}
private void enqueue(Node node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
[if !supportLists]ü [endif]带时间的poll操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//出队线程获取独占锁
takeLock.lockInterruptibly();
try {
//循环直到队列不为空
while (count.get() == 0) {
//超时直接返回null
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//出队,计数器减一
x = dequeue();
c = count.getAndDecrement();
//如果出队前队列不为空则发送信号,激活其他阻塞的出队线程
if (c > 1)
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//当前队列容量为最大值-1则激活入队线程。
if (c == capacity)
signalNotFull();
return x;
}
首先获取独占锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回null。
超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去1前队列元素大于1则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。
最后如果减去1前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程。
[if !supportLists]ü [endif]put操作-生产者
与带超时时间的poll类似不同在于put时候如果当前队列满了它会一直等待其他线程调用notFull.signal才会被唤醒。
[if !supportLists]ü [endif]take操作-消费者
与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒。
[if !supportLists]ü [endif]size操作-消费者
当前队列元素个数,如代码直接使用原子变量count获取。
public int size() {
return count.get();
}
[if !supportLists]ü [endif]peek操作
获取但是不移除当前队列的头元素,没有则返回null。
public E peek() {
//队列空,则返回null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
[if !supportLists]ü [endif]remove操作
删除队列里面的一个元素,有则删除返回true,没有则返回false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。
public boolean remove(Object o) {
if (o == null) return false;
//双重加锁
fullyLock();
try {
//遍历队列找则删除返回true
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
//找不到返回false
return false;
} finally {
//解锁
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
void unlink(Node p, Node trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果当前队列满,删除后,也不忘记最快的唤醒等待的线程
if (count.getAndDecrement() == capacity)
notFull.signal();
}
[if !supportLists]ü [endif]开源框架的使用
tomcat中任务队列TaskQueue。
类结构图:
可知TaskQueue继承了LinkedBlockingQueue并且泛化类型固定了为Runnalbe.重写了offer,poll,take方法。
tomcat中有个线程池ThreadPoolExecutor,在NIOEndPoint中当acceptor线程接受到请求后,会把任务放入队列,然后poller 线程从队列里面获取任务,然后就把任务放入线程池执行。这个ThreadPoolExecutor中的的一个参数就是TaskQueue。
先看看ThreadPoolExecutor的参数如果是普通LinkedBlockingQueue是怎么样的执行逻辑: 当调用线程池方法execute() 方法添加一个任务时:
[if !supportLists]l [endif]如果当前运行的线程数量小于corePoolSize,则创建新线程运行该任务
[if !supportLists]l [endif]如果当前运行的线程数量大于或等于corePoolSize,则将这个任务放入阻塞队列。
[if !supportLists]l [endif]如果当前队列满了,并且当前运行的线程数量小于maximumPoolSize,则创建新线程运行该任务;
[if !supportLists]l [endif]如果当前队列满了,并且当前运行的线程数量大于或等于maximumPoolSize,那么线程池将会抛出RejectedExecutionException异常。
如果线程执行完了当前任务,那么会去队列里面获取一个任务来执行,如果任务执行完了,并且当前线程数大于corePoolSize,那么会根据线程空闲时间keepAliveTime回收一些线程保持线程池corePoolSize个线程。
首先看下线程池中exectue添加任务时候的逻辑:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//当前工作线程个数小于core个数则开新线程执行(1)
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//放入队列(2)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果队列满了则开新线程,但是个数要不超过最大值,超过则返回false
//然后执行reject handler(3)
else if (!addWorker(command, false))
reject(command);
}
可知当当前工作线程个数为corePoolSize后,如果在来任务会把任务添加到队列,队列满了或者入队失败了则开启新线程。
然后看看TaskQueue中重写的offer方法的逻辑:
public boolean offer(Runnable o) {
//如果parent为null则直接调用父类方法
if (parent==null) return super.offer(o);
//如果当前线程池中线程个数达到最大,则无条件调用父类方法
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//如果当前提交的任务小于当前线程池线程数,说明线程用不完,没必要重新开线程
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//如果当前线程池线程个数>core个数但是小于最大个数,则开新线程代替放入队列
if (parent.getPoolSize()
//到了这里,无条件调用父类
return super.offer(o);
}
可知parent.getPoolSize()
LinkedBlockingQueue安全分析总结
仔细思考下阻塞队列是如何实现并发安全的维护队列链表的,先分析下简单的情况就是当队列里面有多个元素时候,由于同时只有一个线程(通过独占锁putLock实现)入队元素并且是操作last节点(,而同时只有一个出队线程(通过独占锁takeLock实现)操作head节点,所以不存在并发安全问题。
考虑当队列为空的时候队列状态为:
这时候假如一个线程调用了take方法,由于队列为空,所以count.get()==0所以当前线程会调用notEmpty.await()把自己挂起,并且放入notEmpty的条件队列,并且释放当前条件变量关联的通过takeLock.lockInterruptibly()获取的独占锁。由于释放了锁,所以这时候其他线程调用take时候就会通过takeLock.lockInterruptibly()获取独占锁,然后同样阻塞到notEmpty.await(),同样会被放入notEmpty的条件队列,也就说在队列为空的情况下可能会有多个线程因为调用take被放入了notEmpty的条件队列。
这时候如果有一个线程调用了put方法,那么就会调用enqueue操作,该操作会在last节点后面添加新元素并且设置last为新节点。然后count.getAndIncrement()先获取当前队列元个数为0保存到c,然后自增count为1,由于c==0所以调用signalNotEmpty激活notEmpty的条件队列里面的阻塞时间最长的线程,这时候take中调用notEmpty.await()的线程会被激活await内部会重新去获取独占锁获取成功则返回,否者被放入AQS的阻塞队列,如果获取成功,那么count.get() >0因为可能多个线程put了,所以调用dequeue从队列获取元素(这时候一定可以获取到),然后调用c = count.getAndDecrement() 把当前计数返回后并减去1,如果c>1 说明当前队列还有其他元素,那么就调用 notEmpty.signal()去激活 notEmpty的条件队列里面的其他阻塞线程。
考虑当队列满的时候:
当队列满的时候调用put方法时候,会由于notFull.await()当前线程被阻塞放入notFull管理的条件队列里面,同理可能会有多个调用put方法的线程都放到了notFull的条件队列里面。
这时候如果有一个线程调用了take方法,调用dequeue()出队一个元素,c = count.getAndDecrement();count值减一;c==capacity;现在队列有一个空的位置,所以调用signalNotFull()激活notFull条件队列里面等待最久的一个线程。
LinkedBlockingQueue简单示例
并发库中的BlockingQueue是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法put()和take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从head取一个对象,如果没有对象,就等待直到有可取的对象。
下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中,另外四个写线程用于取出文件对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该Demo也使用到了线程池和原子整型 (AtomicInteger),AtomicInteger可以在并发情况下达到原子化更新,避免使用了synchronized,而且性能非常高。由 于阻塞队列的put和take操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫“哨兵”,当发现这个哨兵后,写线程就退出。
当然线程池也要显式退出了。
package concurrent; import java.io.File; import java.io.FileFilter; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class TestBlockingQueue { static long randomTime() { return (long) (Math.random() * 1000); } public static void main(String[] args) { //能容纳100个文件 final BlockingQueue queue = new LinkedBlockingQueue(100); //线程池 final ExecutorService exec = Executors.newFixedThreadPool(5); final File root = new File("F:\\JavaLib"); //完成标志 final File exitFile = new File(""); //读个数 final AtomicInteger rc = new AtomicInteger(); //写个数 final AtomicInteger wc = new AtomicInteger(); //读线程 Runnable read = new Runnable() { public void run() { scanFile(root); scanFile(exitFile); } public void scanFile(File file) { if (file.isDirectory()) { File[] files = file.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.isDirectory() || pathname.getPath().endsWith(".java"); } }); for (File one : files) scanFile(one); } else { try { int index = rc.incrementAndGet(); System.out.println("Read0: " + index + " " + file.getPath()); queue.put(file); } catch (InterruptedException e) { } } } }; exec.submit(read);//四个写线程 for (int index = 0; index < 4; index++) { // write thread final int NO = index; Runnable write = new Runnable() { String threadName = "Write" + NO; public void run() { while (true) { try { Thread.sleep(randomTime()); int index = wc.incrementAndGet(); File file = queue.take();//队列已经无对象 if (file == exitFile) {//再次添加"标志",以让其他线程正常退出 queue.put(exitFile); break; } System.out.println(threadName + ": " + index + " " + file.getPath()); } catch (InterruptedException e) { } } } }; exec.submit(write); } exec.shutdown(); }}
[if !supportLists]Ø [endif]PriorityBlockingQueue无界阻塞优先级队列
PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现,研究过数组方式存放最小堆节点的都知道,直接遍历队列元素是无序的。
PriorityBlockingQueue类图结构
如图PriorityBlockingQueue内部有个数组queue用来存放队列元素,size用来存放队列元素个数,allocationSpinLockOffset是用来在扩容队列时候做cas的,目的是保证只有一个线程可以进行扩容。
由于这是一个优先级队列所以有个比较器comparator用来比较元素大小。lock独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty条件变量用来实现take方法阻塞模式。这里没有notFull 条件变量是因为这里的put操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。最后PriorityQueue q用来搞序列化的。
如下构造函数,默认队列容量为11,默认比较器为null;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
PriorityBlockingQueue方法
[if !supportLists]ü [endif]Offer操作
在队列插入一个元素,由于是无界队列,所以一直为成功返回true;
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
//如果当前元素个数>=队列容量,则扩容(1)
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator cmp = comparator;
//默认比较器为null
if (cmp == null)(2)
siftUpComparable(n, e, array);
else
//自定义比较器(3)
siftUpUsingComparator(n, e, array, cmp);
//队列元素增加1,并且激活notEmpty的条件队列里面的一个阻塞线程
size = n + 1;(9)
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
主流程比较简单,下面看看两个主要函数
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); //must release and then re-acquire main lock
Object[] newArray = null;
//cas成功则扩容(4)
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
//oldGap<64则扩容新增oldcap+2,否者扩容50%,并且最大为MAX_ARRAY_SIZE
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
//第一个线程cas成功后,第二个线程会进入这个地方,然后第二个线程让出cpu,尽量让第一个线程执行下面点获取锁,但是这得不到肯定的保证。(5)
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();(6)
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
tryGrow目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用cas控制只有一个线程可以扩容成功。我的理解是为了性能,因为扩容时候是需要花时间的,如果这些操作时候还占用锁那么其他线程在这个时候是不能进行出队操作的,也不能进行入队操作,这大大降低了并发性。
所以在扩容前释放锁,这允许其他出队线程可以进行出队操作,但是由于释放了锁,所以也允许在扩容时候进行入队操作,这就会导致多个线程进行扩容会出现问题,所以这里使用了一个spinlock用cas控制只有一个线程可以进行扩容,失败的线程调用Thread.yield()让出cpu,目的意在让扩容线程扩容后优先调用lock.lock重新获取锁,但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁。
那copy元素数据到新数组为啥放到获取锁后面那?原因应该是因为可见性问题,因为queue并没有被volatile修饰。另外有可能在扩容时候进行了出队操作,如果直接拷贝可能看到的数组元素不是最新的。而通过调用Lock后,获取的数组则是最新的,并且在释放锁前 数组内容不会变化。
具体建堆算法:
private static void siftUpComparable(int k, T x, Object[] array) {
Comparable key = (Comparable) x;
//队列元素个数>0则判断插入位置,否者直接入队(7)
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;(8)
}
下面用图说话模拟下过程:假设队列容量为2
[if !supportLists]· [endif]第一次offer(2)时候
执行(1)为false所以执行(2),由于k=n=size=0;所以执行(8)元素入队,然执行(9)size+1;现在队列状态:
[if !supportLists]· [endif]第二次offer(4)时候
执行(1)为false,所以执行(2)由于k=1,所以进入while循环,parent=0;e=2;key=4;key>e所以break;然后把4存到数据下标为1的地方,这时候队列状态为:
[if !supportLists]· [endif]第三次offer(4)时候
执行(1)为true,所以调用tryGrow,由于2<64所以newCap=2 + (2+2)=6;然后创建新数组并拷贝,然后调用siftUpComparable;k=2>0进入循环 parent=0;e=2;key=6;key>e所以break;然后把6放入下标为2的地方,现在队列状态:
[if !supportLists]· [endif]第四次offer(1)时候
执行(1)为false,所以执行(2)由于k=3,所以进入while循环,parent=0;e=2;key=1; key
[if !supportLists]ü [endif]Poll操作
在队列头部获取并移除一个元素,如果队列为空,则返回null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
主要看dequeue
private E dequeue() {
//队列为空,则返回null
int n = size - 1;
if (n < 0)
return null;
else {
//获取队头元素(1)
Object[] array = queue;
E result = (E) array[0];
//获取对尾元素,并值null(2)
E x = (E) array[n];
array[n] = null;
Comparator cmp = comparator;
if (cmp == null)//cmp=null则调用这个,把对尾元素位置插入到0位置,并且调整堆为最小堆(3)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;(4)
return result;
}
}
private static void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable key = (Comparable)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];(5)
int right = child + 1;(6)
if (right < n &&
((Comparable) c).compareTo((T) array[right]) > 0)(7)
c = array[child = right];
if (key.compareTo((T) c) <= 0)(8)
break;
array[k] = c;
k = child;
}
array[k] = key;(9)
}
}
下面用图说话模拟下过程:
[if !supportLists]· [endif]第一次调用poll()
首先执行(1) result=1;然后执行(2)x=2;这时候队列状态
然后执行(3)后状态为:
执行(4)后的结果:
下面重点说说siftDownComparable这个屌屌的建立最小堆的算法:
首先说下思想,其中k一开始为0,x为数组里面最后一个元素,由于第0个元素为树根,被出队时候要被搞掉,所以建堆要从它的左右孩子节点找一个最小的值来当树根,子树根被搞掉后,会找子树的左右孩子最小的元素来代替,直到树节点为止,还不明白,没关系,看图说话:假如当前队列元素:
那么对于树为:
这时候如果调用了poll();那么result=2;x=11;现在树为:
然后看leftChildVal = 4;rightChildVal = 6; 4<6;所以c=4;也就是获取根节点的左右孩子值小的那一个; 然后看11>4也就是key>c;然后把c放入树根,现在树为:
然后看根的左边孩子4为根的子树我们要为这个字树找一个根节点。
看leftChildVal = 8;rightChildVal = 10; 8<10;所以c=8;也就是获取根节点的左右孩子值小的那一个; 然后看11>8也就是key>c;然后把c放入树根,现在树为:
这时候k=3;half=3所以推出循环,执行(9)后结果为:
这时候队列为:
[if !supportLists]ü [endif]Put操作
内部调用的offer,由于是无界队列,所以不需要阻塞
public void put(E e) {
offer(e); // never need to block
}
[if !supportLists]ü [endif]Take操作
获取队列头元素,如果队列为空则阻塞。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
//如果队列为空,则阻塞,把当前线程放入notEmpty的条件队列
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
这里是阻塞实现,阻塞后直到入队操作调用notEmpty.signal 才会返回。
[if !supportLists]ü [endif]Size操作
获取队列元个数,由于加了独占锁所以返回结果是精确的
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return size;
} finally {
lock.unlock();
}
}
PriorityBlockingQueue小结
PriorityBlockingQueue类似于ArrayBlockingQueue内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个notEmpty条件变量而没有notFull这是因为前者是无界队列,当put时候永远不会处于await所以也不需要被唤醒。
PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。
值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过cas保证同时只有一个线程可以扩容成功。
PriorityBlockingQueue示例
PriorityBlockingQueue类是JDK提供的优先级队列 本身是线程安全的 内部使用显示锁 保证线程安全。
PriorityBlockingQueue存储的对象必须是实现Comparable接口的 因为PriorityBlockingQueue队列会根据内部存储的每一个元素的compareTo方法比较每个元素的大小。这样在take出来的时候会根据优先级 将优先级最小的最先取出。
下面是示例代码
public static PriorityBlockingQueue queue = new PriorityBlockingQueue();
public static void main(String[] args) {
queue.add(new User(1,"wu"));
queue.add(new User(5,"wu5"));
queue.add(new User(23,"wu23"));
queue.add(new User(55,"wu55"));
queue.add(new User(9,"wu9"));
queue.add(new User(3,"wu3"));
for (User user : queue) {
try {
System.out.println(queue.take().name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//静态内部类
static class User implements Comparable{
public User(int age,String name) {
this.age = age;
this.name = name;
}
int age;
String name;
@Override
public int compareTo(User o) {
return this.age > o.age ? -1 : 1;
}
}
[if !supportLists]Ø [endif]SychronousQueue同步队列
SynchronousQueue是一个比较特别的队列,由于在线程池方面有所应用,为了更好的理解线程池的实现原理,此队列源码中充斥着大量的CAS语句,理解起来是有些难度的,为了方便日后回顾,本篇文章会以简洁的图形化方式展示该队列底层的实现原理。
SychronousQueue简单实用
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
如下面的示意图所示:
SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:
package com.concurrent;
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue queue = new SynchronousQueue();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
}
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
一种输出结果如下:
put thread start
take thread start
take from putThread: 1
put thread end
take thread end
从结果可以看出,put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。
SychronousQueue实现原理
不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问。
队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。
公平模式下的模型:
公平模式下,底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。 初始化时,TransferQueue的状态如下:
接着我们进行一些操作:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入队列,自旋一小会后睡眠等待,这时队列状态如下:
2、接着,线程put2执行了put(2)操作,跟前面一样,put2线程入队列,自旋一小会后睡眠等待,这时队列状态如下:
[if !supportLists]3、[endif]这时候,来了一个线程take1,执行了 take操作,由于tail指向put2线程,put2线程跟take1线程配对了(一put一take),这时take1线程不需要入队,但是请注意了,这时候,要唤醒的线程并不是put2,而是put1。
为何?大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显是put1应该优先被唤醒。至于读者可能会有一个疑问,明明是take1线程跟put2线程匹配上了,结果是put1线程被唤醒消费,怎么确保take1线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配队头出队。
执行后put1线程被唤醒,take1线程的 take()方法返回了1(put1线程的数据),这样就实现了线程间的一对一通信,这时候内部状态如下:
4、最后,再来一个线程take2,执行take操作,这时候只有put2线程在等候,而且两个线程匹配上了,线程put2被唤醒, take2线程take操作返回了2(线程put2的数据),这时候队列又回到了起点,如下所示:
以上便是公平模式下,SynchronousQueue的实现模型。总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。
非公平模式下的模型:
我们还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。非公平模式底层的实现使用的是TransferStack,一个栈,实现中用head指针指向栈顶,接着我们看看它的实现模型:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入栈,自旋一小会后睡眠等待,这时栈状态如下:
[if !supportLists]2、[endif]接着,线程put2再次执行了put(2)操作,跟前面一样,put2线程入栈,自旋一小会后睡眠等待,这时栈状态如下:
[if !supportLists]3、[endif]这时候,来了一个线程take1,执行了take操作,这时候发现栈顶为put2线程,匹配成功,但是实现会先把take1线程入栈,然后take1线程循环执行匹配put2线程逻辑,一旦发现没有并发冲突,就会把栈顶指针直接指向 put1线程
步骤一:
步骤二:
[if !supportLists]4、[endif]最后,再来一个线程take2,执行take操作,这跟步骤3的逻辑基本是一致的,take2线程入栈,
然后在循环中匹配put1线程,最终全部匹配完毕,栈变为空,恢复初始状态,如下图所示:
步骤一:
步骤二:
可以从上面流程看出,虽然put1线程先入栈了,但是却是后匹配,这就是非公平的由来。
SychronousQueue总结
SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。
[if !supportLists]Ø [endif]DeplayQueue延时无界阻塞队列
在谈到DelayQueue的使用和原理的时候,我们首先介绍一下DelayQueue,DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。
DelayQueue阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检 查看数据是否过期等,首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要12:00点执行的任务可能12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用DelayQueue。
下面将会对DelayQueue做一个介绍,然后举个例子。并且提供一个Delayed接口的实现和Sample代码。DelayQueue是一个BlockingQueue,其特化的参数是Delayed。(不了解BlockingQueue的同学,先去了解BlockingQueue再看本文)
Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)。DelayQueue内部是使用PriorityQueue实现的。
DelayQueue = BlockingQueue +PriorityQueue + Delayed
DelayQueue定义和原理
DelayQueue的关键元素BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。
他们的基本定义如下
public interface Comparable {
public int compareTo(T o);
}
public interface Delayed extends Comparable {
long getDelay(TimeUnit unit);
}
public class DelayQueue implements BlockingQueue {
private final PriorityQueue q = new PriorityQueue();
}
DelayQueue内部的实现使用了一个优先队列。当调用DelayQueue的offer方法时,把Delayed对象加入到优先队列q中。如下:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}
DelayQueue的take方法,把优先队列q的first拿出来(peek),如果没有达到延时阀值,则进行await处理。如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
}
}
}
} finally {
lock.unlock();
}
}
DelayQueue实例应用
Ps:为了具有调用行为,存放到DelayDeque的元素必须继承Delayed接口。Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制执行下列两个方法。
一下将使用Delay做一个缓存的实现。其中共包括三个类
[if !supportLists]n [endif]Pair
[if !supportLists]n [endif]DelayItem
[if !supportLists]n [endif]Cache
Pair类:
public class Pair {
public K first;
public V second;
public Pair() {}
public Pair(K first, V second) {
this.first = first;
this.second = second;
}
}
一下是对Delay接口的实现:
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class DelayItem implements Delayed {
/** Base of nanosecond timings, to avoid wrapping */
private static final long NANO_ORIGIN = System.nanoTime();
/**
* Returns nanosecond time offset by origin
*/
final static long now() {
return System.nanoTime() - NANO_ORIGIN;
}
/**
* Sequence number to break scheduling ties, and in turn to guarantee FIFO order among tied
* entries.
*/
private static final AtomicLong sequencer = new AtomicLong(0);
/** Sequence number to break ties FIFO */
private final long sequenceNumber;
/** The time the task is enabled to execute in nanoTime units */
private final long time;
private final T item;
public DelayItem(T submit, long timeout) {
this.time = now() + timeout;
this.item = submit;
this.sequenceNumber = sequencer.getAndIncrement();
}
public T getItem() {
return this.item;
}
public long getDelay(TimeUnit unit) {
long d = unit.convert(time - now(), TimeUnit.NANOSECONDS);
return d;
}
public int compareTo(Delayed other) {
if (other == this) // compare zero ONLY if same object
return 0;
if (other instanceof DelayItem) {
DelayItem x = (DelayItem) other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
}
以下是Cache的实现,包括了put和get方法
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Cache {
private static final Logger LOG = Logger.getLogger(Cache.class.getName());
private ConcurrentMap cacheObjMap = new ConcurrentHashMap();
private DelayQueue>> q = new DelayQueue>>();
private Thread daemonThread;
public Cache() {
Runnable daemonTask = new Runnable() {
public void run() {
daemonCheck();
}
};
daemonThread = new Thread(daemonTask);
daemonThread.setDaemon(true);
daemonThread.setName("Cache Daemon");
daemonThread.start();
}
private void daemonCheck() {
if (LOG.isLoggable(Level.INFO))
LOG.info("cache service started.");
for (;;) {
try {
DelayItem> delayItem = q.take();
if (delayItem != null) {
//超时对象处理
Pair pair = delayItem.getItem();
cacheObjMap.remove(pair.first, pair.second); // compare and remove
}
} catch (InterruptedException e) {
if (LOG.isLoggable(Level.SEVERE))
LOG.log(Level.SEVERE, e.getMessage(), e);
break;
}
}
if (LOG.isLoggable(Level.INFO))
LOG.info("cache service stopped.");
}
//添加缓存对象
public void put(K key, V value, long time, TimeUnit unit) {
V oldValue = cacheObjMap.put(key, value);
if (oldValue != null)
q.remove(key);
long nanoTime = TimeUnit.NANOSECONDS.convert(time, unit);
q.put(new DelayItem>(new Pair(key, value), nanoTime));
}
public V get(K key) {
return cacheObjMap.get(key);
}
测试main方法:
//测试入口函数
public static void main(String[] args) throws Exception {
Cache cache = new Cache();
cache.put(1, "aaaa", 3, TimeUnit.SECONDS);
Thread.sleep(1000 * 2);
{
String str = cache.get(1);
System.out.println(str);
}
Thread.sleep(1000 * 2);
{
String str = cache.get(1);
System.out.println(str);
}
}
输出结果为:
aaaa
null
我们看到上面的结果,如果超过延时的时间,那么缓存中数据就会自动丢失,获得就为null。
[if !supportLists]C. [endif]并发(Collection)队列-非阻塞队列
[if !supportLists]Ø [endif]非阻塞队列
首先我们要简单的理解下什么是非阻塞队列:
与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。
在底层,非阻塞队列使用的是CAS(compare andswap)来实现线程执行的非阻塞。
非阻塞队列简单操作
与阻塞队列相同,非阻塞队列中的常用方法,也是出队和入队。
入队方法:
[if !supportLists]n [endif]add():底层调用offer();
[if !supportLists]n [endif]offer():Queue接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回true;
出队方法:
[if !supportLists]n [endif]poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回null;
[if !supportLists]n [endif]peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回null;
[if !supportLists]Ø [endif]非阻塞算法CAS
首先我们需要了解悲观锁和乐观锁
悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。乐观锁:假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。
通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。
首先保证正确性,有必要的话,再去追求性能。
CAS
乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义(这里的swap是“换入”,也就是set),构成了基本的乐观锁。
CAS包含3个操作数:
[if !supportLists]n [endif]需要读写的内存位置V
[if !supportLists]n [endif]进行比较的值A
[if !supportLists]n [endif]拟写入的新值B
当且仅当位置V的值等于A时,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。
一个有意思的事实是,“使用CAS控制并发”与“使用乐观锁”并不等价。CAS只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。下文将分别用CAS实现悲观锁和乐观锁?
[if !supportLists]Ø [endif]ConcurrentLinkedQueue非阻塞无界链表队列
ConcurrentLinkedQueue是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来说队列的长度可以无限扩大。
与其他队列相同,ConcurrentLinkedQueue也采用的是先进先出(FIFO)入队规则,对元素进行排序。当我们向队列中添加元素时,新插入的元素会插入到队列的尾部;而当我们获取一个元素时,它会从队列的头部中取出。
因为ConcurrentLinkedQueue是链表结构,所以当入队时,插入的元素依次向后延伸,形成链表;而出队时,则从链表的第一个元素开始获取,依次递增;
不知道,我这样形容能否让你对链表的入队、出队产生一个大概的思路!
ConcurrentLinkedQuere简单示例
值得注意的是,在使用ConcurrentLinkedQueue时,如果涉及到队列是否为空的判断,切记不可使用size()==0的做法,因为在size()方法中,是通过遍历整个链表来实现的,在队列元素很多的时候,size()方法十分消耗性能和时间,只是单纯的判断队列为空使用isEmpty()即可!!!
public class ConcurrentLinkedQueueTest {
public static int threadCount = 10;
public static ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
static class Offer implements Runnable {
public void run() {
//不建议使用queue.size()==0,影响效率。可以使用!queue.isEmpty()
if(queue.size()==0){
String ele = new Random().nextInt(Integer.MAX_VALUE)+"";
queue.offer(ele);
System.out.println("入队元素为"+ele);
}
}
}
static class Poll implements Runnable {
public void run() {
if(!queue.isEmpty()){
String ele = queue.poll();
System.out.println("出队元素为"+ele);
}
}
}
public static void main(String[] agrs) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for(int x=0;x
executorService.submit(new Offer());
executorService.submit(new Poll());
}
executorService.shutdown();
}
}
一种输出:
入队元素为313732926
出队元素为313732926
入队元素为812655435
出队元素为812655435
入队元素为1893079357
出队元素为1893079357
入队元素为1137820958
出队元素为1137820958
入队元素为1965962048
出队元素为1965962048
出队元素为685567162
入队元素为685567162
出队元素为1441081163
入队元素为1441081163
出队元素为1627184732
入队元素为1627184732
ConcurrentLinkedQuere类图
如图ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存在列表的首尾节点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表。
public ConcurrentLinkedQueue() {
head = tail = new Node(null);
}
如上代码初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。ConcurrentLinkedQuere方法
[if !supportLists]ü [endif]Offer操作
offer操作是在链表末尾添加一个元素,下面看看实现原理。
public boolean offer(E e) {
//e为null则抛出空指针异常
checkNotNull(e);
//构造Node节点构造函数内部调用unsafe.putObject,后面统一讲
final Node newNode = new Node(e);
//从尾节点插入
for (Node t = tail, p = t;;) {
Node q = p.next;
//如果q=null说明p是尾节点则插入
if (q == null) {
//cas插入(1)
if (p.casNext(null, newNode)) {
//cas成功说明新增节点已经被放入链表,然后设置当前尾节点(包含head,1,3,5.。。个节点为尾节点)
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)//(2)
//多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
//重新找新的head,因为新的head后面的节点才是激活的节点
p = (t != (t = tail)) ? t : head;
else
//寻找尾节点(3)
p = (p != t && t != (t = tail)) ? t : q;
}
}
从构造函数知道一开始有个item为null的哨兵节点,并且head和tail都是指向这个节点,然后当一个线程调用offer时候首先
如图首先查找尾节点,q==null,p就是尾节点,所以执行p.casNext通过cas设置p的next为新增节点,这时候p==t所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法,所以可能两个线程同时执行到了(1)进行cas,那么只有一个会成功(假如线程1成功了),成功后的链表为:
失败的线程会循环一次这时候指针为:
这时候会执行(3)所以p=q,然后在循环后指针位置为:
所以没有其他线程干扰的情况下会执行(1)执行cas把新增节点插入到尾部,没有干扰的情况下线程2 cas会成功,然后去更新尾节点tail,由于p!=t所以更新。这时候链表和指针为:
假如线程2cas时候线程3也在执行,那么线程3会失败,循环一次后,线程3的节点状态为:
这时候p!=t ;并且t的原始值为told,t的新值为tnew ,所以told!=tnew,所以 p=tnew=tail;
然后在循环一下后节点状态:
q==null所以执行(1)。
现在就差p==q这个分支还没有走,这个要在执行poll操作后才会出现这个情况。poll后会存在下面的状态
这个时候添加元素时候指针分布为:
所以会执行(2)分支 结果 p=head然后循环,循环后指针分布:
所以执行(1),然后p!=t所以设置tail节点。现在分布图:
自引用的节点会被垃圾回收掉。
[if !supportLists]ü [endif]add操作
add操作是在链表末尾添加一个元素,下面看看实现原理。其实内部调用的还是offer
public boolean add(E e) {
return offer(e);
}
[if !supportLists]ü [endif]poll操作
poll操作是在链表头部获取并且移除一个元素,下面看看实现原理。
public E poll() {
restartFromHead:
//死循环
for (;;) {
//死循环
for (Node h = head, p = h, q;;) {
//保存当前节点值
E item = p.item;
//当前节点有值则cas变为null(1)
if (item != null && p.casItem(item, null)) {
//cas成功标志当前节点以及从链表中移除
if (p != h) //类似tail间隔2设置一次头节点(2)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//当前队列为空则返回null(3)
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//自引用了,则重新找新的队列头节点(4)
else if (p == q)
continue restartFromHead;
else//(5)
p = q;
}
}
}
final void updateHead(Node h, Node p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
当队列为空时候:
可知执行(3)这时候有两种情况,第一没有其他线程添加元素时候(3)结果为true然后因为h!=p为false所以直接返回null。第二在执行q=p.next前,其他线程已经添加了一个元素到队列,这时候(3)返回false,然后执行(5)p=q,然后循环后节点分布:
这时候执行(1)分支,进行cas把当前节点值值为null,同时只有一个线程会成功,cas成功 标示该节点从队列中移除了,然后p!=h,调用updateHead方法,参数为h,p;h!=p所以把p变为当前链表head节点,然后h节点的next指向自己。现在状态为:
cas失败 后 会再次循环,这时候分布图为:
这时候执行(3)返回null.
现在还有个分支(4)没有执行过,那么什么时候会执行那?
这时候执行(1)分支,进行cas把当前节点值值为null,同时只有一个线程A会成功,cas成功 标示该节点从队列中移除了,然后p!=h,调用updateHead方法,假如执行updateHead前另外一个线程B开始poll这时候它p指向为原来的head节点,然后当前线程A执行updateHead这时候B线程链表状态为:
所以会执行(4)重新跳到外层循环,获取当前head,现在状态为:
[if !supportLists]ü [endif]peek操作
peek操作是获取链表头部一个元素(只读取不移除),下面看看实现原理。代码与poll类似,只是少了castItem.并且peek操作会改变head指向,offer后head指向哨兵节点,第一次peek后head会指向第一个真的节点元素。
public E peek() {
restartFromHead:
for (;;) {
for (Node h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
[if !supportLists]ü [endif]size操作
获取当前队列元素个数,在并发环境下不是很有用,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。
public int size() {
int count = 0;
for (Node p = first(); p != null; p = succ(p))
if (p.item != null)
//最大返回Integer.MAX_VALUE
if (++count == Integer.MAX_VALUE)
break;
return count;
}
//获取第一个队列元素(哨兵元素不算),没有则为null
Node first() {
restartFromHead:
for (;;) {
for (Node h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
//获取当前节点的next元素,如果是自引入节点则返回真正头节点
final Node succ(Node p) {
Node next = p.next;
return (p == next) ? head : next;
}
[if !supportLists]ü [endif]remove操作
如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回false
public boolean remove(Object o) {
//查找元素为空,直接返回false
if (o == null) return false;
Node pred = null;
for (Node p = first(); p != null; p = succ(p)) {
E item = p.item;
//相等则使用cas值null,同时一个线程成功,失败的线程循环查找队列中其他元素是否有匹配的。
if (item != null &&
o.equals(item) &&
p.casItem(item, null)) {
//获取next元素
Node next = succ(p);
//如果有前驱节点,并且next不为空则链接前驱节点到next,
if (pred != null && next != null)
pred.casNext(p, next);
return true;
}
pred = p;
}
return false;
}
[if !supportLists]ü [endif]contains操作
判断队列里面是否含有指定对象,由于是遍历整个队列,所以类似size 不是那么精确,有可能调用该方法时候元素还在队列里面,但是遍历过程中才把该元素删除了,那么就会返回false.
public boolean contains(Object o) {
if (o == null) return false;
for (Node p = first(); p != null; p = succ(p)) {
E item = p.item;
if (item != null && o.equals(item))
return true;
}
return false;
}
ConcurrentLinkedQuere的offer方法有意思的问题
offer中有个 判断 t != (t = tail)假如 t=node1;tail=node2;并且node1!=node2那么这个判断是true还是false那,答案是true,这个判断是看当前t是不是和tail相等,相等则返回true否者为false,但是无论结果是啥执行后t的值都是tail。
下面从字节码来分析下为啥。
[if !supportLists]· [endif]一个例子
public static void main(String[] args) {
int t = 2;
int tail = 3;
System.out.println(t != (t = tail));
}
结果为:true
[if !supportLists]· [endif]字节码文件
C:\Users\Simple\Desktop\TeacherCode\Crm_Test\build\classes\com\itheima\crm\util>javap -c Test001
警告:二进制文件Test001包含com.itheima.crm.util.Test001
Compiled from "Test001.java"
public class com.itheima.crm.util.Test001 {
public com.itheima.crm.util.Test001();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_1
8: iload_2
9: dup
10: istore_1
11: if_icmpeq 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #22 // Method java/io/PrintStream.println:(Z)V
22: return
}
我们从上面标黄的字节码文件中分析
一开始栈为空:
[if !supportLists]· [endif]第0行指令作用是把值2入栈栈顶元素为2
[if !supportLists]· [endif]第1行指令作用是将栈顶int类型值保存到局部变量t中
[if !supportLists]· [endif]第2行指令作用是把值3入栈栈顶元素为3
[if !supportLists]· [endif]第3行指令作用是将栈顶int类型值保存到局部变量tail中。
[if !supportLists]· [endif]第4调用打印命令
[if !supportLists]· [endif]第7行指令作用是把变量t中的值入栈
[if !supportLists]· [endif]第8行指令作用是把变量tail中的值入栈
[if !supportLists]· [endif]现在栈里面的元素为3、2,并且3位于栈顶
[if !supportLists]· [endif]第9行指令作用是当前栈顶元素入栈,所以现在栈内容3,3,2
[if !supportLists]· [endif]第10行指令作用是把栈顶元素存放到t,现在栈内容3,2
[if !supportLists]· [endif]第11行指令作用是判断栈顶两个元素值,相等则跳转 18。由于现在栈顶严肃为3,2不相等所以返回true.
[if !supportLists]· [endif]第14行指令作用是把1入栈
[if !supportLists]· [endif]然后回头分析下!=是双目运算符,应该是首先把左边的操作数入栈,然后在去计算了右侧操作数。
ConcurrentLinkedQuere总结
ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
下面总结下如何实现线程安全的,可知入队出队函数都是操作volatile变量:head,tail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。
对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。
[if !supportLists]Ø [endif]ConcurrentHashMap非阻塞Hash集合
ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现,ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析。
ConcurrentLinkedQuere类图
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
ConcurrentLinkedQuere实现原理
众所周知,哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。
HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。如下图
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
ConcurrentLinkedQuere源码解析
ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。
final Segment[] segments;
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)
所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。Segment类似于HashMap,一个Segment维护着一个HashEntry数组
transient volatile HashEntry[] table;
HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
static final class HashEntry {
final int hash;
final K key;
volatile V value;
volatile HashEntry next;
//其他省略
}
我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法
Segment(float lf, int threshold, HashEntry[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
我们来看下ConcurrentHashMap的构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//MAX_SEGMENTS为1<<16=65536,也就是最大并发数为65536
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
//2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
int sshift = 0;
//ssize为segments数组长度,根据concurrentLevel计算得出
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
Segment s0 =
new Segment(loadFactor, (int)(cap * loadFactor),
(HashEntry[])new HashEntry[cap]);
Segment[] ss = (Segment[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。
从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。
其实,put方法对segment也会有所体现
public V put(K key, V value) {
Segment s;
//concurrentHashMap不允许key/value为空
if (value == null)
throw new NullPointerException();
//hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
int hash = hash(key);
//返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
从源码看出,put的主要逻辑也就两步:
1.定位segment并确保定位的Segment已初始化
2.调用Segment的put方法。
Ps:关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
ConcurrentLinkedQuere方法
[if !supportLists]ü [endif]Get操作
public V get(Object key) {
Segment s;
HashEntry[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//先定位Segment,再定位HashEntry
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。
[if !supportLists]ü [endif]Put操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1; //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
ConcurrentLinkedQuere总结
ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable的全表锁在性能上的提升非常之大。本文对ConcurrentHashMap的实现原理进行了详细分析,并解读了部分源码,希望能帮助到有需要的童鞋。
[if !supportLists]Ø [endif]ConcurrentSkipListMap非阻塞Hash跳表集合
大家都是知道TreeMap,它是使用树形结构的方式进行存储数据的线程不安全的Map集合(有序的哈希表),并且可以对Map中的Key进行排序,Key中存储的数据需要实现Comparator接口或使用CompareAble接口的子类来实现排序。
ConcurrentSkipListMap也是和TreeMap,它们都是有序的哈希表。但是,它们是有区别的:
第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
那现在我们需要知道说明是跳表。
什么是SkipList
Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
从概率上保持数据结构的平衡比显示的保持数据结构平衡要简单的多。对于大多数应用,用Skip list要比用树算法相对简单。由于Skip list比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是skip list的常数项会相对小很多。Skip list在空间上也比较节省。一个节点平均只需要1.333个指针(甚至更少)。
下图为Skip list结构图(以7,14,21,32,37,71,85序列为例)
SkipList性质
[if !supportLists](1) [endif]由很多层结构组成,level是通过一定的概率随机产生的。(2) 每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法。(3) 最底层(Level 1)的链表包含所有元素。(4) 如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现。(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
什么是ConcurrentSkipListMap
ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作。 注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。
ConcurrentSkipListMap数据结构
ConcurrentSkipListMap的数据结构,如下图所示:
说明:可以看到ConcurrentSkipListMap的数据结构使用的是跳表,每一个HeadIndex、Index结点都会包含一个对Node的引用,同一垂直方向上的Index、HeadIndex结点都包含了最底层的Node结点的引用。并且层级越高,该层级的结点(HeadIndex和Index)数越少。Node结点之间使用单链表结构。
ConcurrentSkipListMap源码分析
ConcurrentSkipListMap主要用到了Node和Index两种节点的存储方式,通过volatile关键字实现了并发的操作
static final class Node {
final K key;
volatile Object value;//value值
volatile Node next;//next引用
……
}
static class Index {
final Node node;
final Index down;//downy引用
volatile Index right;//右边引用
……
}
[if !supportLists]ü [endif]ConcurrentSkipListMap查找操作
通过SkipList的方式进行查找操作:(下图以“查找91”进行说明:)
红色虚线,表示查找的路径,蓝色向右箭头表示right引用;黑色向下箭头表示down引用;
下面就是源码中的实现(get方法是通过doGet方法来时实现的)
public V get(Object key) {
return doGet(key);
}
//doGet的实现
private V doGet(Object okey) {
Comparable key = comparable(okey);
Node bound = null;
Index q = head;//把头结点作为当前节点的前驱节点
Index r = q.right;//前驱节点的右节点作为当前节点
Node n;
K k;
int c;
for (;;) {//遍历
Index d;
//依次遍历right节点
if (r != null && (n = r.node) != bound && (k = n.key) != null) {
if ((c = key.compareTo(k)) > 0) {//由于key都是升序排列的,所有当前关键字大于所要
查找的key时继续向右遍历
q = r;
r = r.right;
continue;
} else if (c == 0) {
//如果找到了相等的key节点,则返回该Node的value如果value为空可能是其他并发delete
导致的,于是通过另一种
//遍历findNode的方式再查找
Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else
bound = n;
}
//如果一个链表中right没能找到key对应的value,则调整到其down的引用处继续查找
if ((d = q.down) != null) {
q = d;
r = d.right;
} else
break;
}
//如果通过上面的遍历方式,还没能找到key对应的value,再通过Node.next的方式进行查找
for (n = q.node.next; n != null; n = n.next) {
if ((k = n.key) != null) {
if ((c = key.compareTo(k)) == 0) {
Object v = n.value;
return (v != null)? (V)v : getUsingFindNode(key);
} else if (c < 0)
break;
}
}
return null;
}
[if !supportLists]ü [endif]ConcurrentSkipListMap删除操作
通过SkipList的方式进行删除操作:(下图以“删除23”进行说明:)
红色虚线,表示查找的路径,蓝色向右箭头表示right引用;黑色向下箭头表示down引用;
下面就是源码中的实现(remove方法是通过doRemove方法来时实现的)
//remove操作,通过doRemove实现,把所有level中出现关键字key的地方都delete掉
public V remove(Object key) {
return doRemove(key, null);
}
final V doRemove(Object okey, Object value) {
Comparable key = comparable(okey);
for (;;) {
Node b = findPredecessor(key);//得到key的前驱(就是比key小的最大节点)
Node n = b.next;//前驱节点的next引用
for (;;) {//遍历
if (n == null)//如果next引用为空,直接返回
return null;
Node f = n.next;
if (n != b.next) //如果两次获得的b.next不是相同的Node,就跳转
到第一层循环重新获得b和n
break;
Object v = n.value;
if (v == null) { //当n被其他线程delete的时候,其value==null,此时做辅助处理,并重新获取b和n
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) //当其前驱被delet的时候直接跳出,重新获取b和n
break;
int c = key.compareTo(n.key);
if (c < 0)
return null;
if (c > 0) {//当key较大时就继续遍历
b = n;
n = f;
continue;
}
if (value != null && !value.equals(v))
return null;
if (!n.casValue(v, null))
break;
if (!n.appendMarker(f) || !b.casNext(n, f))//casNext方法就是通过比较和设置b(前驱)
的next节点的方式来实现删除操作
findNode(key); //通过尝试findNode的方式继续find
else {
findPredecessor(key); // Clean index
if (head.right == null) //如果head的right引用为空,则表示不存在该level
tryReduceLevel();
}
return (V)v;
}
}
}
[if !supportLists]ü [endif]ConcurrentSkipListMap插入操作
通过SkipList的方式进行插入操作:(下图以“添加55”的两种情况,进行说明:)
在level=2(该level存在)的情况下添加55的图示:只需在level<=2的合适位置插入55即可
在level=4(该level不存在,图示level4是新建的)的情况下添加55的情况:首先新建level4,然后在level<=4的合适位置插入55。
下面就是源码中的实现(put方法是通过doPut方法来时实现的)
//put操作,通过doPut实现
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
Comparable key = comparable(kkey);
for (;;) {
Node b = findPredecessor(key);//前驱
Node n = b.next;
//定位的过程就是和get操作相似
for (;;) {
if (n != null) {
Node f = n.next;
if (n != b.next) //前后值不一致的情况下,跳转到第一层循环重新获得b和n
break;;
Object v = n.value;
if (v == null) { // n被delete的情况下
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) // b被delete的情况,重新获取b和n
break;
int c = key.compareTo(n.key);
if (c > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value))
return (V)v;
else
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
Node z = new Node(kkey, value, n);
if (!b.casNext(n, z))
break; // restart if lost race to append to b
int level = randomLevel();//得到一个随机的level作为该key-value插入的最高level
if (level > 0)
insertIndex(z, level);//进行插入操作
return null;
}
}
}
/**
*获得一个随机的level值
*/
private int randomLevel() {
int x = randomSeed;
x ^= x << 13;
x ^= x >>> 17;
randomSeed = x ^= x << 5;
if ((x & 0x8001) != 0) // test highest and lowest bits
return 0;
int level = 1;
while (((x >>>= 1) & 1) != 0) ++level;
return level;
}
//执行插入操作:如上图所示,有两种可能的情况:
//1.当level存在时,对level<=n都执行insert操作
//2.当level不存在(大于目前的最大level)时,首先添加新的level,然后在执行操作1
private void insertIndex(Node z, int level) {
HeadIndex h = head;
int max = h.level;
if (level <= max) {//情况1
Index idx = null;
for (int i = 1; i <= level; ++i)//首先得到一个包含1~level个级别的down关系的链表,
最后的inx为最高level
idx = new Index(z, idx, null);
addIndex(idx, h, level);//把最高level的idx传给addIndex方法
} else { //情况2 增加一个新的级别
level = max + 1;
Index[] idxs = (Index[])new Index[level+1];
Index idx = null;
for (int i = 1; i <= level; ++i)//该步骤和情况1类似
idxs[i] = idx = new Index(z, idx, null);
HeadIndex oldh;
int k;
for (;;) {
oldh = head;
int oldLevel = oldh.level;
if (level <= oldLevel) { // lost race to add level
k = level;
break;
}
HeadIndex newh = oldh;
Node oldbase = oldh.node;
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex(oldbase, newh, idxs[j], j);//创建新的
if (casHead(oldh, newh)) {
k = oldLevel;
break;
}
}
addIndex(idxs[k], oldh, k);
}
}
/**
*在1~indexlevel层中插入数据
*/
private void addIndex(Index idx, HeadIndex h, int indexLevel) {
// insertionLevel代表要插入的level,该值会在indexLevel~1间遍历一遍
int insertionLevel = indexLevel;
Comparable key = comparable(idx.node.key);
if (key == null) throw new NullPointerException();
//和get操作类似,不同的就是查找的同时在各个level上加入了对应的key
for (;;) {
int j = h.level;
Index q = h;
Index r = q.right;
Index t = idx;
for (;;) {
if (r != null) {
Node n = r.node;
// compare before deletion check avoids needing recheck
int c = key.compareTo(n.key);
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
if (j == insertionLevel) {//在该层level中执行插入操作
// Don't insert index if node already deleted
if (t.indexesDeletedNode()) {
findNode(key); // cleans up
return;
}
if (!q.link(r, t))//执行link操作,其实就是inset的实现部分
break; // restart
if (--insertionLevel == 0) {
// need final deletion check before return
if (t.indexesDeletedNode())
findNode(key);
return;
}
}
if (--j >= insertionLevel && j < indexLevel)//key移动到下一层level
t = t.down;
q = q.down;
r = q.right;
}
}
}
ConcurrentLinkedQuere示例
下面我们看下面示例输出的结果
import java.util.*;import java.util.concurrent.*;
/*
* ConcurrentSkipListMap是“线程安全”的哈希表,而TreeMap是非线程安全的。
*
*下面是“多个线程同时操作并且遍历map”的示例
* (01)当map是ConcurrentSkipListMap对象时,程序能正常运行。
* (02)当map是TreeMap对象时,程序会产生ConcurrentModificationException异常。
*
*/public class ConcurrentSkipListMapDemo1 {
// TODO: map是TreeMap对象时,程序会出错。
//private static Map map = new TreeMap();
private static Map map = new ConcurrentSkipListMap();
public static void main(String[] args) {
//同时启动两个线程对map进行操作!
new MyThread("a").start();
new MyThread("b").start();
}
private static void printAll() {
String key, value;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
key = (String)entry.getKey();
value = (String)entry.getValue();
System.out.print("("+key+", "+value+"), ");
}
System.out.println();
}
private static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0;
while (i++ < 6) {
// “线程名” + "序号"
String val = Thread.currentThread().getName()+i;
map.put(val, "0");
//通过“Iterator”遍历map。
printAll();
}
}
}
}
某一次的运行结果:
(a1, 0), (a1, 0), (b1, 0), (b1, 0),
(a1, 0), (b1, 0), (b2, 0),
(a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0),
(b3, 0), (a1, 0),
(a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0),
(b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0),
(b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0),
(a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0),
(b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0),
(a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),
结果说明:
示例程序中,启动两个线程(线程a和线程b)分别对ConcurrentSkipListMap进行操作。以线程a而言,它会先获取“线程名”+“序号”,然后将该字符串作为key,将“0”作为value,插入到ConcurrentSkipListMap中;接着,遍历并输出ConcurrentSkipListMap中的全部元素。 线程b的操作和线程a一样,只不过线程b的名字和线程a的名字不同。 当map是ConcurrentSkipListMap对象时,程序能正常运行。如果将map改为TreeMap时,程序会产生ConcurrentModificationException异常。
[if !supportLists]2) [endif]java.util.concurrent.atomic包
[if !supportLists]Ø [endif]AtomicBoolean原子性布尔
AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
AtomicBoolean,在这个Boolean值的变化的时候不允许在之间插入,保持操作的原子性。
下面将解释重点方法并举例:
boolean compareAndSet(expectedValue, updateValue),这个方法主要两个作用:
[if !supportLists]1. [endif]比较AtomicBoolean和expect的值,如果一致,执行方法内的语句。其实就是一个if语句
[if !supportLists]2. [endif]把AtomicBoolean的值设成update,比较最要的是这两件事是一气呵成的,这连个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行。为多线程的控制提供了解决的方案
下面我们从代码上解释:
首先我们看下在不使用AtomicBoolean情况下,代码的运行情况:
package zmx.atomic.test;
import java.util.concurrent.TimeUnit;
public class BarWorker implements Runnable {
//静态变量
private static boolean exists = false;
private String name;
public BarWorker(String name) {
this.name = name;
}
@Override
public void run() {
if (!exists) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
// do nothing
}
exists = true;
System.out.println(name + " enter");
try {
System.out.println(name + " working");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave");
exists = false;
} else {
System.out.println(name + " give up");
}
}
public static void main(String[] args) {
BarWorker bar1 = new BarWorker("bar1");
BarWorker bar2 = new BarWorker("bar2");
new Thread(bar1).start();
new Thread(bar2).start();
}
}
运行结果:
bar1 enter
bar2 enter
bar1 working
bar2 working
bar1 leave
bar2 leave
从上面的运行结果我们可看到,两个线程运行时,都对静态变量exists同时做操作,并没有保证exists静态变量的原子性,也就是一个线程在对静态变量exists进行操作到时候,其他线程必须等待或不作为。等待一个线程操作完后,才能对其进行操作。
下面我们将静态变量使用AtomicBoolean来进行操作
package zmx.atomic.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class BarWorker2 implements Runnable {
//静态变量使用AtomicBoolean 进行操作
private static AtomicBoolean exists = new AtomicBoolean(false);
private String name;
public BarWorker2(String name) {
this.name = name;
}
@Override
public void run() {
if (exists.compareAndSet(false, true)) {
System.out.println(name + " enter");
try {
System.out.println(name + " working");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// do nothing
}
System.out.println(name + " leave");
exists.set(false);
} else {
System.out.println(name + " give up");
}
}
public static void main(String[] args) {
BarWorker2 bar1 = new BarWorker2("bar1");
BarWorker2 bar2 = new BarWorker2("bar2");
new Thread(bar1).start();
new Thread(bar2).start();
}
}
运行结果:
bar1 enter
bar1 working
bar2 give up
bar1 leave
可以从上面的运行结果看出仅仅一个线程进行工作,因为exists.compareAndSet(false, true)提供了原子性操作,比较和赋值操作组成了一个原子操作, 中间不会提供可乘之机。使得一个线程操作,其他线程等待或不作为。
下面我们简单介绍下AtomicBoolean的API
创建一个AtomicBoolean
你可以这样创建一个AtomicBoolean:
AtomicBoolean atomicBoolean = new AtomicBoolean();
以上示例新建了一个默认值为false 的 AtomicBoolean。如果你想要为 AtomicBoolean 实例设置一个显式的初始值,那么你可以将初始值传给 AtomicBoolean 的构造子:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
获得 AtomicBoolean的值
你可以通过使用get() 方法来获取一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();
设置AtomicBoolean的值
你可以通过使用set() 方法来设置一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);
以上代码执行后AtomicBoolean 的值为 false。
交换AtomicBoolean的值
你可以通过getAndSet() 方法来交换一个 AtomicBoolean 实例的值。getAndSet() 方法将返回 AtomicBoolean 当前的值,并将为 AtomicBoolean 设置一个新值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);
以上代码执行后oldValue 变量的值为 true,atomicBoolean 实例将持有 false 值。代码成功将 AtomicBoolean 当前值 ture 交换为 false。
比较并设置AtomicBoolean的值
compareAndSet() 方法允许你对 AtomicBoolean 的当前值与一个期望值进行比较,如果当前值等于期望值的 话,将会对AtomicBoolean 设定一个新值。compareAndSet() 方法是原子性的,因此在同一时间之内有单个 线程执行它。因此compareAndSet() 方法可被用于一些类似于锁的同步的简单实现。以下是一个 compareAndSet() 示例:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newValue = false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(
expectedValue, newValue);
本示例对AtomicBoolean 的当前值与 true 值进行比较,如果相等,将 AtomicBoolean 的值更新为 false
[if !supportLists]Ø [endif]AtomicInteger原子性整型
AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
我们先来看看AtomicInteger给我们提供了什么方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
下面通过两个简单的例子来看一下AtomicInteger 的优势在哪: 普通线程同步:
class Test2 {
private volatile int count = 0;
public synchronized void increment() {
count++; //若要线程安全执行执行count++,需要加锁
}
public int getCount() {
return count;
}
}
使用AtomicInteger:
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
从上面的例子中我们可以看出:使用AtomicInteger是非常的安全的.而且因为AtomicInteger由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。AtomicInteger是使用非阻塞算法来实现并发控制的。AtomicInteger的关键域只有一下3个:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset=
unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
private volatile int value;
这里,unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。 注意:value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。
优点:最大的好处就是可以避免多线程的优先级倒置和死锁情况的发生,提升在高并发处理下的性能。
下面我们简单介绍下AtomicInteger的API
创建一个AtomicInteger
创建一个AtomicInteger 示例如下:
AtomicInteger atomicInteger = new AtomicInteger();
本示例将创建一个初始值为0 的 AtomicInteger。如果你想要创建一个给定初始值的 AtomicInteger,你可以这样:
AtomicInteger atomicInteger = new AtomicInteger(123);
本示例将123 作为参数传给 AtomicInteger 的构造子,它将设置 AtomicInteger 实例的初始值为 123。
获得AtomicInteger的值
你可以使用get() 方法获取 AtomicInteger 实例的值。示例如下:
AtomicInteger atomicInteger = new AtomicInteger(123);
int theValue = atomicInteger.get();
设置AtomicInteger的值
你可以通过set() 方法对 AtomicInteger 的值进行重新设置。以下是 AtomicInteger.set() 示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
atomicInteger.set(234);
以上示例创建了一个初始值为123 的 AtomicInteger,而在第二行将其值更新为 234。
比较并设置AtomicInteger的值
AtomicInteger 类也通过了一个原子性的 compareAndSet() 方法。这一方法将 AtomicInteger 实例的当前值与期望值进行比较,如果二者相等,为 AtomicInteger 实例设置一个新值。AtomicInteger.compareAndSet() 代码示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
int expectedValue = 123;
int newValue = 234;
atomicInteger.compareAndSet(expectedValue, newValue);
本示例首先新建一个初始值为123 的 AtomicInteger 实例。然后将 AtomicInteger 与期望值 123 进行比较,如果相等,将 AtomicInteger 的值更新为 234。
增加AtomicInteger的值
AtomicInteger 类包含有一些方法,通过它们你可以增加 AtomicInteger 的值,并获取其值。这些方法如下:
public final int addAndGet(int addValue)//在原来的数值上增加新的值,并返回新值
public final int getAndIncrement()//获取当前的值,并自增
public final int incrementAndget() //自减,并获得自减后的值
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
第一个addAndGet() 方法给 AtomicInteger 增加了一个值,然后返回增加后的值。getAndAdd() 方法为 AtomicInteger 增加了一个值,但返回的是增加以前的 AtomicInteger 的值。具体使用哪一个取决于你的应用场景。以下是这两种方法的示例:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10));
System.out.println(atomicInteger.addAndGet(10));
本示例将打印出0 和 20。例子中,第二行拿到的是加 10 之前的 AtomicInteger 的值。加 10 之前的值是 0。第三行将 AtomicInteger 的值再加 10,并返回加操作之后的值。该值现在是为 20。你当然也可以使用这俩方法为 AtomicInteger 添加负值。结果实际是一个减法操作。getAndIncrement() 和 incrementAndGet() 方法类似于 getAndAdd() 和 addAndGet(),但每次只将 AtomicInteger 的值加 1。
减小AtomicInteger的值
AtomicInteger 类还提供了一些减小 AtomicInteger 的值的原子性方法。这些方法是:
public final int decrementAndGet()
public final int getAndDecrement()
decrementAndGet() 将 AtomicInteger 的值减一,并返回减一后的值。getAndDecrement() 也将 AtomicInteger 的值减一,但它返回的是减一之前的值。
[if !supportLists]Ø [endif]AtomicIntegerArray原子性整型数组
java.util.concurrent.atomic.AtomicIntegerArray类提供了可以以原子方式读取和写入的底层int数组的操作,还包含高级原子操作。 AtomicIntegerArray支持对底层int数组变量的原子操作。 它具有获取和设置方法,如在变量上的读取和写入。 也就是说,一个集合与同一变量上的任何后续get相关联。 原子compareAndSet方法也具有这些内存一致性功能。
AtomicIntegerArray本质上是对int[]类型的封装。使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:
//获得数组第i个下标的元素
public final int get(int i)
//获得数组的长度
public final int length()
//将数组第i个下标设置为newValue,并返回旧的值
public final int getAndSet(int i, int newValue)
//进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true
public final boolean compareAndSet(int i, int expect, int update)
//将第i个下标的元素加1
public final int getAndIncrement(int i)
//将第i个下标的元素减1
public final int getAndDecrement(int i)
//将第i个下标的元素增加delta(delta可以是负数)
public final int getAndAdd(int i, int delta)
下面给出一个简单的示例,展示AtomicIntegerArray使用:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
输出结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
上述代码第2行,申明了一个内含10个元素的数组。第3行定义的线程对数组内10个元素进行累加操作,每个元素各加1000次。第11行,开启10个这样的线程。因此,可以预测,如果线程安全,数组内10个元素的值必然都是10000。反之,如果线程不安全,则部分或者全部数值会小于10000。
[if !supportLists]Ø [endif]AtomicLong、AtomicLongArray原子性整型数组
AtomicLong、AtomicLongArray的API跟AtomicInteger、AtomicIntegerArray在使用方法都是差不多的。区别在于用前者是使用原子方式更新的long值和long数组,后者是使用原子方式更新的Integer值和Integer数组。两者的相同处在于它们此类确实扩展了Number,允许那些处理基于数字类的工具和实用工具进行统一访问。在实际开发中,它们分别用于不同的场景。这个就具体情况具体分析了,下面将举例说明AtomicLong的使用场景(使用AtomicLong生成自增长ID),其他就不在过多介绍。
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
public class AtomicLongTest {
/**
* @param args
*/
public static void main(String[] args) {
final AtomicLong orderIdGenerator = new AtomicLong(0);
final List orders = Collections
.synchronizedList(new ArrayList());
for (int i = 0; i < 10; i++) {
Thread orderCreationThread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
long orderId = orderIdGenerator.incrementAndGet();
Item order = new Item(Thread.currentThread().getName(),
orderId);
orders.add(order);
}
}
});
orderCreationThread.setName("Order Creation Thread " + i);
orderCreationThread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Set orderIds = new HashSet();
for (Item order : orders) {
orderIds.add(order.getID());
System.out.println("Order name:" + order.getItemName()
+"----"+"Order id:" + order.getID());
}
}
}
class Item {
String itemName;
long id;
Item(String n, long id) {
this.itemName = n;
this.id = id;
}
public String getItemName() {
return itemName;
}
public long getID() {
return id;
}
}
输出:
Order name:Order Creation Thread 0----Order id:1
Order name:Order Creation Thread 1----Order id:2
Order name:Order Creation Thread 0----Order id:4
Order name:Order Creation Thread 1----Order id:5
Order name:Order Creation Thread 3----Order id:3
Order name:Order Creation Thread 0----Order id:7
Order name:Order Creation Thread 1----Order id:6
........
Order name:Order Creation Thread 2----Order id:100
从运行结果我们看到,不管是哪个线程。它们获得的ID是不会重复的,保证的ID生成的原子性,避免了线程安全上的问题。
[if !supportLists]3) [endif]java.util.concurrent.lock包
待续...
(三)多线程面试题
[if !supportLists]1. [endif]多线程的创建方式(2017-11-23-wzz)
(1)、继承Thread类:但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:继承Thread类实现多线程,并在合适的地方启动线程
[if !supportLists]1.[endif]public class MyThread extends Thread {
[if !supportLists]2.[endif] public void run() {
[if !supportLists]3.[endif] System.out.println("MyThread.run()");
[if !supportLists]4.[endif] }
[if !supportLists]5.[endif]}
[if !supportLists]6.[endif]MyThread myThread1 = new MyThread();
[if !supportLists]7.[endif]MyThread myThread2 = new MyThread();
[if !supportLists]8.[endif]myThread1.start();
[if !supportLists]9.[endif]myThread2.start();
(2)、实现Runnable接口的方式实现多线程,并且实例化Thread,传入自己的Thread实例,调用run( )方法
[if !supportLists]1.[endif]public class MyThread implements Runnable {
[if !supportLists]2.[endif] public void run() {
[if !supportLists]3.[endif] System.out.println("MyThread.run()");
[if !supportLists]4.[endif] }
[if !supportLists]5.[endif]}
[if !supportLists]6.[endif]MyThread myThread = new MyThread();
[if !supportLists]7.[endif]Thread thread = new Thread(myThread);
[if !supportLists]8.[endif]thread.start();
[if !supportLists](3)[endif]、使用ExecutorService、Callable、Future实现有返回结果的多线程:ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
[if !supportLists]1.[endif]import java.util.concurrent.*;
[if !supportLists]2.[endif]import java.util.Date;
[if !supportLists]3.[endif]import java.util.List;
[if !supportLists]4.[endif]import java.util.ArrayList;
[if !supportLists]5.[endif]
[if !supportLists]6.[endif]/**
[if !supportLists]7.[endif]*有返回值的线程
[if !supportLists]8.[endif]*/
[if !supportLists]9.[endif]@SuppressWarnings("unchecked")
[if !supportLists]10.[endif]public class Test {
[if !supportLists]11.[endif]public static void main(String[] args) throws ExecutionException,
[if !supportLists]12.[endif] InterruptedException {
[if !supportLists]13.[endif]System.out.println("----程序开始运行----");
[if !supportLists]14.[endif] Date date1 = new Date();
[if !supportLists]15.[endif]
[if !supportLists]16.[endif] int taskSize = 5;
[if !supportLists]17.[endif]//创建一个线程池
[if !supportLists]18.[endif] ExecutorService pool = Executors.newFixedThreadPool(taskSize);
[if !supportLists]19.[endif]//创建多个有返回值的任务
[if !supportLists]20.[endif] List list = new ArrayList();
[if !supportLists]21.[endif] for (int i = 0; i < taskSize; i++) {
[if !supportLists]22.[endif] Callable c = new MyCallable(i + " ");
[if !supportLists]23.[endif]//执行任务并获取Future对象
[if !supportLists]24.[endif] Future f = pool.submit(c);
[if !supportLists]25.[endif] // System.out.println(">>>" + f.get().toString());
[if !supportLists]26.[endif] list.add(f);
[if !supportLists]27.[endif] }
[if !supportLists]28.[endif]//关闭线程池
[if !supportLists]29.[endif] pool.shutdown();
[if !supportLists]30.[endif]
[if !supportLists]31.[endif]//获取所有并发任务的运行结果
[if !supportLists]32.[endif] for (Future f : list) {
[if !supportLists]33.[endif]//从Future对象上获取任务的返回值,并输出到控制台
[if !supportLists]34.[endif] System.out.println(">>>" + f.get().toString());
[if !supportLists]35.[endif] }
[if !supportLists]36.[endif]
[if !supportLists]37.[endif] Date date2 = new Date();
[if !supportLists]38.[endif]System.out.println("----程序结束运行----,程序运行时间【"
[if !supportLists]39.[endif]+ (date2.getTime() - date1.getTime()) + "毫秒】");
[if !supportLists]40.[endif]}
[if !supportLists]41.[endif]}
[if !supportLists]42.[endif]
[if !supportLists]43.[endif]class MyCallable implements Callable {
[if !supportLists]44.[endif]private String taskNum;
[if !supportLists]45.[endif]
[if !supportLists]46.[endif]MyCallable(String taskNum) {
[if !supportLists]47.[endif] this.taskNum = taskNum;
[if !supportLists]48.[endif]}
[if !supportLists]49.[endif]
[if !supportLists]50.[endif]public Object call() throws Exception {
[if !supportLists]51.[endif]System.out.println(">>>" + taskNum + "任务启动");
[if !supportLists]52.[endif] Date dateTmp1 = new Date();
[if !supportLists]53.[endif] Thread.sleep(1000);
[if !supportLists]54.[endif] Date dateTmp2 = new Date();
[if !supportLists]55.[endif] long time = dateTmp2.getTime() - dateTmp1.getTime();
[if !supportLists]56.[endif]System.out.println(">>>" + taskNum + "任务终止");
[if !supportLists]57.[endif]return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
[if !supportLists]58.[endif]}
[if !supportLists]59.[endif]}
[if !supportLists]2. [endif]在java中wait和sleep方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。
[if !supportLists]3. [endif]synchronized和volatile关键字的作用
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
[if !supportLists]1.[endif]volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法、和类级别的
[if !supportLists]2.[endif]volatile仅能实现变量的修改可见性,并不能保证原子性;
synchronized则可以保证变量的修改可见性和原子性
[if !supportLists]3.[endif]volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。
[if !supportLists]4.[endif]volatile标记的变量不会被编译器优化;
synchronized标记的变量可以被编译器优化
[if !supportLists]4. [endif]分析线程并发访问代码解释原因
[if !supportLists]1. [endif]public class Counter {
[if !supportLists]2. [endif] private volatile int count = 0;
[if !supportLists]3. [endif] public void inc(){
[if !supportLists]4. [endif] try {
[if !supportLists]5. [endif] Thread.sleep(3);
[if !supportLists]6. [endif] } catch (InterruptedException e) {
[if !supportLists]7. [endif] e.printStackTrace();
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif] count++;
[if !supportLists]10. [endif] }
[if !supportLists]11. [endif] @Override
[if !supportLists]12. [endif] public String toString() {
[if !supportLists]13. [endif] return "[count=" + count + "]";
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
[if !supportLists]16. [endif]//---------------------------------华丽的分割线-----------------------------
[if !supportLists]17. [endif]public class VolatileTest {
[if !supportLists]18. [endif] public static void main(String[] args) {
[if !supportLists]19. [endif] final Counter counter = new Counter();
[if !supportLists]20. [endif] for(int i=0;i<1000;i++){
[if !supportLists]21. [endif] new Thread(new Runnable() {
[if !supportLists]22. [endif] @Override
[if !supportLists]23. [endif] public void run() {
[if !supportLists]24. [endif] counter.inc();
[if !supportLists]25. [endif] }
[if !supportLists]26. [endif] }).start();
[if !supportLists]27. [endif] }
[if !supportLists]28. [endif] System.out.println(counter);
[if !supportLists]29. [endif] }
[if !supportLists]30. [endif]}
上面的代码执行完后输出的结果确定为1000吗?
答案是不一定,或者不等于1000。这是为什么吗?
在java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
也就是说上面主函数中开启了1000个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。因此结果就不可能等于1000了,一般都会小于1000。
上面的解释用一张图表示如下:
(图片来自网络,非本人所绘)
[if !supportLists]5. [endif]什么是线程池,如何使用?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。
[if !supportLists]1. [endif]ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
[if !supportLists]2. [endif]ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
[if !supportLists]3. [endif]ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
[if !supportLists]4. [endif]ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的execute方法即可。
[if !supportLists]6. [endif]常用的线程池有哪些?(2017-11-23-wzz)
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
[if !supportLists]7. [endif]请叙述一下您对线程池的理解?(2015-11-25)
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)
合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
[if !supportLists]8. [endif]线程池的启动策略?(2015-11-25)
官方对线程池的执行过程描述如下:
[if !supportLists]26. [endif] /*
[if !supportLists]27. [endif] * Proceed in 3 steps:
[if !supportLists]28. [endif] *
[if !supportLists]29. [endif] * 1. If fewer than corePoolSize threads are running, try to
[if !supportLists]30. [endif] * start a new thread with the given command as its first
[if !supportLists]31. [endif] * task. The call to addWorker atomically checks runState and
[if !supportLists]32. [endif] * workerCount, and so prevents false alarms that would add
[if !supportLists]33. [endif] * threads when it shouldn't, by returning false.
[if !supportLists]34. [endif] *
[if !supportLists]35. [endif] * 2. If a task can be successfully queued, then we still need
[if !supportLists]36. [endif] * to double-check whether we should have added a thread
[if !supportLists]37. [endif] * (because existing ones died since last checking) or that
[if !supportLists]38. [endif] * the pool shut down since entry into this method. So we
[if !supportLists]39. [endif] * recheck state and if necessary roll back the enqueuing if
[if !supportLists]40. [endif] * stopped, or start a new thread if there are none.
[if !supportLists]41. [endif] *
[if !supportLists]42. [endif] * 3. If we cannot queue task, then we try to add a new
[if !supportLists]43. [endif] * thread. If it fails, we know we are shut down or saturated
[if !supportLists]44. [endif] * and so reject the task.
[if !supportLists]45. [endif] */
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
[if !supportLists]9. [endif]如何控制某个方法允许并发访问线程的个数?(2015-11-30)
[if !supportLists]1. [endif]package com.yange;
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]import java.util.concurrent.Semaphore;
[if !supportLists]4. [endif]/**
[if !supportLists]5. [endif] *
[if !supportLists]6. [endif] * @author wzy 2015-11-30
[if !supportLists]7. [endif] *
[if !supportLists]8. [endif] */
[if !supportLists]9. [endif]public class SemaphoreTest {
[if !supportLists]10. [endif] /*
[if !supportLists]11. [endif]* permits the initial number of permits available. This value may be negative,
[if !supportLists]12. [endif]in which case releases must occur before any acquires will be granted.
[if !supportLists]13. [endif]fair true if this semaphore will guarantee first-in first-out granting of
[if !supportLists]14. [endif]permits under contention, else false
[if !supportLists]15. [endif]*/
[if !supportLists]16. [endif] static Semaphore semaphore = new Semaphore(5,true);
[if !supportLists]17. [endif] public static void main(String[] args) {
[if !supportLists]18. [endif] for(int i=0;i<100;i++){
[if !supportLists]19. [endif] new Thread(new Runnable() {
[if !supportLists]20. [endif]
[if !supportLists]21. [endif] @Override
[if !supportLists]22. [endif] public void run() {
[if !supportLists]23. [endif] test();
[if !supportLists]24. [endif] }
[if !supportLists]25. [endif] }).start();
[if !supportLists]26. [endif] }
[if !supportLists]27. [endif]
[if !supportLists]28. [endif] }
[if !supportLists]29. [endif]
[if !supportLists]30. [endif] public static void test(){
[if !supportLists]31. [endif] try {
[if !supportLists]32. [endif]//申请一个请求
[if !supportLists]33. [endif] semaphore.acquire();
[if !supportLists]34. [endif] } catch (InterruptedException e1) {
[if !supportLists]35. [endif] e1.printStackTrace();
[if !supportLists]36. [endif] }
[if !supportLists]37. [endif] System.out.println(Thread.currentThread().getName()+"进来了");
[if !supportLists]38. [endif] try {
[if !supportLists]39. [endif] Thread.sleep(1000);
[if !supportLists]40. [endif] } catch (InterruptedException e) {
[if !supportLists]41. [endif] e.printStackTrace();
[if !supportLists]42. [endif] }
[if !supportLists]43. [endif] System.out.println(Thread.currentThread().getName()+"走了");
[if !supportLists]44. [endif]//释放一个请求
[if !supportLists]45. [endif] semaphore.release();
[if !supportLists]46. [endif] }
[if !supportLists]47. [endif]}
可以使用Semaphore控制,第16行的构造函数创建了一个Semaphore对象,并且初始化了5个信号。这样的效果是控件test方法最多只能有5个线程并发访问,对于5个线程时就排队等待,走一个来一下。第33行,请求一个信号(消费一个信号),如果信号被用完了则等待,第45行释放一个信号,释放的信号新的线程就可以使用了。
[if !supportLists]10. [endif]三个线程a、b、c并发运行,b,c需要a线程的数据怎么实现(上海3期学员提供)
根据问题的描述,我将问题用以下代码演示,ThreadA、ThreadB、ThreadC,ThreadA用于初始化数据num,只有当num初始化完成之后再让ThreadB和ThreadC获取到初始化后的变量num。
分析过程如下:
考虑到多线程的不确定性,因此我们不能确保ThreadA就一定先于ThreadB和ThreadC前执行,就算ThreadA先执行了,我们也无法保证ThreadA什么时候才能将变量num给初始化完成。因此我们必须让ThreadB和ThreadC去等待ThreadA完成任何后发出的消息。
现在需要解决两个难题,一是让ThreadB和ThreadC等待ThreadA先执行完,二是ThreadA执行完之后给ThreadB和ThreadC发送消息。
解决上面的难题我能想到的两种方案,一是使用纯Java API的Semaphore类来控制线程的等待和释放,二是使用Android提供的Handler消息机制。
[if !supportLists]1. [endif]package com.example;
[if !supportLists]2. [endif]/**
[if !supportLists]3. [endif]*三个线程a、b、c并发运行,b,c需要a线程的数据怎么实现(上海3期学员提供)
[if !supportLists]4. [endif] *
[if !supportLists]5. [endif] */
[if !supportLists]6. [endif]public class ThreadCommunication {
[if !supportLists]7. [endif] private static int num;//定义一个变量作为数据
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] public static void main(String[] args) {
[if !supportLists]10. [endif]
[if !supportLists]11. [endif] Thread threadA = new Thread(new Runnable() {
[if !supportLists]12. [endif]
[if !supportLists]13. [endif] @Override
[if !supportLists]14. [endif] public void run() {
[if !supportLists]15. [endif] try {
[if !supportLists]16. [endif] //模拟耗时操作之后初始化变量num
[if !supportLists]17. [endif] Thread.sleep(1000);
[if !supportLists]18. [endif] num = 1;
[if !supportLists]19. [endif]
[if !supportLists]20. [endif] } catch (InterruptedException e) {
[if !supportLists]21. [endif] e.printStackTrace();
[if !supportLists]22. [endif] }
[if !supportLists]23. [endif] }
[if !supportLists]24. [endif] });
[if !supportLists]25. [endif] Thread threadB = new Thread(new Runnable() {
[if !supportLists]26. [endif]
[if !supportLists]27. [endif] @Override
[if !supportLists]28. [endif] public void run() {
[if !supportLists]29. [endif] System.out.println(Thread.currentThread().getName()+"获取到num的值为:"+num);
[if !supportLists]30. [endif] }
[if !supportLists]31. [endif] });
[if !supportLists]32. [endif] Thread threadC = new Thread(new Runnable() {
[if !supportLists]33. [endif]
[if !supportLists]34. [endif] @Override
[if !supportLists]35. [endif] public void run() {
[if !supportLists]36. [endif] System.out.println(Thread.currentThread().getName()+"获取到num的值为:"+num);
[if !supportLists]37. [endif] }
[if !supportLists]38. [endif] });
[if !supportLists]39. [endif] //同时开启3个线程
[if !supportLists]40. [endif] threadA.start();
[if !supportLists]41. [endif] threadB.start();
[if !supportLists]42. [endif] threadC.start();
[if !supportLists]43. [endif]
[if !supportLists]44. [endif] }
[if !supportLists]45. [endif]}
[if !supportLists]46. [endif]
解决方案一:
[if !supportLists]1. [endif]public class ThreadCommunication {
[if !supportLists]2. [endif] private static int num;
[if !supportLists]3. [endif] /**
[if !supportLists]4. [endif] *定义一个信号量,该类内部维持了多个线程锁,可以阻塞多个线程,释放多个线程,
[if !supportLists]5. [endif]线程的阻塞和释放是通过permit概念来实现的
[if !supportLists]6. [endif] *线程通过semaphore.acquire()方法获取permit,如果当前semaphore有permit则分配给该线程,
[if !supportLists]7. [endif]如果没有则阻塞该线程直到semaphore
[if !supportLists]8. [endif] *调用release()方法释放permit。
[if !supportLists]9. [endif] *构造函数中参数:permit(允许) 个数,
[if !supportLists]10. [endif] */
[if !supportLists]11. [endif] private static Semaphore semaphore = new Semaphore(0);
[if !supportLists]12. [endif] public static void main(String[] args) {
[if !supportLists]13. [endif]
[if !supportLists]14. [endif] Thread threadA = new Thread(new Runnable() {
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] @Override
[if !supportLists]17. [endif] public void run() {
[if !supportLists]18. [endif] try {
[if !supportLists]19. [endif] //模拟耗时操作之后初始化变量num
[if !supportLists]20. [endif] Thread.sleep(1000);
[if !supportLists]21. [endif] num = 1;
[if !supportLists]22. [endif] //初始化完参数后释放两个permit
[if !supportLists]23. [endif] semaphore.release(2);
[if !supportLists]24. [endif]
[if !supportLists]25. [endif] } catch (InterruptedException e) {
[if !supportLists]26. [endif] e.printStackTrace();
[if !supportLists]27. [endif] }
[if !supportLists]28. [endif] }
[if !supportLists]29. [endif] });
[if !supportLists]30. [endif] Thread threadB = new Thread(new Runnable() {
[if !supportLists]31. [endif]
[if !supportLists]32. [endif] @Override
[if !supportLists]33. [endif] public void run() {
[if !supportLists]34. [endif] try {
[if !supportLists]35. [endif] //获取permit,如果semaphore没有可用的permit则等待,如果有则消耗一个
[if !supportLists]36. [endif] semaphore.acquire();
[if !supportLists]37. [endif] } catch (InterruptedException e) {
[if !supportLists]38. [endif] e.printStackTrace();
[if !supportLists]39. [endif] }
[if !supportLists]40. [endif] System.out.println(Thread.currentThread().getName()+"获取到num的值为:"+num);
[if !supportLists]41. [endif] }
[if !supportLists]42. [endif] });
[if !supportLists]43. [endif] Thread threadC = new Thread(new Runnable() {
[if !supportLists]44. [endif]
[if !supportLists]45. [endif] @Override
[if !supportLists]46. [endif] public void run() {
[if !supportLists]47. [endif] try {
[if !supportLists]48. [endif] //获取permit,如果semaphore没有可用的permit则等待,如果有则消耗一个
[if !supportLists]49. [endif] semaphore.acquire();
[if !supportLists]50. [endif] } catch (InterruptedException e) {
[if !supportLists]51. [endif] e.printStackTrace();
[if !supportLists]52. [endif] }
[if !supportLists]53. [endif] System.out.println(Thread.currentThread().getName()+"获取到num的值为:"+num);
[if !supportLists]54. [endif] }
[if !supportLists]55. [endif] });
[if !supportLists]56. [endif] //同时开启3个线程
[if !supportLists]57. [endif] threadA.start();
[if !supportLists]58. [endif] threadB.start();
[if !supportLists]59. [endif] threadC.start();
[if !supportLists]60. [endif]
[if !supportLists]61. [endif] }
[if !supportLists]62. [endif]}
[if !supportLists]11. [endif]同一个类中的2个方法都加了同步锁,多个线程能同时访问同一个类中的这两个方法吗?(2017-2-24)
这个问题需要考虑到Lock与synchronized 两种实现锁的不同情形。因为这种情况下使用Lock 和synchronized 会有截然不同的结果。Lock可以让等待锁的线程响应中断,Lock获取锁,之后需要释放锁。如下代码,多个线程不可访问同一个类中的2个加了Lock锁的方法。
[if !supportLists]63. [endif]package com;
[if !supportLists]64. [endif]import java.util.concurrent.locks.Lock;
[if !supportLists]65. [endif]import java.util.concurrent.locks.ReentrantLock;
[if !supportLists]66. [endif]public class qq {
[if !supportLists]67. [endif]
[if !supportLists]68. [endif] private int count = 0;
[if !supportLists]69. [endif]private Lock lock = new ReentrantLock();//设置lock锁
[if !supportLists]70. [endif]//方法1
[if !supportLists]71. [endif] public Runnable run1 = new Runnable(){
[if !supportLists]72. [endif] public void run() {
[if !supportLists]73. [endif]lock.lock(); //加锁
[if !supportLists]74. [endif] while(count < 1000) {
[if !supportLists]75. [endif] try {
[if !supportLists]76. [endif]//打印是否执行该方法
[if !supportLists]77. [endif] System.out.println(Thread.currentThread().getName() + " run1: "+count++);
[if !supportLists]78. [endif] } catch (Exception e) {
[if !supportLists]79. [endif] e.printStackTrace();
[if !supportLists]80. [endif] }
[if !supportLists]81. [endif] }
[if !supportLists]82. [endif] }
[if !supportLists]83. [endif] lock.unlock();
[if !supportLists]84. [endif] }};
[if !supportLists]85. [endif]//方法2
[if !supportLists]86. [endif] public Runnable run2 = new Runnable(){
[if !supportLists]87. [endif] public void run() {
[if !supportLists]88. [endif] lock.lock();
[if !supportLists]89. [endif] while(count < 1000) {
[if !supportLists]90. [endif] try {
[if !supportLists]91. [endif] System.out.println(Thread.currentThread().getName() +
[if !supportLists]92. [endif] " run2: "+count++);
[if !supportLists]93. [endif] } catch (Exception e) {
[if !supportLists]94. [endif] e.printStackTrace();
[if !supportLists]95. [endif] }
[if !supportLists]96. [endif] }
[if !supportLists]97. [endif] lock.unlock();
[if !supportLists]98. [endif] }};
[if !supportLists]99. [endif]
[if !supportLists]100. [endif]
[if !supportLists]101. [endif]
[if !supportLists]102. [endif] public static void main(String[] args) throws InterruptedException {
[if !supportLists]103. [endif] qq t = new qq(); //创建一个对象
[if !supportLists]104. [endif]new Thread(t.run1).start();//获取该对象的方法1
[if !supportLists]105. [endif]
[if !supportLists]106. [endif]new Thread(t.run2).start();//获取该对象的方法2
[if !supportLists]107. [endif] }
[if !supportLists]108. [endif]}
结果是:
Thread-0 run1: 0
Thread-0 run1: 1
Thread-0 run1: 2
Thread-0 run1: 3
Thread-0 run1: 4
Thread-0 run1: 5
Thread-0 run1: 6
........
而synchronized却不行,使用synchronized时,当我们访问同一个类对象的时候,是同一把锁,所以可以访问该对象的其他synchronized方法。代码如下:
[if !supportLists]1.[endif]package com;
[if !supportLists]2.[endif]import java.util.concurrent.locks.Lock;
[if !supportLists]3.[endif]import java.util.concurrent.locks.ReentrantLock;
[if !supportLists]4.[endif]public class qq {
[if !supportLists]5.[endif] private int count = 0;
[if !supportLists]6.[endif] private Lock lock = new ReentrantLock();
[if !supportLists]7.[endif] public Runnable run1 = new Runnable(){
[if !supportLists]8.[endif] public void run() {
[if !supportLists]9.[endif]synchronized(this) { //设置关键字synchronized,以当前类为锁
[if !supportLists]10.[endif] while(count < 1000) {
[if !supportLists]11.[endif] try {
[if !supportLists]12.[endif]//打印是否执行该方法
[if !supportLists]13.[endif] System.out.println(Thread.currentThread().getName() + " run1: "+count++);
[if !supportLists]14.[endif] } catch (Exception e) {
[if !supportLists]15.[endif] e.printStackTrace();
[if !supportLists]16.[endif] }
[if !supportLists]17.[endif] }
[if !supportLists]18.[endif] }
[if !supportLists]19.[endif] }};
[if !supportLists]20.[endif] public Runnable run2 = new Runnable(){
[if !supportLists]21.[endif] public void run() {
[if !supportLists]22.[endif] synchronized(this) {
[if !supportLists]23.[endif] while(count < 1000) {
[if !supportLists]24.[endif] try {
[if !supportLists]25.[endif] System.out.println(Thread.currentThread().getName()
[if !supportLists]26.[endif] + " run2: "+count++);
[if !supportLists]27.[endif] } catch (Exception e) {
[if !supportLists]28.[endif] e.printStackTrace();
[if !supportLists]29.[endif] }
[if !supportLists]30.[endif] }
[if !supportLists]31.[endif] }
[if !supportLists]32.[endif] }};
[if !supportLists]33.[endif] public static void main(String[] args) throws InterruptedException {
[if !supportLists]34.[endif] qq t = new qq(); //创建一个对象
[if !supportLists]35.[endif]new Thread(t.run1).start(); //获取该对象的方法1
[if !supportLists]36.[endif]new Thread(t.run2).start(); //获取该对象的方法2
[if !supportLists]37.[endif] }
[if !supportLists]38.[endif]}
结果为:
Thread-1 run2: 0
Thread-1 run2: 1
Thread-1 run2: 2
Thread-0 run1: 0
Thread-0 run1: 4 Thread-0 run1: 5 Thread-0 run1: 6
......
[if !supportLists]12. [endif]什么情况下导致线程死锁,遇到线程死锁该怎么解决?(2017-2-24)
11.1 死锁的定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
11.2 死锁产生的必要条件:
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, ..., pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有,如图2-15所示。
11.3 产生死锁的一个例子
[if !supportLists]1.[endif]package itheima.com;
[if !supportLists]2.[endif]/**
[if !supportLists]3.[endif]*一个简单的死锁类
[if !supportLists]4.[endif]*当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
[if !supportLists]5.[endif]*而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
[if !supportLists]6.[endif]* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
[if !supportLists]7.[endif]* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
[if !supportLists]8.[endif]* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
[if !supportLists]9.[endif]*/
[if !supportLists]10.[endif]public class DeadLock implements Runnable {
[if !supportLists]11.[endif] public int flag = 1;
[if !supportLists]12.[endif]//静态对象是类的所有对象共享的
[if !supportLists]13.[endif] private static Object o1 = new Object(), o2 = new Object();
[if !supportLists]14.[endif] public void run() {
[if !supportLists]15.[endif] System.out.println("flag=" + flag);
[if !supportLists]16.[endif] if (flag == 1) {
[if !supportLists]17.[endif] synchronized (o1) {
[if !supportLists]18.[endif] try {
[if !supportLists]19.[endif] Thread.sleep(500);
[if !supportLists]20.[endif] } catch (Exception e) {
[if !supportLists]21.[endif] e.printStackTrace();
[if !supportLists]22.[endif] }
[if !supportLists]23.[endif] synchronized (o2) {
[if !supportLists]24.[endif] System.out.println("1");
[if !supportLists]25.[endif] }
[if !supportLists]26.[endif] }
[if !supportLists]27.[endif] }
[if !supportLists]28.[endif] if (flag == 0) {
[if !supportLists]29.[endif] synchronized (o2) {
[if !supportLists]30.[endif] try {
[if !supportLists]31.[endif] Thread.sleep(500);
[if !supportLists]32.[endif] } catch (Exception e) {
[if !supportLists]33.[endif] e.printStackTrace();
[if !supportLists]34.[endif] }
[if !supportLists]35.[endif] synchronized (o1) {
[if !supportLists]36.[endif] System.out.println("0");
[if !supportLists]37.[endif] }
[if !supportLists]38.[endif] }
[if !supportLists]39.[endif] }
[if !supportLists]40.[endif] }
[if !supportLists]41.[endif] public static void main(String[] args) {
[if !supportLists]42.[endif] DeadLock td1 = new DeadLock();
[if !supportLists]43.[endif] DeadLock td2 = new DeadLock();
[if !supportLists]44.[endif] td1.flag = 1;
[if !supportLists]45.[endif] td2.flag = 0;
[if !supportLists]46.[endif]//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
[if !supportLists]47.[endif]//td2的run()可能在td1的run()之前运行
[if !supportLists]48.[endif] new Thread(td1).start();
[if !supportLists]49.[endif] new Thread(td2).start();
[if !supportLists]50.[endif] }
[if !supportLists]51.[endif]}
11.4 如何避免死锁
在有些情况下死锁是可以避免的。两种用于避免死锁的技术:
[if !supportLists]1)[endif]加锁顺序(线程按照一定的顺序加锁)
[if !supportLists]1.[endif]package itheima.com;
[if !supportLists]2.[endif]public class DeadLock {
[if !supportLists]3.[endif] public int flag = 1;
[if !supportLists]4.[endif]//静态对象是类的所有对象共享的
[if !supportLists]5.[endif] private static Object o1 = new Object(), o2 = new Object();
[if !supportLists]6.[endif] public void money(int flag) {
[if !supportLists]7.[endif] this.flag=flag;
[if !supportLists]8.[endif] if( flag ==1){
[if !supportLists]9.[endif] synchronized (o1) {
[if !supportLists]10.[endif] try {
[if !supportLists]11.[endif] Thread.sleep(500);
[if !supportLists]12.[endif] } catch (Exception e) {
[if !supportLists]13.[endif] e.printStackTrace();
[if !supportLists]14.[endif] }
[if !supportLists]15.[endif] synchronized (o2) {
[if !supportLists]16.[endif]System.out.println("当前的线程是"+
[if !supportLists]17.[endif] Thread.currentThread().getName()+" "+"flag的值"+"1");
[if !supportLists]18.[endif] }
[if !supportLists]19.[endif] }
[if !supportLists]20.[endif] }
[if !supportLists]21.[endif] if(flag ==0){
[if !supportLists]22.[endif] synchronized (o2) {
[if !supportLists]23.[endif] try {
[if !supportLists]24.[endif] Thread.sleep(500);
[if !supportLists]25.[endif] } catch (Exception e) {
[if !supportLists]26.[endif] e.printStackTrace();
[if !supportLists]27.[endif] }
[if !supportLists]28.[endif] synchronized (o1) {
[if !supportLists]29.[endif]System.out.println("当前的线程是"+
[if !supportLists]30.[endif] Thread.currentThread().getName()+" "+"flag的值"+"0");
[if !supportLists]31.[endif] }
[if !supportLists]32.[endif] }
[if !supportLists]33.[endif] }
[if !supportLists]34.[endif] }
[if !supportLists]35.[endif]
[if !supportLists]36.[endif] public static void main(String[] args) {
[if !supportLists]37.[endif] final DeadLock td1 = new DeadLock();
[if !supportLists]38.[endif] final DeadLock td2 = new DeadLock();
[if !supportLists]39.[endif] td1.flag = 1;
[if !supportLists]40.[endif] td2.flag = 0;
[if !supportLists]41.[endif]//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
[if !supportLists]42.[endif]//td2的run()可能在td1的run()之前运行
[if !supportLists]43.[endif] final Thread t1=new Thread(new Runnable(){
[if !supportLists]44.[endif] public void run() {
[if !supportLists]45.[endif] td1.flag = 1;
[if !supportLists]46.[endif] td1.money(1);
[if !supportLists]47.[endif] }
[if !supportLists]48.[endif] });
[if !supportLists]49.[endif] t1.start();
[if !supportLists]50.[endif] Thread t2= new Thread(new Runnable(){
[if !supportLists]51.[endif] public void run() {
[if !supportLists]52.[endif] // TODO Auto-generated method stub
[if !supportLists]53.[endif] try {
[if !supportLists]54.[endif] //让t2等待t1执行完
[if !supportLists]55.[endif] t1.join();//核心代码,让t1执行完后t2才会执行
[if !supportLists]56.[endif] } catch (InterruptedException e) {
[if !supportLists]57.[endif] // TODO Auto-generated catch block
[if !supportLists]58.[endif] e.printStackTrace();
[if !supportLists]59.[endif] }
[if !supportLists]60.[endif] td2.flag = 0;
[if !supportLists]61.[endif] td1.money(0);
[if !supportLists]62.[endif] }
[if !supportLists]63.[endif] });
[if !supportLists]64.[endif] t2.start();
[if !supportLists]65.[endif] }
[if !supportLists]66.[endif]}
结果:
当前的线程是Thread-0 flag的值1
当前的线程是Thread-1 flag的值0
[if !supportLists]2)[endif]加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
[if !supportLists]1.[endif] package itheima.com;
[if !supportLists]2.[endif]import java.util.concurrent.TimeUnit;
[if !supportLists]3.[endif]import java.util.concurrent.locks.Lock;
[if !supportLists]4.[endif]import java.util.concurrent.locks.ReentrantLock;
[if !supportLists]5.[endif]public class DeadLock {
[if !supportLists]6.[endif] public int flag = 1;
[if !supportLists]7.[endif]//静态对象是类的所有对象共享的
[if !supportLists]8.[endif] private static Object o1 = new Object(), o2 = new Object();
[if !supportLists]9.[endif] public void money(int flag) throws InterruptedException {
[if !supportLists]10.[endif] this.flag=flag;
[if !supportLists]11.[endif] if( flag ==1){
[if !supportLists]12.[endif] synchronized (o1) {
[if !supportLists]13.[endif] Thread.sleep(500);
[if !supportLists]14.[endif] synchronized (o2) {
[if !supportLists]15.[endif]System.out.println("当前的线程是"+
[if !supportLists]16.[endif] Thread.currentThread().getName()+" "+"flag的值"+"1");
[if !supportLists]17.[endif] }
[if !supportLists]18.[endif] }
[if !supportLists]19.[endif] }
[if !supportLists]20.[endif] if(flag ==0){
[if !supportLists]21.[endif] synchronized (o2) {
[if !supportLists]22.[endif] Thread.sleep(500);
[if !supportLists]23.[endif] synchronized (o1) {
[if !supportLists]24.[endif]System.out.println("当前的线程是"+
[if !supportLists]25.[endif] Thread.currentThread().getName()+" "+"flag的值"+"0");
[if !supportLists]26.[endif] }
[if !supportLists]27.[endif] }
[if !supportLists]28.[endif] }
[if !supportLists]29.[endif] }
[if !supportLists]30.[endif]
[if !supportLists]31.[endif] public static void main(String[] args) {
[if !supportLists]32.[endif] final Lock lock = new ReentrantLock();
[if !supportLists]33.[endif] final DeadLock td1 = new DeadLock();
[if !supportLists]34.[endif] final DeadLock td2 = new DeadLock();
[if !supportLists]35.[endif] td1.flag = 1;
[if !supportLists]36.[endif] td2.flag = 0;
[if !supportLists]37.[endif]//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
[if !supportLists]38.[endif]//td2的run()可能在td1的run()之前运行
[if !supportLists]39.[endif]
[if !supportLists]40.[endif] final Thread t1=new Thread(new Runnable(){
[if !supportLists]41.[endif] public void run() {
[if !supportLists]42.[endif] // TODO Auto-generated method stub
[if !supportLists]43.[endif] String tName = Thread.currentThread().getName();
[if !supportLists]44.[endif]
[if !supportLists]45.[endif] td1.flag = 1;
[if !supportLists]46.[endif] try {
[if !supportLists]47.[endif]//获取不到锁,就等5秒,如果5秒后还是获取不到就返回false
[if !supportLists]48.[endif] if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
[if !supportLists]49.[endif] System.out.println(tName + "获取到锁!");
[if !supportLists]50.[endif] } else {
[if !supportLists]51.[endif] System.out.println(tName + "获取不到锁!");
[if !supportLists]52.[endif] return;
[if !supportLists]53.[endif] }
[if !supportLists]54.[endif] } catch (Exception e) {
[if !supportLists]55.[endif] e.printStackTrace();
[if !supportLists]56.[endif] }
[if !supportLists]57.[endif]
[if !supportLists]58.[endif] try {
[if !supportLists]59.[endif] td1.money(1);
[if !supportLists]60.[endif] } catch (Exception e) {
[if !supportLists]61.[endif] System.out.println(tName + "出错了!!!");
[if !supportLists]62.[endif] } finally {
[if !supportLists]63.[endif]System.out.println("当前的线程是"+Thread.currentThread().getName()+"释放锁!!");
[if !supportLists]64.[endif] lock.unlock();
[if !supportLists]65.[endif] }
[if !supportLists]66.[endif] }
[if !supportLists]67.[endif] });
[if !supportLists]68.[endif] t1.start();
[if !supportLists]69.[endif] Thread t2= new Thread(new Runnable(){
[if !supportLists]70.[endif] public void run() {
[if !supportLists]71.[endif] String tName = Thread.currentThread().getName();
[if !supportLists]72.[endif] // TODO Auto-generated method stub
[if !supportLists]73.[endif] td1.flag = 1;
[if !supportLists]74.[endif] try {
[if !supportLists]75.[endif]//获取不到锁,就等5秒,如果5秒后还是获取不到就返回false
[if !supportLists]76.[endif] if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
[if !supportLists]77.[endif] System.out.println(tName + "获取到锁!");
[if !supportLists]78.[endif] } else {
[if !supportLists]79.[endif] System.out.println(tName + "获取不到锁!");
[if !supportLists]80.[endif] return;
[if !supportLists]81.[endif] }
[if !supportLists]82.[endif] } catch (Exception e) {
[if !supportLists]83.[endif] e.printStackTrace();
[if !supportLists]84.[endif] }
[if !supportLists]85.[endif] try {
[if !supportLists]86.[endif] td2.money(0);
[if !supportLists]87.[endif] } catch (Exception e) {
[if !supportLists]88.[endif] System.out.println(tName + "出错了!!!");
[if !supportLists]89.[endif] } finally {
[if !supportLists]90.[endif]System.out.println("当前的线程是"+Thread.currentThread().getName()+"释放锁!!");
[if !supportLists]91.[endif] lock.unlock();
[if !supportLists]92.[endif] }
[if !supportLists]93.[endif] }
[if !supportLists]94.[endif] });
[if !supportLists]95.[endif] t2.start();
[if !supportLists]96.[endif] }
[if !supportLists]97.[endif]}
打印结果:
Thread-0获取到锁!
当前的线程是Thread-0 flag的值1
当前的线程是Thread-0释放锁!!
Thread-1获取到锁!
当前的线程是Thread-1 flag的值0
当前的线程是Thread-1释放锁!!
[if !supportLists]13. [endif]Java中多线程间的通信怎么实现?(2017-2-24)
线程通信的方式:
[if !supportLists]1.[endif]共享变量
线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值。线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。这个简单的例子使用了一个持有信号的对象,并提供了set和get方法:
[if !supportLists]1.[endif]package itheima.com;
[if !supportLists]2.[endif]public class MySignal{
[if !supportLists]3.[endif]//共享的变量
[if !supportLists]4.[endif] private boolean hasDataToProcess=false;
[if !supportLists]5.[endif]//取值
[if !supportLists]6.[endif] public boolean getHasDataToProcess() {
[if !supportLists]7.[endif] return hasDataToProcess;
[if !supportLists]8.[endif] }
[if !supportLists]9.[endif]//存值
[if !supportLists]10.[endif] public void setHasDataToProcess(boolean hasDataToProcess) {
[if !supportLists]11.[endif] this.hasDataToProcess = hasDataToProcess;
[if !supportLists]12.[endif] }
[if !supportLists]13.[endif] public static void main(String[] args){
[if !supportLists]14.[endif]//同一个对象
[if !supportLists]15.[endif] final MySignal my=new MySignal();
[if !supportLists]16.[endif]//线程1设置hasDataToProcess值为true
[if !supportLists]17.[endif] final Thread t1=new Thread(new Runnable(){
[if !supportLists]18.[endif] public void run() {
[if !supportLists]19.[endif] my.setHasDataToProcess(true);
[if !supportLists]20.[endif] }
[if !supportLists]21.[endif] });
[if !supportLists]22.[endif] t1.start();
[if !supportLists]23.[endif]//线程2取这个值hasDataToProcess
[if !supportLists]24.[endif] Thread t2=new Thread(new Runnable(){
[if !supportLists]25.[endif] public void run() {
[if !supportLists]26.[endif] try {
[if !supportLists]27.[endif]//等待线程1完成然后取值
[if !supportLists]28.[endif] t1.join();
[if !supportLists]29.[endif] } catch (InterruptedException e) {
[if !supportLists]30.[endif] e.printStackTrace();
[if !supportLists]31.[endif] }
[if !supportLists]32.[endif] my.getHasDataToProcess();
[if !supportLists]33.[endif] System.out.println("t1改变以后的值:" + my.isHasDataToProcess());
[if !supportLists]34.[endif] }
[if !supportLists]35.[endif] });
[if !supportLists]36.[endif] t2.start();
[if !supportLists]37.[endif]}
[if !supportLists]38.[endif]}
结果:
t1改变以后的值:true
[if !supportLists]2.[endif]wait/notify机制
以资源为例,生产者生产一个资源,通知消费者就消费掉一个资源,生产者继续生产资源,消费者消费资源,以此循环。代码如下:
[if !supportLists]1.[endif]package itheima.com;
[if !supportLists]2.[endif]//资源类
[if !supportLists]3.[endif] class Resource{
[if !supportLists]4.[endif] private String name;
[if !supportLists]5.[endif] private int count=1;
[if !supportLists]6.[endif] private boolean flag=false;
[if !supportLists]7.[endif] public synchronized void set(String name){
[if !supportLists]8.[endif] //生产资源
[if !supportLists]9.[endif] while(flag) {
[if !supportLists]10.[endif] try{
[if !supportLists]11.[endif] //线程等待。消费者消费资源
[if !supportLists]12.[endif] wait();
[if !supportLists]13.[endif] }catch(Exception e){}
[if !supportLists]14.[endif] }
[if !supportLists]15.[endif] this.name=name+"---"+count++;
[if !supportLists]16.[endif]System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
[if !supportLists]17.[endif] flag=true;
[if !supportLists]18.[endif]//唤醒等待中的消费者
[if !supportLists]19.[endif] this.notifyAll();
[if !supportLists]20.[endif] }
[if !supportLists]21.[endif] public synchronized void out(){
[if !supportLists]22.[endif] //消费资源
[if !supportLists]23.[endif] while(!flag) {
[if !supportLists]24.[endif] //线程等待,生产者生产资源
[if !supportLists]25.[endif] try{wait();}catch(Exception e){}
[if !supportLists]26.[endif] }
[if !supportLists]27.[endif]System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
[if !supportLists]28.[endif] flag=false;
[if !supportLists]29.[endif]//唤醒生产者,生产资源
[if !supportLists]30.[endif] this.notifyAll();
[if !supportLists]31.[endif] }
[if !supportLists]32.[endif]}
[if !supportLists]33.[endif]//生产者
[if !supportLists]34.[endif] class Producer implements Runnable{
[if !supportLists]35.[endif] private Resource res;
[if !supportLists]36.[endif] Producer(Resource res){
[if !supportLists]37.[endif] this.res=res;
[if !supportLists]38.[endif] }
[if !supportLists]39.[endif] //生产者生产资源
[if !supportLists]40.[endif] public void run(){
[if !supportLists]41.[endif] while(true){
[if !supportLists]42.[endif] res.set("商品");
[if !supportLists]43.[endif] }
[if !supportLists]44.[endif] }
[if !supportLists]45.[endif] }
[if !supportLists]46.[endif]//消费者消费资源
[if !supportLists]47.[endif] class Consumer implements Runnable{
[if !supportLists]48.[endif] private Resource res;
[if !supportLists]49.[endif] Consumer(Resource res){
[if !supportLists]50.[endif] this.res=res;
[if !supportLists]51.[endif] }
[if !supportLists]52.[endif] public void run(){
[if !supportLists]53.[endif] while(true){
[if !supportLists]54.[endif] res.out();
[if !supportLists]55.[endif] }
[if !supportLists]56.[endif] }
[if !supportLists]57.[endif] }
[if !supportLists]58.[endif]public class ProducerConsumerDemo{
[if !supportLists]59.[endif] public static void main(String[] args){
[if !supportLists]60.[endif] Resource r=new Resource();
[if !supportLists]61.[endif] Producer pro=new Producer(r);
[if !supportLists]62.[endif] Consumer con=new Consumer(r);
[if !supportLists]63.[endif] Thread t1=new Thread(pro);
[if !supportLists]64.[endif] Thread t2=new Thread(con);
[if !supportLists]65.[endif] t1.start();
[if !supportLists]66.[endif] t2.start();
[if !supportLists]67.[endif] }
[if !supportLists]68.[endif]}
[if !supportLists]14. [endif]线程和进程的区别(2017-11-23-wzz)
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量cpu资源。
[if !supportLists]15. [endif]请说出同步线程及线程调度相关的方法?(2017-11-23-wzz)
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意:java 5 通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调
[if !supportLists]16. [endif]启动一个线程是调用run()方法还是start()方法?(2017-11-23-wzz)
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。
run()方法是线程启动后要进行回调(callback)的方法。
[if !supportLists]十、[endif]Java内部类
[if !supportLists]1. [endif]静态嵌套类(Static Nested Class) 和内部类(Inner Class)的不同?(2017-11-16-wl)
静态嵌套类:Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。
内部类:需要在外部类实例化后才能实例化,其语法看起来挺诡异的。
[if !supportLists]2. [endif]下面的代码哪些地方会产生编译错误?(2017-11-16-wl)
[if !supportLists]1.[endif]class Outer {
[if !supportLists]2.[endif]
[if !supportLists]3.[endif]class Inner {}
[if !supportLists]4.[endif]
[if !supportLists]5.[endif]public static void foo() { new Inner(); }
[if !supportLists]6.[endif]
[if !supportLists]7.[endif]public void bar() { new Inner(); }
[if !supportLists]8.[endif]
[if !supportLists]9.[endif]public static void main(String[] args) {
[if !supportLists]10.[endif]new Inner();
[if !supportLists]11.[endif]}
[if !supportLists]12.[endif]}
注意:Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo 和 main 方法都是静态方法,静态方法中没有 this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做
[if !supportLists]1.[endif]new Outer().new Inner();
[if !supportLists]第三章 [endif]JavaSE高级
[if !supportLists]一、[endif]Java中的反射
[if !supportLists]1. [endif]说说你对Java中反射的理解
Java中的反射首先是能够获取到Java中要反射类的字节码,获取字节码有三种方法,1.Class.forName(className) 2.类名.class 3.this.getClass()。然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类,这些类提供了丰富的方法可以被我们所使用。
[if !supportLists]二、[endif]Java中的动态代理
[if !supportLists]1. [endif]写一个ArrayList的动态代理类(笔试题)
[if !supportLists]1. [endif]final List list = new ArrayList();
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] List proxyInstance =
[if !supportLists]4. [endif](List)Proxy.newProxyInstance(list.getClass().getClassLoader(),
[if !supportLists]5. [endif]list.getClass().getInterfaces(),
[if !supportLists]6. [endif]new InvocationHandler() {
[if !supportLists]7. [endif]
[if !supportLists]8. [endif] @Override
[if !supportLists]9. [endif] public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
[if !supportLists]10. [endif] return method.invoke(list, args);
[if !supportLists]11. [endif] }
[if !supportLists]12. [endif] });
[if !supportLists]13. [endif] proxyInstance.add("你好");
[if !supportLists]14. [endif] System.out.println(list);
[if !supportLists]2. [endif]动静态代理的区别,什么场景使用?(2015-11-25)
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。
还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。
AOP编程就是基于动态代理实现的,比如著名的Spring框架、Hibernate框架等等都是动态代理的使用例子。
[if !supportLists]三、[endif]Java中的设计模式&回收机制
[if !supportLists]1. [endif]你所知道的设计模式有哪些
Java中一般认为有23种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
[if !supportLists]2. [endif]单例设计模式
最好理解的一种设计模式,分为懒汉式和饿汉式。
饿汉式:
[if !supportLists]1. [endif]public class Singleton {
[if !supportLists]2. [endif] //直接创建对象
[if !supportLists]3. [endif] public static Singleton instance = new Singleton();
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] //私有化构造函数
[if !supportLists]6. [endif] private Singleton() {
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] //返回对象实例
[if !supportLists]10. [endif] public static Singleton getInstance() {
[if !supportLists]11. [endif] return instance;
[if !supportLists]12. [endif] }
[if !supportLists]13. [endif]}
懒汉式:
[if !supportLists]1. [endif]public class Singleton {
[if !supportLists]2. [endif] //声明变量
[if !supportLists]3. [endif] private static volatile Singleton singleton = null;
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] //私有构造函数
[if !supportLists]6. [endif] private Singleton() {
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] //提供对外方法
[if !supportLists]10. [endif] public static Singleton getInstance() {
[if !supportLists]11. [endif] if (singleton == null) {
[if !supportLists]12. [endif] synchronized (Singleton.class) {
[if !supportLists]13. [endif] if (singleton == null) {
[if !supportLists]14. [endif] singleton = new Singleton();
[if !supportLists]15. [endif] }
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif] }
[if !supportLists]18. [endif] return singleton;
[if !supportLists]19. [endif] }
[if !supportLists]20. [endif]}
[if !supportLists]3. [endif]工厂设计模式
工厂模式分为工厂方法模式和抽象工厂模式。
工厂方法模式
工厂方法模式分为三种:普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
普通工厂模式
[if !supportLists]1. [endif]public interface Sender {
[if !supportLists]2. [endif] public void Send();
[if !supportLists]3. [endif]}
[if !supportLists]4. [endif]public class MailSender implements Sender {
[if !supportLists]5. [endif]
[if !supportLists]6. [endif] @Override
[if !supportLists]7. [endif] public void Send() {
[if !supportLists]8. [endif] System.out.println("this is mail sender!");
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif]}
[if !supportLists]11. [endif]public class SmsSender implements Sender {
[if !supportLists]12. [endif]
[if !supportLists]13. [endif] @Override
[if !supportLists]14. [endif] public void Send() {
[if !supportLists]15. [endif] System.out.println("this is sms sender!");
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif]public class SendFactory {
[if !supportLists]19. [endif] public Sender produce(String type) {
[if !supportLists]20. [endif] if ("mail".equals(type)) {
[if !supportLists]21. [endif] return new MailSender();
[if !supportLists]22. [endif] } else if ("sms".equals(type)) {
[if !supportLists]23. [endif] return new SmsSender();
[if !supportLists]24. [endif] } else {
[if !supportLists]25. [endif] System.out.println("请输入正确的类型!");
[if !supportLists]26. [endif] return null;
[if !supportLists]27. [endif] }
[if !supportLists]28. [endif] }
[if !supportLists]29. [endif]}
多个工厂方法模式
该模式是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
[if !supportLists]1. [endif]public class SendFactory {
[if !supportLists]2. [endif] public Sender produceMail(){
[if !supportLists]3. [endif] return new MailSender();
[if !supportLists]4. [endif] }
[if !supportLists]5. [endif]
[if !supportLists]6. [endif] public Sender produceSms(){
[if !supportLists]7. [endif] return new SmsSender();
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif]}
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]public class FactoryTest {
[if !supportLists]12. [endif] public static void main(String[] args) {
[if !supportLists]13. [endif] SendFactory factory = new SendFactory();
[if !supportLists]14. [endif] Sender sender = factory.produceMail();
[if !supportLists]15. [endif] sender.send();
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]}
静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
[if !supportLists]1. [endif]public class SendFactory {
[if !supportLists]2. [endif] public static Sender produceMail(){
[if !supportLists]3. [endif] return new MailSender();
[if !supportLists]4. [endif] }
[if !supportLists]5. [endif]
[if !supportLists]6. [endif] public static Sender produceSms(){
[if !supportLists]7. [endif] return new SmsSender();
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif]}
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]
[if !supportLists]12. [endif]public class FactoryTest {
[if !supportLists]13. [endif] public static void main(String[] args) {
[if !supportLists]14. [endif] Sender sender = SendFactory.produceMail();
[if !supportLists]15. [endif] sender.send();
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]}
抽象工厂模式
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
[if !supportLists]1. [endif]public interface Provider {
[if !supportLists]2. [endif] public Sender produce();
[if !supportLists]3. [endif]}
[if !supportLists]4. [endif]-------------------------------------------------------------------------------------
[if !supportLists]5. [endif]public interface Sender {
[if !supportLists]6. [endif] public void send();
[if !supportLists]7. [endif]}
[if !supportLists]8. [endif]-------------------------------------------------------------------------------------
[if !supportLists]9. [endif]public class MailSender implements Sender {
[if !supportLists]10. [endif]
[if !supportLists]11. [endif] @Override
[if !supportLists]12. [endif] public void send() {
[if !supportLists]13. [endif] System.out.println("this is mail sender!");
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
[if !supportLists]16. [endif]-------------------------------------------------------------------------------------
[if !supportLists]17. [endif]public class SmsSender implements Sender {
[if !supportLists]18. [endif]
[if !supportLists]19. [endif] @Override
[if !supportLists]20. [endif] public void send() {
[if !supportLists]21. [endif] System.out.println("this is sms sender!");
[if !supportLists]22. [endif] }
[if !supportLists]23. [endif]}
[if !supportLists]24. [endif]-------------------------------------------------------------------------------------
[if !supportLists]25. [endif]public class SendSmsFactory implements Provider {
[if !supportLists]26. [endif]
[if !supportLists]27. [endif] @Override
[if !supportLists]28. [endif] public Sender produce() {
[if !supportLists]29. [endif] return new SmsSender();
[if !supportLists]30. [endif] }
[if !supportLists]31. [endif]}
[if !supportLists]1. [endif]public class SendMailFactory implements Provider {
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] @Override
[if !supportLists]4. [endif] public Sender produce() {
[if !supportLists]5. [endif] return new MailSender();
[if !supportLists]6. [endif] }
[if !supportLists]7. [endif]}
[if !supportLists]8. [endif]-------------------------------------------------------------------------------------
[if !supportLists]9. [endif]public class Test {
[if !supportLists]10. [endif] public static void main(String[] args) {
[if !supportLists]11. [endif] Provider provider = new SendMailFactory();
[if !supportLists]12. [endif] Sender sender = provider.produce();
[if !supportLists]13. [endif] sender.send();
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
[if !supportLists]4. [endif]建造者模式(Builder)
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。
[if !supportLists]1. [endif]public class Builder {
[if !supportLists]2. [endif] private List list = new ArrayList();
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] public void produceMailSender(int count) {
[if !supportLists]5. [endif] for (int i = 0; i < count; i++) {
[if !supportLists]6. [endif] list.add(new MailSender());
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif]
[if !supportLists]10. [endif] public void produceSmsSender(int count) {
[if !supportLists]11. [endif] for (int i = 0; i < count; i++) {
[if !supportLists]12. [endif] list.add(new SmsSender());
[if !supportLists]13. [endif] }
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
[if !supportLists]1. [endif]public class Builder {
[if !supportLists]2. [endif] private List list = new ArrayList();
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] public void produceMailSender(int count) {
[if !supportLists]5. [endif] for (int i = 0; i < count; i++) {
[if !supportLists]6. [endif] list.add(new MailSender());
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif]
[if !supportLists]10. [endif] public void produceSmsSender(int count) {
[if !supportLists]11. [endif] for (int i = 0; i < count; i++) {
[if !supportLists]12. [endif] list.add(new SmsSender());
[if !supportLists]13. [endif] }
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]}
[if !supportLists]1. [endif]public class TestBuilder {
[if !supportLists]2. [endif] public static void main(String[] args) {
[if !supportLists]3. [endif] Builder builder = new Builder();
[if !supportLists]4. [endif] builder.produceMailSender(10);
[if !supportLists]5. [endif] }
[if !supportLists]6. [endif]}
[if !supportLists]5. [endif]适配器设计模式
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
类的适配器模式
[if !supportLists]1. [endif]public class Source {
[if !supportLists]2. [endif] public void method1() {
[if !supportLists]3. [endif] System.out.println("this is original method!");
[if !supportLists]4. [endif] }
[if !supportLists]5. [endif]}
[if !supportLists]6. [endif]-------------------------------------------------------------
[if !supportLists]7. [endif]public interface Targetable {
[if !supportLists]8. [endif] /*与原类中的方法相同 */
[if !supportLists]9. [endif] public void method1();
[if !supportLists]10. [endif] /*新类的方法 */
[if !supportLists]11. [endif] public void method2();
[if !supportLists]12. [endif]}
[if !supportLists]13. [endif]public class Adapter extends Source implements Targetable {
[if !supportLists]14. [endif] @Override
[if !supportLists]15. [endif] public void method2() {
[if !supportLists]16. [endif] System.out.println("this is the targetable method!");
[if !supportLists]17. [endif] }
[if !supportLists]18. [endif]}
[if !supportLists]19. [endif]public class AdapterTest {
[if !supportLists]20. [endif] public static void main(String[] args) {
[if !supportLists]21. [endif] Targetable target = new Adapter();
[if !supportLists]22. [endif] target.method1();
[if !supportLists]23. [endif] target.method2();
[if !supportLists]24. [endif] }
[if !supportLists]25. [endif]}
对象的适配器模式
基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。
[if !supportLists]1. [endif]public class Wrapper implements Targetable {
[if !supportLists]2. [endif] private Source source;
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] public Wrapper(Source source) {
[if !supportLists]5. [endif] super();
[if !supportLists]6. [endif] this.source = source;
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] @Override
[if !supportLists]10. [endif] public void method2() {
[if !supportLists]11. [endif] System.out.println("this is the targetable method!");
[if !supportLists]12. [endif] }
[if !supportLists]13. [endif]
[if !supportLists]14. [endif] @Override
[if !supportLists]15. [endif] public void method1() {
[if !supportLists]16. [endif] source.method1();
[if !supportLists]17. [endif] }
[if !supportLists]18. [endif]}
[if !supportLists]19. [endif]--------------------------------------------------------------
[if !supportLists]20. [endif]public class AdapterTest {
[if !supportLists]21. [endif]
[if !supportLists]22. [endif] public static void main(String[] args) {
[if !supportLists]23. [endif] Source source = new Source();
[if !supportLists]24. [endif] Targetable target = new Wrapper(source);
[if !supportLists]25. [endif] target.method1();
[if !supportLists]26. [endif] target.method2();
[if !supportLists]27. [endif] }
[if !supportLists]28. [endif] }
接口的适配器模式
接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。
[if !supportLists]6. [endif]装饰模式(Decorator)
顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
[if !supportLists]1. [endif]public interface Sourceable {
[if !supportLists]2. [endif] public void method();
[if !supportLists]3. [endif]}
[if !supportLists]4. [endif]----------------------------------------------------
[if !supportLists]5. [endif]public class Source implements Sourceable {
[if !supportLists]6. [endif] @Override
[if !supportLists]7. [endif] public void method() {
[if !supportLists]8. [endif] System.out.println("the original method!");
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif]}
[if !supportLists]11. [endif]----------------------------------------------------
[if !supportLists]12. [endif]public class Decorator implements Sourceable {
[if !supportLists]13. [endif] private Sourceable source;
[if !supportLists]14. [endif] public Decorator(Sourceable source) {
[if !supportLists]15. [endif] super();
[if !supportLists]16. [endif] this.source = source;
[if !supportLists]17. [endif] }
[if !supportLists]18. [endif]
[if !supportLists]19. [endif] @Override
[if !supportLists]20. [endif] public void method() {
[if !supportLists]21. [endif] System.out.println("before decorator!");
[if !supportLists]22. [endif] source.method();
[if !supportLists]23. [endif] System.out.println("after decorator!");
[if !supportLists]24. [endif] }
[if !supportLists]25. [endif]}
[if !supportLists]26. [endif]----------------------------------------------------
[if !supportLists]27. [endif]public class DecoratorTest {
[if !supportLists]28. [endif] public static void main(String[] args) {
[if !supportLists]29. [endif] Sourceable source = new Source();
[if !supportLists]30. [endif] Sourceable obj = new Decorator(source);
[if !supportLists]31. [endif] obj.method();
[if !supportLists]32. [endif] }
[if !supportLists]33. [endif]}
[if !supportLists]7. [endif]策略模式(strategy)
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
[if !supportLists]1. [endif]public interface ICalculator {
[if !supportLists]2. [endif] public int calculate(String exp);
[if !supportLists]3. [endif]}
[if !supportLists]4. [endif]---------------------------------------------------------
[if !supportLists]5. [endif]public class Minus extends AbstractCalculator implements ICalculator {
[if !supportLists]6. [endif]
[if !supportLists]7. [endif] @Override
[if !supportLists]8. [endif] public int calculate(String exp) {
[if !supportLists]9. [endif] int arrayInt[] = split(exp, "-");
[if !supportLists]10. [endif] return arrayInt[0] - arrayInt[1];
[if !supportLists]11. [endif] }
[if !supportLists]12. [endif]}
[if !supportLists]13. [endif]---------------------------------------------------------
[if !supportLists]14. [endif]public class Plus extends AbstractCalculator implements ICalculator {
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] @Override
[if !supportLists]17. [endif] public int calculate(String exp) {
[if !supportLists]18. [endif] int arrayInt[] = split(exp, "\\+");
[if !supportLists]19. [endif] return arrayInt[0] + arrayInt[1];
[if !supportLists]20. [endif] }
[if !supportLists]21. [endif]}
[if !supportLists]22. [endif]--------------------------------------------------------
[if !supportLists]23. [endif]public class AbstractCalculator {
[if !supportLists]24. [endif] public int[] split(String exp, String opt) {
[if !supportLists]25. [endif] String array[] = exp.split(opt);
[if !supportLists]26. [endif] int arrayInt[] = new int[2];
[if !supportLists]27. [endif] arrayInt[0] = Integer.parseInt(array[0]);
[if !supportLists]28. [endif] arrayInt[1] = Integer.parseInt(array[1]);
[if !supportLists]29. [endif] return arrayInt;
[if !supportLists]30. [endif] }
[if !supportLists]31. [endif]}
[if !supportLists]1. [endif]public class StrategyTest {
[if !supportLists]2. [endif] public static void main(String[] args) {
[if !supportLists]3. [endif] String exp = "2+8";
[if !supportLists]4. [endif] ICalculator cal = new Plus();
[if !supportLists]5. [endif] int result = cal.calculate(exp);
[if !supportLists]6. [endif] System.out.println(result);
[if !supportLists]7. [endif] }
[if !supportLists]8. [endif]}
[if !supportLists]8. [endif]观察者模式(Observer)
观察者模式很好理解,类似于邮件订阅和RSS订阅,当我们浏览一些博客或wiki时,经常会看到RSS图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。
[if !supportLists]1. [endif]public interface Observer {
[if !supportLists]2. [endif] public void update();
[if !supportLists]3. [endif]}
[if !supportLists]4. [endif]
[if !supportLists]5. [endif]public class Observer1 implements Observer {
[if !supportLists]6. [endif] @Override
[if !supportLists]7. [endif] public void update() {
[if !supportLists]8. [endif] System.out.println("observer1 has received!");
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif]}
[if !supportLists]11. [endif]
[if !supportLists]12. [endif]public class Observer2 implements Observer {
[if !supportLists]13. [endif] @Override
[if !supportLists]14. [endif] public void update() {
[if !supportLists]15. [endif] System.out.println("observer2 has received!");
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif]
[if !supportLists]19. [endif]public interface Subject {
[if !supportLists]20. [endif] /*增加观察者*/
[if !supportLists]21. [endif] public void add(Observer observer);
[if !supportLists]22. [endif]
[if !supportLists]23. [endif]/*删除观察者*/
[if !supportLists]24. [endif] public void del(Observer observer);
25./*通知所有的观察者*/
[if !supportLists]1. [endif] public void notifyObservers();
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]/*自身的操作*/
[if !supportLists]4. [endif] public void operation();
[if !supportLists]5. [endif]}
[if !supportLists]6. [endif]
[if !supportLists]7. [endif]public abstract class AbstractSubject implements Subject {
[if !supportLists]8. [endif]
[if !supportLists]9. [endif] private Vector vector = new Vector();
[if !supportLists]10. [endif]
[if !supportLists]11. [endif] @Override
[if !supportLists]12. [endif] public void add(Observer observer) {
[if !supportLists]13. [endif] vector.add(observer);
[if !supportLists]14. [endif] }
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] @Override
[if !supportLists]17. [endif] public void del(Observer observer) {
[if !supportLists]18. [endif] vector.remove(observer);
[if !supportLists]19. [endif] }
[if !supportLists]20. [endif]
[if !supportLists]21. [endif] @Override
[if !supportLists]22. [endif] public void notifyObservers() {
[if !supportLists]23. [endif] Enumeration enumo = vector.elements();
[if !supportLists]24. [endif] while (enumo.hasMoreElements()) {
[if !supportLists]25. [endif] enumo.nextElement().update();
[if !supportLists]26. [endif] }
[if !supportLists]27. [endif] }
[if !supportLists]28. [endif]}
[if !supportLists]29. [endif]
[if !supportLists]30. [endif]public class MySubject extends AbstractSubject {
[if !supportLists]31. [endif]
[if !supportLists]32. [endif] @Override
[if !supportLists]33. [endif] public void operation() {
[if !supportLists]34. [endif] System.out.println("update self!");
[if !supportLists]35. [endif] notifyObservers();
[if !supportLists]36. [endif] }
[if !supportLists]37. [endif]}
[if !supportLists]38. [endif]
[if !supportLists]39. [endif]public class ObserverTest {
[if !supportLists]40. [endif] public static void main(String[] args) {
[if !supportLists]41. [endif] Subject sub = new MySubject();
[if !supportLists]42. [endif] sub.add(new Observer1());
[if !supportLists]43. [endif] sub.add(new Observer2());
[if !supportLists]44. [endif] sub.operation();
[if !supportLists]45. [endif] }
[if !supportLists]46. [endif]}
[if !supportLists]9. [endif]JVM垃圾回收机制和常见算法
理论上来讲Sun公司只定义了垃圾回收机制规则而不局限于其实现算法,因此不同厂商生产的虚拟机采用的算法也不尽相同。
GC(Garbage Collector)在回收对象前首先必须发现那些无用的对象,如何去发现定位这些无用的对象?常用的搜索算法如下:
[if !supportLists]1)[endif]引用计数器算法(废弃)
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。
[if !supportLists]2)[endif]根搜索算法(使用)
根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。
GC Roots对象包括:
a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b) 方法区域中的类静态属性引用的对象。
c) 方法区域中常量引用的对象。
d) 本地方法栈中JNI(Native方法)的引用的对象。
通过上面的算法搜索到无用对象之后,就是回收过程,回收算法如下:
[if !supportLists]1)[endif]标记—清除算法(Mark-Sweep)(DVM使用的算法)
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
[if !supportLists]2)[endif]复制算法(Copying)
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1)。
[if !supportLists]3)[endif]标记—整理算法(Mark-Compact)
标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。
[if !supportLists]4)[endif]分代收集(Generational Collection)
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。
[if !supportLists]10. [endif]谈谈JVM的内存结构和内存分配
[if !supportLists]a)[endif] Java内存模型
Java虚拟机将其管辖的内存大致分三个逻辑部分:方法区(Method Area)、Java栈和Java堆。
1、方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。
常数池,源代码中的命名常量、String常量和static变量保存在方法区。
2、Java Stack是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。
最典型的Stack应用是方法的调用,Java虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法则对应的 方法帧被弹出(pop)。栈中存储的数据也是运行时确定的。
3、Java堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。
堆中存储的数据常常是大小、数量和生命期在编译时无法确定的。Java对象的内存总是在heap中分配。
我们每天都在写代码,每天都在使用JVM的内存。
[if !supportLists]b)[endif] java内存分配
1、基础数据类型直接在栈空间分配;
2、方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
3、引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
4、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
5、局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收;
6、方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
7、字符串常量在 DATA 区域分配 ,this 在堆空间分配;
8、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!
[if !supportLists]11. [endif]Java中引用类型都有哪些?(重要)
Java中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用(StrongReference)
这个就不多说,我们写代码天天在用的就是强引用。如果一个对象被被人拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
Java的对象是位于heap中的,heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下代码:
[if !supportLists]1. [endif] String abc=new String("abc"); //1
[if !supportLists]2. [endif] SoftReference softRef=new SoftReference(abc); //2
[if !supportLists]3. [endif] WeakReference weakRef = new WeakReference(abc); //3
[if !supportLists]4. [endif] abc=null; //4
[if !supportLists]5. [endif] softRef.clear();//5
第一行在heap堆中创建内容为“abc”的对象,并建立abc到该对象的强引用,该对象是强可及的。
第二行和第三行分别建立对heap中对象的软引用和弱引用,此时heap中的abc对象已经有3个引用,显然此时abc对象仍是强可及的。
第四行之后heap中对象不再是强可及的,变成软可及的。
第五行执行之后变成弱可及的。
软引用(SoftReference)
如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。当gc决定要收集软引用时执行以下过程,以上面的softRef为例:
1 首先将softRef的referent(abc)设置为null,不再引用heap中的new String("abc")对象。
2 将heap中的new String("abc")对象设置为可结束的(finalizable)。
3 当heap中的new String("abc")对象的finalize()方法被运行而且该对象占用的内存被释放, softRef被添加到它的ReferenceQueue(如果有的话)中。
注意:对ReferenceQueue软引用和弱引用可以有可无,但是虚引用必须有。
被Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。一直要到 JVM 内存不足且没有Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。如此一来 SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。
弱引用(WeakReference)
如果一个对象只具有弱引用,那该类就是可有可无的对象,因为只要该对象被gc扫描到了随时都会把它干掉。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
建立虚引用之后通过get方法返回结果始终为null,通过源代码你会发现,虚引用通向会把引用的对象写进referent,只是get方法返回结果为null。先看一下和gc交互的过程再说一下他的作用。
1 不把referent设置为null, 直接把heap中的new String("abc")对象设置为可结束的(finalizable)。
2 与软引用和弱引用不同, 先把PhantomRefrence对象添加到它的ReferenceQueue中.然后在释放虚可及的对象。
[if !supportLists]12. [endif]heap和stack有什么区别(2017-2-23)
从以下几个方面阐述堆(heap)和栈(stack)的区别。
1. 申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:需要程序员自己申请,并指明大小,在c中malloc函数,对于Java需要手动new Object()的形式开辟
2. 申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3. 申请大小的限制
stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4. 申请效率的比较:
stack:由系统自动分配,速度较快。但程序员是无法控制的。
heap:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
5. heap和stack中的存储内容
stack: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
6. 数据结构层面的区别
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
7. 拓展知识(Java中堆栈的应用)
1). 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
2). 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾回收器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3). Java中的数据类型有两种。
一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量(自动变量:只在定义它们的时候才创建,在定义它们的函数返回时系统回收变量所占存储空间。对这些变量存储空间的分配和回收是由系统自动完成的。)。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
4).每个JVM的线程都有自己的私有的栈空间,随线程创建而创建,java的stack存放的是frames,java的stack和c的不同,只是存放本地变量,返回值和调用方法,不允许直接push和pop frames ,因为frames 可能是有heap分配的,所以java的stack分配的内存不需要是连续的。java的heap是所有线程共享的,堆存放所有 runtime data ,里面是所有的对象实例和数组,heap是JVM启动时创建。
5). String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。
5.1). 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
(1)先定义一个名为str的对String类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。
我们再来更进一步,将以上代码改成:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2); //bcd, abc
System.out.println(str1==str2); //false
这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。
再修改原来代码:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true
str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。
我们再接着看以下的代码。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
6). 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。
7). 结论与建议:
(1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。
(3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。
(4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。
如果java不能成功分配heap的空间,将抛出OutOfMemoryError。
[if !supportLists]13. [endif]解释内存中的栈 (stack) 、堆 (heap) 和方法区 (method area) 的用法(2017-11-12-wl)
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为 From Survivor 和 To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、"hello"和常量都是放在常量池中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。
String str = new String("hello");
上面的语句中变量str 放在栈上,用 new 创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
[if !supportLists]四、[endif]Java的类加载器(2015-12-2)
[if !supportLists]1. [endif]Java的类加载器的种类都有哪些?
1、根类加载器(Bootstrap) --C++写的 ,看不到源码
2、扩展类加载器(Extension) --加载位置 :jre\lib\ext中
3、系统(应用)类加载器(System\App) --加载位置 :classpath中
4、自定义加载器(必须继承ClassLoader)
[if !supportLists]2. [endif]类什么时候被初始化?
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
只有这6中情况才会导致类的类的初始化。
类的初始化步骤:
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
[if !supportLists]3. [endif]Java类加载体系之ClassLoader双亲委托机制 (2017-2-24)
java是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全沙箱分别是:
[if !supportLists]1) [endif]类加载体系
[if !supportLists]2) [endif].class文件检验器
[if !supportLists]3) [endif]内置于Java虚拟机(及语言)的安全特性
[if !supportLists]4) [endif]安全管理器及Java API
主要讲解类的加载体系:
java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .class文件,先上图:
BootStrapClassLoader:启动类加载器,该ClassLoader是jvm在启动时创建的,用于加载 $JAVA_HOME$/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
ExtClassLoader:扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader会加载 $JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)。
AppClassLoader:应用程序类加载器,该ClassLoader同样是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader会加载java环境变量CLASSPATH所指定的路径下的类库,而CLASSPATH所指定的路径可以通过System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的class目录)。
CustomClassLoader:自定义类加载器,该ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了。
前面谈到了ClassLoader的几类加载器,而ClassLoader使用双亲委派机制来加载class文件的。ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器CustomClassLoader):
1)当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2)当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3)如果BootStrapClassLoader加载失败(例如在$JAVA_HOME$/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4)若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
下面贴下ClassLoader的loadClass(String name, boolean resolve)的源码:
[if !supportLists]1.[endif]protected synchronized Class loadClass(String name, boolean resolve)
[if !supportLists]2.[endif] throws ClassNotFoundException {
[if !supportLists]3.[endif] // 首先找缓存是否有class
[if !supportLists]4.[endif] Class c = findLoadedClass(name);
[if !supportLists]5.[endif] if (c == null) {
[if !supportLists]6.[endif]//没有判断有没有父类
[if !supportLists]7.[endif] try {
[if !supportLists]8.[endif] if (parent != null) {
[if !supportLists]9.[endif]//有的话,用父类递归获取class
[if !supportLists]10.[endif] c = parent.loadClass(name, false);
[if !supportLists]11.[endif] } else {
[if !supportLists]12.[endif]//没有父类。通过这个方法来加载
[if !supportLists]13.[endif] c = findBootstrapClassOrNull(name);
[if !supportLists]14.[endif] }
[if !supportLists]15.[endif] } catch (ClassNotFoundException e) {
[if !supportLists]16.[endif] // ClassNotFoundException thrown if class not found
[if !supportLists]17.[endif] // from the non-null parent class loader
[if !supportLists]18.[endif] }
[if !supportLists]19.[endif] if (c == null) {
[if !supportLists]20.[endif] // 如果还是没有找到,调用findClass(name)去找这个类
[if !supportLists]21.[endif] c = findClass(name);
[if !supportLists]22.[endif] }
[if !supportLists]23.[endif] }
[if !supportLists]24.[endif] if (resolve) {
[if !supportLists]25.[endif] resolveClass(c);
[if !supportLists]26.[endif] }
[if !supportLists]27.[endif] return c;
[if !supportLists]28.[endif] }
代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有parent,有的话就用parent来递归的loadClass,然而ExtClassLoader并没有设置parent,则会通过findBootstrapClassOrNull来加载class,而findBootstrapClassOrNull则会通过JNI方法”private native Class findBootstrapClass(String name)“来使用BootStrapClassLoader来加载class。
然后如果parent未找到class,则会调用findClass来加载class,findClass是一个protected的空方法,可以覆盖它以便自定义class加载过程。
另外,虽然ClassLoader加载类是使用loadClass方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写loadClass,这样就不会覆盖了类加载默认的双亲委派机制。
双亲委派托机制为什么安全
举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节码。而一些来源的class文件是不可靠的,比如我可以自定义一个java.lang.Integer类来覆盖jdk中默认的Integer类,例如下面这样:
[if !supportLists]1. [endif]package java.lang;
[if !supportLists]2. [endif]public class Integer {
[if !supportLists]3. [endif] public Integer(int value) {
[if !supportLists]4. [endif] System.exit(0);
[if !supportLists]5. [endif] }
[if !supportLists]6. [endif]}
初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer类永远不会被调用,以为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个,可以看下下面这测试个用例:
[if !supportLists]1.[endif]public static void main(String... args) {
[if !supportLists]2.[endif] Integer i = new Integer(1);
[if !supportLists]3.[endif] System.err.println(i);
[if !supportLists]4.[endif] }
执行时JVM并未在new Integer(1)时退出,说明未使用自定义的Integer,于是就保证了安全性。
[if !supportLists]4. [endif]描述一下JVM加载class (2017-11-15-wl)
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:
如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。
从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java 程序提供对 Bootstrap 的引用。
下面是关于几个类加载器的说明:
• Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
• Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;
• System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量classpath
或者系统属性java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
[if !supportLists]5. [endif]获得一个类对象有哪些方式?(2017-11-23-wzz)
类型.class,例如:String.class
对象.getClass(),例如:”hello”.getClass()
Class.forName(),例如:Class.forName(“java.lang.String”)
[if !supportLists]五、[endif]JVM基础知识(2017-11-16-wl)
[if !supportLists]1. [endif]既然有GC机制,为什么还会有内存泄露的情况(2017-11-16-wl)
理论上Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。
例如hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
下面例子中的代码也会导致内存泄露。
[if !supportLists]1. [endif]import java.util.Arrays;
[if !supportLists]2. [endif]import java.util.EmptyStackException;
[if !supportLists]3. [endif]public class MyStack {
[if !supportLists]4. [endif] private T[] elements;
[if !supportLists]5. [endif] private int size = 0;
[if !supportLists]6. [endif] private static final int INIT_CAPACITY = 16;
[if !supportLists]7. [endif] public MyStack() {
[if !supportLists]8. [endif] elements = (T[]) new Object[INIT_CAPACITY];
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif] public void push(T elem) {
[if !supportLists]11. [endif] ensureCapacity();
[if !supportLists]12. [endif] elements[size++] = elem;
[if !supportLists]13. [endif] }
[if !supportLists]14. [endif] public T pop() {
[if !supportLists]15. [endif] if(size == 0)throw new EmptyStackException();
[if !supportLists]16. [endif] return elements[--size];
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif] private void ensureCapacity() {
[if !supportLists]19. [endif] if(elements.length == size) {
[if !supportLists]20. [endif] elements = Arrays.copyOf(elements, 2 * size + 1);
[if !supportLists]21. [endif] }
[if !supportLists]22. [endif]}
[if !supportLists]23. [endif]}
上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发 Disk Paging (物理内存与硬盘的虚拟内存交换数据),甚至造成 OutOfMemoryError。
[if !supportLists]六、[endif]GC基础知识(2017-11-16-wl)
[if !supportLists]1. [endif]Java中为什么会有GC机制呢?(2017-11-16-wl)
Java中为什么会有GC机制呢?
[if !supportLists]· [endif]安全性考虑;-- for security.
[if !supportLists]· [endif]减少内存泄露;-- erase memory leak in some degree.
[if !supportLists]· [endif]减少程序员工作量。-- Programmers don't worry about memory releasing.
[if !supportLists]2. [endif]对于Java的GC哪些内存需要回收(2017-11-16-wl)
内存运行时JVM会有一个运行时数据区来管理内存。它主要包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap).
而其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构确定是哪个时就已知了,因此这3个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。
但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。
总而言之,GC主要进行回收的内存是JVM中的方法区和堆;
[if !supportLists]3. [endif]Java的GC什么时候回收垃圾(2017-11-16-wl)
在面试中经常会碰到这样一个问题(事实上笔者也碰到过):如何判断一个对象已经死去?
很容易想到的一个答案是:对一个对象添加引用计数器。每当有地方引用它时,计数器值加1;当引用失效时,计数器值减1.而当计数器的值为0时这个对象就不会再被使用,判断为已死。是不是简单又直观。然而,很遗憾。这种做法是错误的!为什么是错的呢?事实上,用引用计数法确实在大部分情况下是一个不错的解决方案,而在实际的应用中也有不少案例,但它却无法解决对象之间的循环引用问题。比如对象A中有一个字段指向了对象B,而对象B中也有一个字段指向了对象A,而事实上他们俩都不再使用,但计数器的值永远都不可能为0,也就不会被回收,然后就发生了内存泄露。
所以,正确的做法应该是怎样呢? 在Java,C#等语言中,比较主流的判定一个对象已死的方法是:可达性分析(Reachability Analysis).所有生成的对象都是一个称为"GC Roots"的根的子树。从GC Roots开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链可以到达时,就称这个对象是不可达的(不可引用的),也就是可以被GC回收了。
无论是引用计数器还是可达性分析,判定对象是否存活都与引用有关!那么,如何定义对象的引用呢?
我们希望给出这样一类描述:当内存空间还够时,能够保存在内存中;如果进行了垃圾回收之后内存空间仍旧非常紧张,则可以抛弃这些对象。所以根据不同的需求,给出如下四种引用,根据引用类型的不同,GC回收时也会有不同的操作:
1)强引用(Strong Reference):Object obj = new Object();只要强引用还存在,GC永远不会回收掉被引用的对象。
2)软引用(Soft Reference):描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象列入回收范围进行二次回收(即系统将会发生内存溢出了,才会对他们进行回收。)
弱引用(Weak Reference):程度比软引用还要弱一些。这些对象只能生存到下次GC之前。当GC工作时,无论内存是否足够都会将其回收(即只要进行GC,就会对他们进行回收。)
虚引用(Phantom Reference):一个对象是否存在虚引用,完全不会对其生存时间构成影响。
关于方法区中需要回收的是一些废弃的常量和无用的类。
1.废弃的常量的回收。这里看引用计数就可以了。没有对象引用该常量就可以放心的回收了。
2.无用的类的回收。什么是无用的类呢?
A.该类所有的实例都已经被回收。也就是Java堆中不存在该类的任何实例;
B.加载该类的ClassLoader已经被回收;
C.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
总而言之:
对于堆中的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。而根据我们实际对引用的不同需求,又分成了4中引用,每种引用的回收机制也是不同的。
对于方法区中的常量和类,当一个常量没有任何对象引用它,它就可以被回收了。而对于类,如果可以判定它为无用类,就可以被回收了。
[if !supportLists]七、[endif]Java8的新特性以及使用(2017-12-02-wl)
[if !supportLists]1. [endif]通过10个示例来初步认识Java8中的lambda表达式(2017-12-02-wl)
我个人对Java 8发布非常激动,尤其是lambda表达式和流API。越来越多的了解它们,我能写出更干净的代码。虽然一开始并不是这样。第一次看到用lambda表达式写出来的Java代码时,我对这种神秘的语法感到非常失望,认为它们把Java搞得不可读,但我错了。花了一天时间做了一些lambda表达式和流API示例的练习后,我开心的看到了更清晰的Java代码。这有点像学习泛型,第一次见的时候我很讨厌它。我甚至继续使用老版Java 1.4来处理集合,直到有一天,朋友跟我介绍了使用泛型的好处(才意识到它的好处)。所以基本立场就是,不要畏惧lambda表达式以及方法引用的神秘语法,做几次练习,从集合类中提取、过滤数据之后,你就会喜欢上它。下面让我们开启学习Java 8 lambda表达式的学习之旅吧~
本小节中先不说lambda表达的含义和繁琐的概念。我们先从最简单的示例来介绍java8中的lambda表达式
例1、用lambda表达式实现Runnable
// Java 8之前:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();
//Java 8方式:
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();
输出:
too much code, for too little to do
Lambda expression rocks !!
这个例子向我们展示了Java 8 lambda表达式的语法。你可以使用lambda写出如下代码:
(params) -> expression
(params) -> statement
(params) -> { statements }
例如,如果你的方法不对参数进行修改、重写,只是在控制台打印点东西的话,那么可以这样写:
() -> System.out.println("Hello Lambda Expressions");
如果你的方法接收两个参数,那么可以写成如下这样:
(int even, int odd) -> even + odd
顺便提一句,通常都会把lambda表达式内部变量的名字起得短一些。这样能使代码更简短,放在同一行。所以,在上述代码中,变量名选用a、b或者x、y会比even、odd要好。
例2、使用Java 8 lambda表达式进行事件处理
如果你用过Swing API编程,你就会记得怎样写事件监听代码。这又是一个旧版本简单匿名类的经典用例,但现在可以不这样了。你可以用lambda表达式写出更好的事件监听代码,如下所示:
// Java 8之前:
JButton show = new JButton("Show");
show.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Event handling without lambda expression is boring");
}
});
// Java 8方式:
show.addActionListener((e) -> {
System.out.println("Light, Camera, Action !! Lambda expressions Rocks");
});
Java开发者经常使用匿名类的另一个地方是为 Collections.sort() 定制 Comparator。在Java 8中,你可以用更可读的lambda表达式换掉丑陋的匿名类。我把这个留做练习,应该不难,可以按照我在使用lambda表达式实现 Runnable 和 ActionListener 的过程中的套路来做。
例3、使用Java 8 lambda表达式进行事件处理 使用lambda表达式对列表进行迭代
如果你使过几年Java,你就知道针对集合类,最常见的操作就是进行迭代,并将业务逻辑应用于各个元素,例如处理订单、交易和事件的列表。由于Java是命令式语言,Java 8之前的所有循环代码都是顺序的,即可以对其元素进行并行化处理。如果你想做并行过滤,就需要自己写代码,这并不是那么容易。通过引入lambda表达式和默认方法,将做什么和怎么做的问题分开了,这意味着Java集合现在知道怎样做迭代,并可以在API层面对集合元素进行并行处理。下面的例子里,我将介绍如何在使用lambda或不使用lambda表达式的情况下迭代列表。你可以看到列表现在有了一个 forEach() 方法,它可以迭代所有对象,并将你的lambda代码应用在其中。
// Java 8之前:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
System.out.println(feature);
}
// Java 8之后:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
features.forEach(n -> System.out.println(n));
//使用Java 8的方法引用更方便,方法引用由::双冒号操作符标示,
//看起来像C++的作用域解析运算符
features.forEach(System.out::println);
输出:
Lambdas
Default Method
Stream API
Date and Time API
列表循环的最后一个例子展示了如何在Java 8中使用方法引用(method reference)。你可以看到C++里面的双冒号、范围解析操作符现在在Java 8中用来表示方法引用。
例4、使用lambda表达式和函数式接口Predicate
除了在语言层面支持函数式编程风格,Java 8也添加了一个包,叫做 java.util.function。它包含了很多类,用来支持Java的函数式编程。其中一个便是Predicate,使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。下面是Java 8 Predicate 的例子,展示了过滤集合数据的多种常用方法。Predicate接口非常适用于做过滤。
public static void main(args[]){
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
System.out.println("Languages which starts with J :");
filter(languages, (str)->str.startsWith("J"));
System.out.println("Languages which ends with a ");
filter(languages, (str)->str.endsWith("a"));
System.out.println("Print all languages :");
filter(languages, (str)->true);
System.out.println("Print no language : ");
filter(languages, (str)->false);
System.out.println("Print language whose length greater than 4:");
filter(languages, (str)->str.length() > 4);
}
public static void filter(List names, Predicate condition) {
for(String name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}
// filter更好的办法--filter方法改进
public static void filter(List names, Predicate condition) {
names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {
System.out.println(name + " ");
});
}
可以看到,Stream API的过滤方法也接受一个Predicate,这意味着可以将我们定制的 filter() 方法替换成写在里面的内联代码,这就是lambda表达式的魔力。另外,Predicate接口也允许进行多重条件的测试,下个例子将要讲到。
例5、如何在lambda表达式中加入Predicate
上个例子说到,java.util.function.Predicate 允许将两个或更多的 Predicate 合成一个。它提供类似于逻辑操作符AND和OR的方法,名字叫做and()、or()和xor(),用于将传入 filter() 方法的条件合并起来。例如,要得到所有以J开始,长度为四个字母的语言,可以定义两个独立的 Predicate 示例分别表示每一个条件,然后用 Predicate.and() 方法将它们合并起来,如下所示:
//甚至可以用and()、or()和xor()逻辑函数来合并Predicate,
//例如要找到所有以J开始,长度为四个字母的名字,你可以合并两个Predicate并传入
Predicate startsWithJ = (n) -> n.startsWith("J");
Predicate fourLetterLong = (n) -> n.length() == 4;
names.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));
类似地,也可以使用or() 和 xor() 方法。本例着重介绍了如下要点:可按需要将 Predicate 作为单独条件然后将其合并起来使用。简而言之,你可以以传统Java命令方式使用 Predicate 接口,也可以充分利用lambda表达式达到事半功倍的效果。
例6、Java 8中使用lambda表达式的Map和Reduce示例
本例介绍最广为人知的函数式编程概念map。它允许你将对象进行转换。例如在本例中,我们将 costBeforeTax 列表的每个元素转换成为税后的值。我们将 x -> x*x lambda表达式传到 map() 方法,后者将其应用到流中的每一个元素。然后用 forEach() 将列表元素打印出来。使用流API的收集器类,可以得到所有含税的开销。有 toList() 这样的方法将 map 或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。你甚至可以用流API的 reduce() 方法将所有数字合成一个,下一个例子将会讲到。
//不使用lambda表达式为每个订单加上12%的税
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for (Integer cost : costBeforeTax) {
double price = cost + .12*cost;
System.out.println(price);
}
//使用lambda表达式
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
costBeforeTax.stream().map((cost) -> cost + .12*cost).forEach(System.out::println);
在上面例子中,可以看到map将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduceh() 函数可以接受lambda表达式,并对所有值进行合并。IntStream这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个Java 8的Map Reduce示例里,我们首先对所有价格应用 12% 的VAT,然后用 reduce() 方法计算总和。
//为每个订单加上12%的税
//老方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = 0;
for (Integer cost : costBeforeTax) {
double price = cost + .12*cost;
total = total + price;
}
System.out.println("Total : " + total);
//新方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost) -> sum + cost).get();
System.out.println("Total : " + bill);
例7、通过过滤创建一个String列表
过滤是Java开发者在大规模集合上的一个常用操作,而现在使用lambda表达式和流API过滤大规模数据集合是惊人的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个lambda表达式作为过滤逻辑。下面的例子是用lambda表达式过滤Java集合,将帮助理解。
//创建一个字符串列表,每个字符串长度大于2
List costBeforeTax = Arrays.asList("abc","bcd","defg","jk");
List filtered = strList.stream().filter(x -> x.length()> 2).collect(Collectors.toList());
System.out.printf("Original List : %s, filtered list : %s %n", strList, filtered);
输出:
Original List : [abc, , bcd, , defg, jk], filtered list : [abc, bcd, defg]
另外,关于filter() 方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用filter()方法则是获得一个新的列表,且其每个元素符合过滤原则。
例8、对列表的每个元素应用函数
我们通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用map() 方法,可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了,如下所示。
//将字符串换成大写并用逗号链接起来
List G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada");
String G7Countries = G7.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(", "));
System.out.println(G7Countries);
输出:
USA, JAPAN, FRANCE, GERMANY, ITALY, U.K., CANADA
例9、复制不同的值,创建一个子列表
本例展示了如何利用流的distinct() 方法来对集合进行去重
//用所有不同的数字创建一个正方形列表
List numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
System.out.printf("Original List : %s, Square Without duplicates : %s %n", numbers, distinct);
输出:
Original List : [9, 10, 3, 4, 7, 3, 4], Square Without duplicates : [81, 100, 9, 16, 49]
例10、计算集合元素的最大值、最小值、总和以及平均值
IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。
//获取数字的个数、最小值、最大值、总和以及平均值
List primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : " + stats.getMax());
System.out.println("Lowest prime number in List : " + stats.getMin());
System.out.println("Sum of all prime numbers : " + stats.getSum());
System.out.println("Average of all prime numbers : " + stats.getAverage());
输出:
Highest prime number in List : 29
Lowest prime number in List : 2
Sum of all prime numbers : 129
Average of all prime numbers : 12.9
Java 8的10个lambda表达式,这对于新手来说是个合适的任务量,你可能需要亲自运行示例程序以便掌握。试着修改要求创建自己的例子,达到快速学习的目的。
补充:从例子中我们可以可以看到,以前写的匿名内部类都用了lambda表达式代替了。那么,我们简单谈谈“lambda表达式&匿名内部类”
两者不用:
[if !supportLists]1. [endif]关键字this
[if !supportLists](1) [endif]匿名内部类中的this代表匿名类
[if !supportLists](2) [endif]Lambda表达式中的this代表lambda表达式的类
[if !supportLists]2. [endif]编译方式不同
[if !supportLists](1) [endif]匿名内部类中会编译成一个.class文件,文件命名方式为:主类+$+(1,2,3.......)
[if !supportLists](2) [endif]Java编译器将lambda表达式编译成类的私有方法。使用了Java 7的 invokedynamic 字节码指令来动态绑定这个方法
[if !supportLists]2. [endif]Java8中的lambda表达式要点(2017-12-02-wl)
通过面10个小示例中学习,我们下面说下lambda表达式的6个要点
要点1:lambda表达式的使用位置
预定义使用了@Functional 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式。
要点2:lambda表达式和方法引用
lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数。本例中的lambda表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); //使用方法引用
然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地lambda表达式,如下所示:
list.forEach((String s) -> System.out.println("*" + s + "*"));
事实上,可以省略这里的lambda参数的类型声明,编译器可以从列表的类属性推测出来。
要点3:lambda表达式内部引用资源
lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。
要点4:lambda表达式也成闭包
Lambda表达式在Java中又称为闭包或匿名函数,所以如果有同事把它叫闭包的时候,不用惊讶。
要点5:lambda表达式的编译方式
Lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用JDK中的 javap 工具来反编译class文件。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样:
private static java.lang.Object lambda$0(java.lang.String);
要点6:lambda表达式的限制
lambda表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量。
List primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { factor++; });
Error:
Compile time error : "local variables referenced from a lambda expression must be final or effectively final"
另外,只是访问它而不作修改是可以的,如下所示:
List primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });
输出:
4
6
10
14
因此,它看起来更像不可变闭包,类似于Python。
[if !supportLists]3. [endif]Java8中的Optional类的解析(2017-12-02-wl)
身为一名Java程序员,大家可能都有这样的经历:调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法。我们首先要判断这个返回值是否为null,只有在非空的前提下才能将其作为其他方法的参数。这正是一些类似Guava的外部API试图解决的问题。一些JVM编程语言比如Scala、Ceylon等已经将对在核心API中解决了这个问题。在我的前一篇文章中,介绍了Scala是如何解决了这个问题。
新版本的Java,比如Java 8引入了一个新的Optional类。Optional类的Javadoc描述如下:
这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
下面会逐个探讨Optional类包含的方法,并通过一两个示例展示如何使用。
方法1:Optional.of()
作用:为非null的值创建一个Optional。
说明:of方法通过工厂方法创建Optional类。需要注意的是,创建对象时传入的参数不能为null。如果传入参数为null,则抛出NullPointerException 。
//调用工厂方法创建Optional实例
Optional name = Optional.of("Sanaulla");
//传入参数为null,抛出NullPointerException.
Optional someNull = Optional.of(null);
方法2:Optional.ofNullable()
作用:为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。
说明:ofNullable与of方法相似,唯一的区别是可以接受参数为null的情况。
//下面创建了一个不包含任何值的Optional实例
//例如,值为'null'
Optional empty = Optional.ofNullable(null);
方法3:Optional.isPresent()
作用:判断预期值是否存在
说明:如果值存在返回true,否则返回false。
//isPresent方法用来检查Optional实例中是否包含值
Optional name = Optional.of("Sanaulla");
if (name.isPresent()) {
//在Optional实例内调用get()返回已存在的值
System.out.println(name.get());//输出Sanaulla
}
方法4:Optional.get()
作用:如果Optional有值则将其返回,否则抛出NoSuchElementException。
说明:上面的示例中,get方法用来得到Optional实例中的值。下面我们看一个抛出NoSuchElementException的例子
//执行下面的代码会输出:No value present
try {
Optional empty = Optional.ofNullable(null);
//在空的Optional实例上调用get(),抛出NoSuchElementException
System.out.println(empty.get());
} catch (NoSuchElementException ex) {
System.out.println(ex.getMessage());
}
方法5:Optional.ifPresent()
作用:如果Optional实例有值则为其调用consumer,否则不做处理
说明:要理解ifPresent方法,首先需要了解Consumer类。简答地说,Consumer类包含一个抽象方法。该抽象方法对传入的值进行处理,但没有返回值。Java8支持不用接口直接通过lambda表达式传入参数,如果Optional实例有值,调用ifPresent()可以接受接口段或lambda表达式
//ifPresent方法接受lambda表达式作为参数。
//lambda表达式对Optional的值调用consumer进行处理。
Optional name = Optional.of("Sanaulla");
name.ifPresent((value) -> {
System.out.println("The length of the value is: " + value.length());
});
方法7:Optional.orElse()
作用:如果有值则将其返回,否则返回指定的其它值。
说明:如果Optional实例有值则将其返回,否则返回orElse方法传入的参数。示例如下:
Optional name = Optional.of("Sanaulla");
Optional someNull = Optional.of(null);
//如果值不为null,orElse方法返回Optional实例的值。
//如果为null,返回传入的消息。
//输出:There is no value present!
System.out.println(empty.orElse("There is no value present!"));
//输出:Sanaulla
System.out.println(name.orElse("There is some value!"));
方法8:Optional.orElseGet()
作用:如果有值则将其返回,否则返回指定的其它值。
说明:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值
Optional name = Optional.of("Sanaulla");
Optional someNull = Optional.of(null);
//orElseGet与orElse方法类似,区别在于orElse传入的是默认值,
//orElseGet可以接受一个lambda表达式生成默认值。
//输出:Default Value
System.out.println(empty.orElseGet(() -> "Default Value"));
//输出:Sanaulla
System.out.println(name.orElseGet(() -> "Default Value"));
方法9:Optional.orElseThrow()
作用:如果有值则将其返回,否则抛出supplier接口创建的异常。
说明:在orElseGet方法中,我们传入一个Supplier接口。然而,在orElseThrow中我们可以传入一个lambda表达式或方法,如果值不存在来抛出异常
try {
Optional empty= Optional.of(null);
//orElseThrow与orElse方法类似。与返回默认值不同,
//orElseThrow会抛出lambda表达式或方法生成的异常
empty.orElseThrow(ValueAbsentException::new);
} catch (Throwable ex) {
//输出: No value present in the Optional instance
System.out.println(ex.getMessage());
}
ValueAbsentException定义如下:
class ValueAbsentException extends Throwable {
public ValueAbsentException() {
super();
}
public ValueAbsentException(String msg) {
super(msg);
}
@Override
public String getMessage() {
return "No value present in the Optional instance";
}
}
方法10:Optional.map()
作用:如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。
说明:map方法用来对Optional实例的值执行一系列操作。通过一组实现了Function接口的lambda表达式传入操作。
Optional name = Optional.of("Sanaulla");
//map方法执行传入的lambda表达式参数对Optional实例的值进行修改。
//为lambda表达式的返回值创建新的Optional实例作为map方法的返回值。
Optional upperName = name.map((value) -> value.toUpperCase());
System.out.println(upperName.orElse("No value found"));
方法11:Optional.flatMap()
作用:如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。flatMap与map(Funtion)方法类似,区别在于flatMap中的mapper返回值必须是Optional。调用结束时,flatMap不会对结果用Optional封装。
说明:flatMap方法与map方法类似,区别在于mapping函数的返回值不同。map方法的mapping函数返回值可以是任何类型T,而flatMap方法的mapping函数必须是Optional。
Optional name = Optional.of("Sanaulla");
//flatMap与map(Function)非常类似,区别在于传入方法的lambda表达式的返回类型。
//map方法中的lambda表达式返回值可以是任意类型,在map函数返回之前会包装为Optional。
//但flatMap方法中的lambda表达式返回值必须是Optionl实例。
upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
System.out.println(upperName.orElse("No value found"));//输出SANAULLA
方法12:Optional.filter()
作用:如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。
说明:filter个方法通过传入限定条件对Optional实例的值进行过滤。这里可以传入一个lambda表达式。对于filter函数我们应该传入实现了Predicate接口的lambda表达式。
Optional name = Optional.of("Sanaulla");
//filter方法检查给定的Option值是否满足某些条件。
//如果满足则返回同一个Option实例,否则返回空Optional。
Optional longName = name.filter((value) -> value.length() > 6);
System.out.println(longName.orElse("The name is less than 6 characters"));//输出Sanaulla
//另一个例子是Optional值不满足filter指定的条件。
Optional anotherName = Optional.of("Sana");
Optional shortName = anotherName.filter((value) -> value.length() > 6);
//输出:name长度不足6字符
System.out.println(shortName.orElse("The name is less than 6 characters"));
总结:Optional方法
以上,我们介绍了Optional类的各个方法。下面通过一个完整的示例对用法集中展示。
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
64
65
66
67
68
69
public class OptionalDemo {
public static void main(String[] args) {
//创建Optional实例,也可以通过方法返回值得到。
Optional name = Optional.of("Sanaulla");
//创建没有值的Optional实例,例如值为'null'
Optional empty = Optional.ofNullable(null);
//isPresent方法用来检查Optional实例是否有值。
if (name.isPresent()) {
//调用get()返回Optional值。
System.out.println(name.get());
}
try {
//在Optional实例上调用get()抛出NoSuchElementException。
System.out.println(empty.get());
} catch (NoSuchElementException ex) {
System.out.println(ex.getMessage());
}
//ifPresent方法接受lambda表达式参数。
//如果Optional值不为空,lambda表达式会处理并在其上执行操作。
name.ifPresent((value) -> {
System.out.println("The length of the value is: " + value.length());
});
//如果有值orElse方法会返回Optional实例,否则返回传入的错误信息。
System.out.println(empty.orElse("There is no value present!"));
System.out.println(name.orElse("There is some value!"));
//orElseGet与orElse类似,区别在于传入的默认值。
//orElseGet接受lambda表达式生成默认值。
System.out.println(empty.orElseGet(() -> "Default Value"));
System.out.println(name.orElseGet(() -> "Default Value"));
try {
//orElseThrow与orElse方法类似,区别在于返回值。
//orElseThrow抛出由传入的lambda表达式/方法生成异常。
empty.orElseThrow(ValueAbsentException::new);
} catch (Throwable ex) {
System.out.println(ex.getMessage());
}
//map方法通过传入的lambda表达式修改Optonal实例默认值。
//lambda表达式返回值会包装为Optional实例。
Optional upperName = name.map((value) -> value.toUpperCase());
System.out.println(upperName.orElse("No value found"));
//flatMap与map(Funtion)非常相似,区别在于lambda表达式的返回值。
//map方法的lambda表达式返回值可以是任何类型,但是返回值会包装成Optional实例。
//但是flatMap方法的lambda返回值总是Optional类型。
upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
System.out.println(upperName.orElse("No value found"));
//filter方法检查Optiona值是否满足给定条件。
//如果满足返回Optional实例值,否则返回空Optional。
Optional longName = name.filter((value) -> value.length() > 6);
System.out.println(longName.orElse("The name is less than 6 characters"));
//另一个示例,Optional值不满足给定条件。
Optional anotherName = Optional.of("Sana");
Optional shortName = anotherName.filter((value) -> value.length() > 6);
System.out.println(shortName.orElse("The name is less than 6 characters"));
}
}
[if !supportLists]八、[endif]在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?(2017-11-23-gxb)
引起内存溢出的原因有很多种,常见的有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小;
内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。
[if !supportLists]第四章 [endif]JavaWEB 基础
一、JDBC技术
[if !supportLists]1. [endif]说下原生jdbc操作数据库流程?(2017-11-25-wzz)
第一步:Class.forName()加载数据库连接驱动;
第二步:DriverManager.getConnection()获取数据连接对象;
第三步:根据SQL获取sql会话对象,有2种方式 Statement、PreparedStatement ;
第四步:执行SQL处理结果集,执行SQL前如果有参数值就设置参数值setXXX();
第五步:关闭结果集、关闭会话、关闭连接。
详细代码请看(封装):http://blog.csdn.net/qq_29542611/article/details/52426006
[if !supportLists]2. [endif]什么要使用PreparedStatement?(2017-11-25-wzz)
1、 PreparedStatement接口继承Statement, PreparedStatement 实例包含已编译的 SQL 语句,所以其执行速度要快于 Statement 对象。
2、作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数
3、在JDBC应用中,在任何时候都不要使用Statement,原因如下:
一、代码的可读性和可维护性.Statement需要不断地拼接,而PreparedStatement不会。
二、PreparedStatement尽最大可能提高性能.DB有缓存机制,相同的预编译语句再次被调用不会再次需要编译。
三、最重要的一点是极大地提高了安全性.Statement容易被SQL注入,而PreparedStatementc传入的内容不会和sql语句发生任何匹配关系。
[if !supportLists]3. [endif]关系数据库中连接池的机制是什么?(2017-12-6-lyq)
前提:为数据库连接建立一个缓冲池。
1:从连接池获取或创建可用连接
2:使用完毕之后,把连接返回给连接池
3:在系统关闭前,断开所有连接并释放连接占用的系统资源
4:能够处理无效连接,限制连接池中的连接总数不低于或者不超过某个限定值。
其中有几个概念需要大家理解:
最小连接数是连接池一直保持的数据连接。如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费掉。
最大连接数是连接池能申请的最大连接数。如果数据连接请求超过此数,后面的数据连接请求将被加入到等待队列中,这会影响之后的数据库操作。
如果最小连接数与最大连接数相差太大,那么,最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。
上面的解释,可以这样理解:数据库池连接数量一直保持一个不少于最小连接数的数量,当数量不够时,数据库会创建一些连接,直到一个最大连接数,之后连接数据库就会等待。
三、Http协议
[if !supportLists]1. [endif]http的长连接和短连接(2017-11-14-lyq)
HTTP协议有HTTP/1.0版本和HTTP/1.1版本。HTTP1.1默认保持长连接(HTTP persistent connection,也翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。
在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。从HTTP/1.1起,默认使用的是长连接,用以保持连接特性。
[if !supportLists]2. [endif]HTTP/1.1与HTTP/1.0的区别(2017-11-21-wzy)
参考原文:http://blog.csdn.net/forgotaboutgirl/article/details/6936982
[if !supportLists]1 [endif]可扩展性
[if !supportLists]a) [endif]HTTP/1.1 在消息中增加版本号,用于兼容性判断。
[if !supportLists]b) [endif]HTTP/1.1增加了OPTIONS方法,它允许客户端获取一个服务器支持的方法列表。
[if !supportLists]c) [endif]为了与未来的协议规范兼容,HTTP/1.1在请求消息中包含了Upgrade头域,通过该头域,客户端可以让服务器知道它能够支持的其它备用通信协议,服务器可以据此进行协议切换,使用备用协议与客户端进行通信。
[if !supportLists]2 [endif]缓存
在HTTP/1.0中,使用Expire头域来判断资源的fresh或stale,并使用条件请求(conditional request)来判断资源是否仍有效。HTTP/1.1在1.0的基础上加入了一些cache的新特性,当缓存对象的Age超过Expire时变为stale对象,cache不需要直接抛弃stale对象,而是与源服务器进行重新激活(revalidation)。
[if !supportLists]3 [endif]带宽优化
HTTP/1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了。例如,客户端只需要显示一个文档的部分内容,又比如下载大文件时需要支持断点续传功能,而不是在发生断连后不得不重新下载完整的包。
HTTP/1.1中在请求消息中引入了range头域,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码为206(Partial Content),它可以防止Cache将响应误以为是完整的一个对象。
另外一种情况是请求消息中如果包含比较大的实体内容,但不确定服务器是否能够接收该请求(如是否有权限),此时若贸然发出带实体的请求,如果被拒绝也会浪费带宽。
HTTP/1.1加入了一个新的状态码100(Continue)。客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized);如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。注意,HTTP/1.0的客户端不支持100响应码。但可以让客户端在请求消息中加入Expect头域,并将它的值设置为100-continue。
节省带宽资源的一个非常有效的做法就是压缩要传送的数据。Content-Encoding是对消息进行端到端(end-to-end)的编码,它可能是资源在服务器上保存的固有格式(如jpeg图片格式);在请求消息中加入Accept-Encoding头域,它可以告诉服务器客户端能够解码的编码方式。
[if !supportLists]4 [endif]长连接
HTTP/1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。此外,由于大多数网页的流量都比较小,一次TCP连接很少能通过slow-start区,不利于提高带宽利用率。
HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。
HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。
[if !supportLists]5 [endif]消息传递
HTTP消息中可以包含任意长度的实体,通常它们使用Content-Length来给出消息结束标志。但是,对于很多动态产生的响应,只能通过缓冲完整的消息来判断消息的大小,但这样做会加大延迟。如果不使用长连接,还可以通过连接关闭的信号来判定一个消息的结束。
HTTP/1.1中引入了Chunkedtransfer-coding来解决上面这个问题,发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载。
在HTTP/1.0中,有一个Content-MD5的头域,要计算这个头域需要发送方缓冲完整个消息后才能进行。而HTTP/1.1中,采用chunked分块传递的消息在最后一个块(零长度)结束之后会再传递一个拖尾(trailer),它包含一个或多个头域,这些头域是发送方在传递完所有块之后再计算出值的。发送方会在消息中包含一个Trailer头域告诉接收方这个拖尾的存在。
[if !supportLists]6 [endif]Host头域
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。此外,服务器应该接受以绝对路径标记的资源请求。
[if !supportLists]7 [endif]错误提示
HTTP/1.0中只定义了16个状态响应码,对错误或警告的提示不够具体。HTTP/1.1引入了一个Warning头域,增加对错误或警告信息的描述。
此外,在HTTP/1.1中新增了24个状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
[if !supportLists]3. [endif]http常见的状态码有哪些?(2017-11-23-wzz)
200 OK //客户端请求成功
301 Moved Permanently(永久移除),请求的URL已移走。Response中应该包含一个Location URL, 说明资源现在所处的位置
302 found 重定向
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
[if !supportLists]4. [endif]GET和POST的区别?(2017-11-23-wzz)
从表面现像上面看GET和POST的区别:
1. GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连,如:login.action?name=zhagnsan&password=123456。POST把提交的数据则放置在是HTTP包的包体中。
2. GET方式提交的数据最多只能是1024字节,理论上POST没有限制,可传较大量的数据。其实这样说是错误的,不准确的:
“GET方式提交的数据最多只能是1024字节",因为GET是通过URL提交数据,那么GET可提交的数据量就跟URL的长度有直接关系了。而实际上,URL不存在参数上限的问题,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。
3.POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存,(2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用GET提交数据还可能会造成Cross-site request forgery攻击。
Get是向服务器发索取数据的一种请求,而Post是向服务器提交数据的一种请求,在FORM(表单)中,Method默认为"GET",实质上,GET和POST只是发送机制不同,并不是一个取一个发!
参考原文:https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html
[if !supportLists]5. [endif]http中重定向和请求转发的区别?(2017-11-23-wzz)
本质区别:转发是服务器行为,重定向是客户端行为。
重定向特点:两次请求,浏览器地址发生变化,可以访问自己web之外的资源,传输的数据会丢失。
请求转发特点:一次强求,浏览器地址不变,访问的是自己本身的web资源,传输的数据不会丢失。
四、Cookie和Session
[if !supportLists]1. [endif]Cookie和Session的区别(2017-11-15-lyq)
Cookie是web服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个web服务器存储cookie。以后浏览器再给特定的web服务器发送请求时,同时会发送所有为该服务器存储的cookie。
Session是存储在web服务器端的一块信息。session对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。
Cookie和session的不同点:
1、无论客户端做怎样的设置,session都能够正常工作。当客户端禁用cookie时将无法使用cookie。
2、在存储的数据量方面:session能够存储任意的java对象,cookie只能存储String类型的对象。
[if !supportLists]2. [endif]session共享怎么做的(分布式如何实现session共享)?
参考原文:http://blog.csdn.net/sxiaobei/article/details/57086489
问题描述:一个用户在登录成功以后会把用户信息存储在session当中,这时session所在服务器为server1,那么用户在session失效之前如果再次使用app,那么可能会被路由到server2,这时问题来了,server没有该用户的session,所以需要用户重新登录,这时的用户体验会非常不好,所以我们想如何实现多台server之间共享session,让用户状态得以保存。
1、服务器实现的session复制或session共享,这类型的共享session是和服务器紧密相关的,比如webSphere或JBOSS在搭建集群时候可以配置实现session复制或session共享,但是这种方式有一个致命的缺点,就是不好扩展和移植,比如我们更换服务器,那么就要修改服务器配置。
2、利用成熟的技术做session复制,比如12306使用的gemfire,比如常见的内存数据库如redis或memorycache,这类方案虽然比较普适,但是严重依赖于第三方,这样当第三方服务器出现问题的时候,那么将是应用的灾难。
3、将session维护在客户端,很容易想到就是利用cookie,但是客户端存在风险,数据不安全,而且可以存放的数据量比较小,所以将session维护在客户端还要对session中的信息加密。
我们实现的方案可以说是第二种方案和第三种方案的合体,可以利用gemfire实现session复制共享,还可以将session维护在redis中实现session共享,同时可以将session维护在客户端的cookie中,但是前提是数据要加密。这三种方式可以迅速切换,而不影响应用正常执行。我们在实践中,首选gemfire或者redis作为session共享的载体,一旦session不稳定出现问题的时候,可以紧急切换cookie维护session作为备用,不影响应用提供服务。
这里主要讲解redis和cookie方案,gemfire比较复杂大家可以自行查看gemfire工作原理。利用redis做session共享,首先需要与业务逻辑代码解耦,不然session共享将没有意义,其次支持动态切换到客户端cookie模式。redis的方案是,重写服务器中的HttpSession和HttpServletRequest,首先实现HttpSession接口,重写session的所有方法,将session以hash值的方式存在redis中,一个session的key就是sessionID,setAtrribute重写之后就是更新redis中的数据,getAttribute重写之后就是获取redis中的数据,等等需要将HttpSession的接口一一实现。
实现了HttpSesson,那么我们先将该session类叫做MySession(当然实践中不是这么命名的),当MySession出现之后问题才开始,怎么能在不影响业务逻辑代码的情况下,还能让原本的request.getSession()获取到的是MySession,而不是服务器原生的session。这里,我决定重写服务器的HttpServletRequet,这里先称为MyRequest,但是这可不是单纯的重写,我需要在原生的request基础上重写,于是我决定在filter中,实现request的偷梁换柱,我的思路是这样的,MyRequest的构建器,必须以request作为参数,于是我在filter中将服务器原生的request(也有可能是框架封装过的request),当做参数new出来一个MyRequest,并且MyRequest也实现了HttpServletRequest接口,其实就是对原生request的一个增强,这里主要重写了几个request的方法,但是最重要的是重写了request.getSession(),写到这里大家应该都明白为什么重写这个方法了吧,当然是为了获取MySession,于是这样就在filter中,偷偷的将原生的request换成MyRequest了,然后再将替换过的request传入chan.doFilter(),这样filter时候的代码都使用的是MyRequest了,同时对业务代码是透明的,业务代码获取session的方法仍然是request.getSession(),但其实获取到的已经是MySession了,这样对session的操作已经变成了对redis的操作。这样实现的好处有两个,第一开发人员不需要对session共享做任何关注,session共享对用户是透明的;第二,filter是可配置的,通过filter的方式可以将session共享做成一项可插拔的功能,没有任何侵入性。
这个时候已经实现了一套可插拔的session共享的框架了,但是我们想到如果redis服务出了问题,这时我们该怎么办呢,于是我们延续redis的想法,想到可以将session维护在客户端内(加密的cookie),当然实现方法还是一样的,我们重写HttpSession接口,实现其所有方法,比如setAttribute就是写入cookie,getAttribute就是读取cookie,我们可以将重写的session称作MySession2,这时怎么让开发人员透明的获取到MySession2呢,实现方法还是在filter内偷梁换柱,在MyRequest加一个判断,读取sessionType配置,如果sessionType是redis的,那么getSession的时候获取到的是MySession,如果sessionType是coolie的,那么getSession的时候获取到的是MySession2,以此类推,用同样的方法就可以获取到MySession 3,4,5,6等等。
这样两种方式都有了,那么我们怎实现两种session共享方式的快速切换呢,刚刚我提到一个sessionType,这是用来决定获取到session的类型的,只要变换sessionType就能实现两种session共享方式的切换,但是sessionType必须对所有的服务器都是一致的,如果不一致那将会出现比较严重的问题,我们目前是将sessionType维护在环境变量里,如果要切换sessionType就要重启每一台服务器,完成session共享的转换,但是当服务器太多的时候将是一种灾难。而且重启服务意味着服务的中断,所以这样的方式只适合服务器规模比较小,而且用户量比较少的情况,当服务器太多的时候,务必需要一种协调技术,能够让服务器能够及时获取切换的通知。基于这样的原因,我们选用zookeeper作为配置平台,每一台服务器都会订阅zookeeper上的配置,当我们切换sessionType之后,所有服务器都会订阅到修改之后的配置,那么切换就会立即生效,当然可能会有短暂的时间延迟,但这是可以接受的。
[if !supportLists]3. [endif]在单点登录中,如果cookie被禁用了怎么办?(2017-11-23-gxb)
单点登录的原理是后端生成一个session ID,然后设置到 cookie,后面的所有请求浏览器都会带上 cookie,然后服务端从 cookie 里获取 session ID,再查询到用户信息。所以,保持登录的关键不是 cookie,而是通过 cookie 保存和传输的 session ID,其本质是能获取用户信息的数据。除了 cookie,还通常使用 HTTP 请求头来传输。但是这个请求头浏览器不会像 cookie 一样自动携带,需要手工处理。
五、jsp技术
[if !supportLists]1. [endif]什么是jsp,什么是Servlet?jsp和Servlet有什么区别?(2017-11-23-wzz)
jsp本质上就是一个Servlet,它是Servlet的一种特殊形式(由SUN公司推出),每个jsp页面都是一个servlet实例。
Servlet是由Java提供用于开发web服务器应用程序的一个组件,运行在服务端,由servlet容器管理,用来生成动态内容。一个servlet实例是实现了特殊接口Servlet的Java类,所有自定义的servlet均必须实现Servlet接口。
区别:
jsp是html页面中内嵌的Java代码,侧重页面显示;
Servlet是html代码和Java代码分离,侧重逻辑控制,mvc设计思想中jsp位于视图层,servlet位于控制层
Jsp运行机制:如下图
JVM只能识别Java类,并不能识别jsp代码!web容器收到以.jsp为扩展名的url请求时,会将访问请求交给tomcat中jsp引擎处理,每个jsp页面第一次被访问时,jsp引擎将jsp代码解释为一个servlet源程序,接着编译servlet源程序生成.class文件,再有web容器servlet引擎去装载执行servlet程序,实现页面交互。
[if !supportLists]2. [endif]jsp有哪些域对象和内置对象及他们的作用?(2017-11-25-wzz)
四大域对象:
(1)pageContext page域-指当前页面,在当前jsp页面有效,跳到其它页面失效
(2)request request域-指一次请求范围内有效,从http请求到服务器处理结束,返回响应的整个过程。在这个过程中使用forward(请求转发)方式跳转多个jsp,在这些页面里你都可以使用这个变量
(3)session session域-指当前会话有效范围,浏览器从打开到关闭过程中,转发、重定向均可以使用
(4)application context域-指只能在同一个web中使用,服务器未关闭或者重启,数据就有效
九大内置对象:
生命周期作用域使用情况
Request一次请求只在Jsp页面内有效用于接受通过HTTP协议传送到服务器的数据(包括头信息、系统信息、请求方式以及请求参数等)。
Reponse一次响应只在Jsp页面内有效表示服务器端对客户端的回应。主要用于设置头信息、跳转、Cookie等
Session从存入数据开始,默认闲置30分钟后失效会话内有效用于存储特定的用户会话所需的信息
Outhttp://www.cnblogs.com/leirenyuan/p/6016063.html 用于在Web浏览器内输出信息,并且管理应用服务器上的输出缓冲区
PageContext详细了解:
http://www.cnblogs.com/leirenyuan/p/6016063.html
用于存取其他隐含对象,如request、reponse、session、application 等对象。(实际上,pageContext对象提供了对JSP页面所有的对象及命名空间的访问。
Pagehttp://www.cnblogs.com/leirenyuan/p/6016063.html page对象代表JSP本身(对应this),只有在JSP页面内才是合法的
Exceptionhttp://www.cnblogs.com/leirenyuan/p/6016063.html 显示异常信息,必须在page 指令中设定< %@ page isErrorPage="true" %>才能使用,在一般的JSP页面中使用该对象将无法编译JSP文件
Application服务器启动发送第一个请求时就产生了Application对象,直到服务器关闭。 用于存储和访问来自任何页面的变量
所有的用户分享一个 Application 对象
Confighttp://www.cnblogs.com/leirenyuan/p/6016063.html 取得服务器的配置信息
六、XML技术
[if !supportLists]1. [endif]什么是xml,使用xml的优缺点,xml的解析器有哪几种,分别有什么区别?(2017-11-25-wzz)
xml是一种可扩展性标记语言,支持自定义标签(使用前必须预定义)使用DTD和XML Schema标准化XML结构。
具体了解xml详见:http://www.importnew.com/10839.html
优点:用于配置文件,格式统一,符合标准;用于在互不兼容的系统间交互数据,共享数据方便;
缺点:xml文件格式复杂,数据传输占流量,服务端和客户端解析xml文件占用大量资源且不易维护
Xml常用解析器有2种,分别是:DOM和SAX;
主要区别在于它们解析xml文档的方式不同。使用DOM解析,xml文档以DOM
树形结构加载入内存,而SAX采用的是事件模型,
详细区别:https://wenku.baidu.com/view/fc3fb5610b1c59eef8c7b410.html
[if !supportLists]第五章 [endif]JavaWEB高级
[if !supportLists]一、[endif]Filter和Listener
未完待续......
二、AJAX
[if !supportLists]1. [endif]谈谈你对ajax的认识?(2017-11-23-wzz)
Ajax是一种创建交互式网页应用的的网页开发技术;Asynchronous JavaScript and XML”的缩写。
Ajax的优势:
通过异步模式,提升了用户体验。
优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用。
Ajax引擎在客户端运行,承担了一部分本来由服务器承担的工作,从而减少了大用户量下的服务器负载。
Ajax的最大特点:
可以实现局部刷新,在不更新整个页面的前提下维护数据,提升用户体验度。
注意:
ajax在实际项目开发中使用率非常高(牢固掌握),针对ajax的详细描述:http://www.jb51.net/article/93258.htm
[if !supportLists]2. [endif]jsonp原理(2017-11-21-gxb)
JavaScript是一种在Web开发中经常使用的前端动态脚本技术。在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容。
JavaScript这个安全策略在进行多iframe或多窗口编程、以及Ajax编程时显得尤为重要。根据这个策略,在baidu.com下的页面中包含的JavaScript代码,不能访问在google.com域名下的页面内容;甚至不同的子域名之间的页面也不能通过JavaScript代码互相访问。对于Ajax的影响在于,通过XMLHttpRequest实现的Ajax请求,不能向不同的域提交请求,例如,在abc.example.com下的页面,不能向def.example.com提交Ajax请求,等等。
然而,当进行一些比较深入的前端编程的时候,不可避免地需要进行跨域操作,这时候“同源策略”就显得过于苛刻。JSONP跨域GET请求是一个常用的解决方案,下面我们来看一下JSONP跨域是如何实现的,并且探讨下JSONP跨域的原理。
jsonp的最基本的原理是:动态添加一个标签,使用script标签的src属性没有跨域的限制的特点实现跨域。首先在客户端注册一个callback, 然后把callback的名字传给服务器。此时,服务器先生成 json 数据。 然后以 javascript 语法的方式,生成一个function , function 名字就是传递上来的参数 jsonp。最后将 json 数据直接以入参的方式,放置到 function 中,这样就生成了一段 js 语法的文档,返回给客户端。
客户端浏览器,解析script标签,并执行返回的 javascript 文档,此时数据作为参数,传入到了客户端预先定义好的 callback 函数里。
参考资料:http://www.nowamagic.net/librarys/veda/detail/224
三、Linux
[if !supportLists]1. [endif]说一下常用的Linux命令
列出文件列表:ls 【参数 -a -l】
创建目录和移除目录:mkdir rmdir
用于显示文件后几行内容:tail
打包:tar -xvf
打包并压缩:tar -zcvf
查找字符串:grep
显示当前所在目录:pwd
创建空文件:touch
编辑器:vim vi
列出文件列表:ls 【参数 -a -l】
创建目录和移除目录:mkdir rmdir
用于显示文件后几行内容:tail
打包:tar -xvf
打包并压缩:tar -zcvf
查找字符串:grep
显示当前所在目录:pwd
创建空文件:touch
编辑器:vim vi
[if !supportLists]2. [endif]Linux中如何查看日志?(2017-11-21-gxb)
动态打印日志信息:tail –f 日志文件
参考资料:https://www.cnblogs.com/zdz8207/p/linux-log-tail-cat-tac.html
[if !supportLists]3. [endif]Linux怎么关闭进程(2017-11-21-gxb)
通常用ps 查看进程PID ,用kill命令终止进程。
ps 命令用于查看当前正在运行的进程。
grep 是搜索
例如:ps -ef | grep java
表示查看所有进程里CMD是java的进程信息。
ps -aux | grep java
-aux 显示所有状态
kill 命令用于终止进程。
例如:kill -9 [PID]
-9表示强迫进程立即停止。
[if !supportLists]四、[endif]常见的前端框架有哪些
[if !supportLists]1. [endif]EasyUI(2017-11-23-lyq)
EasyUI是一种基于jQuery的用户界面插件集合。easyui为创建现代化,互动,JavaScript应用程序,提供必要的功能。使用easyui你不需要写很多代码,你只需要通过编写一些简单HTML标记,就可以定义用户界面。优势:开源免费,页面也还说的过去。
easyUI入门:
页面引入必要的js和css样式文件,文件引入顺序为:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]
[if !supportLists]4. [endif]
[if !supportLists]5. [endif]
[if !supportLists]6. [endif]
[if !supportLists]7. [endif]
[if !supportLists]8. [endif]
[if !supportLists]9. [endif]
[if !supportLists]10. [endif]
然后在页面写easyUI代码就行,easyUI提供了很多样式:
示例如下:
实现代码如下:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]
[if !supportLists]3. [endif]
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] Basic Dialog - jQuery EasyUI Demo
[if !supportLists]6. [endif]
[if !supportLists]7. [endif]
[if !supportLists]8. [endif]
[if !supportLists]9. [endif]
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]
[if !supportLists]12. [endif]
[if !supportLists]13. [endif]
[if !supportLists]14. [endif]
Click below button to open or close dialog.
[if !supportLists]15. [endif]
[if !supportLists]16. [endif] Open
[if !supportLists]17. [endif] Close
[if !supportLists]18. [endif]
[if !supportLists]19. [endif]
[if !supportLists]20. [endif] The dialog content.
[if !supportLists]21. [endif]
[if !supportLists]22. [endif]
[if !supportLists]23. [endif]
[if !supportLists]24. [endif]
[if !supportLists]2. [endif]MiniUI(2017-11-23-lyq)
基于jquery 的框架,开发的界面功能都很丰富。jQuery MiniUI - 快速开发 WebUI。它能缩短开发时间,减少代码量,使开发者更专注于业务和服务端,轻松实现界面开发,带来绝佳的用户体验。使用 MiniUI,开发者可以快速创建 Ajax 无刷新、 B/S 快速录入数据、 CRUD、 Master-Detail、菜单工具栏、弹出面板、布局导航、数据验证、分页表格、树、树形表格等典型 WEB 应用系统界面。缺点:收费,没有源码,基于这个开发如果想对功能做扩展就需要找他们的团队进行升级!
提供以下几大类控件:
表格控件
树形控件
布局控件:标题面板、弹出面板、折叠分割器、布局器、表单布局器等
导航控件:分页导航器、导航菜单、选项卡、菜单、工具栏等。
表单控件:多选输入框、弹出选择框、文本输入框、数字输入框、日期选择框、下拉选择框、下拉树形选择框、下拉表格选择框、文件上传控件、多选框、列表框、多选框组、单选框组、按钮等
富文本编辑器
图表控件:柱状图、饼图、线形图、双轴图等。
技术亮点:
快速开发:使用Html配置界面,减少80%界面代码量。
易学易用:简单的API设计,可以独立、组合使用控件。
性能优化:内置数据懒加载、低内存开销、快速界面布局等机制。
丰富控件:包含表格、树、数据验证、布局导航等超过50个控件。
超强表格:提供锁定列、多表头、分页排序、行过滤、数据汇总、单元格编辑、详细行、Excel导出等功能。
第三方兼容:与ExtJS、jQuery、YUI、Dojo等任意第三方控件无缝集成。
浏览器兼容:支持IE6+、FireFox、Chrome等。
跨平台支持:支持Java、.NET、PHP等。
示例如下:
实现代码如下:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif] showTreeIcon="true" textField="text" idField="id"
[if !supportLists]3. [endif] allowDrag="true" allowDrop="true"
[if !supportLists]4. [endif] >
[if !supportLists]5. [endif]
[if !supportLists]1. [endif]jQueryUI(2017-11-23-lyq)
jQuery UI 是一套 jQuery 的页面 UI 插件,包含很多种常用的页面空间,例如 Tabs(如本站首页右上角部分) 、拉帘效果(本站首页左上角)、对话框、拖放效果、日期选择、颜色选择、数据排序、窗体大小调整等等非常多的内容。
技术亮点:
简单易用:继承jQuery 简易使用特性,提供高度抽象接口,短期改善网站易用性。
开源免费:采用MIT & GPL 双协议授权,轻松满足自由产品至企业产品各种授权需求。
广泛兼容:兼容各主流桌面浏览器。包括IE 6+、Firefox 2+、Safari 3+、Opera 9+、Chrome 1+。
轻便快捷:组件间相对独立,可按需加载,避免浪费带宽拖慢网页打开速度。
标准先进:支持WAI-ARIA,通过标准 XHTML 代码提供渐进增强,保证低端环境可访问性。
美观多变:提供近20 种预设主题,并可自定义多达 60 项可配置样式规则,提供 24 种背景纹理选择。
度娘上搜jQueryUI的api,其用法与easyUI、MiniUI都大同小异,此处将不再举例。
[if !supportLists]2. [endif]Vue.js(2017-11-23-lyq)
参考原文:https://cn.vuejs.org/v2/guide/
Vue.js (读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。
Vue.js起步:
引入相应文件:
[if !supportLists]1. [endif]
声明式渲染:
Vue.js 的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进 DOM 的系统:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] {{ message }}
[if !supportLists]4. [endif]
[if !supportLists]5. [endif]var app = new Vue({
[if !supportLists]6. [endif] el: '#app',
[if !supportLists]7. [endif] data: {
[if !supportLists]8. [endif] message: 'Hello Vue!'
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif]})
通过浏览器查看效果图为:
创建vue实例:
每个Vue 应用都是通过 Vue 函数创建一个新的 Vue 实例开始的,当创建一个 Vue 实例时,你可以传入一个选项对象。可以使用这些选项来创建你想要的行为。
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]var vm = new Vue({
[if !supportLists]3. [endif]//选项
[if !supportLists]4. [endif]})
实例生命周期:
每个Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。比如created 钩子可以用来在一个实例被创建之后执行代码:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]new Vue({
[if !supportLists]3. [endif] data: {
[if !supportLists]4. [endif] a: 1
[if !supportLists]5. [endif] },
[if !supportLists]6. [endif] created: function () {
[if !supportLists]7. [endif]// `this`指向 vm 实例
[if !supportLists]8. [endif] console.log('a is: ' + this.a)
[if !supportLists]9. [endif] }
[if !supportLists]10. [endif]})
[if !supportLists]11. [endif]// => "a is: 1"
[if !supportLists]3. [endif]AngularJS (2017-11-23-lyq)
参考原文:http://www.angularjs.net.cn/api/
AngularJS 是google 开发者设计的一个前端开发框架,它是由是由JavaScript 编写的一个JS框架。通常它是用来在静态网页构建动态应用不足而设计的。
AngularJS 特点如下:
1、 数据绑定: AngularJS是数据双向绑定。
2、 MVVM(Model-View-ViewModel)模式: Model 简单数据对象,View 视图(如HTML,JSP等),ViewModel是用来提供数据和方法,和View 进行交互。这种设计模式使得代码解耦合。
3、依赖注入:AngularJS支持注入方式把需要的对象,方法等注入到指定的对象中。
4、 指令: AngularJS内部自带各种常用指令,同时也支持开发者自定义指令。
5、HTML模板和扩展HTML: AngularJS可以定义与HTML兼容的自定义模板。
AngularJS 的Api:
AngularJS提供了很多功能丰富的组件,处理核心的ng组件外,还扩展了很多常用的功能组件,如ngRoute(路由),ngAnimate(动画),ngTouch(移动端操作)等,只需要引入相应的头文件,并依赖注入你的工作模块,则可使用。
ng (core module):AngularJS的默认模块,包含AngularJS的所有核心组件。
指令(directive)这是指令的核心集合,你可以在你的模板代码中使用它们来构建一个AngularJS应用。
一些例子包括:ngClick,ngInclude,ngRepeat,等等
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: $compile,$http,$location,等等
过滤器(filter)在组件模块中的核心过滤器是用来转换模板数据。
一些例子包括: filter,date,currency,lowercase,uppercase等等
全局函数(function)核心的全局函数作为angularjs对象。这些核心功能在您的应用程序的原生的JavaScript操作有用。
一些例子包括: angular.copy(),angular.equals(),angular.element()等等
ngRoute:AgularJS的路由模块,你能使用ngRoute结合"#"定义你的地址访问。引入angular-route.js文件,然后在你当前的工作模块依赖注入ngRoute模块。
服务(service)下面这些服务用作AngularJS的路由管理。
$routeParams- 解析返回路由中带有的参数
$route- 用于构建各个路由的url、view、controller这三者的关系
$routeProvider- 提供路由配置
指令(Directive)指令ngView提供不同路由模板插入的视图层
ngAnimate:AngularJS的动画模块,使用ngAnimate各种核心指令能为你的应用程序提供动画效果。动画可使用css或者JavaScript回调函数。引入angular-animate.js文件,然后在你当前的工作模块依赖注入ngAnimate模块。
服务(service)使用$animate来在你的指令代码中触发动画操作。
CSS-based animations按照nganimate的CSS命名结构参考CSS转换/关键帧动画在AngularJS。
一旦定义,动画可以通过引用CSS在HTML模板代码触发。
JS-based animations使用module.animation()来注册一个 JavaScript动画。
一旦注册,动画可以通过引用CSS在HTML模板代码触发。
ngAria:帮助制作AngularJS 自定义组件的新模块。引入angular-aria.js 文件,然后在你当前的工作模块依赖注入ngAria模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
ngResource:AngularJS的动画模块,使用ngAnimate各种核心指令能为你的应用程序提供动画效果。动画可使用css或者JavaScript回调函数。引入angular-resource.js 文件,然后在你当前的工作模块依赖注入ngResource模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
ngCookies:ngCookies模块提供了一个方便的包用于读取和写入浏览器的cookies。
引入angular-cookies.js 文件,然后在你当前的工作模块依赖注入ngCookies模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
ngTouch:AngularJS的动画模块,使用ngAnimate各种核心指令能为你的应用程序提供动画效果。动画可使用css或者JavaScript回调函数。引入angular-touch.js文件,然后在你当前的工作模块依赖注入ngTouch模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
ngSanitize:使用ngSanitize可安全地解析和在你的应用程序中操作HTML数据。
引入angular-sanitize.js 文件,然后在你当前的工作模块依赖注入ngSanitize模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
ngMock:AngularJS的动画模块,使用ngAnimate各种核心指令能为你的应用程序提供动画效果。动画可使用css或者JavaScript回调函数。引入angular-animate.js 文件,然后在你当前的工作模块依赖注入ngMock模块。
服务(service)这是服务的核心集合,依赖注入后可在你的应用中使用。
一些例子包括: ngClick, ngInclude, ngRepeat等等
[if !supportLists]第六章 [endif]数据库
[if !supportLists]一、[endif]Mysql
[if !supportLists]1. [endif]SQL的select语句完整的执行顺序(2017-11-15-lyq)
SQL Select语句完整的执行顺序:
1、from子句组装来自不同数据源的数据;
2、where子句基于指定的条件对记录行进行筛选;
3、group by子句将数据划分为多个分组;
4、使用聚集函数进行计算;
5、使用having子句筛选分组;
6、计算所有的表达式;
7、select 的字段;
8、使用order by对结果集进行排序。
SQL语言不同于其他编程语言的最明显特征是处理代码的顺序。在大多数据库语言中,代码按编码顺序被处理。但在SQL语句中,第一个被处理的子句式FROM,而不是第一出现的SELECT。SQL查询处理的步骤序号:
(1) FROM
(2) JOIN
(3) ON
(4) WHERE
(5) GROUP BY
(6) WITH {CUBE | ROLLUP}
(7) HAVING
(8) SELECT
(9) DISTINCT
(9) ORDER BY
(10)
以上每个步骤都会产生一个虚拟表,该虚拟表被用作下一个步骤的输入。这些虚拟表对调用者(客户端应用程序或者外部查询)不可用。只有最后一步生成的表才会会给调用者。如果没有在查询中指定某一个子句,将跳过相应的步骤。
逻辑查询处理阶段简介:
[if !supportLists]1、 [endif]FROM:对FROM子句中的前两个表执行笛卡尔积(交叉联接),生成虚拟表VT1。
[if !supportLists]2、 [endif]ON:对VT1应用ON筛选器,只有那些使为真才被插入到TV2。
[if !supportLists]3、 [endif]OUTER (JOIN):如果指定了OUTER JOIN(相对于CROSS JOIN或INNER JOIN),保留表中未找到匹配的行将作为外部行添加到VT2,生成TV3。如果FROM子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤1到步骤3,直到处理完所有的表位置。
[if !supportLists]4、 [endif]WHERE:对TV3应用WHERE筛选器,只有使为true的行才插入TV4。
[if !supportLists]5、 [endif]GROUP BY:按GROUP BY子句中的列列表对TV4中的行进行分组,生成TV5。
[if !supportLists]6、 [endif]CUTE|ROLLUP:把超组插入VT5,生成VT6。
[if !supportLists]7、 [endif]HAVING:对VT6应用HAVING筛选器,只有使为true的组插入到VT7。
[if !supportLists]8、 [endif]SELECT:处理SELECT列表,产生VT8。
[if !supportLists]9、 [endif]DISTINCT:将重复的行从VT8中删除,产品VT9。
[if !supportLists]10、 [endif]ORDER BY:将VT9中的行按ORDER BY子句中的列列表顺序,生成一个游标(VC10)。
[if !supportLists]11、 [endif]TOP:从VC10的开始处选择指定数量或比例的行,生成表TV11,并返回给调用者。
where子句中的条件书写顺序
[if !supportLists]2. [endif]SQL之聚合函数(2017-11-15-lyq)
聚合函数是对一组值进行计算并返回单一的值的函数,它经常与select语句中的group by子句一同使用。
[if !supportLists]a. [endif]avg():返回的是指定组中的平均值,空值被忽略。
[if !supportLists]b. [endif]count():返回的是指定组中的项目个数。
[if !supportLists]c. [endif]max():返回指定数据中的最大值。
[if !supportLists]d. [endif]min():返回指定数据中的最小值。
[if !supportLists]e. [endif]sum():返回指定数据的和,只能用于数字列,空值忽略。
[if !supportLists]f. [endif]group by():对数据进行分组,对执行完group by之后的组进行聚合函数的运算,计算每一组的值。最后用having去掉不符合条件的组,having子句中的每一个元素必须出现在select列表中(只针对于mysql)。
[if !supportLists]3. [endif]SQL之连接查询(左连接和右连接的区别)(2017-11-15-lyq)
外连接:
左连接(左外连接):以左表作为基准进行查询,左表数据会全部显示出来,右表如果和左表匹配的数据则显示相应字段的数据,如果不匹配则显示为null。
右连接(右外连接):以右表作为基准进行查询,右表数据会全部显示出来,左表如果和右表匹配的数据则显示相应字段的数据,如果不匹配则显示为null。
全连接:先以左表进行左外连接,再以右表进行右外连接。
内连接:
显示表之间有连接匹配的所有行。
[if !supportLists]4. [endif]SQL之sql注入(2017-11-15-lyq)
通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。举例:当执行的sql为 select * from user where username =“admin” or “a”=“a”时,sql语句恒成立,参数admin毫无意义。
防止sql注入的方式:
[if !supportLists]1. [endif]预编译语句:如,select * from user where username = ?,sql语句语义不会发生改变,sql语句中变量用?表示,即使传递参数时为“admin or‘a’= ‘a’”,也会把这整体当做一个字符创去查询。
[if !supportLists]2. [endif]Mybatis框架中的mapper方式中的 # 也能很大程度的防止sql注入($无法防止sql注入)。
[if !supportLists]5. [endif]Mysql性能优化(2017-11-15-lyq)
[if !supportLists]1、[endif]当只要一行数据时使用limit 1
查询时如果已知会得到一条数据,这种情况下加上limit 1 会增加性能。因为mysql数据库引擎会在找到一条结果停止搜索,而不是继续查询下一条是否符合标准直到所有记录查询完毕。
[if !supportLists]2、[endif]选择正确的数据库引擎
Mysql中有两个引擎MyISAM和InnoDB,每个引擎有利有弊。
MyISAM适用于一些大量查询的应用,但对于有大量写功能的应用不是很好。甚至你只需要update一个字段整个表都会被锁起来。而别的进程就算是读操作也不行要等到当前update操作完成之后才能继续进行。另外,MyISAM对于select count(*)这类操作是超级快的。
InnoDB的趋势会是一个非常复杂的存储引擎,对于一些小的应用会比MyISAM还慢,但是支持“行锁”,所以在写操作比较多的时候会比较优秀。并且,它支持很多的高级应用,例如:事物。
[if !supportLists]3. [endif]用not exists代替not in
Not exists用到了连接能够发挥已经建立好的索引的作用,not in不能使用索引。Not in是最慢的方式要同每条记录比较,在数据量比较大的操作红不建议使用这种方式。
[if !supportLists]4. [endif]对操作符的优化,尽量不采用不利于索引的操作符
如:in not in is null is not null <> 等
某个字段总要拿来搜索,为其建立索引:
Mysql中可以利用alter table语句来为表中的字段添加索引,语法为:alter table 表明 add index (字段名);
[if !supportLists]6. [endif]必看sql面试题(学生表_课程表_成绩表_教师表)(2017-11-25-wzz)
给大家推荐一篇非常好的博客,该博客中收集了最常见的Mysql常见面试题和笔试题。
博客链接:http://www.cnblogs.com/qixuejia/p/3637735.html
[if !supportLists]7. [endif]Mysql数据库架构图(2017-11-25-wzz)
MyISAM和InnoDB是最常见的两种存储引擎,特点如下。
MyISAM存储引擎
MyISAM是MySQL官方提供默认的存储引擎,其特点是不支持事务、表锁和全文索引,对于一些OLAP(联机分析处理)系统,操作速度快。
每个MyISAM在磁盘上存储成三个文件。文件名都和表名相同,扩展名分别是.frm(存储表定义)、.MYD (MYData,存储数据)、.MYI (MYIndex,存储索引)。这里特别要注意的是MyISAM不缓存数据文件,只缓存索引文件。
InnoDB存储引擎
InnoDB存储引擎支持事务,主要面向OLTP(联机事务处理过程)方面的应用,其特点是行锁设置、支持外键,并支持类似于Oracle的非锁定读,即默认情况下读不产生锁。InnoDB将数据放在一个逻辑表空间中(类似Oracle)。InnoDB通过多版本并发控制来获得高并发性,实现了ANSI标准的4种隔离级别,默认为Repeatable,使用一种被称为next-key locking的策略避免幻读。
对于表中数据的存储,InnoDB采用类似Oracle索引组织表Clustered的方式进行存储。
InnoDB 存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。但是对比Myisam的存储引擎,InnoDB 写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。
InnoDB体系架构
[if !supportLists]8. [endif]Mysql架构器中各个模块都是什么?(2017-11-25-wzz)
(1)、连接管理与安全验证是什么?
每个客户端都会建立一个与服务器连接的线程,服务器会有一个线程池来管理这些 连接;如果客户端需要连接到MYSQL数据库还需要进行验证,包括用户名、密码、 主机信息等。
(2)、解析器是什么?
解析器的作用主要是分析查询语句,最终生成解析树;首先解析器会对查询语句的语法进行分析,分析语法是否有问题。还有解析器会查询缓存,如果在缓存中有对应的语句,就返回查询结果不进行接下来的优化执行操作。前提是缓存中的数据没有被修改,当然如果被修改了也会被清出缓存。
(3)、优化器怎么用?
优化器的作用主要是对查询语句进行优化操作,包括选择合适的索引,数据的读取方式,包括获取查询的开销信息,统计信息等,这也是为什么图中会有优化器指向存储引擎的箭头。之前在别的文章没有看到优化器跟存储引擎之间的关系,在这里我个人的理解是因为优化器需要通过存储引擎获取查询的大致数据和统计信息。
(4)、执行器是什么?
执行器包括执行查询语句,返回查询结果,生成执行计划包括与存储引擎的一些处理操作。
[if !supportLists]9. [endif]Mysql存储引擎有哪些?(2017-11-25-wzz)
(1)、InnoDB存储引擎
InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB是默认的MySQL引擎。
(2)、MyISAM存储引擎
MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事物。
(3)、MEMORY存储引擎
MEMORY存储引擎将表中的数据存储到内存中,未查询和引用其他表数据提供快速访问。
(4)、NDB存储引擎
DB存储引擎是一个集群存储引擎,类似于Oracle的RAC,但它是Share Nothing的架构,因此能提供更高级别的高可用性和可扩展性。NDB的特点是数据全部放在内存中,因此通过主键查找非常快。
关于NDB,有一个问题需要注意,它的连接(join)操作是在MySQL数据库层完成,不是在存储引擎层完成,这意味着,复杂的join操作需要巨大的网络开销,查询速度会很慢。
[if !supportLists](5)[endif]、Memory (Heap) 存储引擎
Memory存储引擎(之前称为Heap)将表中数据存放在内存中,如果数据库重启或崩溃,数据丢失,因此它非常适合存储临时数据。
[if !supportLists](6)[endif]、Archive存储引擎
正如其名称所示,Archive非常适合存储归档数据,如日志信息。它只支持INSERT和SELECT操作,其设计的主要目的是提供高速的插入和压缩功能。
[if !supportLists](7)[endif]、Federated存储引擎
Federated存储引擎不存放数据,它至少指向一台远程MySQL数据库服务器上的表,非常类似于Oracle的透明网关。
(8)、Maria存储引擎
Maria存储引擎是新开发的引擎,其设计目标是用来取代原有的MyISAM存储引擎,从而成为MySQL默认的存储引擎。
上述引擎中,InnoDB是事务安全的存储引擎,设计上借鉴了很多Oracle的架构思想,一般而言,在OLTP应用中,InnoDB应该作为核心应用表的首先存储引擎。InnoDB是由第三方的Innobase Oy公司开发,现已被Oracle收购,创始人是Heikki Tuuri,芬兰赫尔辛基人,和著名的Linux创始人Linus是校友。
[if !supportLists]10. [endif]MySQL事务介绍(2017-11-25-wzz)
MySQL和其它的数据库产品有一个很大的不同就是事务由存储引擎所决定,例如MYISAM,MEMORY,ARCHIVE都不支持事务,事务就是为了解决一组查询要么全部执行成功,要么全部执行失败。
MySQL事务默认是采取自动提交的模式,除非显示开始一个事务。
SHOW VARIABLES LIKE 'AUTOCOMMIT';
修改自动提交模式,0=OFF,1=ON
注意:修改自动提交对非事务类型的表是无效的,因为它们本身就没有提交和回滚的概念,还有一些命令是会强制自动提交的,比如DLL命令、lock tables等。
SET AUTOCOMMIT=OFF或 SET AUTOCOMMIT=0
10.1事务的四大特征是什么?
数据库事务transanction正确执行的四个基本要素。ACID,原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability)。
(1)原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
(2)一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
(3)隔离性:隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆, 必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
(4)持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
10.2 Mysql中四种隔离级别分别是什么?
隔离级别脏读不可重复读幻读
Read uncommitted(读未提交)是是是
Read committed(读已提交)否是是
Repeatable read(可重复读)否否是
Serializable(串行读)否否否
读未提交(READ UNCOMMITTED):未提交读隔离级别也叫读脏,就是事务可以读取其它事务未提交的数据。
读已提交(READ COMMITTED):在其它数据库系统比如SQL Server默认的隔离级别就是提交读,已提交读隔离级别就是在事务未提交之前所做的修改其它事务是不可见的。
可重复读(REPEATABLE READ):保证同一个事务中的多次相同的查询的结果是一致的,比如一个事务一开始查询了一条记录然后过了几秒钟又执行了相同的查询,保证两次查询的结果是相同的,可重复读也是mysql的默认隔离级别。
可串行化(SERIALIZABLE):可串行化就是保证读取的范围内没有新的数据插入,比如事务第一次查询得到某个范围的数据,第二次查询也同样得到了相同范围的数据,中间没有新的数据插入到该范围中。
[if !supportLists]11. [endif]MySQL怎么创建存储过程(2017-11-25-wzz)
MySQL存储过程是从MySQL5.0开始增加的新功能。存储过程的优点有一箩筐。不过最主要的还是执行效率和SQL 代码封装。特别是 SQL 代码封装功能,如果没有存储过程,在外部程序访问数据库时,要组织很多 SQL 语句。特别是业务逻辑复杂的时候,一大堆的 SQL 和条件夹杂在代码中,让人不寒而栗。现在有了 MySQL 存储过程,业务逻辑可以封装存储过程中,这样不仅容易维护,而且执行效率也高。
一、创建MySQL存储过程
下面代码创建了一个叫pr_add 的MySQL 存储过程,这个MySQL 存储过程有两个int 类型的输入参数 “a”、“b”,返回这两个参数的和。
1)drop procedure if exists pr_add; (备注:如果存在pr_add的存储过程,则先删掉)
2)计算两个数之和(备注:实现计算两个整数之和的功能)
create procedure pr_add ( a int, b int ) begin declare c int;
if a is null then set a = 0;
end if;
if b is null then set b = 0;
end if;
set c = a + b;
select c as sum;
[if !supportLists]二、[endif]调用MySQL 存储过程
call pr_add(10, 20);
[if !supportLists]12. [endif]