1、现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
答: thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 使用线程的join方法,该方法的作用是“等待线程执行结束”,即join()方法后面的代码块都要等待现场执行结束后才能执行 。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //使调用线程 t 在此之前执行完毕。
t.join(1000); //等待 t 线程,等待时间是1000毫秒
package cglib;
public class List1
{
public static void main(String[] args)
{
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();
t2.start();
t1.start();
}
}
输出:
t1
t2
t3
2、3*0.1==0.3返回false
因为 3*0.1 java生成0.30000000000000004,
修改成 float i=(float) (3*0.1);
i==0.3 ;
不管是什么数, 在计算机中最终都会被转化为 0 和 1 进行存储, 所以需要弄明白以下几点问题
- 一个小数如何转化为二进制
- 浮点数的二进制如何存储
浮点数的二进制表示
首先我们要了解浮点数二进制表示, 有以下两个原则:
- 整数部分对 2 取余然后逆序排列
- 小数部分乘 2 取整数部分, 然后顺序排列
0.1 的表示是什么?
我们继续按照浮点数的二进制表示来计算
0.1 * 2 = 0.2 整数部分取 0
0.2 * 2 = 0.4 整数部分取 0
0.4 * 2 = 0.8 整数部分取 0
0.8 * 2 = 1.6 整数部分取 1
0.6 * 2 = 1.2 整数部分取 1
0.2 * 2 = 0.4 整数部分取 0
…
所以你会发现, 0.1 的二进制表示是 0.00011001100110011001100110011……0011
0011作为二进制小数的循环节不断的进行循环.
这就引出了一个问题, 你永远不能存下 0.1 的二进制, 即使你把全世界的硬盘都放在一起, 也存不下 0.1 的二进制小数.
浮点数的二进制存储
Python 和 C 一样, 采用 IEEE 754 规范来存储浮点数. IEEE 754 对双精度浮点数的存储规范将 64 bit 分为 3 部分.
- 第 1 bit 位用来存储 符号, 决定这个数是正数还是负数
- 然后使用 11 bit 来存储指数部分
- 剩下的 52 bit 用来存储尾数
Double-precision_floating-point_format
而且可以指出的是, double 能存储的数的个数是有限的, double 能代表的数必然不超过 2^64 个, 那么现实世界上有多少个小数呢? 无限个. 计算机能做的只能是一个接近这个小数的值, 是这个值在一定精度下与逻辑认为的值相等. 换句话说, 每个小数的存储(但是不是所有的), 都会伴有精度的丢失.
0.1 在计算机存储中真正的数字是 0.1000000000000000055511151231257827021181583404541015625
0.2 是
0.200000000000000011102230246251565404236316680908203125
0.3 是
0.299999999999999988897769753748434595763683319091796875
这不是bug,原因在与十进制到二进制的转换导致的精度问题!其次这几乎出现在很多的编程
语言中:C、C++、Java、Javascript、Python中,准确的说:“使用了IEEE754浮点数格式”来存储浮点类型(float 32,double 64)的任何编程语言都有这个问题!
简要介绍下IEEE 754浮点格式:它用科学记数法以底数为2的小数来表示浮点数。IEEE浮点数(共32位)用1位表示数字符号,用8为表示指数,用23为来表示尾数(即小数部分)。此处指数用移码存储,尾数则是原码(没有符号位)。之所以用移码是因为移码的负数的符号位为0,这可以保证浮点数0的所有位都是0。双精度浮点数(64位),使用1位符号位、11位指数位、52位尾数位来表示。
因为科学记数法有很多种方式来表示给定的数字,所以要规范化浮点数,以便用底数为2并且小数点左边为1的小数来表示(注意是二进制的,所以只要不为0则一定有一位为1),按照需要调节指数就可以得到所需的数字。例如:十进制的1.25 => 二进制的1.01 => 则存储时指数为0、尾数为1.01、符号位为0.(十进制转二进制)
回到开头,为什么“0.1+0.2=0.30000000000000004”?首先声明这是javascript语言计算的结果(注意Javascript的数字类型是以64位的IEEE 754格式存储的)。正如同十进制无法精确表示1/3(0.33333...)一样,二进制也有无法精确表示的值。例如1/10。64位浮点数情况下:
十进制0.1=> 二进制0.00011001100110011...(循环0011)
=>尾数为1.1001100110011001100...1100(共52位,除了小数点左边的1),指数为-4(二进制移码为00000000010),符号位为0=> 存储为:0 00000000100 10011001100110011...11001=> 因为尾数最多52位,所以实际存储的值为0.00011001100110011001100110011001100110011001100110011001
十进制0.2=> 二进制0.0011001100110011...(循环0011)
=>尾数为1.1001100110011001100...1100(共52位,除了小数点左边的1),指数为-3(二进制移码为00000000011),符号位为0=> 存储为:0 00000000011 10011001100110011...11001
因为尾数最多52位,所以实际存储的值为0.00110011001100110011001100110011001100110011001100110011
3、说下java堆空间结构及常用的jvm内存分析命令和工具
空间结构:
1. New Generation
又称为新生代,程序中新建的对象都将分配到新生代中,新生代又由Eden Space和两块Survivor Space构成,可通过-Xmn参数来指定其大小,Eden Space的大小和两块Survivor Space的大小比例默认为8,即当New Generation的大小为10M时,Eden Space的大小为8M,两块Survivor Space各占1M,这个比例可通过-XX:SurvivorRatio来指定。
2. Old Generation
又称为旧生代,用于存放程序中经过几次垃圾回收还存活的对象,例如缓存的对象等,旧生代所占用的内存大小即为-Xmx指定的大小减去-Xmn指定的大小。
常用的jvm内存分析命令和工具
1:gc日志输出
在jvm启动参数中加入 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime,jvm将会按照这些参数顺序输出gc概要信息,详细信息,gc时间信息,gc造成 的应用暂停时间。如果在刚才的参数后面加入参数 -Xloggc:文件路径,gc信息将会输出到指定的文件中。其他参数还有
-verbose:gc和-XX:+PrintTenuringDistribution等。
2:jconsole
jconsole是jdk自带的一个内存分析工具,它提供了图形界面。可以查看到被监控的jvm的内存信息,线程信息,类加载信息,MBean信息。
jconsole位于jdk目录下的bin目录,在windows下是jconsole.exe,在unix和linux下是 jconsole.sh,jconsole可以监控本地应用,也可以监控远程应用。 要监控本地应用,执行jconsole pid,pid就是运行的java进程id,如果不带上pid参数,则执行jconsole命令后,会看到一个对话框弹出,上面列出了本地的java进 程,可以选择一个进行监控。如果要远程监控,则要在远程服务器的jvm参数里加入一些东西,因为jconsole的远程监控基于jmx的
4、哪些情况下索引会失效
- 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
2.对于多列索引,不是使用的第一部分,则不会使用索引
3.like查询是以%开头
4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5.如果mysql估计使用全表扫描要比使用索引快,则不使用索引,
如查询谓词没有使用索引的主要边界,可能会导致不走索引。比如,你查询的是SELECT * FROM T WHERE Y=XXX;假如你的T表上有一个包含Y值的组合索引,但是优化器会认为需要一行行的扫描会更有效,这个时候,优化器可能会选择TABLE ACCESS FULL,但是如果换成了SELECT Y FROM T WHERE Y = XXX,优化器会直接去索引中找到Y的值,因为从B树中就可以找到相应的值。
6. 对索引列进行运算导致索引失效,我所指的对索引列进行运算包括(+,-,*,/,! 等)
错误的例子:select * from test where id-1=9;
正确的例子:select * from test where id=10;
7. 使用Oracle内部函数导致索引失效.对于这样情况应当创建基于函数的索引.
错误的例子:select * from test where round(id)=10; 说明,此时id的索引已经不起作用了
正确的例子:首先建立函数索引,create index test_id_fbi_idx on test(round(id));然后 select * from test where round(id)=10; 这时函数索引起作用了
8. 以下使用会使索引失效,应避免使用;
a. 使用 <> 、not in 、not exist、!=
b、当变量采用的是times变量,而表的字段采用的是date变量时.或相反情况。
9. 不要将空的变量值直接与比较运算符(符号)比较。
如果变量可能为空,应使用 IS NULL 或 IS NOT NULL 进行比较,或者使用 ISNULL 函数。
此外,查看索引的使用情况。
10、 如果在B树索引中有一个空值,那么查询诸如SELECT COUNT(*) FROM T 的时候,因为HASHSET中不能存储空值的,所以优化器不会走索引,有两种方式可以让索引有效,一种是SELECT COUNT(*) FROM T WHERE XXX IS NOT NULL或者是不能为空
11、 在Oracle的初始化参数中,有一个参数是一次读取的数据块的数目,比如你的表只有几个数据块大小,而且可以被Oracle一次性抓取,那么就没有使用 索引的必要了,因为抓取索引还需要去根据rowid从数据块中获取相应的元素值,因此在表特别小的情况下,索引没有用到是情理当中的事情。
12、很长时间没有做表分析,或者重新收集表状态信息了,在数据字典中,表的统计信息是不准确的,这个情况下,可能会使用错误的索引,这个效率可能也是比较低的。
show status like ‘Handler_read%’;
handler_read_key:这个值越高越好,越高表示使用索引查询到的次数
handler_read_rnd_next:这个值越高,说明查询低效
5、
在JDBC中使用PreparedStatement代替Statement,预防SQL注入,
这是因为PreparedStatement不允许在插入时改变查询的逻辑结构
Statement和PreparedStatement的关系与区别在于:
① PreparedStatement类是Statement类的子类,拥有更多强大的功能。
② PreparedStatement类可以防止SQL注入攻击的问题
③ PreparedStatement会对SQL语句进行预编译,以减轻数据库服务器的压力,而Statement则无法做到。
Statement主要用于执行静态SQL语句,即内容固定不变的SQL语句。Statement每执行一次都要对传入的SQL语句编译一次,效率较差。
Statement :
String sql = "select * from user where name='" +name+ "'";
页面带过来的表单数据name值为' or 1=1 or name='
我们若将表单填写的数据带到代码中的SQL语句,就形成如下的SQL命令:
select * from user where name='' or 1=1 or name=''
可以看到使用Statement对象就是将两个字符串拼接形成的SQL语句,这样做很可能会将判断条件改变,如上面的命令,在where语句中出现了 or 1=1 这样一定会返回true的语句,就如同程序发送一条“select * from user where true”的句子,那么数据库执行这条语句根本不需要筛选条件,只要数据库有任意用户,都可以告诉程序你找到了该指定用户,那么我们连密码都不用填的只需 要恶意SQL语句即可登录网站。这就是一个SQL注入的典型例子。
PreparedStatement :
sql = "select * from users where NAME = ? and PWD = ?";
而使用PreparedStatement则不会,因为 PreparedStatement的预编译,会将表单中所填写的数据进行编译,这种编译是包含字符过滤的编译,就好像对html进行过滤转义一样,这字 符过滤最关键的因素在于PreparedStatement使用的是占位符,而不会像Statement那样因为拼接字符串而引入了引号, 可以看到在PreparedStatement中即使接收的表单数据中SQL语句以引号包围,由于程序中的SQL语句使用占位符,因此就相当于条件为 where name=' or 1=1 or name=',显然数据库并没有这样的记录,因此防止了SQL注入的问题。
某些情况下,SQL语句只是其中的参数有所不同,其余子句完全相同,适用于PreparedStatement。 PreparedStatement 实例包含已事先编译的 SQL 语句,SQL 语句可有一个或多个 IN 参数,IN参数的值在 SQL 语句创建时未被指定。该语句为每个 IN 参数保留一个问号(“?”)作为占位符。
每个问号的值必须在该语句执行之前,通过适当的setInt或者setString 等方法提供。
由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。
通常批量处理时使用PreparedStatement。
6、
Servlet —— 只有一个实例
Servlet的生命周期:
(1)装载Servlet。该操作一般是动态执行。然而,Server通常会提供一个管理的选项,用于在Server启动时强制装载和初始化特定的Servlet。
(2)Server创建一个Servlet的实例
(3)Server调用Servlet的init()方法
(4)一个客户端的请求到达Server
(5)Server创建一个请求对象
(6)Server创建一个响应对象
(7)Server激活Servlet的Service()方法,传递请求、响应对象作为参数
(8)service()方法获得关于请求对象的信息,处理请求,访问其他资源,获得需要的信息
(9)service()方法使用响应对象的方法,将响应传回Server,最终到达客户端。Service()方法可以激活其他方法以处理请求,如doGet()或doPost()或程序员自己开发的新的方法。
(10)对于更多的客户端请求,Server创建新的请求和响应对象,仍然激活此Servlet的service()方法,将这两个对象作为参数传递给 该方法。如此重复以上的循环,但无需再调用init()方法。因为,一般Servlet只初始化一次(只有一个实例),而当Server不再需要 Servlet时(如异常或Server关闭),Server将调用Servlet的destroy()方法。
这个生命周期是相当好理解的。唯一的一点,就是,为什么Servlet只有一个实例?
出于性能的考虑:特别的对于门户网站而言,每一个Servlet在每一秒内的并发访问量都可以是成千上万的。在一个面向模块化开发的现在,常常一个点击 操作就被定义为一个Servlet的实现,而如果Servlet的每一次被访问,都创建一个新的实例的话,服务器的可用资源消耗量将是一个相当重要的问 题。退一步,一般Servlet的访问是很快的,每一个实例被快速的创建,又被快速的回收,GC的回收速度也跟不上,频繁的内存操作也将可能带来次生的问 题。所以,Servlet的“单一实例化”是一个很重要的策略。
7、编写一个基于guava中的缓存组件,有哪些场景需要考虑,怎么解决
为什么要有本地缓存?
在 系统中,有些数据,数据量小,但是访问十分频繁(例如国家标准行政区域数据),针对这种场景,需要将数据搞到应用的本地缓存中,以提升系统的访问效率,减 少无谓的数据库访问(数据库访问占用数据库连接,同时网络消耗比较大),但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略。
为什么是本地缓存,而不是分布式的集群缓存?
目前的数据,大多是业务无关的小数据缓存,没有必要搞分布式的集群缓存,目前涉及到订单和商品的数据,会直接走DB进行请求,再加上分布式缓存的构建,集群维护成本比较高,不太适合紧急的业务项目。
这里介绍一下缓存使用的三个阶段(摘自info架构师文档)
本地缓存在那个区域?
目前考虑的是占用了JVM的heap区域,再细化一点的就是heap中的old区,目前的数据量来看,都是一些小数据,加起来没有几百兆,放在heap区 域最快最方便。后期如果需要放置在本地缓存的数据大的时候,可以考虑在off-heap区域,但是off-heap区域的话,需要考虑对象的序列化(因为 off-heap区域存储的是二进制的数据),另外一个的话就是off-heap的GC问题。其实,如果真的数据量比较大,那其实就可以考虑搞一个集中式 的缓存系统,可以是单机,也可以是集群,来承担缓存的作用。
搞一个单例模式,里面有个Map的变量来放置数据
关于单例模式,一个既简单又复杂的模式(http://iamzhongyong.iteye.com/blog/1539642)
非常典型的代码如下:
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 |
|
这种能不能用?可以用,但是非常局限
1 2 3 4 5 6 7 8 |
|
引入EhCache来构建缓存(详细介绍: http://raychase.iteye.com/blog/1545906)
EhCahce的核心类:
A、CacheManager:Cache的管理类;
B、Cache:具体的cache类信息,负责缓存的get和put等操作
C、CacheConfiguration :cache的配置信息,包含策略、最大值等信息
D、Element:cache中单条缓存数据的单位
典型的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
当然,Cache的配置信息,可以通过配置文件制定了。。。
优点:功能强大,有失效策略、最大数量设置等,缓存的持久化只有企业版才有,组件的缓存同步,可以通过jgroup来实现
缺点:功能强大的同时,也使其更加复杂
引入guava的cacheBuilder来构建缓存
这个非常强大、简单,通过一个CacheBuilder类就可以满足需求。
缺点就是如果要组件同步的话,需要自己实现这个功能。
典型的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
缓存预热怎么搞?
A、全量预热,固定的时间段移除所有,然后再全量预热
适用场景:
1、数据更新不频繁,例如每天晚上3点更新即可的需求;
2、数据基本没有变化,例如全国区域性数据;
B、增量预热(缓存查询,没有,则查询数据库,有则放入缓存)
适用场景:
1、 数据更新要求缓存中同步更新的场景
集群内部,缓存的一致性如何保证?
如果采用ehcache的话,可以使用框架本身的JGroup来实现组内机器之间的缓存同步。
如果是采用google的cacheBuilder的话,需要自己实现缓存的同步。
A、非实时生效数据:数据的更新不会时时发生,应用启动的时候更新即可,然后定时程序定时去清理缓存;
B、需要实时生效数据:启动时可预热也可不预热,但是缓存数据变更后,集群之间需要同步
内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 Cache在真实场景中有着相当广的使用范围。例如,当一个值要通过昂贵的计算和检索来获取,并且这个结果会被多次使用到,这个时候你就应该考虑使用缓存 了。 Cache有点类似于ConcurrentMap,但并不是完全一样。最根本的区别在于ConcurrentMap会持有添加到Map中的所有元素直到元 素被移除为止。Cache通过相关的配置项可以自动驱逐相关的缓存项,达到约束缓存占用的目的。有些情景中LoadingCache通过自动的内存载入甚 至可以不进行驱逐缓存项。 通常,Guava缓存工具适用于如下的情景: 1、你愿意牺牲一些内存来提升速度。 2、你期待缓存项的查询大于一次。 3、 你的缓存数据不超过内存(Guava缓存是单个应用中的本地缓存。它不会将数据存储到文件中,或者外部服务器。如果不适合你,可以考虑一下 Memcached) 注意:如果你不需要缓存的这些特性,那么使用ConcurrentHashMap会有更好的内存效率,但是如果想基于旧有的ConcurrentMap复制实现Cache的一些特性,那么可能是非常困难或者根本不可能。
使用缓存,就存在缓存数据一致性的问题,和缓存数据的更新敏感度的问题,这个就是缓存的数据更新问题。
如果是分布式缓存,就另外涉及到分布式的数据一致性问题,这里仅针对本地缓存进行讨论。
针对本地缓存,更新方法有很多种,比如最常用的:
- 被动更新: 是先从缓存获取,没有则回源取,再放回缓存;
- 主动更新: 发现数据改变后直接更新缓存(在多机环境下,一般不会采用)
在高并发场景下,被动更新的回源是要格外小心的,也就是雪崩穿透问题: 如果有太多请求在同一时间回源,后端服务如果无法支撑这么高并发,容易引发后端服务崩溃。
这时Guava Cache上场了,Guava Cache里的CacheLoader在回源的load方法上加了控制,对于同一个key,只让一个请求回源load,其他线程阻塞等待结果。同时,在 Guava里可以通过配置expireAfterAccess/expireAfterWrite设定key的过期时间,key过期后就单线程回源加载并 放回缓存。
样通过Guava Cache简简单单就较为安全地实现了缓存的被动更新操作。
为什么是”较为安全”呢?因为如果同一时间仍有太多的不同key过期,还是会有大量请求穿透缓存而请求到后端服务上,仍然有可能使后端服务崩溃,有什么办法解决这个问题呢?
1.将key的过期时间加个随机值,避免大家一起过期(前提是对业务不影响), 2.自己控制回源的并发数,即使有一万个key要更新,也只让100个可以回源,其余的9900个等着,(可以通过Guava的Striped实现) 3.在过期前主动更新,更新完成后将过期时间延长
另外,如果对刚才说的对于同一个key,只让一个请求回源,其他线程等待觉得还不爽,虽然对后端服务不会造成压力,但我的请求都还是blocked了,整个请求还是会被堵一下。
别急,Guava Cache还提供了一个refreshAfterWrite的配置项,定时刷新数据,刷新时仍只有一个线程回源取数据,但其他线程只会稍微等一会,没等到 就返回旧值,整个请求看起来就比较平滑了。为什么又是“比较平滑”呢?因为默认的刷新回源线程是同步的,如果想达到全过程平滑的效果,可以将刷新回源线程 做成异步方式。
这样数据的更新都是在后台异步做了,但这样也是有一定的代价的,比如过了刷新时间,仍可能拿到旧值,直到拿回数据更新缓存后才会返回新值。
因为这个refresh动作并不是主动发起的: 比如设置了5秒refresh一下,Guava的做法并不是真的每5秒刷一次,而是等请求到了之后,发现需要refresh时才会真的更新。所以,这一点 需要注意,比如虽然设置了5秒刷新,但如果超过1分钟都没有请求(假设key没有过期),当1分零1秒有请求来时,仍有可能返回旧值。
以下是关于设置Expire过期和Refresh刷新(sync/async)两种方式,Guava Cache对请求回源的处理示意图: