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]
Basic Dialog
[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]MySQL触发器怎么写?(2017-11-25-wzz)
MySQL包含对触发器的支持。触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。
在MySQL中,创建触发器语法如下:
CREATE TRIGGER trigger_name
trigger_time
trigger_event ON tbl_name
FOR EACH ROW
trigger_stmt
其中:
trigger_name:标识触发器名称,用户自行指定;
trigger_time:标识触发时机,取值为 BEFORE 或 AFTER;
trigger_event:标识触发事件,取值为 INSERT、UPDATE 或 DELETE;
tbl_name:标识建立触发器的表名,即在哪张表上建立触发器;
trigger_stmt:触发器程序体,可以是一句SQL语句,或者用 BEGIN 和 END 包含的多条语句。
由此可见,可以建立6种触发器,即:BEFORE INSERT、BEFORE UPDATE、BEFORE DELETE、AFTER INSERT、AFTER UPDATE、AFTER DELETE。
另外有一个限制是不能同时在一个表上建立2个相同类型的触发器,因此在一个表上最多建立6个触发器。
假设系统中有两个表:
1)班级表 class(班级号 classID, 班内学生数 stuCount)
2)学生表 student(学号 stuID, 所属班级号 classID)
要创建触发器来使班级表中的班内学生数随着学生的添加自动更新,代码如下:
create trigger tri_stuInsert after insert
on student for each row
begin
declare c int;
set c = (select stuCount from class where classID=new.classID);
update class set stuCount = c + 1 where classID = new.classID;
查看触发器:
和查看数据库(show databases;)查看表格(show tables;)一样,查看触发器的语法如下:
SHOW TRIGGERS [FROM schema_name];
其中,schema_name 即 Schema 的名称,在 MySQL 中 Schema 和 Database 是一样的,也就是说,可以指定数据库名,这样就不必先“USE database_name;”了。
删除触发器:
和删除数据库、删除表格一样,删除触发器的语法如下:
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name
[if !supportLists]13. [endif]MySQL语句优化(2017-11-26-wzz)
13.1 where子句中可以对字段进行null值判断吗?
可以,比如select id from t where num is null 这样的sql也是可以的。但是最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库。不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了,不管是否插入值(NULL也包含在内),都是占用100个字符的空间的,如果是varchar 这样的变长字段,null不占用空间。可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num = 0。
13.2 select * from admin left join log on admin.admin_id = log.admin_id where log.admin_id>10如何优化?
优化为:select * from (select * from admin where admin_id>10) T1 lef join log on T1.admin_id = log.admin_id。
使用JOIN 时候,应该用小的结果驱动大的结果(left join 左边表结果尽量小如果有条件应该放到左边先处理,right join 同理反向),同时尽量把牵涉到多表联合的查询拆分多个query(多个连表查询效率低,容易到之后锁表和阻塞)。
13.3 limit的基数比较大时使用between
例如:select * from admin order by admin_id limit 100000,10
优化为:select * from admin where admin_id between 100000 and 100010 order by admin_id。
13.4尽量避免在列上做运算,这样导致索引失效
例如:select * from admin where year(admin_time)>2014
优化为:select * from admin where admin_time> '2014-01-01′
[if !supportLists]14. [endif]MySQL中文乱码问题完美解决方案(2017-12-07-lwl)
解决乱码的核心思想是统一编码。我们在使用MySQL建数据库和建表时应尽量使用统一的编码,强烈推荐的是utf8编码,因为该编码几乎可以兼容世界上所有的字符。
数据库在安装的时候可以设置默认编码,在安装时就一定要设置为utf8编码。设置之后再创建的数据库和表如果不指定编码,默认都会使用utf8编码,省去了很多麻烦。
数据库软件安装好之后可以通过如下命令查看默认编码:
[if !supportLists]1、[endif]查询数据库软件使用的默认编码格式
show variables like“%colla%”;
show varables like“%char%”
其中collation,代表了字符串排序(比较)的规则,如果值是utf8_general_ci,代表使用utf8字符集大小写不敏感的自然方式比较。
如果character_set的值不为utf8,那么可以使用如下命令修改为utf8。
[if !supportLists]2、[endif]修改数据库默认编码为utf8
SET character_set_client='utf8';
SET character_set_connection='utf8';
SET character_set_results='utf8';
如果不想设置数据库软件的全局默认编码,也可以单独修改或者设置某个具体数据库的编码也可以单独修改或设置某个数据库中某个表的编码。
[if !supportLists]3、[endif]创建数据库的时候指定使用utf8编码
CREATE DATABASE `test`
CHARACTER SET 'utf8'
COLLATE 'utf8_general_ci';
[if !supportLists]4、[endif]创建表的时候指定使用utf8编码
CREATE TABLE `database_user` (
`ID` varchar(40) NOT NULL default '',
`UserID` varchar(40) NOT NULL default '',
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
如果数据库已经创建好了,可以使用show database 数据库名;和 show create table 表名;查看一下数据库和表的字符集是否为utf8,如果不是则在命令行下面可以用如下命令,将数据库和表编码修改为utf8.
[if !supportLists]5、[endif]修改具体某数据库或表的编码
ALTER DATABASE `db_name` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
ALTER TABLE `tb_name` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
[if !supportLists]15. [endif]如何提高MySQL的安全性(2017-12-8-lwl)
1.如果MySQL客户端和服务器端的连接需要跨越并通过不可信任的网络,那么需要使用ssh隧道来加密该连接的通信。
2.使用set password语句来修改用户的密码,先“mysql -u root”登陆数据库系统,然后“mysql> update mysql.user set password=password(’newpwd’)”,最后执行“flush privileges”。
3.MySQL需要提防的攻击有,防偷听、篡改、回放、拒绝服务等,不涉及可用性和容错方面。对所有的连接、查询、其他操作使用基于ACL(ACL(访问控制列表)是一种路由器配置和控制网络访问的一种有力的工具,它可控制路由器应该允许或拒绝数据包通过,可监控流量,可自上向下检查网络的安全性,可检查和过滤数据和限制不必要的路由更新,因此让网络资源节约成本的ACL配置技术在生活中越来越广泛应用。)即访问控制列表的安全措施来完成。
4.设置除了root用户外的其他任何用户不允许访问mysql主数据库中的user表;
5.使用grant和revoke语句来进行用户访问控制的工作;
6.不要使用明文密码,而是使用md5()和sha1()等单向的哈系函数来设置密码;
7.不要选用字典中的字来做密码;
8.采用防火墙可以去掉50%的外部危险,让数据库系统躲在防火墙后面工作,或放置在DMZ(DMZ是英文“demilitarized zone”的缩写,隔离区,它是为了解决安装防火墙后外部网络的访问用户不能访问内部网络服务器的问题,而设立的一个非安全系统与安全系统之间的缓冲区。)区域中;
9.从因特网上用nmap来扫描3306端口,也可用telnet server_host 3306的方法测试,不允许从非信任网络中访问数据库服务器的3306号tcp端口,需要在防火墙或路由器上做设定;
10.服务端要对SQL进行预编译,避免SQL注入攻击,例如where id=234,别人却输入where id=234 or 1=1。
11.在传递数据给mysql时检查一下大小;
12.应用程序连接到数据库时应该使用一般的用户帐号,开放少数必要的权限给该用户;
13.学会使用tcpdump和strings工具来查看传输数据的安全性,例如tcpdump -l -i eth0 -w -src or dst port 3306 strings。以普通用户来启动mysql数据库服务;
14.确信在mysql目录中只有启动数据库服务的用户才可以对文件有读和写的权限;
15.不许将process或super权限付给非管理用户,该mysqladmin processlist可以列举出当前执行的查询文本;super权限可用于切断客户端连接、改变服务器运行参数状态、控制拷贝复制数据库的服务器;
16.如果不相信dns服务公司的服务,可以在主机名称允许表中只设置ip数字地址;
17.使用max_user_connections变量来使mysqld服务进程,对一个指定帐户限定连接数;
18.grant语句也支持资源控制选项;
19.启动mysqld服务进程的安全选项开关,–local-infile=0或1,若是0则客户端程序就无法使用local load data了,赋权的一个例子grant insert(user) on mysql.user to ‘user_name’@'host_name’;若使用–skip-grant-tables系统将对任何用户的访问不做任何访问控制,但可以用 mysqladmin flush-privileges或mysqladmin reload来开启访问控制;默认情况是show databases语句对所有用户开放,可以用–skip-show-databases来关闭掉。
23.碰到error 1045(28000) access denied for user ‘root’@'localhost’ (using password:no)错误时,你需要重新设置密码,具体方法是:先用–skip-grant-tables参数启动mysqld,然后执行 mysql -u root mysql,mysql>update user set password=password(’newpassword’) where user=’root’;mysql>flush privileges;,最后重新启动mysql就可以了。
[if !supportLists]二、[endif]Oracle
[if !supportLists]1. [endif]什么是存储过程,使用存储过程的好处?(2017-11-25-wzz)
存储过程(Stored Procedure )是一组为了完成特定功能的SQL 语句集,经编译后存储在数据库中。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象,任何一个设计良好的数据库应用程序都应该用到存储过程。
优点:
(1)允许模块化程序设计,就是说只需要创建一次过程,以后在程序中就可以调用该过程任意次。
(2)允许更快执行,如果某操作需要执行大量 SQL 语句或重复执行,存储过程比 SQL 语句执行的要快。
(3)减少网络流量,例如一个需要数百行的 SQL 代码的操作有一条执行语句完成,不需要在网络中发
送数百行代码。
(4) 更好的安全机制,对于没有权限执行存储过程的用户,也可授权他们执行存储过程。
存储过程的具体使用详见:http://www.cnblogs.com/yank/p/4235609.html
[if !supportLists]2. [endif]Oracle存储过程怎么创建?(2017-11-25-wzz)
存储过程创建语法:
create or replace procedure存储过程名(param1 in type,param2 out type)
as
变量1类型(值范围);
变量2类型(值范围);
Begin
Select count(*) into变量1 from 表A where列名=param1;
If (判断条件) then
Select列名 into 变量2 from 表A where列名=param1;
Dbms_output。Put_line(‘打印信息’);
Elsif (判断条件) then
Dbms_output。Put_line(‘打印信息’);
Else
Raise异常名(NO_DATA_FOUND);
End if;
Exception
When others then
Rollback;
End;
注意事项:
1. 存储过程参数不带取值范围,in表示传入,out表示输出
2. 变量带取值范围,后面接分号
3. 在判断语句前最好先用count(*)函数判断是否存在该条操作记录
4. 用select 。。。into。。。给变量赋值
5. 在代码中抛异常用 raise+异常名
[if !supportLists]3. [endif]如何使用Oracle的游标?(2017-11-25-wzz)
参考博客:https://www.cnblogs.com/sc-xx/archive/2011/12/03/2275084.html
(1)、Oracle中的游标分为显示游标和隐式游标
(2)、显示游标是用cursor...is命令定义的游标,它可以对查询语句(select)返回的多条记录进行处理;
(3)、隐式游标是在执行插入 (insert)、删除(delete)、修改(update) 和返回单条记录的查询(select)语句时由PL/SQL自动定义的。
(4)、显式游标的操作:打开游标、操作游标、关闭游标;PL/SQL隐式地打开SQL游标,并在它内部处理SQL语句,然后关闭它。
[if !supportLists]4. [endif]Oracle中字符串用什么连接?(2017-11-25-wzz)
Oracle中使用 || 这个符号连接字符串 如 ‘abc’ || ‘d’ 的结果是abcd。
[if !supportLists]5. [endif]Oracle中是如何进行分页查询的?(2017-11-25-wzz)
Oracle中使用rownum来进行分页, 这个是效率最好的分页方法,hibernate也是使用rownum来进行Oralce分页的
select * from
( select rownum r,a from tabName where rownum <= 20 )
where r > 10
[if !supportLists]6. [endif]存储过程和存储函数的特点和区别?(2017-11-25-wzz)
特点:
[if !supportLists](1)[endif]、一般来说,存储过程实现的功能要复杂一点,而函数的实现的功能针对性比较强。
[if !supportLists](2)[endif]、对于存储过程来说可以返回参数,而函数只能返回值或者表对象。
[if !supportLists](3)[endif]、存储过程一般是作为一个独立的部分来执行,而函数可以作为查询语句的一个部分来调用,由于函数可以返回一个表对象,因此它可以在查询语句中位于FROM关键字的后面。
区别:
(1)、函数必须有返回值,而过程没有.
(2)、函数可以单独执行.而过程必须通过execute执行.
(3)、函数可以嵌入到SQL语句中执行.而过程不行.
其实我们可以将比较复杂的查询写成函数.然后到存储过程中去调用这些函数.
[if !supportLists]7. [endif]存储过程与SQL的对比?(2017-11-21-gxb)
优势:
1、提高性能 SQL语句在创建过程时进行分析和编译。 存储过程是预编译的,在首次运行一个存储过程时,查询优化器对其进行分析、优化,并给出最终被存在系统表中的存储计划,这样,在执行过程时便可节省此开销。2、降低网络开销 存储过程调用时只需用提供存储过程名和必要的参数信息,从而可降低网络的流量。3、便于进行代码移植 数据库专业人员可以随时对存储过程进行修改,但对应用程序源代码却毫无影响,从而极大的提高了程序的可移植性。4、更强的安全性 1)系统管理员可以对执行的某一个存储过程进行权限限制,避免非授权用户对数据的访问 2)在通过网络调用过程时,只有对执行过程的调用是可见的。 因此,恶意用户无法看到表和数据库对象名称、嵌入自己的 Transact-SQL 语句或搜索关键数据。 3)使用过程参数有助于避免 SQL 注入攻击。 因为参数输入被视作文字值而非可执行代码,所以,攻击者将命令插入过程内的 Transact-SQL 语句并损害安全性将更为困难。 4)可以对过程进行加密,这有助于对源代码进行模糊处理。
劣势:
1、存储过程需要专门的数据库开发人员进行维护,但实际情况是,往往由程序开发员人员兼职
2、设计逻辑变更,修改存储过程没有SQL灵活
[if !supportLists]8. [endif]你觉得存储过程和SQL语句该使用哪个?(2017-11-21-gxb)
1、在一些高效率或者规范性要求比较高的项目,建议采用存储过程 2、对于一般项目建议采用参数化命令方式,是存储过程与SQL语句一种折中的方式 3、对于一些算法要求比较高,涉及多条数据逻辑,建议采用存储过程
[if !supportLists]9. [endif]触发器的作用有哪些?(2017-11-21-gxb)
1)触发器可通过数据库中的相关表实现级联更改;通过级联引用完整性约束可以更有效地执行这些更改。
2)触发器可以强制比用 CHECK 约束定义的约束更为复杂的约束。与 CHECK 约束不同,触发器可以引用其它表中的列。例如,触发器可以使用另一个表中的 SELECT 比较插入或更新的数据,以及执行其它操作,如修改数据或显示用户定义错误信息。
3)触发器还可以强制执行业务规则
4)触发器也可以评估数据修改前后的表状态,并根据其差异采取对策。
参考资料:http://www.cnblogs.com/yank/p/4193820.html
[if !supportLists]10. [endif]在千万级的数据库查询中,如何提高效率?(2017-11-23-gxb)
1)数据库设计方面 a. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 b. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0
c. 并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
d. 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
e. 应尽可能的避免更新索引数据列,因为索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新索引数据列,那么需要考虑是否应将该索引建为索引。
f. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
g. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
h. 尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
i. 避免频繁创建和删除临时表,以减少系统表资源的消耗。
j. 临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
k. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
l. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
2)SQL语句方面
a. 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
b. 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num=10 or num=20 可以这样查询: select id from t where num=10 union all select id from t where num=20
c. in 和 not in 也要慎用,否则会导致全表扫描,如: select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了: select id from t where num between 1 and 3
d. 下面的查询也将导致全表扫描: select id from t where name like ‘%abc%’
e. 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描: select id from t where num=@num 可以改为强制查询使用索引: select id from t with(index(索引名)) where num=@num
f. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如: select id from t where num/2=100 应改为: select id from t where num=100*2
g. 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如: select id from t where substring(name,1,3)=‘abc’–name以abc开头的id select id from t where datediff(day,createdate,’2005-11-30′)=0–‘2005-11-30’生成的id 应改为: select id from t where name like ‘abc%’ select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′
h. 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
i. 不要写一些没有意义的查询,如需要生成一个空表结构: select col1,col2 into #t from t where 1=0 这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样: create table #t(…)
j. 很多时候用 exists 代替 in 是一个好的选择: select num from a where num in(select num from b) 用下面的语句替换: select num from a where exists(select 1 from b where num=a.num)
k. 任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
l. 尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
m. 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
n. 尽量避免大事务操作,提高系统并发能力。
3)java方面:重点内容
a.尽可能的少造对象。
b.合理摆正系统设计的位置。大量数据操作,和少量数据操作一定是分开的。大量的数据操作,肯定不是ORM框架搞定的。,
c.使用jDBC链接数据库操作数据
d.控制好内存,让数据流起来,而不是全部读到内存再处理,而是边读取边处理;
e.合理利用内存,有的数据要缓存
[if !supportLists]第七章 [endif]框架
[if !supportLists]一、[endif]SpringMVC
[if !supportLists]1. [endif]SpringMVC的工作原理(2017-11-13-lyq)
a. 用户向服务器发送请求,请求被springMVC前端控制器DispatchServlet捕获;
b. DispatcherServle对请求URL进行解析,得到请求资源标识符(URL),然后根据该URL调用HandlerMapping将请求映射到处理器HandlerExcutionChain;
c. DispatchServlet根据获得Handler选择一个合适的HandlerAdapter适配器处理;
d. Handler对数据处理完成以后将返回一个ModelAndView()对象给DisPatchServlet;
e. Handler返回的ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherSevlet通过ViewResolver试图解析器将逻辑视图转化为真正的视图View;
h. DispatcherServle通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view并返回给客户端;
[if !supportLists]2. [endif]SpringMVC常用注解都有哪些?(2017-11-24-gxb)
@requestMapping 用于请求 url 映射。
@RequestBody 注解实现接收 http 请求的 json 数据,将 json 数据转换为 java 对象。
@ResponseBody 注解实现将 controller 方法返回对象转化为 json 响应给客户。
[if !supportLists]3. [endif]如何开启注解处理器和适配器?(2017-11-24-gxb)
我们在项目中一般会在springmvc.xml 中通过开启 来实现注解处
理器和适配器的开启。
[if !supportLists]4. [endif]如何解决get和post乱码问题?(2017-11-24-gxb)
解决post 请求乱码:我们可以在 web.xml 里边配置一个 CharacterEncodingFilter 过滤器。 设置为 utf-8.
解决get 请求的乱码:有两种方法。对于 get 请求中文参数出现乱码解决方法有两个:
1. 修改tomcat 配置文件添加编码与工程编码一致。
2.另外一种方法对参数进行重新编码 String userName = New String(Request.getParameter(“userName”).getBytes(“ISO8859-1”), “utf-8”);
[if !supportLists]二、[endif]Spring
[if !supportLists]1. [endif]谈谈你对Spring的理解(2017-11-13-lyq)
Spring是一个开源框架,为简化企业级应用开发而生。Spring可以是使简单的JavaBean实现以前只有EJB才能实现的功能。Spring是一个IOC和AOP容器框架。
Spring容器的主要核心是:
控制反转(IOC),传统的java开发模式中,当需要一个对象时,我们会自己使用new或者getInstance等直接或者间接调用构造方法创建一个对象。而在spring开发模式中,spring容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring提供的对象就可以了,这是控制反转的思想。
依赖注入(DI),spring使用javaBean对象的set方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP底层是动态代理,如果是接口采用JDK动态代理,如果是类采用CGLIB方式实现动态代理。
[if !supportLists]2. [endif]Spring中的设计模式(2017-11-13-lyq)
a. 单例模式——spring中两种代理方式,若目标对象实现了若干接口,spring使用jdk的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring使用CGLIB库生成目标类的子类。
单例模式——在spring的配置文件中设置bean默认为单例模式。
b. 模板方式模式——用来解决代码重复的问题。
比如:RestTemplate、JmsTemplate、JpaTemplate
d. 前端控制器模式——spring提供了前端控制器DispatherServlet来对请求进行分发。
e. 试图帮助(view helper)——spring提供了一系列的JSP标签,高效宏来帮助将分散的代码整合在试图中。
f. 依赖注入——贯穿于BeanFactory/ApplacationContext接口的核心理念。
g. 工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring中使用beanFactory来创建对象的实例。
[if !supportLists]3. [endif]Spring的常用注解(2017-11-13-lyq)
Spring在2.5版本以后开始支持注解的方式来配置依赖注入。可以用注解的方式来代替xml中bean的描述。注解注入将会被容器在XML注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。
注解装配在spring中默认是关闭的。所以需要在spring的核心配置文件中配置一下才能使用基于注解的装配模式。配置方式如下:
常用的注解:
@Required:该注解应用于设值方法
@Autowired:该注解应用于有值设值方法、非设值方法、构造方法和变量。
@Qualifier:该注解和@Autowired搭配使用,用于消除特定bean自动装配的歧义。
[if !supportLists]4. [endif]简单介绍一下Spring bean的生命周期(2017-11-21-gxb)
bean定义:在配置文件里面用来进行定义。
bean初始化:有两种方式初始化:
1.在配置文件中通过指定init-method属性来完成
2.实现org.springframwork.beans.factory.InitializingBean接口
bean调用:有三种方式可以得到bean实例,并进行调用
bean销毁:销毁有两种方式
1.使用配置文件指定的destroy-method属性
2.实现org.springframwork.bean.factory.DisposeableBean接口
参考资料:https://www.cnblogs.com/zrtqsk/p/3735273.html
[if !supportLists]5. [endif]Spring结构图(2017-11-22-lyq)
[if !supportLists](1)[endif]核心容器:包括Core、Beans、Context、EL模块。
Core模块:封装了框架依赖的最底层部分,包括资源访问、类型转换及一些常用工具类。
Beans模块:提供了框架的基础部分,包括反转控制和依赖注入。其中Bean Factory是容器核心,本质是“工厂设计模式”的实现,而且无需编程实现“单例设计模式”,单例完全由容器控制,而且提倡面向接口编程,而非面向实现编程;所有应用程序对象及对象间关系由框架管理,从而真正把你从程序逻辑中把维护对象之间的依赖关系提取出来,所有这些依赖关系都由BeanFactory来维护。
Context模块:以Core和Beans为基础,集成Beans模块功能并添加资源绑定、数据验证、国际化、Java EE支持、容器生命周期、事件传播等;核心接口是ApplicationContext。
EL模块:提供强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从Spring 容器获取Bean,它也支持列表投影、选择和一般的列表聚合等。
(2)AOP、Aspects模块:
AOP模块:Spring AOP模块提供了符合 AOP Alliance规范的面向方面的编程(aspect-oriented programming)实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态的把这些功能添加到需要的代码中;这样各专其职,降低业务逻辑和通用功能的耦合。
Aspects模块:提供了对AspectJ的集成,AspectJ提供了比Spring ASP更强大的功能。
数据访问/集成模块:该模块包括了JDBC、ORM、OXM、JMS和事务管理。
事务模块:该模块用于Spring管理事务,只要是Spring管理对象都能得到Spring管理事务的好处,无需在代码中进行事务控制了,而且支持编程和声明性的事务管理。
JDBC模块:提供了一个JBDC的样例模板,使用这些模板能消除传统冗长的JDBC编码还有必须的事务控制,而且能享受到Spring管理事务的好处。
ORM模块:提供与流行的“对象-关系”映射框架的无缝集成,包括Hibernate、JPA、MyBatis等。而且可以使用Spring事务管理,无需额外控制事务。
OXM模块:提供了一个对Object/XML映射实现,将java对象映射成XML数据,或者将XML数据映射成java对象,Object/XML映射实现包括JAXB、Castor、XMLBeans和XStream。
JMS模块:用于JMS(Java Messaging Service),提供一套 “消息生产者、消息消费者”模板用于更加简单的使用JMS,JMS用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
Web/Remoting模块:Web/Remoting模块包含了Web、Web-Servlet、Web-Struts、Web-Porlet模块。
Web模块:提供了基础的web功能。例如多文件上传、集成IoC容器、远程过程访问(RMI、Hessian、Burlap)以及Web Service支持,并提供一个RestTemplate类来提供方便的Restful services访问。
Web-Servlet模块:提供了一个Spring MVC Web框架实现。Spring MVC框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的JSP标签,完全无缝与Spring其他技术协作。
Web-Struts模块:提供了与Struts无缝集成,Struts1.x 和Struts2.x都支持
Test模块: Spring支持Junit和TestNG测试框架,而且还额外提供了一些基于Spring的测试功能,比如在测试Web框架时,模拟Http请求的功能。
[if !supportLists]6. [endif]Spring能帮我们做什么?(2017-11-22-lyq)
[if !supportLists]a. [endif]Spring能帮我们根据配置文件创建及组装对象之间的依赖关系。
Spring根据配置文件来进行创建及组装对象间依赖关系,只需要改配置文件即可
[if !supportLists]b. [endif]Spring 面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。
Spring 面向切面编程能提供一种更好的方式来完成,一般通过配置方式,而且不需要在现有代码中添加任何额外代码,现有代码专注业务逻辑。
[if !supportLists]c. [endif]Spring能非常简单的帮我们管理数据库事务。
采用Spring,我们只需获取连接,执行SQL,其他事物相关的都交给Spring来管理了。
[if !supportLists]d. [endif]Spring还能与第三方数据库访问框架(如Hibernate、JPA)无缝集成,而且自己也提供了一套JDBC访问模板,来方便数据库访问。
[if !supportLists]e. [endif]Spring还能与第三方Web(如Struts、JSF)框架无缝集成,而且自己也提供了一套Spring MVC框架,来方便web层搭建。
[if !supportLists]f. [endif]Spring能方便的与Java EE(如Java Mail、任务调度)整合,与更多技术整合(比如缓存框架)。
[if !supportLists]7. [endif]请描述一下Spring的事务(2017-11-22-lyq)
声明式事务管理的定义:用在Spring配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。
基于TransactionInterceptor 的声明式事务管理:两个次要的属性: transactionManager,用来指定一个事务治理器,并将具体事务相关的操作请托给它;其他一个是 Properties 类型的 transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符,而值就是表现呼应方法的所运用的事务属性。
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]......
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] class="org.springframework.transaction.interceptor.TransactionInterceptor">
[if !supportLists]5. [endif]
[if !supportLists]6. [endif]
[if !supportLists]7. [endif]
[if !supportLists]8. [endif] PROPAGATION_REQUIRED
[if !supportLists]9. [endif]
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]
[if !supportLists]12. [endif]
[if !supportLists]13. [endif] class="footmark.spring.core.tx.declare.origin.BankServiceImpl">
[if !supportLists]14. [endif]
[if !supportLists]15. [endif]
[if !supportLists]16. [endif]
[if !supportLists]17. [endif] class="org.springframework.aop.framework.ProxyFactoryBean">
[if !supportLists]18. [endif]
[if !supportLists]19. [endif]
[if !supportLists]20. [endif]
[if !supportLists]21. [endif]
[if !supportLists]22. [endif]
[if !supportLists]23. [endif]
[if !supportLists]24. [endif]
[if !supportLists]25. [endif]......
[if !supportLists]26. [endif]
基于TransactionProxyFactoryBean 的声明式事务管理:设置配置文件与先前比照简化了许多。我们把这类设置配置文件格式称为 Spring 经典的声明式事务治理
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]......
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] class="footmark.spring.core.tx.declare.classic.BankServiceImpl">
[if !supportLists]5. [endif]
[if !supportLists]6. [endif]
[if !supportLists]7. [endif]
[if !supportLists]8. [endif] class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
[if !supportLists]9. [endif]
[if !supportLists]10. [endif]
[if !supportLists]11. [endif]
[if !supportLists]12. [endif]
[if !supportLists]13. [endif] PROPAGATION_REQUIRED
[if !supportLists]14. [endif]
[if !supportLists]15. [endif]
[if !supportLists]16. [endif]
[if !supportLists]17. [endif]......
[if !supportLists]18. [endif]
基于 命名空间的声明式事务治理:在前两种方法的基础上,Spring 2.x 引入了 命名空间,连络行使 命名空间,带给开发人员设置配备声明式事务的全新体验。
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]......
[if !supportLists]3. [endif]
[if !supportLists]4. [endif] class="footmark.spring.core.tx.declare.namespace.BankServiceImpl">
[if !supportLists]5. [endif]
[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]
[if !supportLists]15. [endif]
[if !supportLists]16. [endif]
[if !supportLists]17. [endif]......
[if !supportLists]18. [endif]
基于@Transactional 的声明式事务管理:Spring 2.x 还引入了基于 Annotation 的体式格式,具体次要触及@Transactional 标注。@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切 public 方法将都具有该类型的事务属性。
[if !supportLists]1. [endif]@Transactional(propagation = Propagation.REQUIRED)
[if !supportLists]2. [endif]public boolean transfer(Long fromId, Long toId, double amount) {
[if !supportLists]3. [endif] return bankDao.transfer(fromId, toId, amount);
[if !supportLists]4. [endif]}
编程式事物管理的定义:在代码中显式挪用beginTransaction()、commit()、rollback()等事务治理相关的方法,这就是编程式事务管理。Spring对事物的编程式管理有基于底层API的编程式管理和基于 TransactionTemplate 的编程式事务管理两种方式。
基于底层API的编程式管理:凭证PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三个焦点接口,来实现编程式事务管理。
[if !supportLists]5. [endif]Public class BankServiceImpl implements BancService{
[if !supportLists]6. [endif]Private BanckDao bankDao;
[if !supportLists]7. [endif]private TransactionDefinition txDefinition;
[if !supportLists]8. [endif]private PlatformTransactionManager txManager;
[if !supportLists]9. [endif]......
[if !supportLists]10. [endif]public boolean transfer(Long fromId, Long toId, double amount) {
[if !supportLists]11. [endif]TransactionStatus txStatus = txManager.getTransaction(txDefinition);
[if !supportLists]12. [endif]boolean result = false;
[if !supportLists]13. [endif]try {
[if !supportLists]14. [endif]result = bankDao.transfer(fromId, toId, amount);
[if !supportLists]15. [endif]txManager.commit(txStatus);
[if !supportLists]16. [endif]} catch (Exception e) {
[if !supportLists]17. [endif]result = false;
[if !supportLists]18. [endif]txManager.rollback(txStatus);
[if !supportLists]19. [endif]System.out.println("Transfer Error!");
[if !supportLists]20. [endif]}
[if !supportLists]21. [endif]return result;
[if !supportLists]22. [endif]}
[if !supportLists]23. [endif]}
基于TransactionTemplate 的编程式事务管理:为了不损坏代码原有的条理性,避免出现每一个方法中都包括相同的启动事物、提交、回滚事物样板代码的现象,spring提供了transactionTemplate模板来实现编程式事务管理。
[if !supportLists]1. [endif]public class BankServiceImpl implements BankService {
[if !supportLists]2. [endif]private BankDao bankDao;
[if !supportLists]3. [endif]private TransactionTemplate transactionTemplate;
[if !supportLists]4. [endif]......
[if !supportLists]5. [endif]public boolean transfer(final Long fromId, final Long toId, final double amount) {
[if !supportLists]6. [endif]return (Boolean) transactionTemplate.execute(new TransactionCallback(){
[if !supportLists]7. [endif]public Object doInTransaction(TransactionStatus status) {
[if !supportLists]8. [endif]Object result;
[if !supportLists]9. [endif]try {
[if !supportLists]10. [endif]result = bankDao.transfer(fromId, toId, amount);
[if !supportLists]11. [endif]} catch (Exception e) {
[if !supportLists]12. [endif]status.setRollbackOnly();
[if !supportLists]13. [endif]result = false;
[if !supportLists]14. [endif]System.out.println("Transfer Error!");
[if !supportLists]15. [endif]}
[if !supportLists]16. [endif]return result;
[if !supportLists]17. [endif]}
[if !supportLists]18. [endif]});
[if !supportLists]19. [endif]}
[if !supportLists]20. [endif]}
编程式事务与声明式事务的区别:
1)编程式事务是自己写事务处理的类,然后调用
2)声明式事务是在配置文件中配置,一般搭配在框架里面使用!
[if !supportLists]8. [endif]BeanFactory 常用的实现类有哪些?(2017-12-03-gxb)
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。常用的BeanFactory 实现有DefaultListableBeanFactory 、 XmlBeanFactory 、 ApplicationContext等。
XMLBeanFactory,最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
[if !supportLists]9. [endif]解释Spring JDBC、Spring DAO和Spring ORM(2017-12-03-gxb)
Spring-DAO 并非 Spring 的一个模块,它实际上是指示你写 DAO 操作、写好 DAO 操作的一些规范。因此,对于访问你的数据它既没有提供接口也没有提供实现更没有提供模板。在写一个 DAO 的时候,你应该使用 @Repository 对其进行注解,这样底层技术(JDBC,Hibernate,JPA,等等)的相关异常才能一致性地翻译为相应的 DataAccessException 子类。
Spring-JDBC 提供了 Jdbc 模板类,它移除了连接代码以帮你专注于 SQL 查询和相关参数。Spring-JDBC 还提供了一个 JdbcDaoSupport,这样你可以对你的 DAO 进行扩展开发。它主要定义了两个属性:一个 DataSource 和一个 JdbcTemplate,它们都可以用来实现 DAO 方法。JdbcDaoSupport 还提供了一个将 SQL 异常转换为 Spring DataAccessExceptions 的异常翻译器。
Spring-ORM 是一个囊括了很多持久层技术(JPA,JDO,Hibernate,iBatis)的总括模块。对于这些技术中的每一个,Spring 都提供了集成类,这样每一种技术都能够在遵循 Spring 的配置原则下进行使用,并平稳地和 Spring 事务管理进行集成。
对于每一种技术,配置主要在于将一个DataSource bean 注入到某种 SessionFactory 或者 EntityManagerFactory 等 bean 中。纯 JDBC 不需要这样的一个集成类(JdbcTemplate 除外),因为 JDBC 仅依赖于一个 DataSource。
如果你计划使用一种ORM 技术,比如 JPA 或者 Hibernate,那么你就不需要 Spring-JDBC 模块了,你需要的是这个 Spring-ORM 模块。
[if !supportLists]10. [endif]简单介绍一下Spring WEB 模块。(2017-12-03-gxb)
Spring的WEB模块是构建在application context 模块基础之上,提供一个适合web应用的上下文。这个模块也包括支持多种面向web的任务,如透明地处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。它也有对Jakarta Struts的支持。
[if !supportLists]11. [endif]Spring配置文件有什么作用?(2017-12-03-gxb)
Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。
[if !supportLists]12. [endif]什么是Spring IOC 容器?(2017-12-03-gxb)
IOC 控制反转:Spring IOC 负责创建对象,管理对象。通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
[if !supportLists]13. [endif]IOC的优点是什么?
IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。
[if !supportLists]14. [endif]ApplicationContext的实现类有哪些?(2017-12-03-gxb)
FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。
WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。
[if !supportLists]15. [endif]BeanFactory与AppliacationContext有什么区别(2017-12-03-gxb)
1. BeanFactory
基础类型的IOC容器,提供完成的IOC服务支持。如果没有特殊指定,默认采用延迟初始化策略。相对来说,容器启动初期速度较快,所需资源有限。
2.ApplicationContext
ApplicationContext是在BeanFactory的基础上构建,是相对比较高级的容器实现,除了BeanFactory的所有支持外,ApplicationContext还提供了事件发布、国际化支持等功能。ApplicationContext管理的对象,在容器启动后默认全部初始化并且绑定完成。
[if !supportLists]16. [endif]什么是Spring的依赖注入?(2017-12-04-gxb)
平常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做。
[if !supportLists]17. [endif]有哪些不同类型的IOC(依赖注入)方式?(2017-12-04-gxb)
Spring提供了多种依赖注入的方式。
1.Set注入
2.构造器注入
3.静态工厂的方法注入
4.实例工厂的方法注入
参考资料:https://www.cnblogs.com/java-class/p/4727775.html
[if !supportLists]18. [endif]什么是Spring beans?(2017-12-04-gxb)
Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中 的形式定义。
Spring 框架定义的beans都是单例beans。
[if !supportLists]19. [endif]一个Spring Beans的定义需要包含什么?(2017-12-04-gxb)
一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。
[if !supportLists]20. [endif]你怎样定义类的作用域?(2017-12-04-gxb)
当定义一个 在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如,当Spring要在需要的时候每次生产一个新的bean实例,bean的scope属性被指定为prototype。另一方面,一个bean每次使用的时候必须返回同一个实例,这个bean的scope 属性必须设为 singleton。
[if !supportLists]21. [endif]Spring支持的几种bean的作用域。(2017-12-04-gxb)
Spring框架支持以下五种bean的作用域:
singleton : bean在每个Spring ioc 容器中只有一个实例。
prototype:一个bean的定义可以有多个实例。
request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
缺省的Spring bean 的作用域是Singleton。
[if !supportLists]22. [endif]Spring框架中的单例bean是线程安全的吗?(2017-12-04-gxb)
Spring框架中的单例bean不是线程安全的。
[if !supportLists]23. [endif]什么是Spring的内部bean?(2017-12-04-gxb)
当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean,为了定义inner bean,在Spring 的 基于XML的 配置元数据中,可以在 或 元素内使用 元素,内部bean通常是匿名的,它们的Scope一般是prototype。
[if !supportLists]24. [endif]在Spring中如何注入一个java集合?(2017-12-04-gxb)
Spring提供以下几种集合的配置元素:
类型用于注入一列值,允许有相同的值。
类型用于注入一组值,不允许有相同的值。
类型用于注入一组键值对,键和值都可以为任意类型。
类型用于注入一组键值对,键和值都只能为String类型。
[if !supportLists]25. [endif]什么是bean的自动装配?(2017-12-04-gxb)
无须在Spring配置文件中描述javaBean之间的依赖关系(如配置、)。IOC容器会自动建立javabean之间的关联关系。
[if !supportLists]26. [endif]解释不同方式的自动装配。(2017-12-04-gxb)
有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。
1)no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。
2)byName:通过参数名自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
3)byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
4)constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
5)autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。
[if !supportLists]27. [endif]什么是基于Java的Spring注解配置? 给一些注解的例子(2017-12-05-gxb)
基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。
以@Configuration 注解为例,它用来标记类可以当做一个bean的定义,被Spring IOC容器使用。另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。
[if !supportLists]28. [endif]什么是基于注解的容器配置?(2017-12-05-gxb)
相对于XML文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明。开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不是使用xml表述bean的装配关系。
[if !supportLists]29. [endif]怎样开启注解装配?(2017-12-05-gxb)
注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 元素。
[if !supportLists]30. [endif]在Spring框架中如何更有效地使用JDBC?(2017-12-05-gxb)
使用SpringJDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate。
JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理。
[if !supportLists]31. [endif]使用Spring通过什么方式访问Hibernate?(2017-12-05-gxb)
在Spring中有两种方式访问Hibernate:
1)控制反转HibernateTemplate和 Callback。
2)继承HibernateDAOSupport提供一个AOP 拦截器。
[if !supportLists]32. [endif]Spring支持的ORM框架有哪些?(2017-12-05-gxb)
Spring支持以下ORM:
Hibernate、iBatis、JPA (Java Persistence API)、TopLink、JDO (Java Data Objects)、OJB
[if !supportLists]33. [endif]简单解释一下spring的AOP(2017-12-05-gxb)
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。
[if !supportLists]34. [endif]在Spring AOP 中,关注点和横切关注的区别是什么?(2017-12-05-gxb)
关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。
[if !supportLists]35. [endif]什么是连接点?(2017-12-05-gxb)
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
[if !supportLists]36. [endif]Spring的通知是什么?有哪几种类型?(2017-12-05-gxb)
通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用五种类型的通知:
1)before:前置通知,在一个方法执行前被调用。
2)after: 在方法执行之后调用的通知,无论方法执行是否成功。
3)after-returning: 仅当方法成功完成后执行的通知。
4)after-throwing: 在方法抛出异常退出时执行的通知。
5)around: 在方法执行之前和之后调用的通知。
[if !supportLists]37. [endif]什么是切点?(2017-12-05-gxb)
切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。
[if !supportLists]38. [endif]什么是目标对象?(2017-12-05-gxb)
被一个或者多个切面所通知的对象。它通常是一个代理对象。也指被通知(advised)对象。
[if !supportLists]39. [endif]什么是代理?(2017-12-05-gxb)
代理是通知目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。
[if !supportLists]40. [endif]什么是织入?什么是织入应用的不同点?(2017-12-05-gxb)
织入是将切面和到其他应用类型或对象连接或创建一个被通知对象的过程。织入可以在编译时,加载时,或运行时完成。
[if !supportLists]三、[endif]Shiro
[if !supportLists]1. [endif]简单介绍一下Shiro框架(2017-11-23-gxb)
Apache Shiro是Java的一个安全框架。使用shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。 Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。 Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
[if !supportLists]2. [endif]Shiro主要的四个组件(2017-12-2-wzz)
1)SecurityManager
典型的Facade,Shiro 通过它对外提供安全管理的各种服务。 ü
2)Authenticator
对“Who are you ?”进行核实。通常涉及用户名和密码。 这个组件负责收集 principals 和 credentials,并将它们提交给应用系统。如果提交的 credentials 跟应用系统中提供的 credentials 吻合,就能够继续访问,否则需要重新提交 principals 和 credentials, 或者直接终止访问。 ü
3)Authorizer
身份份验证通过后,由这个组件对登录人员进行访问控制的筛查,比如“who can do what”, 或者“who can do which actions”。 Shiro 采用“基于 Realm”的方法,即用户(又称 Subject)、 用户组、角 色和 permission 的聚合体。
4)Session Manager
这个组件保证了异构客户端的访问,配置简单。它是基于POJO/J2SE 的,不跟任何的客户 端或者协议绑定。
[if !supportLists]3. [endif]Shiro运行原理(2017-12-2-wzz)
1、Application Code:应用程序代码,就是我们自己的编码,如果在程序中需要进 行权限控制,需要调用 Subject 的 API。
2、Subject:主体,代表的了当前用户。所有的 Subject 都绑定到 SecurityManager, 与 Subject 的所有交互都会委托给 SecurityManager,可以将 Subject 当成一个 门面,而真正执行者是 SecurityManager 。ü
3、SecurityManage:安全管理器,所有与安全有关的操作都会与 SecurityManager 交互,并且它管理所有的 Subject 。ü
4、Realm:域 shiro 是从 Realm 来获取安全数据(用户,角色,权限)。就是说 SecurityManager
要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户 身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否 能进行操作; 可以把 Realm 看成 DataSource,即安全数据源 。
[if !supportLists]4. [endif]Shiro的四种权限控制方式(2017-12-2-wzz)
1)url 级别权限控制
2)方法注解权限控制
3)代码级别权限控制
页面标签权限控制
详见网址:https://www.cnblogs.com/cocosili/p/7103025.html
[if !supportLists]5. [endif]授权实现的流程(2017-12-2-wzz)
(1)、什么是粗颗粒和细颗粒权限?
对资源类型的管理称为粗颗粒度权限控制,即只控制到菜单、按钮、方法,粗粒度的例子比如:用户具有用 户管理的权限,具有导出订单明细的权限。对资源实例的控制称为细颗粒度权限管理,即控制到数据级别的权限, 比如:用户只允许修改本部门的员工信息,用户只允许导出自己创建的订单明细。
总结:
粗颗粒权限:针对url链接的控制。
细颗粒权限:针对数据级别的控制。
比如:查询用户权限。
卫生局可以查询所有用户。
卫生室可以查询本单位的用户。
通常在service中编程实现。
(2)、粗颗粒和细颗粒如何授权?
对于粗颗粒度的授权可以很容易做系统架构级别的功能,即系统功能操作使用统一的粗颗粒度的权限管理。
对于细颗粒度的授权不建议做成系统架构级别的功能,因为对数据级别的控制是系统的业务需求,随着业务需求的变更业务功能变化的可能性很大,建议对数据级别的权限控制在业务层个性化开发,比如:用户只允许修改自己创建的商品信息可以在service接口添加校验实现,service接口需要传入当前操作人的标识,与商品信息创建人标识对比,不一致则不允许修改商品信息。
粗颗粒权限:可以使用过虑器统一拦截url。
细颗粒权限:在service中控制,在程序级别来控制,个性化 编程。
[if !supportLists]四、[endif]Mybatis
[if !supportLists]1. [endif]Mybatis中#和$的区别?(2017-11-23-gxb)
#相当于对数据 加上 双引号,$相当于直接显示数据
[if !supportLists]1. [endif]#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id".2. $将传入的数据直接显示生成在sql中。如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为order by id.3. #方式能够很大程度防止sql注入。4.$方式无法防止Sql注入。5.$方式一般用于传入数据库对象,例如传入表名.6.一般能用#的就别用$.
[if !supportLists]2. [endif]Mybatis的编程步骤是什么样的?(2017-12-2-wzz)
1、创建SqlSessionFactory
2、通过SqlSessionFactory创建SqlSession
3、通过sqlsession执行数据库操作
4、调用session.commit()提交事务
5、调用session.close()关闭会话
[if !supportLists]3. [endif]JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的?(2017-12-2-wzz)
1. 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。
2. Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3. 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决:Mybatis自动将java对象映射至sql语句。
4. 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象。
[if !supportLists]4. [endif]使用MyBatis的mapper接口调用时有哪些要求?(2017-12-2-wzz)
1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同
2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同
3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
4. Mapper.xml文件中的namespace即是mapper接口的类路径。
[if !supportLists]5. [endif]Mybatis中一级缓存与二级缓存?(2017-12-4-lyq)
[if !supportLists]1. [endif]一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
[if !supportLists]2. [endif]二级缓存与一级缓存其机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。作用域为namespance是指对该namespance对应的配置文件中所有的select操作结果都缓存,这样不同线程之间就可以共用二级缓存。启动二级缓存:在mapper配置文件中:。
二级缓存可以设置返回的缓存对象策略:。当readOnly="true"时,表示二级缓存返回给所有调用者同一个缓存对象实例,调用者可以update获取的缓存实例,但是这样可能会造成其他调用者出现数据不一致的情况(因为所有调用者调用的是同一个实例)。当readOnly="false"时,返回给调用者的是二级缓存总缓存对象的拷贝,即不同调用者获取的是缓存对象不同的实例,这样调用者对各自的缓存对象的修改不会影响到其他的调用者,即是安全的,所以默认是readOnly="false";
[if !supportLists]3. [endif]对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
[if !supportLists]6. [endif]MyBatis在insert插入操作时返回主键ID(2017-12-4-lyq)
数据库为MySql时:
[if !supportLists]1. [endif]
“keyProperty”表示返回的id要保存到对象的那个属性中,“useGeneratedKeys”表示主键id为自增长模式。MySQL中做以上配置就OK了
数据库为Oracle时:
[if !supportLists]1. [endif]
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] SELECT SEQ_USER.NEXTVAL as userId from DUAL
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] insert into user (user_id, user_name, modified, state)
[if !supportLists]6. [endif] values (#{userId,jdbcType=INTEGER}, #{userName,jdbcType=VARCHAR}, #{modified,jdbcType=TIMESTAMP}, #{state,jdbcType=INTEGER})
[if !supportLists]7. [endif]
由于Oracle没有自增长一说法,只有序列这种模仿自增的形式,所以不能再使用“useGeneratedKeys”属性。
而是使用将ID获取并赋值到对象的属性中,insert插入操作时正常插入id。
[if !supportLists]五、[endif]Struts2
[if !supportLists]1. [endif]简单介绍一下Struts2(2017-11-24-gxb)
Struts2 框架是一个按照 MVC 设计模式设计的 WEB 层框架,是在 struts 1 和 WebWork 的技术基础上进行了合并的全新的框架。其全新的 Struts 2 的体系结构与 Struts 1 的体 系结构差别巨大。Struts 2 以 WebWork 为核心,采用拦截器的机制来处理用户的请求, 这样的设计也使得业务逻辑控制器能够与 ServletAPI 完全脱离开。
我们可以把struts2 理解为一个大大的 servlet,而这个 servlet 就是 ActionServlet。struts2 在处理客户端请求时,会先读取 web.xml 配置文件,根据前端控制器将符合条件的请求 分给各个不同的 Action 处理。 在此之前,会把 ActionServlet 会把数据封装成一个 javaBean。
Struts2 框架提供了许多的拦截器,在封装数据的过程中,我们可以对数据进行一些操 作,例如:数据校验等等。
当Action 执行完后要返回一个结果视图,这个结果视图可以跟据 struts2 的配置文件中 配置,选择转发或者重定向。
[if !supportLists]2. [endif]Struts2的执行流程了解么?(2017-11-24-gxb)
参考资料:http://blog.csdn.net/wjw0130/article/details/46371847
Struts2的官方文档附带了Struts2的架构图。从这张图可以很好的去理解Struts2
关于图中的Key:
Servlet Filters:过滤器链,客户端的所有请求都要经过Filter链的处理。
Struts Core:Struts2的核心部分,但是Struts2已经帮我们做好了,我们不需要去做这个
Interceptors,Struts2的拦截器。Struts2提供了很多默认的拦截器,可以完成日常开发的绝大部分工作;而我们自定义的拦截器,用来实现实际的客户业务需要的功能。
User Created,由开发人员创建的,包括struts.xml、Action、Template,这些是每个使用Struts2来进行开发的人员都必须会的。
[if !supportLists]1. [endif]FilterDispatcher是整个Struts2的调度中心,也就是MVC中的C(控制中心),根据ActionMapper的结果来决定是否处理请求,如果ActionMapper指出该URL应该被Struts2处理,那么它将会执行Action处理,并停止过滤器链上还没有执行的过滤器。
[if !supportLists]2. [endif]ActionMapper 会判断这个请求是否应该被Struts2处理,如果需要Struts2处理,ActionMapper会返回一个对象来描述请求对应的ActionInvocation的信息。
[if !supportLists]3. [endif]ActionProxy,它会创建一个ActionInvocation实例,位于Action和xwork之间,使得我们在将来有机会引入更多的实现方式,比如通过WebService来实现等。
[if !supportLists]4. [endif]ConfigurationManager是xwork配置的管理中心,可以把它看做struts.xml这个配置文件在内存中的对应。
[if !supportLists]5. [endif]struts.xml,是开发人员必须光顾的地方。是Stuts2的应用配置文件,负责诸如URL与Action之间映射关系的配置、以及执行后页面跳转的Result配置等。
[if !supportLists]6. [endif]ActionInvocation:真正调用并执行Action,它拥有一个Action实例和这个Action所依赖的拦截器实例。ActionInvocation会按照指定的顺序去执行这些拦截器、Action以及相应的Result。
Interceptor(拦截器):是Struts2的基石,类似于JavaWeb的Filter,拦截器是一些无状态的类,拦截器可以自动拦截Action,它们给开发者提供了在Action运行之前或Result运行之后来执行一些功能代码的机会。
[if !supportLists]7. [endif]Action:用来处理请求,封装数据。
[if !supportLists]3. [endif]Struts2中Action配置的注意事项有哪些?(2017-11-24-gxb)
[if !supportLists]1. [endif]name 包名称,在 struts2 的配置文件中,包名不能重复,name 并不是真正包名,只是为
了管理Action
[if !supportLists]2. [endif]namespace 和 的 name 属性,决定 Action 的访问路径 (以/开始 )
[if !supportLists]3. [endif]extends 继承哪个包,通常开发中继承 struts-default 包 (struts-default 包 在 struts-default.xml 中定义 )【可以使用包中默认的拦截器和结果集】
[if !supportLists]4. [endif]拦截器和过滤器有哪些区别?(2017-11-24-gxb)
* 拦截器是基于 java 的反射机制的,而过滤器是基于函数回调
* 拦截器不依赖与 servlet 容器,而过滤器依赖与 servlet 容器
* 拦截器只能对 action 请求起作用,而过滤器则可以对几乎所有的请求起作用
* 拦截器可以访问 action 上下文、值栈里的对象,而过滤器不能
* 在 action 的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一 次
[if !supportLists]5. [endif]Struts2的封装方式有哪些?(2017-11-24-gxb)
一、属性封装
1. 在action中设置成员变量,变量名与表单中的name属性值相同
2. 生成变量的set方法
实例:获取用户输入的用户名和密码
jsp页面如下:
java代码如下:
二、模型驱动(常用)
1. action实现 ModeDriven 接口
2. 在action里创建实体类对象
3. 实现接口的getModel方法并返回所创建的对象
示例:获取用户输入的用户名和密码
jsp页面如下:
java代码如下:
需注意的是表单name的值应与类的属性名相同。
三、表达式封装
1. 在action 中声明实体类
2. 生成实体类的set 和 get 方法
3. 在表单输入项的name 属性值里面写表达式
jsp页面如下:
java代码如下:
[if !supportLists]6. [endif]简单介绍一下Struts2的值栈。(2017-11-24-gxb)
值栈是对应每一个请求对象的数据存储中心。Struts2的一个很重要的特点就是引入了值栈。之前我们通过缓存或者模型驱动在action和页面之间传递数据,数据混乱,并且
难以管理,缓存还有时间和数量限制,使用起来非常的困难。值栈的引入解决了这个问题,它可以统一管理页面和action之间的数据,供action、result、interceptor等使用。我们大多数情
况下不需要考虑值栈在哪里,里面有什么,只需要去获取自己需要的数据就可以了,大大的降低了开发人员的工作量和逻辑复杂性。
参考资料:https://www.cnblogs.com/hlhdidi/p/6185836.html
[if !supportLists]7. [endif]SpringMVC和Struts2的区别?(2017-11-23-gxb)
1、Struts2是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应,所以说从架构本身上SpringMVC就容易实现restful url,而struts2的架构实现起来要费劲,因为Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了。
2、由上边原因,SpringMVC的方法之间基本上独立的,独享request response数据,请求数据通过参数获取,处理结果通过ModelMap交回给框架,方法之间不共享变量,而Struts2搞的就比较乱,虽然方法之间也是独立的,但其所有Action变量是共享的,这不会影响程序运行,却给我们编码 读程序时带来麻烦,每次来了请求就创建一个Action,一个Action对象对应一个request上下文。 3、由于Struts2需要针对每个request进行封装,把request,session等servlet生命周期的变量封装成一个一个Map,供给每个Action使用,并保证线程安全,所以在原则上,是比较耗费内存的。
4、 拦截器实现机制上,Struts2有以自己的interceptor机制,SpringMVC用的是独立的AOP方式,这样导致Struts2的配置文件量还是比SpringMVC大。
5、SpringMVC的入口是servlet,而Struts2是filter(这里要指出,filter和servlet是不同的。),这就导致了二者的机制不同,这里就牵涉到servlet和filter的区别了。
6、SpringMVC集成了Ajax,使用非常方便,只需一个注解@ResponseBody就可以实现,然后直接返回响应文本即可,而Struts2拦截器集成了Ajax,在Action中处理时一般必须安装插件或者自己写代码集成进去,使用起来也相对不方便。
7、SpringMVC验证支持JSR303,处理起来相对更加灵活方便,而Struts2验证比较繁琐,感觉太烦乱。
8、Spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高(当然Struts2也可以通过不同的目录结构和相关配置做到SpringMVC一样的效果,但是需要xml配置的地方不少)。
9、 设计思想上,Struts2更加符合OOP的编程思想, SpringMVC就比较谨慎,在servlet上扩展。
10、SpringMVC开发效率和性能高于Struts2。 11、SpringMVC可以认为已经100%零配置。
[if !supportLists]8. [endif]Struts2中的 # 和 % 分别是做什么的?(2017-11-30-wzz)
(1)使用#获取context里面数据
(2)向request域放值(获取context里面数据,写ognl时候,首先添加符号#context的key名称.域对象名称)
(3)在页面中使用ognl获取
(4)%在struts2标签中表单标签
在struts2标签里面使用ognl表达式,如果直接在struts2表单标签里面使用ognl表达式不识别,只有%之后才会识别。
[if !supportLists]9. [endif]Struts2中有哪些常用结果类型?(2017-12-1-lyq)
1)dispatcher :默认的请求转发的结果类型,Action 转发给 JSP
2) chain :Action转发到另一个Action (同一次请求)
3) redirect : 重定向,重定向到一个路径信息,路径信息没有限制(不在一个请求中),Action重定向到 JSP
4) redirectAction :Action重定向到另一个Action
5)stream :将原始数据作为流传递回浏览器端,该结果类型对下载的内容和图片非常有用。
6)freemarker :呈现freemarker模板。
7)plaintext :返回普通文本内容。
[if !supportLists]六、[endif]Hibernate
[if !supportLists]1. [endif]简述一下hibernate的开发流程(2017-11-24-gxb)
第一步:加载 hibernate 的配置文件,读取配置文件的参数(jdbc 连接参数,数据 库方言,hbm 表与对象关系映射文件)
第二步:创建 SessionFactory 会话工厂(内部有连接池)
第三步:打开 session 获取连接,构造 session 对象(一次会话维持一个数据连接,也是一级缓存)
第四步:开启事务
第五步:进行操作
第六步:提交事务
第七步:关闭 session(会话)将连接释放
第八步:关闭连接池
[if !supportLists]2. [endif]hibernate中对象的三种状态(2017-11-24-gxb)
瞬时态(临时态、自由态):不存在持久化标识 OID,尚未与 Hibernate Session 关联对象, 被认为处于瞬时态,失去引用将被 JVM 回收
持久态:存在持久化标识 OID,与当前 session 有关联,并且相关联的 session 没有关闭 , 并且事务未提交
脱管态(离线态、游离态):存在持久化标识 OID,但没有与当前 session 关联,脱管状态 改变 hibernate 不能检测到
[if !supportLists]3. [endif]hibernate的缓存机制。(2017-11-24-gxb)
Hibernate缓存分为两层:Hibernate的一级缓存和Hibernate二级缓存。
1.Hibernate一级缓存(Session的缓存):
(1)Session实现了第一级Cache,属于事务级数据缓冲。一旦事务结束,缓存随之失效。一个Session的生命周期对应一个数据库事务或一个程序事务。
(2)Session-Cache总是被打开并且不能被关闭的。
(3)Session-Cache保证一个Session中两次请求同一个对象时,取得的对象是同一个Java实例,有时它可以避免不必要的数据冲突。
a.在对于同一个对象进行循环引用时,不至于产生堆栈溢出。
b.当数据库事务结束时,对于同一数据表行,不会产生数据冲突。因为对于数据库中的一行,最多有一个对象来表示它。
c.一个事务中可能会有很多个处理单元,在每一个处理单元中做的操作都会立即被其他的数据单元得知。
2.Hibernate二级缓存(SessionFactory的缓存):
(1)二级缓存是SessionFactory范围内的缓存,所有的Session共享同一个二级缓存。在二级缓存中保存持久化实例的散装形式的数据。
(2)持久化不同的数据需要不同的Cache策略,比如一些因素将影响Cache策略的选择:数据的读/写比例、数据表是否能被其他的应用程序所访问等。
(3)设置Hibernate二级缓存需要分两步:首先,确认使用什么数据并发策略。然后,配置缓存过期时间并设置Cache提供器。
[if !supportLists]4. [endif]Hibernate的查询方式有哪些?(2017-11-24-gxb)
Hibernate的查询方式常见的主要分为三种: HQL, QBC(命名查询), 以及使用原生SQL查询(SqlQuery)
参考资料:http://blog.csdn.net/u010963948/article/details/16818043
[if !supportLists]5. [endif]Hibernate和Mybatis的区别?(2017-11-23-gxb)
两者相同点:
1)Hibernate与MyBatis都可以是通过SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory 生成Session,最后由Session来开启执行事务和SQL语句。其中SessionFactoryBuider,SessionFactory,Session的生命周期都是差不多的。
2)Hibernate和MyBatis都支持JDBC和JTA事务处理。
Mybatis优势:
1)MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
2)MyBatis容易掌握,而Hibernate门槛较高。
Hibernate优势:
1)Hibernate的DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
2)Hibernate对对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便。
3)Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
4)Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。
[if !supportLists]6. [endif]Hibernate和JDBC优缺点对比(2017-11-29-wzz)
相同点:
1)两者都是java数据库操作的中间件、
2)两者对数据库进行直接操作的对象都是线程不安全的,都需要及时关闭。
3)两者都可对数据库的更新操作进行显式的事务处理。
不同点:
JDBC是SUN公司提供一套操作数据库的规范,使用java代码操作数据库。Hibernate是一个基于jdbc的主流持久化框架,对JDBC访问数据库的代码做了封装。
使用的SQL语言不同:JDBC使用的是基于关系型数据库的标准SQL语言,Hibernate使用的是HQL(Hibernate query language)语言。
操作的对象不同:JDBC操作的是数据,将数据通过SQL语句直接传送到数据库中执行,Hibernate操作的是持久化对象,由底层持久化对象的数据更新到数据库中。
数据状态不同:JDBC操作的数据是“瞬时”的,变量的值无法与数据库中的值保持一致,而Hibernate操作的数据是可持久的,即持久化对象的数据属性的值是可以跟数据库中的值保持一致的。
[if !supportLists]7. [endif]关于Hibernate的orm思想你了解多少?(2017-11-29-wzz)
ORM指的是对象关系型映射(Object RelationShip Mapping ),指的就是我们通过创建实体类对象和数据库中的表关系进行一一对应,来实现通过操作实体类对象来更改数据库里边的数据信息。这里边起到关键作用的是通过Hibernate的映射文件+Hibernate的核心配置文件。
详细内容请见:http://blog.csdn.net/wanghuan203/article/details/7566518
[if !supportLists]8. [endif]get和load的区别?(2017-11-30-wzz)
(1)get是立即加载,load是延时加载。
(2)get会先查一级缓存,再查二级缓存,然后查数据库;load会先查一级缓存,如果没有找到,就创建代理对象,等需要的时候去查询二级缓存和数据库。(这里就体现load的延迟加载的特性。)
(3)get如果没有找到会返回null,load如果没有找到会抛出异常。
(4)当我们使用session.load()方法来加载一个对象时,此时并不会发出sql语句,当前得到的这个对象其实是一个代理对象,这个代理对象只保存了实体对象的id值,只有当我们要使用这个对象,得到其它属性时,这个时候才会发出sql语句,从数据库中去查询我们的对象;相对于load的延迟加载方式,get就直接的多,当我们使用session.get()方法来得到一个对象时,不管我们使不使用这个对象,此时都会发出sql语句去从数据库中查询出来。
[if !supportLists]9. [endif]如何进行Hibernate的优化?(2017-11-30-wzz)
[if !supportLists](1)[endif]数据库设计调整。
[if !supportLists](2)[endif]HQL优化。
[if !supportLists](3)[endif]API的正确使用(如根据不同的业务类型选用不同的集合及查询API)。
[if !supportLists](4)[endif]主配置参数(日志,查询缓存,fetch_size, batch_size等)。
[if !supportLists](5)[endif]映射文件优化(ID生成策略,二级缓存,延迟加载,关联优化)。
[if !supportLists](6)[endif]一级缓存的管理。
[if !supportLists](7)[endif]针对二级缓存,还有许多特有的策略。
[if !supportLists](8)[endif]事务控制策略。
详情解释请见:https://www.cnblogs.com/xhj123/p/6106088.html
[if !supportLists]10. [endif]什么是Hibernate 延迟加载?(2017-12-1-lyq)
延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要
数据的时候,才真正执行数据加载操作。在Hibernate中提供了对实体对象的延迟加载以及
对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。
延迟加载的过程:通过代理(Proxy)机制来实现延迟加载。Hibernate从数据库获取某一个对象数据时、获取某一个对象的集合属性值时,或获取某一个对象所关联的另一个对象时,由于没有使用该对象的数据(除标识符外),Hibernate并不从数据库加载真正的数据,而只是为该对象创建一个代理对象来代表这个对象,这个对象上的所有属性都为默认值;只有在真正需要使用该对象的数据时才创建这个真正的对象,真正从数据库中加载它的数据。
[if !supportLists]11. [endif]No Session问题原理及解决方法?(2017-12-4-lyq)
Nosession问题报错如下:
根据字面上的意思,是指代理不能被初始化,session已经关闭。
Nosession问题产生的原因:
当执行Session的load()方法时,Hibernate不会立即执行查询所查询对象关联的对象(在此我们统称被关联的对象类为A类),仅仅返回A类的代理类的实例,这个代理类具由以下特征:
(1)由Hibernate在运行时动态生成,它扩展了A类,因此它继承了A类的所有属性和方法,但它的实现对于应用程序是透明的。
(2)当Hibernate创建A类代理类实例时,仅仅初始化了它的OID属性,其他属性都为null,因此这个代理类实例占用的内存很少。
(3)当应用程序第一次访问A代理类实例时(例如调用a..getXXX()或a.setXXX()方法),Hibernate会初始化代理类实例,在初始化过程中执行select语句,真正从数据库中加载A对象的所有数据。但有个例外,那就是当应用程序访问A代理类实例的getId()方法时,Hibernate不会初始化代理类实例,因为在创建代理类实例时OID就存在了,不必到数据库中去查询。
提示:Hibernate采用CGLIB工具来生成持久化类的代理类。CGLIB是一个功能强大的Java字节码生成工具,它能够在程序运行时动态生成扩展 Java类或者实现Java接口的代理类。
因为Hibernate中如果采用load加载的话(默认的是延迟加载),也就是lazy=true操作,因此,当调用完load后,session即可关闭。因为我们的session只是放置到了Dao层,表现层根本获取不到,所以在表现层调用的时候,session已经关闭,报错。
[if !supportLists]12. [endif]Spring的两种代理JDK和CGLIB的区别浅谈(2017-12-4-lyq)
Java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
参考资料:http://blog.csdn.net/tanga842428/article/details/52716875
[if !supportLists]13. [endif]叙述Session的缓存的作用(2017-12-9-lwl)
(1)减少访问数据库的频率。应用程序从内存中读取持久化对象的速度显然比到数据库中查询数据的速度快多了,因此Session的缓存可以提高数据访问的性能。
(2)保证缓存中的对象与数据库中的相关记录保持同步。当缓存中持久化对象的状态发生了变换,Session并不会立即执行相关的SQL语句,这使得Session能够把几条相关的SQL语句合并为一条SQL语句,以便减少访问数据库的次数,从而提高应用程序的性能。
[if !supportLists]14. [endif]Session的清理和清空有什么区别?(2017-12-10-lwl)
Session清理缓存是指按照缓存中对象的状态的变化来同步更新数据库;清空是Session的关闭;
[if !supportLists]15. [endif]请简述Session的特点有哪些?(2017-12-10-lwl)
(1)不是线程安全的,因此在设计软件架构时,应该避免多个线程共享同一个Session实例。
(2)Session实例是轻量级的,所谓轻量级是指它的创建和销毁不需要消耗太多的资源。这意味着在程序中可以经常创建或销毁Session对象,例如为每个客户请求分配单独的Session实例,或者为每个工作单元分配单独的Session实例。
(3)在Session 中,每个数据库操作都是在一个事务(transaction)中进行的,这样就可以隔离开不同的操作(甚至包括只读操作)。
[if !supportLists]16. [endif]比较Hibernate三种检索策略的优缺点(2017-12-10-lwl)
1、立即检索
优点:对应用程序完全透明,不管对象处于持久化状态,还是游离状态,应用程序都可以方便的从一个对象导航到与它关联的对象;
缺点:1.select语句太多;2.可能会加载应用程序不需要访问的对象白白浪费许多内存空间;
2、延迟检索
优点:由应用程序决定需要加载哪些对象,可以避免可执行多余的select语句,以及避免加载应用程序不需要访问的对象。因此能提高检索性能,并且能节省内存空间;
缺点:应用程序如果希望访问游离状态代理类实例,必须保证他在持久化状态时已经被初始化;
3、 迫切左外连接检索
优点:1、对应用程序完全透明,不管对象处于持久化状态,还是游离状态,应用程序都可以方便地冲一个对象导航到与它关联的对象。2、使用了外连接,select语句数目少;
缺点:1、可能会加载应用程序不需要访问的对象,白白浪费许多内存空间;2、复杂的数据库表连接也会影响检索性能;
[if !supportLists]七、[endif]Quartz定时任务
[if !supportLists]1.[endif]什么是Quartz 框架(2017-12-2-wzz)
Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。
2.配置文件 applicationContext_job.xml各个属性作用(2017-12-2-wzz)
(1)、Job:表示一个任务(工作),要执行的具体内容。
(2)、JobDetail:表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。
(3)、Trigger:代表一个调度参数的配置,什么时候去调。
(4)、Scheduler:代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。
3.Cron表达式详解(2017-12-2-wzz)
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义。
域:
Seconds(秒):可出现", - * /"四个字符,有效范围为0-59的整数。
Minutes(分钟):可出现", - * /"四个字符,有效范围为0-59的整数 。
Hours(小时):可出现", - * /"四个字符,有效范围为0-23的整数 。
DayofMonth(日of月):可出现", - * / ? L W C"八个字符,有效范围为0-31的整数。
Month(月):可出现", - * /"四个字符,有效范围为1-12的整数。
DayofWeek(日of星期):可出现", - * / ? L C #"四个字符,有效范围为1-7的整数1表示星期天,2表示星期一, 依次类推。
Year(年):可出现", - * /"四个字符,有效范围为1970-2099年。
[if !supportLists]4.[endif]如何监控Quartz 的 job 执行状态:运行中,暂停中,等待中? (2017-12-2-wzz)
通过往表(新建一个操作日志表)里插入日志的形式:
1)运行中:通过 JobListener 监听器来实现运行时更改表信息。
2)暂停中:调用 scheduler.pauseTrigger()方法时,更改表中 job 信息。
3)等待中:新添加的 job 默认给其等待中的状态,也是更改表中的 job 信息 但是上面这种形式的麻烦之处是得频繁的往表里插入数据。
[if !supportLists]第八章 [endif]最新技术
[if !supportLists]一、[endif]Redis
[if !supportLists]1. [endif]Redis的特点?(2017-11-25-wzz)
Redis是由意大利人Salvatore Sanfilippo(网名:antirez)开发的一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),该软件使用C语言编写,典型的NoSQL数据库服务器,Redis是一个key-value存储系统,它支持丰富的数据类型,如:string、list、set、zset(sorted set)、hash。
Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个 数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘 上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次 读写操作,是已知性能最快的Key-Value DB。
Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单 个value的最大限制是1GB,不像 memcached只能保存1MB的数据,另外Redis 也可以对存入的Key-Value设置expire时间。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
[if !supportLists]2. [endif]为什么redis需要把所有数据放到内存中?(2017-11-25-wzz)
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
[if !supportLists]3. [endif]Redis常见的性能问题都有哪些?如何解决?(2017-11-25-wzz)
(1)、Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
(2)、Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
(3)、Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
(4)、Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
[if !supportLists]4. [endif]Redis最适合的场景有哪些?(2017-11-25-wzz)
(1)、会话缓存(Session Cache)
(2)、全页缓存(FPC)
(3)、队列
(4)、排行榜/计数器
(5)、发布/订阅
[if !supportLists]5. [endif]Memcache与Redis的区别都有哪些?(2017-11-25-wzz)
(1)、存储方式不同,Memcache是把数据全部存在内存中,数据不能超过内存的大小,断电后数据库会挂掉。Redis有部分存在硬盘上,这样能保证数据的持久性。
(2)、数据支持的类型不同 memcahe对数据类型支持相对简单,redis有复杂的数据类型。
(3)、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
(4)、支持的value大小不一样 redis最大可以达到1GB,而memcache只有1MB。
[if !supportLists]6. [endif]Redis用过RedisNX吗?Redis有哪几种数据结构?(2017-11-14-lyq)
反正我是不知道redisnx是什么,度娘也不清楚,如果面试中问道自己没有接触过或者没有听过的技术可以直接大胆的告诉他,没有接触过,或者没有听过。
Redis的数据结构有五种,分别是:
String——字符串
String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字(当数字类型用 Long 可以表示的时候encoding 就是整型,其他都存储在 sdshdr 当做字符串)。 Hash——字典
在Memcached 中,我们经常将一些结构化的信息打包成 hashmap,在客户端序列化后存储为一个字符串的值(一般是 JSON 格式),比如用户的昵称、年龄、性别、积分等。 List——列表
List 说白了就是链表(redis 使用双端链表实现的 List),相信学过数据结构知识的人都应该能理解其结构。
Set——集合
Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。 Sorted Set——有序集合
和Sets相比,Sorted Sets是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,
[if !supportLists]1. [endif]带有权重的元素,比如一个游戏的用户得分排行榜 2.比较复杂的数据结构,一般用到的场景不算太多
[if !supportLists]7. [endif]Redis的优缺点(2017-11-22-lyq)
优点:
[if !supportLists]a) [endif]性能极高– Redis 能支持超过 100K+ 每秒的读写频率。
[if !supportLists]b) [endif]丰富的数据类型– Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets数据类型操作。
[if !supportLists]c) [endif]原子– Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行。
attention原子性定义:例如,A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行,这种特性就叫原子性。
·d)丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。
缺点:
a). 由于是内存数据库,所以,单台机器,存储的数据量,跟机器本身的内存大小。虽然redis本身有key过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
b). 如果进行完整重同步,由于需要生成rdb文件,并进行传输,会占用主机的CPU,并会消耗现网的带宽。不过redis2.8版本,已经有部分重同步的功能,但是还是有可能有完整重同步的。比如,新上线的备机。
c). 修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中,redis不能
提供服务。
[if !supportLists]8. [endif]Redis的持久化(2017-11-23-lyq)
RDB 持久化:该机制可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
AOF 持久化:记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小
无持久化:让数据只在服务器运行时存在。
同时应用AOF 和 RDB:当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。
RDB的优缺点:
优点:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
缺点:如果你需要尽量避免在服务器故障时丢失数据,那么RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。
AOF的优缺点。
优点:
1、使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
2、Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
缺点:
对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
[if !supportLists]二、[endif]消息队列ActiveMQ
[if !supportLists]1. [endif]如何使用ActiveMQ解决分布式事务?(2017-11-21-gxb)
在互联网应用中,基本都会有用户注册的功能。在注册的同时,我们会做出如下操作:
[if !supportLists]1. [endif]收集用户录入信息,保存到数据库
[if !supportLists]2. [endif]向用户的手机或邮箱发送验证码
等等…
如果是传统的集中式架构,实现这个功能非常简单:开启一个本地事务,往本地数据库中插入一条用户数据,发送验证码,提交事物。
但是在分布式架构中,用户和发送验证码是两个独立的服务,它们都有各自的数据库,那么就不能通过本地事物保证操作的原子性。这时我们就需要用到ActiveMQ(消息队列)来为我们实现这个需求。
在用户进行注册操作的时候,我们为该操作创建一条消息,当用户信息保存成功时,把这条消息发送到消息队列。验证码系统会监听消息,一旦接受到消息,就会给该用户发送验证码。
问题:
1.如何防止消息重复发送?
解决方法很简单:增加消息状态表。通俗来说就是一个账本,用来记录消息的处理状态,每次处理消息之前,都去状态表中查询一次,如果已经有相同的消息存在,那么不处理,可以防止重复发送。
[if !supportLists]2. [endif]了解哪些消息队列?(2017-11-24-gxb)
ActiveMQ、RabbitMQ、kafka。
RabbitMQ: RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。
ActiveMQ: ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。
Kafka/Jafka: Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
MQ选型对比图
[if !supportLists]3. [endif]ActiveMQ如果消息发送失败怎么办?(2017-11-24-gxb)
Activemq 有两种通信方式,点到点形式和发布订阅模式。
如果是点到点模式的话,如果消息发送不成功此 消息默认会保存到 activemq 服务端知道有消费者将其消费,所以此时消息是不会丢失的。
如果是发布订阅模式的通信方式,默认情况下只通知一次,如果接收不到此消息就没有了。这种场景只适 用于对消息送达率要求不高的情况。如果要求消息必须送达不可以丢失的话,需要配置持久订阅。每个订阅端定义一个 id,在订阅是向 activemq 注册。发布消息和接收消息时需要配置发送模式为持久化。此时 如果客户端接收不到消息,消息会持久化到服务端,直到客户端正常接收后为止。
[if !supportLists]三、[endif]Dubbo
[if !supportLists]1. [endif]Dubbo的容错机制有哪些。(2017-11-23-gxb)
Dubbo官网提出总共有六种容错策略
1)Failover Cluster 模式
失败自动切换,当出现失败,重试其它服务器。(默认)
2)Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
3)Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
4)Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
5)Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks=”2”来设置最大并行数。
6)Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持) 通常用于通知所有提供者更新缓存或日志等本地资源信息。
总结:在实际应用中查询语句容错策略建议使用默认Failover Cluster ,而增删改建议使用 Failfast Cluster 或者 使用 Failover Cluster(retries=”0”) 策略 防止出现数据 重复添加等等其它问题!建议在设计接口时候把查询接口方法单独做一个接口提供查询。
[if !supportLists]2. [endif]使用dubbo遇到过哪些问题?(2017-11-23-gxb)
[if !supportLists]1. [endif]增加提供服务版本号和消费服务版本号
这个具体来说不算是一个问题,而是一种问题的解决方案,在我们的实际工作中会面临各种环境资源短缺的问题,也是很实际的问题,刚开始我们还可以提供一个服务进行相关的开发和测试,但是当有多个环境多个版本,多个任务的时候就不满足我们的需求,这时候我们可以通过给提供方增加版本的方式来区分.这样能够剩下很多的物理资源,同时为今后更换接口定义发布在线时,可不停机发布,使用版本号.
引用只会找相应版本的服务,例如:
[if !supportLists]2. [endif]dubbo reference注解问题
@Reference只能在springbean实例对应的当前类中使用,暂时无法在父类使用;如果确实要在父类声明一个引用,可通过配置文件配置dubbo:reference,然后在需要引用的地方跟引用springbean一样就可以了.
3.出现RpcException: No provider available for remote service异常,表示没有可用的服务提供者
1). 检查连接的注册中心是否正确
2). 到注册中心查看相应的服务提供者是否存在
3). 检查服务提供者是否正常运行
4. 服务提供者没挂,但在注册中心里看不到
首先,确认服务提供者是否连接了正确的注册中心,不只是检查配置中的注册中心地址,而且要检查实际的网络连接。
其次,看服务提供者是否非常繁忙,比如压力测试,以至于没有CPU片段向注册中心发送心跳,这种情况,减小压力,将自动恢复。
[if !supportLists]3. [endif]Dubbo的连接方式有哪些?(2017-12-1-lyq)
Dubbo的客户端和服务端有三种连接方式,分别是:广播,直连和使用zookeeper注册中心。
3.1、Dubbo广播
这种方式是dubbo官方入门程序所使用的连接方式,但是这种方式有很多问题。在企业开发中,不使用广播的方式。
taotao-manager服务端配置:
[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]
客户端配置taotao-manager-web的配置如下:
[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]
3.2、Dubbo直连
这种方式在企业中一般在开发中环境中使用,但是生产环境很少使用,因为服务是直接调用,没有使用注册中心,很难对服务进行管理。Dubbo直连,首先要取消广播,然后客户端直接到指定需要的服务的url获取服务即可。
服务端配置:taotao-manager的修改如下,取消广播,注册中心地址为N/A
[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]
[if !supportLists]11. [endif]
客户端配置:taotao-manager-web配置如下,取消广播,从指定的url中获取服务
[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]
3.3、zookeeper注册中心
Dubbo注册中心和广播注册中心配置类似,不过需要指定注册中心类型和注册中心地址,这个时候就不是把服务信息进行广播了,而是告诉给注册中心进行管理,这个时候我们就需要有一个注册中心。
官方推荐使用zookeeper作为注册中心。
3.3.1、Zookeeper介绍
zookeeper在dubbo所处的位置:
1)Provider: 暴露服务的服务提供方。
2)Consumer: 调用远程服务的服务消费方。
3)Registry: 服务注册与发现的注册中心。
4) Monitor: 统计服务的调用次调和调用时间的监控中心。
5)Container: 服务运行容器。
调用关系说明:
1)服务容器负责启动,加载,运行服务提供者。
2)服务提供者在启动时,向注册中心注册自己提供的服务。
3)服务消费者在启动时,向注册中心订阅自己所需的服务。
4)注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
5)服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
6)服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
[if !supportLists]四、[endif]并发相关
[if !supportLists]1. [endif]如何测试并发量?(2017-11-23-gxb)
可以使用apache提供的ab工具测试。
参考资料:http://blog.csdn.net/whynottrythis/article/details/46495309
[if !supportLists]五、[endif]Nginx
[if !supportLists]1. [endif]Nginx反向代理为什么能够提升服务器性能?(2017-11-24-gxb)
对于后端是动态服务来说,比如 Java 和 PHP。这类服务器(如 JBoss 和 PHP-FPM)的 IO 处理能力往往不高。 Nginx 有个好处是它会把 Request 在读取完整之前 buffer 住,这样交给后端的就是一个完整的 HTTP
请求,从而提高后端的效率,而不是断断续续的传递(互联网上连接速度一般比较慢)。 同样,Nginx 也可以把 response 给 buffer 住,同样也是减轻后端的压力。
[if !supportLists]2. [endif]Nginx 和 Apache 各有什么优缺点? (2017-11-24-gxb)
nginx 相对 apache 的优点:
1)轻量级,同样起web服务,比apache占用更少的内存及资源
2)抗并发,nginx处理请求是异步非阻塞的,而apache则是阻塞型的,在高并发下nginx能保持
3)低资源低消耗高性能
4)高度模块化的设计,编写模块相对简单
5)社区活跃,各种高性能模块出品迅速啊
apache 相对 nginx 的优点:
1)rewrite,比nginx的rewrite强大
2)模块超多,基本想到的都可以找到
3)少bug,nginx的bug相对较多
4)超稳定,一般来说,需要性能的 web 服务,用 nginx 。 如果不需要性能只求稳定,那就 apache 吧。
[if !supportLists]3. [endif]Nginx 多进程模型是如何实现高并发的?(2017-12-5-lyq)
进程数与并发数不存在很直接的关系。这取决取server 采用的工作方式。如果一个 server 采用一个进程负责一个 request 的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。
Nginx 的异步非阻塞工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。apache 是如何利用的呢,简单来说:同样的 4 个进程,如果采用一个进程负责一个 request 的方式,那么,同时进来 4 个 request 之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第5 个 request 进来了。就无法及时反应了,因为 4 个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个 request,就新开个进程来处理。nginx 不这样,每进来一个 request,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request,并等待请求返回。那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件: “如果 upstream
返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有 request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件, worker 才会来接手,这个request 才会接着往下走。
由于web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server机器上的时间片不多。这是几个进程就解决高并发的秘密所在。webserver 刚好属于网络 io 密集型应用,不算是计算密集型。异步,非阻塞,使用 epoll,和大量细节处的优化。也正是 nginx 之所以然的技术基石。
[if !supportLists]六、[endif]Zookeeper
[if !supportLists]1. [endif]简单介绍一下zookeeper以及zookeeper的原理。(2017-11-24-gxb)
ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是Google 的 Chubby 一个开源的实现,是Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、 功能稳定的系统提供给用户。
ZooKeeper 包含一个简单的原语集, 提供 Java 和 C 的接口。
ZooKeeper 代码版本中,提供了分布式独享锁、选举、队列的接口,代码在 zookeeper-3.4.3\src\recipes。其中分布锁和队列有 Java 和 C 两个版本,选举只有 Java 版本。
原理:
ZooKeeper 是以 Fast Paxos 算法为基础的,Paxos 算法存在活锁的问题,即当有多个 proposer 交错提交时,有可能互相排斥导致没有一个 proposer 能提交成功,而 Fast Paxos 作了一些优化,通过选举产生一个 leader (领导者),只有 leader 才能提交 proposer,具体 算法可见 Fast Paxos。因此,要想弄懂 ZooKeeper 首先得对 Fast Paxos 有所了解。
ZooKeeper 的基本运转流程:1、选举 Leader。2、同步数据。3、选举 Leader 过程中算法有很多,但要达到的选举标准是一致的。 4、Leader 要具有最高的执行 ID,类似 root 权限。 5、集群中大多数的机器得到响应并 follow 选出的 Leader。
[if !supportLists]七、[endif]solr
[if !supportLists]1. [endif]简单介绍一下solr(2017-11-24-gxb)
Solr 是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service 的 API 接口。 用户可以通过 http 请求,向搜索引擎服务器提交一定格式的 XML 文件,生成索引;也可以 通过 Http Get 操作提出查找请求,并得到 XML 格式的返回结果。
特点:
Solr 是一个高性能,采用 Java5 开发,基于 Lucene 的全文搜索服务器。同时对其进行 了扩展,提供了比 Lucene 更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能 进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。
工作方式:
文档通过Http 利用 XML 加到一个搜索集合中。查询该集合也是通过 http 收到一个 XML/JSON 响应来实现。它的主要特性包括:高效、灵活的缓存功能,垂直搜索功能,高亮 显示搜索结果,通过索引复制来提高可用性,提供一套强大 Data Schema 来定义字段,类 型和设置文本分析,提供基于Web 的管理界面等。
[if !supportLists]2. [endif]solr怎么设置搜索结果排名靠前?(2017-11-24-gxb)
可以设置文档中域的boost 值,boost 值越高,计算出来的相关度得分就越高,排名也就越靠前。此方法可以把热点商品或者推广商品的排名提高。
[if !supportLists]3. [endif]solr中IK分词器原理是什么?(2017-11-24-gxb)
Ik 分词器的分词原理本质上是词典分词。先在内存中初始化一个词典,然后在分词过程中挨个读取字符,和字典中的字符相匹配,把文档中的所有的词语拆分出来的过程。
[if !supportLists]八、[endif]webService
[if !supportLists]1. [endif]什么是webService?(2017-11-24-lyq)
WebService 是一种跨编程语言和跨操作系统平台的远程调用技术。所谓跨编程语言和跨操作平台,就是说服务端程序采用 java 编写,客户端程序则可以采用其他编程语言编写,反之亦然!跨操作系统平台则是指服务端程序和客户端程序可以在不同的操作系统上。
[if !supportLists]2. [endif]常见的远程调用技术(2017-11-24-lyq)
RMI是java语言本身提供的远程通讯协议,稳定高效,是EJB的基础。但它只能用于JAVA程序之间的通讯。
Hessian和Burlap是caucho公司提供的开源协议,基于HTTP传输,服务端不用开防火墙端口。协议的规范公开,可以用于任意语言。跨平台有点小问题。
Httpinvoker是SpringFramework提供的远程通讯协议,只能用于JAVA程序间的通讯,且服务端和客户端必须使用SpringFramework。
Web service是连接异构系统或异构语言的首选协议,它使用SOAP形式通讯,可以用于任何语言,目前的许多开发工具对其的支持也很好。
效率相比:RMI > Httpinvoker >= Hessian >> Burlap >> web service。
[if !supportLists]九、[endif]Restful
[if !supportLists]1. [endif]谈谈你对restful的理解以及在项目中的使用?(2017-11-30-wzz)
注意:下面回答内容来自百度百科。
一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。
给大家推荐如下一篇博客,该博客从多个维度讲解了什么是Restful并且给了Restful风格样式的API接口。
http://blog.csdn.net/liuwenbiao1203/article/details/52351129
[if !supportLists]第九章 [endif]企业实战面试题
[if !supportLists]一、[endif]智慧星(2017-11-25-wmm)
[if !supportLists]1. [endif]选择题
1.1 在Java中,负责对字节代码解释执行的是(B)
A. 应用服务器
B. 虚拟机
C. 垃圾回收器
D. 编译器
1.2 一个栈的输入序列为1 2 3 4 5,则下列序列中不可能是栈输出的序列的是(A)
A. 5 4 1 3 2
B. 2 3 4 1 5
C. 1 5 4 3 2
D. 2 3 1 4 5
1.3下列那一个选项按照顺序包括了OSI模型的7个层次( C)
A. 物理层 数据链路层 传输层 网络层 会话层 表示层 应用层
B. 物理层 数据链路层 会话层 网络层 传输层 表示层 应用层
C. 物理层 数据链路层 网络层 传输层 会话层 表示层 应用层
D. 网络层 传输层 物理层 数据链路层 会话层 表示层 应用层
1.4当客户度关闭一个从连接池中获取的连接, 会发生下面哪一种情况?(A)
A. 连接不会关闭, 只是简单地归还给连接池
B. 连接被关闭 , 但又被重新打开并归还给连接池
C. 连接永久性关闭
1.5以下哪些不是javaScript 的全局函数( C)
A. eval
B. escape
C. setTimeout
D. parseFloat
1.6你使用mkdir命令创建一个临时的文件夹/tmp/aaa, 并将一些文件复制其中,使用完后要删除/mnt/tmp文件夹及其中的所有文件, 应该使用命令(B)
A. rm /tmp/aaa
B. rm –r /tmp/aaa
C. rmdir –r /tmp/aaa
D. rmdir /tmp/aaa
1.7在UML提供的图中, ( C)用于按数据顺序描述对象间的交互
A. 协作图
B. 网络图
C. 序列图
D. 状态图
1.8下面有关系统并发访问数估算数据哪个最有效:(B)
A. 高峰时段日处理业务量100000
B. 高峰时段平均每秒请求数80
C. 同时在线用户100
D. 平均每秒用户请求 50
1.9不同级别的用户对同一对象拥有不同的访问权利或某个客户端不能直接操作到某个对象,但有必须和那个对象有所互动, 这种情况最好使用什么设计模式.( D)
A.Bridge 模式
B. Fa?ade 模式
C. Adapter 模式
D. Proxy 模式
1.10下面哪个Set是排序的? (C)
A. LinkedHashSet
B. HashSet
C. TreeSet
D. AbstractSet
[if !supportLists]2. [endif]编程题
2.1 用1,2 , 2 ,3, 4 ,5这6个数字, 用Java 写一个main 函数, 打印出所有不同的排列, 如: 512234, 412345等, 要求: “4”不能在第三位,“3”与”5”不能相连。
import java.util.Iterator;
import java.util.TreeSet;
public class numberRandom {
String[] stra = {"1","2","2","3","4","5"};
int n = stra.length;
boolean[] visited = new boolean[n];
String result = "";
TreeSet ts = new TreeSet();
int[][] a = new int[n][n];
private void searchMap()
{
for(int i=0;i
{
for(int j=0;j
{
if(i==j)
{
a[i][j]=0;
}else{
a[i][j]=1;
}
}
}
//3和5不能相连
a[3][5]=0;
a[5][3]=0;
//开始遍历
for(int i=0;i
{
search(i);
}
Iterator it = ts.iterator();
while(it.hasNext())
{
String str =it.next();
//4不能在第三位
if(str.indexOf("4")!=2){
System.out.println(str);
}
}
}
private void search(int startIndex){
visited[startIndex] = true;
result = result + stra[startIndex];
if(result.length() ==n)
{
ts.add(result);
}
for(int j=0;j
{
if(a[startIndex][j]==1&&visited[j]==false)
{
search(j);
}else
{
continue;
}
}
//一个result结束后踢掉最后一个,寻找别的可能性,若没有的话,则继续向前踢掉当前最后一个
result = result.substring(0,result.length()-1);
visited[startIndex] = false;
}
public static void main(String[] args){
new numberRandom().searchMap();
}
}
2.2一个数如果恰好等于它的因子之和,这个数就称为”完数”.例如 6 = 1+2+3。编程找出1000以内的所有完数。
public class wsTest { public static void main(String[] args) { for(int m=2;m<1000;m++){ int s=0; for(int i=1;i
[if !supportLists]二、[endif]中讯志远科技(2017-11-26-wmm)
[if !supportLists]1. [endif]问答题
1.1下面程序的运行结果是?为什么?
String str1 = “hello”;
String str2 = “he”+new String(“llo”);
String str3 = “he”+”llo”;
System.err.println(str1== str2);
System.err.println(str1 == str3);
答案: false true
1.2 HashSet里的元素是不能重复的, 那用什么方法来区分重复与否呢?
答案:当往集合在添加元素时,调用add(Object)方法时候,首先会调用Object的hashCode()方法判hashCode是否已经存在,如不存在则直接插入元素;
如果已存在则调用Object对象的equals()方法判断是否返回true,如果为true则说明元素已经 存在,如为false则插入元素。
1.3. List ,Set, Map是否继承来自Collection接口? 存取元素时, 有何差异?
答案:List,Set是继承Collection接口; Map不是。
List:元素有放入顺序,元素可重复 ,通过下标来存取 和值来存取
Map:元素按键值对存取,无放入顺序
Set:元素无存取顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)
1.4.简述Java中的值传递和引用传递?
参考网址:https://www.cnblogs.com/zhangshiwen/p/5830062.html
按值传递是指的是在方法调用时,传递的参数是按值的拷贝传递。
按值传递重要特点:传递的是值的拷贝,也就是说传递后就互不相关了
示例如下:
public class TempTest {
private void test1(int a){
a = 5;
System.out.println("test1方法中的a="+a);
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
t.test1(a);//传递后,test1方法对变量值的改变不影响这里的a
System.out.println(”main方法中的a=”+a);
}
}
运行结果是:
test1方法中的a=5
main方法中的a=3
按引用传递是指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
示例如下:
public class TempTest {
private void test1(A a){
a.age = 20;
System.out.println("test1方法中的age="+a.age);
}
public static void main(String[] args) {
TempTest t = new TempTest();
A a = new A();
a.age = 10;
t.test1(a);
System.out.println(”main方法中的age=”+a.age);
}
}
class A{
public int age = 0;
运行结果如下:
[java] view plain copy
test1方法中的age=20
main方法中的age=20
1.5 switch是否作用在byte上, 是否能作用在long上, 是否能作用在String上?
答案:switch可作用于char byte short int;
switch可作用于char byte short int对应的包装类;
switch不可作用于long double float boolean,包括他们的包装类;
1.6 Java语言如何进行异常处理? 请写出几个常见的运行时异常的编译时异常
答案:java语言进行异常处理的方式有:
throws: throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常)
throw : throw是语句抛出一个异常。
常见的运行时异常的编译时异常:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ClassNotFoundException - 类找不到异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
1.7简述数据库事务和实际工作中的作用
所谓事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位。例如,在关系数据库中,一个事务可以是一条SQL语句、一组SQL语句或整个程序。
简单举个例子就是你要同时修改数据库中两个不同表的时候,如果它们不是一个事务的话,当第一个表修改完,可是第二表改修出现了异常而没能修改的情况下,就只有第二个表回到未修改之前的状态,而第一个表已经被修改完毕。而当你把它们设定为一个事务的时候,当第一个表修改完,可是第二表改修出现了异常而没能修改的情况下,第一个表和第二个表都要回到未修改的状态!这就是所谓的事务回滚。
例如,在将资金从一个帐户转移到另一个帐户的银行应用中,一个帐户将一定的金额贷记到一个数据库表中,同时另一个帐户将相同的金额借记到另一个数据库表中。由于计算机可能会因停电、网络中断等而出现故障,因此有可能更新了一个表中的行,但没有更新另一个表中的行。如果数据库支持事务,则可以将数据库操作组成一个事务,以防止因这些事件而使数据库出现不一致。如果事务中的某个点发生故障,则所有更新都可以回滚到事务开始之前的状态。如果没有发生故障,则通过以完成状态提交事务来完成更新。
[if !supportLists]三、[endif]腾讯(2016年校招面试题2017-11-29-wzy)
[if !supportLists]1. [endif]选择题
1.1已知一棵二叉树,如果先序遍历的节点顺序是: ADCEFGHB ,中序遍历是: CDFEGHAB ,则后序遍历结果为:( D )
A. CFHGEBDA
B. CDFEGHBA
C. FGHCDEBA
D. CFHGEDBA
知识点
对于二叉树的遍历方式一般分为三种先序、中序、后序三种方式:
先序遍历(根左右)
若二叉树为空,则不进行任何操作:否则
1、访问根结点。
2、先序方式遍历左子树。
3、先序遍历右子树。
中序遍历(左根右)
若二叉树为空,则不进行任何操作:否则
1、中序遍历左子树。
2、访问根结点。
3、中序遍历右子树。
后序遍历(左右根)
若二叉树为空,则不进行任何操作:否则
1、后序遍历左子树。
2、后序遍历右子树。
3、放问根结点。
因此,根据题目给出的先序遍历和中序遍历,可以画出二叉树:
1.2下列哪两个数据结构,同时具有较高的查找和删除性能?(CD)
A. 有序数组
B. 有序链表
C. AVL 树
D. Hash 表
知识点:
平衡二叉树的查找,插入和删除性能都是O(logN) ,其中查找和删除性能较好;哈希表的查找、插入和删除性能都是 O(1) ,都是最好的。所以最后的结果选择: CD
1.3下列排序算法中,哪些时间复杂度不会超过 nlogn?(BC)
A. 快速排序
B. 堆排序
C. 归并排序
D. 冒泡排序
知识点
根据上图,观察平均情况,最好最差情况的时间复杂度基本可以知道答案了,最后结果选择:BC。
1.4初始序列为 1 8 6 2 5 4 7 3 一组数采用堆排序,当建堆(小根堆)完毕时,堆所对应的二叉树中序遍历序列为:( A )
A. 8 3 2 5 1 6 4 7
B. 3 2 8 5 1 4 6 7
C. 3 8 2 5 1 6 7 4
D. 8 2 3 5 1 4 7 6
初始化序列:1 8 6 2 5 4 7 3,,小根堆就是要求结点的值小于其左右孩子结点的值,左右孩子的大小没有关系,那么小根堆排序之后为:1 2 4 3 5 6 7 8;
中序遍历:左根右,故遍历结果为:8 3 2 5 1 6 4 7
故最后选择的结果:A
[if !supportLists]1. [endif]当n = 5 时,下列函数的返回值是:(A)
int foo(int n)
{
if(n<2)return n;
return foo(n-1)+foo(n-2);
}
A.5
B.7
C.8
D.1
1.5 S市 A ,B 共有两个区,人口比例为 3:5 ,据历史统计 A 区的犯罪率为 0.01% ,B 区为 0.015% ,现有一起新案件发生在 S 市,那么案件发生在 A 区的可能性有多大?(C)
A.37.5%
B.32.5%
C.28.6%
D.26.1%
这道题首先得了解犯罪率是什么?犯罪率就是犯罪人数与总人口数的比。因此可以直接得出公式:( 3 0.01% ) / ( 3 0.01% + 5 * 0.015% ) = 28.6%
当然如果不好理解的话,我们可以实例化,比如B 区假设 5000 人,A 区 3000 人,A 区的犯罪率为 0.01%,那么 A 区犯罪人数为 30 人,B 区的犯罪率为 0.015% ,那么 B 区的犯罪人数为 75 人 ,求发生在 A 区的可能性,就是说 A 区的犯罪人数在总犯罪人数的多少,也就是 30/(30+75)=0.2857
当然,也可以回归到我们高中遗忘的知识:
假设C表示犯案属性
在A区犯案概率:P(C|A)=0.01%
在B区犯案概率:P(C|B)=0.015%
在A区概率:P(A)=3/8
在B区概率:P(B)=5/8
犯案概率:P(C)=(3/80.01%+5/80.015%)
根据贝叶斯公式:P(A|C) = P(A,C) / P(C) = [P(C|A) P(A)] / [ P(C|A) P(A)+ P(C|B) P(B) ] 也可以算出答案来
故,最后结果选择为:C
1.6 Unix系统中,哪些可以用于进程间的通信?(ABCD )
A.Socket
B.共享内存
C.消息队列
D.信号量
知识点
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
故最后选择的结果为:ABCD
1.7静态变量通常存储在进程哪个区?( C )
A.栈区
B.堆区
C.全局区
D.代码区
静态变量的修饰关键字:static,又称静态全局变量。故最后选择的结果为: C
1.8如何提供查询Name字段的性能( B )
A. 在Name字段上添加主键
B. 在Name字段上添加索引
C. 在Age字段上添加主键
D. 在Age字段上添加索引
1.9 IP地址131.153.12.71是一个(B)类IP地址
A.A
B.B
C.C
D.D
知识点
1.10浏览器访问某页面,HTTP 协议返回状态码为 403 时表示:( B )
A. 找不到该页面
B. 禁止访问
C. 内部服务器访问
D. 服务器繁忙
1.11如果某系统 15*4=112 成立,则系统采用的是( A )进制
A.6
B.7
C.8
D.9
这题因为是选择题,我们可以直接从A 的选项开始,假设是 6 进制的,我们把等式 15 4 = 112 转为十进制,就是 11 4 = 44,最后验证等式是否成立,明显等式是成立的,因此答案已经出来了,选择 A 。
当然我们也可以假设是X 进制,且我们知道 X 大于 5, 则:(x+5)4 = xx +x +2,所以最后计算的结果也为 6。
1.12 TCP和 IP 分别对应了 OSI 中的哪几层?(CD)
A. Application layer
B. Presentation layer
C. Transport layer
D. Network layer
知识点
1.13一个栈的入栈序列是A,B,C,D,E,则栈的不可能的输出序列是?(C)
A.EDCBA
B.DECBA
C.DCEAB
D.ABCDE
堆栈分别是先进后出,后进先出,
选项a 是 abcde 先入栈,然后依次出栈,正好是 edcba
选项b 是 abcd 先依次入栈,然后 d 出栈, e 再入栈, e 出栈
选项c 是错误的,不可能 a 先出栈
选项d 是 a 入栈,然后 a 出栈;b 再入栈, b 出栈.依此类推
最后的结果选择C。
1.14同一进程下的线程可以共享以下?(BD)
A.stack
B.data section
C.register set
D.file fd
知识点
线程共享的内容包括:
1.进程代码段
2.进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)
3.进程打开的文件描述符、
4.信号的处理器、
5.进程的当前目录和
6.进程用户ID与进程组ID
线程独有的内容包括:
1.线程ID
2.寄存器组的值
3.线程的堆栈
4.错误返回码
5.线程的信号屏蔽码
所以选择为BD。
1.15对于派生类的构造函数,在定义对象时构造函数的执行顺序为?(D)
1:成员对象的构造函数
2:基类的构造函数
3:派生类本身的构造函数
A.123
B.231
C.321
D.213
1.16递归函数最终会结束,那么这个函数一定?(B)
A. 使用了局部变量
B. 有一个分支不调用自身
C. 使用了全局变量或者使用了一个或多个参数
D. 没有循环调用
1.17编译过程中,语法分析器的任务是(BCD)
A. 分析单词是怎样构成的
B. 分析单词串是如何构成语言和说明的
C. 分析语句和说明是如何构成程序的
D. 分析程序的结构
知识点
1.词法分析(lexical analysis)
词法分析是编译过程的第一个阶段。这个阶段的任务是从左到右的读取每个字符,然后根据构词规则识别单词。词法分析可以用lex等工具自动生成。
2.语法分析(syntax analysis)
语法分析是编译过程的一个逻辑阶段。语法分析在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断程序在结构上是否正确。
3.语义分析(semantic analysis)
属于逻辑阶段。对源程序进行上下文有关性质的审查,类型检查。如赋值语句左右端类型匹配问题。
所以BCD 都属于词法分析,选择结果为 BCD。
1.18同步机制应该遵循哪些基本准则?(ABCD)
A.空闲让进
B.忙则等待
C.有限等待
D.让权等待
1.19进程进入等待状态有哪几种方式?(D)
A. CPU调度给优先级更高的线程
B. 阻塞的线程获得资源或者信号
C. 在时间片轮转的情况下,如果时间片到了
D. 获得spinlock未果
1.20设计模式中,属于结构型模式的有哪些?(BC)
A. 状态模式
B. 装饰模式
C. 代理模式
D. 观察者模式
[if !supportLists]四、[endif]北京宝蓝德股份科技有限公司(2017-12-03-wmm)
1.选择题
1.1下面代码的运行结果是(C)
public class Test{
public static void main (String[] args){
List a = null;
test(a);
System.out.println(a.size());
}
public static void test(List a){
a=new arrayList();
a.add(“abc”);
}
}
[if !supportLists]A.[endif]0
[if !supportLists]B.[endif]1
[if !supportLists]C.[endif]Java.lang.NullPointerException
[if !supportLists]D.[endif]以上都不正确
1.2 Linux下查看进程占用的CPU的百分比, 使用工具(A)
[if !supportLists]A. [endif]Ps
[if !supportLists]B. [endif]Cat
[if !supportLists]C. [endif]More
[if !supportLists]D. [endif]Sep
1.3 JVM内存里哪个区域不可能发生OutOfMerncyError( A)
[if !supportLists]A. [endif]程序计数器
[if !supportLists]B. [endif]堆
[if !supportLists]C. [endif]方法区
[if !supportLists]D. [endif]本地方法栈
1.4 下面关于阻塞队列(java.util.concurrent.BlockingQueue)的说法不正确的是(C)
[if !supportLists]A. [endif]阻塞队列是线程安全的
[if !supportLists]B. [endif]阻塞队列的主要应用场景是“生产者-消费者”模型
[if !supportLists]C. [endif]阻塞队列里的元素不能为null
[if !supportLists]D. [endif]阻塞队列的实现必须显示地设置容量
1.5 如果现在需要创建一组任务,他们并行的执行工作,然后进行下一个步骤之前等待,直至所有的任务都完成,而去这种控制可以重用多次,这种情形使用java.util.concurrent包中引入哪种同步工具最适合(B)
[if !supportLists]A. [endif]CountDownLatch
[if !supportLists]B. [endif]CyclicBarrier
[if !supportLists]C. [endif]Semaphore
[if !supportLists]D. [endif]FutureTask
2.问答题
2.1 java中, 为什么基类不能做为HashMap的键值, 而只能是引用类型,把引用类型作为HashMap的键值, 需要注意哪些地方?
答案:引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为null,而原始类型实例变量的缺省值与它们的类型有关。
2.2 编写一个工具类StringUtil,提供方法 int compare(char[] v1 ,char[] v2)方法,比较字符串v1,v2 ,如果按照字符顺序v1>v2则return 1 ,v1=v2 则return 0, v1。
public class StringUtil{
int compare(char[] v1,char[] v2) {
String str1 = new String(v1);
String str2 = new String(v2);
int result = str1.compareTo(str2);
return result == 0 ? 0 : (result > 0 ? 1 : -1);
}
}
2.3 Java出现OutOfMemoryError(OOM)的原因有那些?出现OOM错误后,怎么解决?
参考播客:http://www.jianshu.com/p/2fdee831ed03
触发java.lang.OutOfMemoryError:最常见的原因就是应用程序需要的堆空间是大的,但是JVM提供的却小。这个的解决方法就是提供大的堆空间即可。除此之外还有复杂的原因:,
内存泄露:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。
解决方案:
第一个解决方案是显而易见的,你应该确保有足够的堆空间来正常运行你的应用程序,在JVM的启动配置中增加如下配置:
-Xmx1024m
流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发java.lang.OutOfMemoryError: Java heap space异常
解决方案,如果你的应用程序确实内存不足,增加堆内存会解决GC overhead limit问题,就如下面这样,给你的应用程序1G的堆内存:
java -Xmx1024m com.yourcompany.YourClass
[if !supportLists]五、[endif]智慧流(2017-12-04-wmm)
1.选择题
1.1 下列关于栈的描述错误的是(B)
A.栈是先进后出的线性表
B.栈只能顺序存储
C.栈具有记忆功能
D.对栈的插入和删除操作中,不需要改变栈底指针
1.2 对于长度为n的线性表,在最坏的情况下,下列个排序法所对应的比较次数中正确的是(D)
A.冒泡排序为n/2
B.冒泡排序为n
C.快速排序为n
D.快速排序为n(n-1)/2
1.3 阅读下列代码后,下列正确的说法是(A)
public class Person{
int arr[] = new int[10];
public static void main(String args[ ]){
System.out.println(arr[1]);
}
}
A 编译时将产生错误
B 编译时正确,运行时将产生错误
C 输出空
D 输出0
1.4 执行以下程序后输出的结果是(D)
public class Test {
public static void main(String[] args) {
StringBuffer a = new StringBuffer("A");
StringBuffer b = new StringBuffer("B");
operator(a,b);
System.out.println(a+","+b);
}
public static void operator(StringBuffer x,StringBuffer y){
x.append(y);
y=x;
}
}
A . A,A
B. A,B
C. B,B
D. AB,B
1.5下列不属于持久化的是(A)
[if !supportLists]A. [endif]把对象转换成为字符串的形式通过网络传输,在另一端接收到字符串把对象还原出来
[if !supportLists]B. [endif]把程序数据从数据库中读出来
[if !supportLists]C. [endif]从XML配置文件中读取程序的配置信息
[if !supportLists]D. [endif]把程序数据保存为文件
1.6下列代码输出的结果是(C)
int x= 0;
int y=10;
do{
y--;
++x;
}while(x<6);
System.out.println();
}
A. 5,6
B. 5,5
C. 6,5
D. 6,6
1.7下列程序段输出的结果是(B)
Void complicatedexpression_f(){
int x=20,y=30;
boolean j;
j=x>50&&y>60|| x>50&& y<-60 || x<-50&&y>60 || x<-50&& y<-60;
System.out.println(j);
}
[if !supportLists]A. [endif]true
[if !supportLists]B. [endif]false
[if !supportLists]C. [endif]1
[if !supportLists]D. [endif]001
1.8一个栈的输入序列为123,则下列序列中不可能是栈输出的序列的是(C)
A. 2 3 1
B. 3 2 1
C. 3 1 2
D. 1 2 3
[if !supportLists]8、[endif]当n = 5时, 下列函数的返回值是(D)
int foo(int n){
if(n<2) return n;
return foo(n-1)+foo(n-2);
}
A. 1
B. 8
C. 7
D. 5
1.9设有一个二维数组A[m][n],假设A[0][0]存放的位置在644(10),A[2][2]存放的文职在676(10)每个元素占一个空间,问A[3][3](10)存放在什么位置? 脚注(10)表示用10进制表示(C)
A. 688
B. 678
C. 692
D. 699
1.10下列代码执行结果是(B)
public static void main (String args[]){
Thread t = new Thread(){
public void run(){
pong();
}
};
t.run();
System.out.print("ping");
}
static void pong(){
System.out.print("pong");
}
A. pingpong
B. pongping
C. pingpong和pongping都有可能
D.都有可能
1.11下面程序能正常运行吗(可以)
Public class NULL{
Public static void haha(){
System.out.println(“haha”);
}
Public static void main(String[] args){
((NULL)null).haha();
}
}
[if !supportLists]2. [endif]问答题
2.1 解释一下什么是Servlet,说一说Servlet的生命周期
Servlet是一种服务器端的Java应用程序,具有独立于平台和协议的特性,可以生成动态的Web页面。 它担当客户请求(Web浏览器或其他HTTP客户程序)与服务器响应(HTTP服务器上的数据库或应用程序)的中间层。 Servlet是位于Web 服务器内部的服务器端的Java应用程序,与传统的从命令行启动的Java应用程序不同,Servlet由Web服务器进行加载,该Web服务器必须包含支持Servlet的Java虚拟机
Servlet生命周期可以分成四个阶段:加载和实例化、初始化、服务、销毁。
当客户第一次请求时,首先判断是否存在Servlet对象,若不存在,则由Web容器创建对象,而后调用init()方法对其初始化,此初始化方法在整个Servlet生命周期中只调用一次。
完成Servlet对象的创建和实例化之后,Web容器会调用Servlet对象的service()方法来处理请求。
当Web容器关闭或者Servlet对象要从容器中被删除时,会自动调用destory()方法。
2.2过滤器有哪些作用和用法?
对于一个web应用程序来说,过滤器是处于web容器内的一个组件,它会过滤特定请求资源请求信息和响应信息。一个请求来到时,web容器会判断是否有过滤器与该信息资源相关联,如果有则交给过滤器处理,然后再交给目标资源,响应的时候则以相反的顺序交给过滤器处理,最后再返回给用户浏览器。
常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件等。
2.3写出一个冒泡排序
从大到小:
public void BigAndSmall(){
int arr[]={-5,29,7,10,5,16};
for(int i=1;i
for(int j=0;j
if(arr[j]
int temp;
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
for(int i =0;i
System.out.print(" "+arr[i]+" ");
}
}
[if !supportLists]1. [endif]写出一个单例的实现(懒加载方式)
public class LazySingleton {
private LazySingleton(){
}
private static class SingletonHolder{
private static LazySingleton instance = new LazySingleton();
}
public static LazySingleton getInstance(){
return SingletonHolder.instance;
}
}
[if !supportLists]3. [endif]逻辑思维题
3.1 4 2 12 28 80 (C)
A. 124
B. 96
C. 216
D. 348
3.2 2006年某人连续打工24天,共赚了190元(日工资10元,星期日工资5元,星期日休息无工资)。已知他打工是从1月下旬的某一天开始的,这个月的1日恰好是星期日,这人打工结束的那一天是2月(C)日
A. 2月6日
B. 2月14日
C. 2月18日
D. 2月21日
2.3 由甲地到乙地有一天线路的巴士,全程行驶时间42分钟,到达总站后,司机至少休息10分钟,巴士就掉头行驶,如果这条线路甲,乙两边总站每隔8分钟都发一辆(不必是同一时间),则这条线路至少需要是多少俩巴士(C)
A. 15
B. 14
C. 13
D.12
2.4 编号为1至10的10个果盘中,每盘都盛有水果,共盛放100个。其中第一盘里有16个,并且编号相邻的三个果盘中水果是的和都相等,求第8盘中水果最多可能有几个(A)
A. 11
B. 12
C. 13
14. 14
2.5 根据下面图片选出正确的答案(B)
2.6 一只蜗牛掉进20米深的井中,白天往上爬3米,晚上有掉下去2米,请问要几天才能爬出来?
第一天爬了3米,然后掉了2米,实际上爬了1米;
第二天从1米处开绐向上爬了3米,然后掉了2米,实际上爬了2米;
第三天从2米处开绐向上爬了3米,然后掉了2米,实际上爬了3米;
. .......
第十八天从17米处开始向上爬了3米,嘿刚好是20米.到了
正解:18
2.7按规律填数字1, 1, 2, 3, ?。
答案:5。
2.8 假设一个池塘,里面有无穷多的水,现在有2个空水壶容积分别是5升和6升,问如何用这两只水壶取得3升水。
答案:5L桶打满水,全部倒入6L桶;
5L桶再次打满,往6L桶倒水至其满。此时5L桶留下4L水;
6L桶清空,将5L桶中的4L水倒入6L桶;
5L桶打满水,往6L桶倒水至其满,则5L桶中得3L水。
2.9 在房里有三盏灯,房外有三个开关,在房外看不见房内的情况,你只能进门一次,你用什么方法来区分那个开关控制哪一盏灯。
答案:先打开第一个开关,开一会再关上,然后打开第二个开关进入房间
再摸一下每个灯,发热的那盏是第一个开关的,
亮的那盏是第二个开关的,
没变化的那盏是第三个开关的。
2.10 两个盲人,他们各自买个两双黑袜和白袜,8双袜子的布质,大小完全相同,每双袜子都有1张商标纸连着,两位盲人不小心把8双袜子混在的一起,问他们怎样才能取回黑袜和白袜各两双。
答案:把每双袜子分成两只。 每人各拿一只。 这样,每人手中就有四只黑袜,四只白袜。 每人也就有两双黑袜,两双白袜了。
2.11 一楼到十楼的每层电梯门口都方和一颗钻石,钻石大小不一,你乘坐电梯从一楼到十楼,每层楼电梯门都会打开一次,手里只能拿一颗钻石,问怎样才能拿到最大的钻石。
答案:电梯每层都会开一下的,所以,在第一层就拿,到第二层,看到更大就换一下,更小就不换,一直这样上去,到最上层后,拿到的就是最大的
[if !supportLists]六、[endif]某公司 (2017-12-05-wmm)
[if !supportLists]1. [endif]选择题
1.1 ArrayList list = new ArrayList(20);语句中的list集合大小扩充了几次(A)
A.0
B.1
C.2
D.3
1.2 如果去掉了main方法的static修饰符会怎样(B)
A.程序无法翻译
B.程序能正常编译,运行时或抛出NoSuchMethodError异常
C.程序能正常编译,正常运行
D.程序能正常编译,正常运行一会会立刻退出
1.3启动java程序进程时,输入一下哪个参数可以实现年轻代的堆大小为50M(C )
A.-Xms50M
B.-Xmx50M
C.-Xmn50M
D.-Xss50M
1.4下面程序输出的结果是()
static boolean foo(char c) {
System.out.print(c);
return true;
}
public static void main(String[] args) {
int i = 0;
for (foo('A'); foo('B') && (i < 2); foo('C')) {
i++;
foo('D');
}
}
输出结果为:(A)
A. ABDCBDCB
B. ABDCDBCB
C. ABDBCDCB
D. ABDBCDCB
1.5下面哪些是Thread类的方法(A,B)
A.start()
B.run()
C.exit()
D.getPriority()
1.6以下语句输出的结果是什么(C)
System.out.print(Integer.MAX_VALUE*2);
System.out.print(Integer.MIN_VALUE*2);
A. -2-1
B. -1-2
C. -20
D. -1-1
1.7log4j的优先级从高到低的排序为(A)
A. error>warn>info>debug
B. warn>info>debug>error
C. warn >debug>error>info
D. error>warn>debug>info
1.8下列哪些方法可以使线程从运行状态进入到阻塞状态(BCD)
A.notify
B.wait
C.sleep
D.yield
1.9下列关于Thread类提供的线程控制的方法中,错误的一项是(A)
A. 在线程A中执行线程B的join()方法,则线程A等待直到B执行完成
B. 线程A通过调用interrupt()方法来中断其阻塞状态。
C. currentThread()方法返回当前线程的引用
D. 若线程A调用方法isAlive()返回为true,则说明A正在执行中
1.10设String s1 =”Topwalk”;String s2 =”Company”;以下方法可以得到字符串“TopwalkCompany” 有:(ABD)
A. s2+s1;
B. s1.concat(s2)
C. s1.append(s2);
D.StringBuffer buf = new StringBuffer(s1);buf.append(s2);
1.11 String a = new String(“1”+”2”)最终创建了几个对象(B)
A.1
B.2
C.3
D.4
1.12 int 类型占用(C)个字节?
A.2
B.4
C.8
D.16
1.13.下列那一条语句可以实现快速的复制一张数据库表(C)
A. select * into b from a where 1<>1;
B. creat table b as select * from a where 0=1;
C. insert into b as select * from a where 1<>1;
D. insert into b select * from a where 1<>1;
1.14 属于单利模式的特点的是(ACD)
A. 提供了对唯一实现的受控访问
B. 允许可变数目的实例
C. 单利模式的抽象层会导致单例类扩展有和那的困难
D. 单利模式很容易导致数据库的连接池溢出
1.15 选择Oracle的分页语句的关键字(A)
A. rownum
B. limit
C.TOP
D. pagenum
1.16 选出可以查询出所有的表和视图的方法:(B)
A.preparedStatement.getMetaData().getTables(***);
B.connection.getMetaData().getTables(***);
C.result.getMetaData().getTables(***);
D..DiverManager.getMeta().getTables(***);
1.17可以监控到数据库变化的机制有哪些(AB)
A. 存储过程
B. 数据库日志
C. 触发器
D. 物化视图
1.18 清空表所有数据的性能最优的语句是哪一个(B)
A. delete from tsuer;
B. truncate table tuser;ss
C. drop table tuser;
D. delete tuser;
1.19文件对外共享的协议有哪几个(AB)
A. FTP
B. Windows共享
C. TCP
D.SSH
1.20 关于Java中国特殊符号的用法正确的是(AD)
A. 判断一个字符串str中是否含有“.”,可以根据str.indexOf(“.”)是否等于-1判断。
B. 判断一个字符串str是否含有“.”,可以根据str.indexOf(“\\.”)是否等于-1判断。
C. 根据“.”分隔字符串str的写法可以是str.split(“\\.”)
D. 根据“.”分隔字符串str的写法可以是str.split(“.”)
1.21 根据以下代码回答问题,放置什么方法在地6行,会引起编译错误的是(B)
1 class Super{
2 public float getNum(){
3 }
4 }
5 public class Sub extends Super{
6
7 }
A. public float getNum{return 4,0f;}
B.public void getNum(){};
C.public void getNum(double d()){}
D.public double getNum(float d){return 4,0d;}
1.22 根据以下代码回答问题:输出结果是什么?(B)
public class Foo{
public static void main(String args[]){
try{return;}
finally{System.out.println(“Finally”);}
}
}
A. print out nothing;
B. print out “Finally”
C. 编译错误
D. 以上都不对
1.23根据以下代码回答问题,请问输出i和j的值是多少(D)
int i=1,j=10;
do{
if(i++>--j) continue;
}while(i<5)
A. i=6 j=5
B i=5 j=5
C i=6 j=4
D i=5 j=6
1.24 请问java关键字?(CD)
A. run
B. low
C. import
D. implement
1.25 以下哪些不属于约束(CD)
A.主键
B.外键
C.索引
D.唯一索引
E.not null
1.26下列关于数据库连接池的说法中哪个是错误的(D)
A. 服务器启动时会初始建立一定数量的池连接,并一直维持不少于此数目的池连接
B.客户端程序需要连接时,池驱动程序会返回一个使用的池连接并将其使用计数加1;
C. 如果当前没有空闲连接,驱动程序就会再新建一定数量的连接,新建连接的数量可以由配置参数决定。
D. 当使用池连接调用完成后,池驱动程序将此连接标记为空间,其他调用就可以使用这个连接
1.27 以下哪句是对索引的错误描述(C)
A. 选择性差的索引只会降低DML语句的执行速度
B. 选择性强的索引只有被Access Path使用到才是有用的索引
C. 过多的索引只会阻碍性能的提升,而不是加速性能
D.在适当的时候将最常用的列放在复合索引的最前面
E. 索引和表的数据都存储在同一个Segment中
1.28 关于锁locks,描述正确的是(A)
A. 当一个事务在表上防止了共享锁(shared lock),其他事务,能阅读表里的数据
B. 当一个事务在表上防止了共享锁(shared lock),其他事务,能更新表里的数据
C. 当一个事务在表上防止了排他锁(exclusive lock),其他事务,能阅读表里的数据
D. 当一个事务在表上防止了排他锁(exclusive lock),其他事务,能更新表里的数据
1.29 如下那种情况下,Oracle不会使用Full Table Scean(D)
A.缺乏索引,特别是在列上使用了函数,如果要利用索引,则需要使用函数索引。
B.当访问的数据占整个表中的大部分数据时
C.如果时一个表的high water mark 数据块数少于初始化参数DB_FILE_MULTIBLOCK_READ_COUNT
D.本次查询可以用到该张表的一个引用,但是该表具有多个索引包含用于过滤的字段
[if !supportLists]2. [endif]问答题
2.1如何判断两个单向链表相交,如何找相交点?
答案参考:http://blog.csdn.net/jiary5201314/article/details/50990349
第一种情况:两个链表均不含有环
思路:
1)、直接法
采用暴力的方法,遍历两个链表,判断第一个链表的每个结点是否在第二个链表中,时间复杂度为O(len1*len2),耗时很大。
2)、hash计数法
如果两个链表相交,则两个链表就会有共同的结点;而结点地址又是结点唯一标识。因而判断两个链表中是否存在地址一致的节点,就可以知道是否相交了。可以对第一个链表的节点地址进行hash排序,建立hash表,然后针对第二个链表的每个节点的地址查询hash表,如果它在hash表中出现,则说明两个链表有共 同的结点。这个方法的时间复杂度为:O(max(len1+len2);但同时还得增加O(len1)的存储空间存储哈希表。这样减少了时间复杂度,增加 了存储空间。
以链表节点地址为值,遍历第一个链表,使用Hash保存所有节点地址值,结束条件为到最后一个节点(无环)或Hash中该地址值已经存在(有环)。
再遍历第二个链表,判断节点地址值是否已经存在于上面创建的Hash表中。
这个方面可以解决题目中的所有情况,时间复杂度为O(m+n),m和n分别是两个链表中节点数量。由于节点地址指针就是一个整型,假设链表都是在堆中动态创建的,可以使用堆的起始地址作为偏移量,以地址减去这个偏移量作为Hash函数
3)、第三种思路是比较奇特的,在编程之美上看到的。先遍历第一个链表到他的尾部,然后将尾部的next指针指向第二个链表(尾部指针的next本来指向的是null)。这样两个链表就合成了一个链表,判断原来的两个链表是否相交也就转变成了判断新的链表是否有环的问题了:即判断单链表是否有环?
这样进行转换后就可以从链表头部进行判断了,其实并不用。通过简单的了解我们就很容易知道,如果新链表是有环的,那么原来第二个链表的头部一定在环上。因此我们就可以从第二个链表的头部进行遍历的,从而减少了时间复杂度(减少的时间复杂度是第一个链表的长度)。
下图是一个简单的演示:
这种方法可以判断两个链表是否相交,但不太容易找出他们的交点。
4)、仔细研究两个链表,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。示意图如下:
判断出两个链表相交后就是判断他们的交点了。假设第一个链表长度为len1,第二个问len2,然后找出长度较长的,让长度较长的链表指针向后移动|len1 - len2| (len1-len2的绝对值),然后在开始遍历两个链表,判断节点是否相同即可。
2.2如何从cookie中拿到session?
答案:session 在服务器端,cookie 在客户端(浏览器),session 默认被存在在服务器的一个文件里(不是内存),session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id); session 可以放在 文件、数据库、或内存中都可以。用户验证这种场合一般会用session ,因此,维持一个会话的核心就是客户端的唯一标识,即 session id
2.3. System.out.println(3/2); System.out.println(3.0/2); System.out.println(3.0/2.0); 分别会打印什么结果?
答案:1, 1.5,1.5
2.4打印出下面两个数组的交集,结果不能重复
String[] arr1 = {"112","wqw","2121"};
String[] arr2 = {"112","aad","ewqw"};
打印出两个数组的交集,结果不能重复
答案:
public class test {
@Test//测试程序
public void test()
String[] arr1 = {"112","wqw","2121"};
String[] arr2 = {"112","aad","ewqw"};
String[] result=StringIntersection(arr1,arr2);
for (String str:result){
System.out.printf(str);
}
}
//取两个string数组的交集
public String[] StringIntersection(String[] arr1,String[] arr2){
Map map = new HashMap();
List list = new LinkedList();
//取出str1数组的值存放到map集合中,将值作为key,所以的value都设置为false
for (String str1:arr1){
if (!map.containsKey(str1)){
map.put(str1,Boolean.FALSE);
}
}
//取出str2数组的值循环判断是否有重复的key,如果有就将value设置为true
for (String str2:arr2){
if (map.containsKey(str2)){
map.put(str2,Boolean.TRUE);
}
}
//取出map中所有value为true的key值,存放到list中
for (Map.Entry entry:map.entrySet()){
if (entry.getValue().equals(Boolean.TRUE)){
list.add(entry.getKey());
}
}
//声明String数组存储交集
String[] result={};
return list.toArray(result);
}
}
2.5 SpringMvc 拦截器用过吗?什么场景会用到,过滤器,拦截器,监听器有什么区别?
拦截器是指通过统一拦截从浏览器发往服务器的请求来完成功能的增强。
使用场景:解决请求的共性问题(乱码问题、权限验证问题)
过滤器
Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码、做一些业务逻辑判断等。其工作原理是,只要你在web.xml文件配置好要拦截的客户端请求,它都会帮你拦截到请求,此时你就可以对请求或响应(Request、Response)统一设置编码,简化操作;同时还可进行逻辑判断,如用户是否已经登陆、有没有权限访问该页面等等工作。它是随你的web应用启动而启动的,只初始化一次,以后就可以拦截相关请求,只有当你的web应用停止或重新部署的时候才销毁。
监听器
现在来说说Servlet的监听器Listener,它是实现了javax.servlet.ServletContextListener 接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。主要作用是: 做一些初始化的内容添加工作、设置一些基本的内容、比如一些参数或者是一些固定的对象等等
拦截器
拦截器是在面向切面编程中应用的,就是在你的service或者一个方法前调用一个方法,或者在方法后调用一个方法。是基于JAVA的反射机制。拦截器不是在web.xml
1).过滤器:所谓过滤器顾名思义是用来过滤的,在java web中,你传入的request,response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者struts的action进行业务逻辑,比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入servlet或者struts的action前统一设置字符集,或者去除掉一些非法字符(聊天室经常用到的,一些骂人的话)。filter 流程是线性的, url传来之后,检查之后,可保持原来的流程继续向下执行,被下一个filter, servlet接收等.
2).监听器:这个东西在c/s模式里面经常用到,他会对特定的事件产生产生一个处理。监听在很多模式下用到。比如说观察者模式,就是一个监听来的。又比如struts可以用监听来启动。Servlet监听器用于监听一些重要事件的发生,监听器对象可以在事情发生前、发生后可以做一些必要的处理。
3).java的拦截器 主要是用在插件上,扩展件上比如 hivernate spring struts2等 有点类似面向切片的技术,在用之前先要在配置文件即xml文件里声明一段的那个东西。
2.6 ThreadLocal的原理和应用场景
参考文档:http://blog.csdn.net/sonny543/article/details/51336457
每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。
ThreadLocal的应用场景:
最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等
2.7 简述TCP的三次握手
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
1).第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态, 等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)
2).第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
3)第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1), 此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。
2.8 SpringMVC request接收设置是线程安全的吗?
参考文档:http://blog.csdn.net/q1512451239/article/details/53122512
答案:是线程安全的,request、response以及requestcontext在使用时不需要进行同步。而根据spring的默认规则,controller对于beanfactory而言是单例的。即controller只有一个, controller中的request等实例对象也只有一个
2.9 列举Maven常见的六种依赖范围
答案:
1.compile:编译依赖范围(默认),对其三种都有效
2.test:测试依赖范围,只对测试classpath有效
3.runtime:运行依赖范围,只对测试和运行有效,编译主代码无效,例如JDBC
4.provided:已提供依赖范围,只对编译和测试有效,运行时无效,例如selvet-api
5.system:系统依赖范围.谨慎使用.例如本地的,maven仓库之外的类库文件
6.import(maven2.0.9以上):导入依赖范围,不会对其他三种有影响
2.10 Mybatis如何防止sql注入?mybatis拦截器了解过吗,应用场景是什么?
答案:Mybatis使用#{}经过预编译的,是安全的,防止sql注入。
Mybatis拦截器只能拦截四种类型的接口:Executor、StatementHandler、 ParameterHandler和ResultSetHandler。这是在Mybatis的Configuration中写死了的,如果要支持拦截其他接口就需要我们重写Mybatis的Configuration。Mybatis可以对这四个接口中所有的方法进行拦截。Mybatis拦截器常常会被用来进行分页处理。
2.11简单解释自动装配的各种模式,或者叫装配方式。
在Spring框架中共有5种自动装配:
no:这是Spring框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在bean 定义中用标签明确的设置依赖关系。
byName:该选项可以根据bean名称设置依赖关系。当向一个bean中自动装配一个属 性时,容器将根据bean的名称自动在在配置文件中查询一个匹配的bean。如果找到的 话,就装配这个属性,如果没找到的话就报错。
byType:该选项可以根据bean类型设置依赖关系。当向一个bean中自动装配一个属性时,容器将根据bean的类型自动在在配置文件中查询一个匹配的bean。如果找到的话,就装配这个属性,如果没找到的话就报错。
constructor:造器的自动装配和byType模式类似,但是仅仅适用于与有构造器相同参数 的bean,如果在容器中没有找到与构造器参数类型一致的bean,那么将会抛出异常。
autodetect:该模式自动探测使用构造器自动装配或者byType自动装配。首先,首先会 尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在bean内部 没有找到相应的构造器或者是无参构造器,容器就会自动选择byTpe的自动装配方式。
2.12 mvc的各个部分都有哪些技术来实现?如何实现的?
MVC 是 Model-View-Controller 的简写。 Model 代表的是应用的业务逻辑( 通过JavaBean, EJB 组件实现), View 是应用的表示面( 由 JSP 页面产生), Controller 是提供应用的处理过程控制(一般是一个Servlet), 通过这种设计模型把应用逻辑, 处 理过程和显示逻辑分成不同的组件实现。这些组件可以进行交互和重用。
2.13 反射机制一般应用在什么场景?
答案:反射机制的应用场景:
1)逆向代码 ,例如反编译
3)与注解相结合的框架 例如Retrofit
4)单纯的反射机制应用框架 例如EventBus 2.x
5)动态生成类框架 例如Gson
2.14设计Java程序,假设有50瓶饮料,喝完三个空瓶可以换一瓶饮料,依次类推,请问总共喝了多少饮料。
答案:
public class Buy {
public static void main(String[] args) {
int n = 50; //初始饮料总数
int i=0; //兑换次数
while(true){
n -= 3; //喝3瓶
n++; //兑换1瓶
i++; //兑换次数+1
if(n<3)
{
System.out.println ("共喝了"+(50+i)+"瓶");
break;
}
}
}
}
2.15 根据某年某月某日,输出这是一年中第几天。
答案:
/定义存储年月日的变量
int year = 0, month = 0, day = 0;
//提示用户输入
printf("请输入年月日(比如1990-1-1):");
//接受用户输入, 切记,scanf中""内的格式是什么, 输入的格式必须一致
scanf("%d-%d-%d", &year,&month, &day);
//定义一个数组存放每个月的天数
int dayOfMonth[12] = {31, 28, 31, 30, 31,30, 31, 31, 30, 31, 30, 31};
//判断是否不是闰年
if (year % 400 == 0 || (year % 4 == 0&& year % 100 != 0)) {
//闰年二月29天
dayOfMonth[1] = 29;
}
//定义变量记录是第几天
int whichDay = 0;
//例如3月20日是一年的第几天, 计算方法 1月的天数 + 2月的天数 + 20
for (int i = 0; i < month - 1; i++) {
whichDay += dayOfMonth[i];
}
whichDay += day;
printf("是一年的%d天\n", whichDay);
2.16利润与奖金,某公司销售10万元到20万元的奖金10%,在20万元的奖金10万元以上的奖金7.5%,到40万元超出20万元的部分奖金为5%,到60万元的超出40万元的部分奖金3%,到100万元的超出60万元部分奖金1%,请输出说的奖金。
答案:
public class Test1 {
public static void main(String[] args) {
float jiangjin=0;
Scanner scan=new Scanner(System.in);
System.out.print("请输入利润:");
float num=scan.nextInt();
if(num<=100000){
jiangjin=(float) (num*0.1);
}
else if(num<=200000){
jiangjin=(float) ((num-100000)*0.075+100000*0.1);
}
else if(num<=400000){
jiangjin=(float) ((num-200000)*0.5 +100000*0.175);
}
else if(num<=600000){
jiangjin=(float) ((num-400000)*0.3 +100000*0.175+200000*0.5);
}
else if(num<=1000000){
jiangjin=(float) ((num-600000)*0.015 +100000*0.175+200000*0.5+200000*0.3);
}
else{
jiangjin=(float) ((num-1000000)*0.01 +100000*0.175+200000*0.5+200000*0.3+400000*0.015);
}
System.out.println("奖金:"+jiangjin);
}
}
2.17请描述工作流机制(JBPM/BPEL)等
参考:http://blog.csdn.net/leroy008/article/details/8058187
2.18 除了懒汉式和饿汉式你还了解那些单利模式?
答案:
第一种:双重查锁模式
public class DoubleCheckLock {
private static DoubleCheckLock instance = null;
private DoubleCheckLock() {
}
public static DoubleCheckLock getInstance() {
if (instance == null) {
synchronized (DoubleCheckLock.class) {
if (instance == null) {
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
第二种:枚举单例
public enum SingleEnum {
INSTANCE;
public void doSomething() {
ToastUtils.showLongToast("do something...");
}
}
第三种:静态内部类方式
public class StaticInner {
private static StaticInner instance;
public static StaticInner getInstance() {
return SingletonHolder.STATIC_INNER;
}
private static class SingletonHolder {
private static final StaticInner STATIC_INNER = new StaticInner();
}
}
2.19 简述SSH的概念以及中主要的设计思想?
SSH是 struts+spring+hibernate的一个集成框架,是目前比较流行的一种Web应用程序开源框架。
集成SSH框架的系统从职责上分为四层:表示层、业务逻辑层、数据持久层和域模块层,以帮助开发人员在短期内搭建结构清晰、可复用性好、维护方便的Web应用程序。其中使用Struts作为系统的整体基础架构,负责MVC的分离,在Struts框架的模型部分,控制业务跳转,利用Hibernate框架对持久层提供支持,Spring做管理,管理struts和hibernate。具体做法是:用面向对象的分析方法根据需求提出一些模型,将这些模型实现为基本的Java对象,然后编写基本的DAO(Data Access Objects)接口,并给出Hibernate的DAO实现,采用Hibernate架构实现的DAO类来实现Java类与数据库之间的转换和访问,最后由Spring做管理
20..unix下如何让命令在后台执行?
要让程序在后台执行,只需在命令行的最后加上“&”符号。
例如:$ find . -name abc -print&;
2.21 rm -i与rm -r 个实现什么功能?
rm -i --interactive 交互模式删除文件,删除文件前给出提示。
rm -r -r或-R:递归处理,将指定目录下的所有文件与子目录一并处理。
2.22什么是乐观锁,什么是悲观锁,两者的区别是什么?
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
2.23日志打印的log4j的配置中%t表示什么?
答案:%t 输出产生该日志事件的线程名
扩展:%M是输出方法的名字、%m是输出代码指定的日志信息。
指定的打印信息的具体格式ConversionPattern,具体参数:
%m 输出代码中指定的消息
%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%n 输出一个回车换行符,Windows平台为"rn”,Unix平台为"n”
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%: 输出一个”%”字符
%F: 输出日志消息产生时所在的文件名称
%M: 输出执行方法
%L: 输出代码中的行号
2.24 Spring中什么时候引起NotWritablePropertyException和Could not open calss path resource[ApplicationContext.xml]
出现NotWritablePropertyException异常的原因一般是在ApplicationContext.xml中property name的错误等相关错误。
[if !supportLists]七、[endif]华胜天成(2017-12-11-wzy)
[if !supportLists]1. [endif]不定项选择题
[if !supportLists]1.1 [endif]关于Web应用程序,下列说法错误的是(b)。
a) WEB-INF目录存在于web应用的根目录下
b) WEB-INF目录与classes 目录平行
c) web.xml在WEB-INF目录下
d) Web应用程序可以打包为war文件
解释:classes目录位于WEB-INF目录之下,而不是平行的关系。
[if !supportLists]1.2 [endif]有关Servlet的生命周期说法正确的有( cd)。
a) Servlet的生命周期由Servlet实例控制
b) init()方法在创建完Servlet实例后对其进行初始化,传递的参数为实现ServletContext接口的对象
c) service()方法响应客户端发出的请求
d) destroy()方法释放Servlet实例
解释:Servlet的生命周期是由Servlet容器(Tomcat就是常见的Servlet容器)管理的,因此a不对。
Servlet中的init()方法有两个重载,一个是空参的,另外一个是带ServletConfig形参的,而不是ServletContext,因此b不对。
关于d选项,说法其实并不好,正确的表达应该是因为Servlet实例要释放(销毁)了,才会先调用destroy()方法。
[if !supportLists]1.3 [endif]有关会话跟踪技术描述正确的是(abc)。
a) Cookie是Web服务器发送给客户端的一小段信息,客户端请求时,可以读取该信息发送到服务器端
b) 关闭浏览器意味着会话ID丢失,但所有与原会话关联的会话数据仍保留在服务器上,直至会话过期
c) 在禁用Cookie时可以使用URL重写技术跟踪会话
d) 隐藏表单域将字段添加到HTML表单并在客户端浏览器中显示
[if !supportLists]1.4 [endif]以下web.xml片断( d )正确地声明servlet 上下文参数。
a)
MAX
100
b)
c)
d)
MAX
100
补充:关于context-param和init-param的作用,请参考下文。
https://www.cnblogs.com/hzj-/articles/1689836.html
[if !supportLists]1.5 [endif]以下(a)可用于检索session属性userid的值。
a) session. getAttribute (“userid”);
b) session. setAttribute (“userid”);
c) request. getParameter (“userid”);
d) request. getAttribute (“userid”);
[if !supportLists]1.6 [endif]下列JSP代码,以下(cd)可放置在//1处,不会发生编译错误。
<%
for(int i = 0; i < 10; i++) {
//1
}
%>
a) <%= i %>
b) i
c) %><%= i %><%
d) 不写任何内容
[if !supportLists]1.7 [endif]考虑下面两个JSP文件代码片断:
test1.jsp:
<% pageContext.setAttribute(”ten”,new Integer(10));%>
//1
test2.jsp:
数字为:<%= pageContext.getAttribute(”ten”)%>
以下(c )放置在test1.jsp中的//1处,当请求test1.jsp时正确输出test2.jsp中的内容。
a)
b)
c) <%@ include file=”test2.jsp” %>
d) 由于pageContext对象的scope属性为page,所以test2.jsp不能访问test1.jsp定义的属性
解释:JSP中include的两种方法,1、属于动态引入/2、<%@ include file=”test2.jsp” %>属于静态引入。两者的区别如下:
1.执行时间上的区别
<%@ include file=”test2.jsp”%> 是在翻译阶段执行(将JSP页面转换成servlet的阶段)。通俗的话讲就是先合并在一起,然后再编译成一个Servlet文件。
在请求处理阶段执行。通俗的话讲就是先各自编译,然后在处理请求的�