MySQL数据库优化。
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分)
创建索引:alter table xx add index index_name(col_name.....)
最佳左前缀:
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
like以通配符开头('%abc...')mysql索引失效会变成全表扫描的操作
不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
永远是小表驱动大表
Explain进行性能分析:
查询着重看type里面的级别
从好到坏:system>const>eq_ref>ref>rang>index>all
最常见到rang,最好到ref
事务:
单个逻辑工作单元,执行的一系列操作。要么完全地执行,要么完全地不执行。
事务四大特征
A 原子性 B 一致性 C 隔离性 D 持久性
如果没有事务隔离级别会出现什么情况?
脏读:一个事务处理过程里读取了另一个未提交的事务中的数据
不可重复读:一个事务多次查询同一个范围的数据,查询结果不一样,这是由于另一个事务修改这个范围的数据(这个在大多数情况下面都不是问题,本来就是按照最后一次读的结果为准)
幻读:事务非独立执行时发生的一种现象,一个事务修改了数据,但是另个事务又新添加了一条数据,这时候在来读取数据,就好像没有修改.
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
不可重复读和幻读都是读取另个事务已经提交的数据,而脏读是读取没有另一个事务没有提交的数据不可重复读和幻读区别在,不可重复读重点在于同一数据,出现不同结果,重点在于修改。而幻读是新插入了一条数据,重点在于新增或者删除
事务四大隔离级别
1.串行化:避免脏读、不可重复读、幻读
2.可重复读:可避免脏读、不可重复读的发生。
3.读已提交:可避免脏读的发生。
4.读未提交:最低级别,任何情况都无法保证。
update 如果含有where条件,而且where条件使主键筛选,就锁行,否则锁表delete和update类似insert,也会锁行对该行进行操作时才会阻塞
mysql锁机制:
mysql中使用select for update的必须针对InnoDB,才能起作用。
关于mysql事务行锁for update实现写锁的功能InnoDB行级锁有共享锁(S LOCK)和排他锁(X LOCK)两种。
共享锁允许事物读一行记录,不允许任何线程对该行记录进行修改。
排他锁允许当前事物删除或更新一行记录,其他线程不能操作该记录
FOR UPDATE 在读取行上设置一个排他锁.假設有個表單 products ,裡面有 id 跟 name 二個欄位,id 是主鍵。
例1: (明確指定主鍵,並且有此筆資料,row lock)
SELECT * FROM products WHERE id='3' FOR UPDATE;
例2: (明確指定主鍵,若查無此筆資料,無 lock)
SELECT * FROM products WHERE id='-1' FOR UPDATE;
例3: (無主鍵,table lock)
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
例3: (主鍵不明確,table lock)
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
例4: (主鍵不明確,table lock)
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
SELECT ... LOCK IN SHARE MODE; 在读取行上设置一个共享锁
内连接和外连接
内连接 :连接结果只包含符合条件的行,参与连接的两个表都应该符合连接条件。 inner join
外连接: 连接结果不仅包含符合条件的行,而且还包含自身的不符合条件的行,分为左外连接,右外连接,全连接
多表连接查询的一个例子:
SELECT T1.C1,T2.CX,T3.CY
FROM TAB1 T1
INNER JOIN TAB2 T2 ON (T1.C1=T2.C2)
INNER JOIN TAB3 T3 ON (T1.C1=T2.C3)
LEFT OUTER JOIN TAB4 ON(T2.C2=T3.C3);
WHERE T1.X >T3.Y;
索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构
索引的作用及代价?如何建好索引?索引的原理
他为啥能提高速度:索引就是通过事先排好序,从而在查找时可以应用二叉查找等高效率的算法。
作用:1.通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性,2.可以大大加快 数据的检索速度
代价:1.创建索引和维护索引(在进行增加、删除、修改,需要动态维护索引)要耗费时间,这种时间随着数据 量的增加而增加
2.索引需要占物理空间
如何建好索引?
1、主键建立唯一索引,经常需要搜索的列上
2、经常用在连接的列上,这些列主要是一些外键
3、在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
4、在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
对于有些列不应该创建索引
1、对于那些在查询中很少使用或者参考的列不应该创建索引
2、对于那些只有很少数据值的列也不应该增加索引
3、当修改性能远远大于检索性能时,不应该创建索引
索引的原理
mysql采用的btree数据结构,索引列的数据是有序的,查找的时候,采用二叉查找,也不会查询全扫描的情况
MyISAM与InnoDB的区别是什么?
1.事务支持
MyISAM:强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。
InnoDB:提供事务支持事务
2.表锁差异
MyISAM:只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,
如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。
InnoDB:支持事务和行级锁,是innodb的最大特色。
行锁大幅度提高了多用户并发操作的新能。但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。
3.表主键
MyISAM:允许没有任何索引和主键的表存在,索引都是保存数据行的地址。
InnoDB:如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,
附加索引保存的是主键的值
4.外键
MyISAM:不支持
InnoDB:支持
MyISAM和InnoDB索引实现对比
MyISAM:采用b+tree的方式,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复
最后在叶子节点的data域保存数据记录的地址,通过地址在映射具体数据。
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分
InnoDB:使用B+Tree作为索引结构,InnoDB的数据文件本身就是索引文件, MyISAM的这是数据文件和索引文件分开的。
如果是主索引,这叶子节点直接保存的数据,这种索引叫做聚集索引,所以InnoDB引擎必须有主键,没有系统都会默认创建一个
而对于辅助索引,叶子节点的data域存储相应记录主键的值
辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,
因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。
再例如,用非单调的字段作为主键在InnoDB中不是个好做法,因为InnoDB数据文件本身是一颗B+Tree,
非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,
十分低效,而使用自增字段作为主键则是一个很好的选择。
聚集索引:该索引中键值的逻辑顺序决定了表中相应行的物理顺序
非聚集索引:该索引中索引的逻辑顺序与磁盘上真正的数据行的物理存储顺序不同
java基础知识
什么是值传递和引用传递
值传递:形式参数类型是基本数据类型,方法调用的时候,形式参数只是用实际参数的值初始化自己的存储单元内容,实际参数和形式参数的值分别储存在两个不同的地址单元,所以改变形式参数的值,不会影响实际参数
引用传递:形式参数类型是引用数据类型参数,方法调用时,实际参数是对象(或数组),这时实际参数与形式参数指向同一个地址,所以在方法执行中,对形式参数的操作实际上就是对实际参数的操作,这个结果在方法结束后被保留了下来,所以方法执行中形式参数的改变将会影响实际参数。
==和equals区别
“==” 如果是基本类型比较,则比较值是否相等,如果是引用类型,则比较是否指向的同一个堆内存地址
equal 我们不能单纯的说equals到底比较的是什么,重要的是看要去看equals方法里面的逻辑,默认的对象比较的是是否指向同一个地址
默认代码:
public boolean equals(Object obj) {
return (this == obj);
}
但是比如String里面就是重写了equals方法,修改成了比较内容是否相等
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
java 标识符可以是字母、数字、$、_(下划线),不可用数字开头,不能是java 的关键字
优先级比较: 淡云一笔安洛三幅 ( 单目 >运算>移位>比较>按位>逻辑> 三目 > 赋值 )
int j = 0;
int i = 0;
System.out.println(++i + j++);
输出1
while(true) 和for(;;)区别
逻辑成次上面他们没有区别,但是在jvm编译之后for(;;)指令少,不占用寄存器,而且没有判断跳转,比while (1)好。
final关键字:
final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化
位运算
重要公式:公式-n=~n+1可推出~n=-n-1
int i=10 那么根据公式: ~n=-10-1=-11
Arrays.asList方法作用
将一个数组转化为一个List对象,这个方法会返回一个ArrayList类型的对象,
这个ArrayList类并非java.util.ArrayList类,而是Arrays类的静态内部类!
用这个对象对列表进行添加删除更新操作,就会报UnsupportedOperationException异常
JDK中提供了三个ClassLoader,根据层级从高到低为:
1.Bootstrap ClassLoader,主要加载JVM自身工作需要的类。 加载的是系统类
2.Extension ClassLoader,主要加载%JAVA_HOME%\lib\ext目录下的库类。 加载的扩展类
3.Application ClassLoader,主要加载Classpath指定的库类,一般情况下这是程序中的默认类加载器,也是ClassLoader.getSystemClassLoader() 的返回值
JVM加载类的实现方式,我们称为 双亲委托模型:
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父加载器,每一层的类加载器都是如此,
因此所有的类加载请求最终都应该传送到顶层的Bootstrap ClassLoader中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载
双亲委托模型的重要用途是为了解决类载入过程中的安全性问题,防止如果有人写了一个恶意的基础类(如java.lang.String)并加载到JVM将引起严重后果。
类加载步骤
(1)加载,把class文件加载到jvm中,
(2)验证,确保 class 文件的字节流包含的信息符合当前 jvm 的要求 有文件格式验证, 元数据验证, 字节码验证, 符号引用验证等
(3)准备,正式给类变量分配内存并设置类变量初始值的阶段, 初始化为各数据类型的零值
(4)解析,把常量值内的符号引用替换为直接引用的过程
(5)初始化,执行类构造器<clinit>()方法
(6)使用 根据相应的业务逻辑代码使用该类
(7)卸载 类从方法区移除
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。(这个Class对应的类的所有的实例都是用完毕了)
- 加载该类的ClassLoader已经被GC。()
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。(这个类本身的对象使用完毕)
比较典型的自定义classloader使用情况就是给类加密。
java编译的代码可以轻易的被反编译,有些企业会给自己的类做特殊的加密,防止反编译,类加密后就不能再用java的
classloader去加载类了,这时就需要自定义classloader,再加载类的时候先解密类,然后再加载
ClassLoader的委托模型其实也表现了责任链的设计模式
方法的重写:(发生在子类和父类间)
1.方法名、参数列表、返回值类型都必须相同
2.访问修饰符必须大于或等于被重写的方法
3.重写的方法中,不能抛出新的异常或被重写的方法更多、更大的异常,如果有异常,则一定会抛出异常。
也就是说,只能抛出相同的异常或是被重写方法异常的子异常,还可以抛出非编译异常(RuntimeException)
4.重写方法只会存在于具有继承关系的子类中,而当父类中的方法用private修饰时,即使子类中有重名方法,也不叫方法的重写
5.非静态方法不能被重写成静态方法
5.在访问修饰符不是private的情况下,final修饰的方法不能被重写,final就是最终的意思,就是不需要修改他了
6.静态方法不能被重写,原因:方法的重写体现了多态,而且静态方法、final方法、private修饰的方法,都是在编译的时候已经绑定好了
如果父类中有一个静态的方法,子类也有一个与其方法名,参数类型,参数个数都一样的方法,
并且也有static关键字修饰,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写
举个列子:普通父子关系类,当使用的时候后, Parent p=new Son1(); 这句话可以推断出来 如果调用了重写的方法,到底是调用的哪个子类的
但是静态方法,这样使用Parent.test();这样根本推不出来调用的哪个子类的重写的方法
这也证明了static修饰的方法不能重写。
方法重载:(发生在同一个类中)
1.方法名必须相同
2.参数列表必须不同
3.其他修饰符可以相同,也可以不同
4.可以抛出不同异常
5.final修饰的方法可以被重载
hashCode()和equals()
当在使用set集合时候,需要重写对象的hashCode和equals方法
原因:把一个元素放入一个集合流程:先判断此对象的hashCode和集合里面的是否相等
如果相等,在通过equals比较,如果不相等,直接存放
equal在相等,就判定这个对象在集合中已经存在
删除流程也是如此,先通过hashcode寻找如果有,在用equals比较,找到要删除的对象,进行删除,
而如果hashcode没有找到,直接返回,删除失败,没有找到对应的,需要删除的对象
这里有个关系:hashCode相等,但是不一定equal为true ,但是equal为true则hashCode一定相等
ArrayList扩容
int newCapacity = (oldCapacity * 3)/2 + 1;
原有容量的1.5倍+1,然后通过底层的复制方法将原有数据复制过来
ArrayList分配的初始空间为10,HashSet分配的初始空间为16
每次添加数据的时候,都会去比对需要的容量和老的容量,谁大,
如果需要的大,则扩容,在比较,如果扩容之后比需要的大就采用扩容的,如果没有需要的大,就直接采用需要的容易
copy是采用的Arrays.copyOf()方法进行copy
2.ArrayList和LinkedList的具体实现和应用场景
ArrayList适合需要大量进行随机访问的场景;
LinkedList则适合需要在集合进行增删的场景
Vector是线程安全的,stack继承Vector,所以也是线程安全的
hashTable和HashMap区别
1.hashMap的key,value可以为null,hashTable不能
2.hashMap继承的是AbstractMap类,hashTable实现dictionary类,这两个类都实现的是map接口
3.hashmap线程不安全,hashTable线程安全
4.HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
HashMap多线程不安全的原因是:hashMap在多线程环境下,当需要reSize扩容的时候,会出现问题。造成下次读的时候,死循环.
HashMap知识点:
哈希表数据结构:链表+数组
HashMap其实也是一个线性的数组实现的,但是这个数组数据类型是hashMap里面内部类Entry
Entry有三个属性 key value next 采用的数据结构就是链表
hashMap的键使用注意点:
HashMap中不可以把可变对象作为key来保存记录
什么是可变对象? 就是对象引用没有变,但是对象里面的属性改变了,导致key在计算hashcode和equals的时候,找不到对应的value
这里就是一个经典面试题:可不可以采用stringbuffer作为key,答案是明显的,不可以,因为StringBuffer是一个可变对象
所以我们在使用HashMap,key一般是String,Integer等不可变对象。
HashMap的扩容和添加都是进行头插法进行添加的
影响hashMap性能因素?
首先hashMap默认的负载因子(load factor),其默认值为0.75,如果hashMap的值>=hashMap数组长度*负载因子就会扩容
增大负载因子,可以在原来的数组中存储更多的值,减少扩容从而可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,
但会增加hash碰撞,导致查询数据的时间开销增加,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);
减小负载因子,会增加扩容次数,减少碰撞,会提高数据查询的性能,但会增加 Hash 表所占用的内存空间,因为扩容了
故我们在hashmap需要存放的元素数量可以预估的情况下,预先设定一个初始容量,来避免自动扩容的操作来提高性能。
还有就是减少“碰撞“
hashMap中的负载因子作用
首先负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量
HashMap为什么线程不安全(hash碰撞与扩容导致)
hashMap里面容量为啥一定为2的幂
static int indexFor(int h, int length) {
return h & (length-1);
} 核心就在这里,
这里h是通过K的hashCode最终计算出来的哈希值,并不是hashCode本身,而是在hashCode之上又经过一层运算的hash值,length是目前容量。
这块的处理很有玄机,与容量一定为2的幂环环相扣,
当容量一定是2^n时,h & (length - 1) == h % length,它俩是等价不等效的,位运算效率非常高,
实际开发中,很多的数值运算以及逻辑判断都可以转换成位运算,
但是位运算通常是难以理解的,因为其本身就是给电脑运算的,运算的是二进制,而不是给人类运算的,人类运算的是十进制,
这也是位运算在普遍的开发者中间不太流行的原因(门槛太高)。
这个等式实际上可以推理出来,2^n转换成二进制就是1+n个0,减1之后就是0+n个1,如16 -> 10000,15 -> 01111,
那根据&位运算的规则,都为1(真)时,才为1,那0≤运算后的结果≤15,假设h <= 15,那么运算后的结果就是h本身,h >15,
运算后的结果就是最后四位二进制做&运算后的值,最终,就是%运算后的余数。
我想,这就是容量必须为2的幂的原因
HashSet:
1、HashSet基于HashMap实现,无容量限制。
2、HashSet是非线程安全的。
3、HashSet不保证有序。
TreeSet:
1、TreeSet基于TreeMap实现,支持排序。
2、TreeSet是非线程安全的。
从对HashSet和TreeSet的描述来看,TreeSet和HashSet一样,也是完全基于Map来实现的,
并且都不支持get(int)来获取指定位置的元素(需要遍历获取),另外TreeSet还提供了一些排序方面的支持。
例如传入Comparator实现、descendingSet以及descendingIterator等。
TreeMap:
1、TreeMap是一个典型的基于红黑树的Map实现,因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。
2、TreeMap是非线程安全的。
Java中hashmap的解决办法就是采用的链地址法。
解决hash冲突的方法:
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表
开发地址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,
以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi
求查找成功不成功的平均查找长度参考地址:http://blog.csdn.net/u011080472/article/details/51177412
静态内部类和内部类区别:
非静态内部类能够访问外部类的静态和非静态成员。
静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员
原因:非静态内部类实例化需要在外部类实例化,之后才能实例化,这时候就可以保证外部类的属性已经初始化了
而对于静态内部类来说,实例化不依托于外部类是否实例化,所以这样不知道外部类是否初始化了成员变量,从而静态内部类不能使用外部类的非静态变量
//静态的内部类
TestClass.Inner1 inner1 = new Inner1(); //和普通的顶层类new的方法一样
//普通内部成员类
TestClass tc = new TestClass();
TestClass.Inner2 inner2 = tc.new Inner2(); //注意这里的使用方式
ThreadLocal会为每一个线程提供一个独立的变量副本
java多线程编程:
什么是线程?如何开启和结束?
线程是程序执行流的最小单元
首先明确线程安全:
线程安全就是说多线程访问同一代码,不会产生不确定的结果
线程与进程的区别,各有什么优缺点
1.进程有自己独立的地址空间;而线程共享进程的地址空间;
2.一个程序至少有一个进程,一个进程至少有一个线程;
3.线程是处理器调度的基本单位,但进程不是;
4.二者均可并发执行
多线程优点:
程序逻辑和控制方式简单; 所有线程可以直接共享内存和变量
缺点:
线程之间的同步和加锁控制比较麻烦, 一个线程的崩溃可能影响到整个程序的稳定性
多进程优点:
每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系
可以尽量减少线程加锁/解锁的影响,极大提高性能
缺点:
逻辑控制复杂,需要和主程序交互; 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大
开启:.start()方法
多个线程之间怎么共享数据
1.多个线程执行一个任务,就一个runnable对象,比如抢票,
针对这种情况,我们只需要写一个类实现Runnable接口即可,在run()方法中对这个票进行减一,
然后将这个Runnalbe扔给多个线程去执行,自然它们就操作同一个data了
当然run方法里面还是需要同步代码块,保证数据正确
2.多个线程执行多个任务,多个runnale对象
两个线程执行data增,两个线程执行data减。针对这种情况,我们要实现两个Runnable了,因为很明显有两个不同的任务了,一个任务执行data增,
另一个任务执行data减。为了便于维护,可以将两个任务方法放到一个类中,然后将data也放在这个类中,然后传到不同的Runnable中,即可完成数据的共享
http://blog.csdn.net/eson_15/article/details/51546302 这个地址总结的不错
线程的五种状态
新建 就绪 运行 死亡
阻塞
新建:只是new出来了线程,并没调用start()
就绪:当start()方法调用时,线程首先进入就绪状态
运行:可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码
阻塞:线程因为某种原因放弃了cpu 使用权
阻塞分为三种情况:
(1)等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中
(2)同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(3)其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡:线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生
java的锁:
这个是站在程序员的角度分类锁的,数据库那个不是这个角度.
乐观锁(cas(check and set)):
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,
更新一次版本号+1,乐观锁用户读多写少的场景,提高吞吐量,乐观锁常用些
数据库设计的时候设计一个专门的版本字段,当更新的时候,根据id查询需要更新的那行记录,就可以得到当前最新的版本
更新的时候,where后面除了id=? 还需要and version=? 这样如果当前version版本没有改变,这可以修改成功,否则修改失败
悲观锁:认为都会去改那个记录,所以修改之前,会锁起来
阻塞队列:BlockingQueue,当队列为空的时候,再去取数据调用take方法会阻塞,当队列满了的时候,再去调用put方法会阻塞
notify()和notifyAll()有什么区别?
notifyAll使所有原来在该对象上等待被notify的线程统统唤醒,一旦该对象被解锁,他们就会去竞争
notify他只是随机选择一个wait状态线程进行通知,并使它获得该对象上的锁,这样容易造成死锁
线程状态,BLOCKED和WAITING有什么区别?
synchronized(Obj) {
Obj.wait();
}
有两个线程t1,t2。
t1线程先进入同步代码块,这时候调用wait方法,会进入waiting状态,等待其他线程去notify这个线程
t2后来,这时候会在synchronized这里阻塞。进入blocked状态
产生死锁必要条件:
1.互斥条件:一个资源每次只能被一个进程使用
2.请求与保持条件:一个进程因请求一个资源而阻塞,它会保持自己的资源,不放
3.不剥夺条件:进程已获得的资源,在没有使用完前,不能被剥夺
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
怎么避免死锁:
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测
Lock 和synchronized 区别
Lock是一个接口,实现类为ReentrantLock,Lock的功能比synchronized强大,提供了可中断锁,还可以判断是否得到了当前的锁,Lock最后需要手动释放锁
synchronied是java里面的一个关键字,synchronized不需要手动释放,大并发量的情况下,Lock比synchronized性能好
Lock是用CAS来实现的
JDK 1.6以上synchronized也改用CAS来实现了,所以两者性能差不多
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,
我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
Lock还提供了公平锁机制,对于申请要求执行这段代码的线程按照先来先得的原则按顺序执行,
在new ReentrantLock(Boolean isfair),这里可以传递boolean来初始化是否使用公平锁
ConcurrentHashMap
采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力
ConcurrentHashMap中的分段锁称为Segment
这些并发类集合单个操作是线程安全,但是组合在一个方法里面可能就不安全,比如先get在put,一个线程get完毕之后,在执行put之前,另一个线程在get,
这时候put的值会少1,造成线程不安全,所以需要把这两个操作在同步 代码快里面执行
ConcurrentHashMap中的数据结构:Segment类型的数组,一个Segment其实就是一个类Hash Table的结构Segment类里面又包含一个数据结构HashEntry<K,V>的数组;
java的内存模型:
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存
举个简单的例子:在java中,执行下面这个语句:
i = 10;
执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中,而这里写入主存时间不确定。
volatile关键字:
第一:当线程2修改值的时候,使用volatile关键字修饰的变量值会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取
这样就保证了可见性
volatile关键字能保证有序性:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕
使用场景:通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
volatile分析文章:http://www.importnew.com/18126.html
StringBuilder:线程非安全的
StringBuffer:线程安全的
现在的jvm主要都是基于分代的垃圾收集方式,把堆分为分为young old perm ,其中young又分为eden和Survivor 这里eden和Survior区的比例是8:1:1
每个代采用不同的收集器,收集器又是基于不同的垃圾回收算法来实现的
最基本的java回收算法:复制算法和标记清理算法
复制算法:两个区域A和B,初始对象在A,继续存活的对象被转移到B。此为新生代最常用的算法
标记清除:一块区域,标记要回收的对象,然后回收,一定会出现碎片,那么引出
标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象
两个概念:新生代和年老代
新生代:初始对象,生命周期短的
永久代:长时间存在的对象
整个java的垃圾回收是新生代和年老代的协作,这种叫做分代回收。
Serial New收集器是针对新生代的收集器,采用的是复制算法
Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理
Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法
Serial Old(串行)收集器,新生代采用复制,老年代采用标记整理
Parallel Old(并行)收集器,针对老年代,标记整理
CMS收集器,基于标记-清除算法,容易出现大量空间碎片,以获取最短回收停顿时间为目标的收集器
当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,
但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器:整体上是基于标记-整理 ,局部采用复制,G1除了追求低停顿外,还能建立可预测的停顿时间模型
综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。
什么情况产生java.lang.OutOfMemoryError(java内存溢出)
1、异常时没有加finally{}来释放某些资源
2、Scope(范围)定义不对,这个很简单了,方法的局部变量定义成类的变量,类的静态变量
3、字符串处理时候,避免使用String该用StringBuffer(线程安全)、或者StringBuilder
4、对象所需占用的内存空间大于堆的可用空间
示例代码:
List<byte[]> byteList = new ArrayList<byte[]>();
byteList.add(new byte[1000 * 1024 * 1024]);
5、线程太多,内存不够新建线程
什么情况产生java.lang.StackOverflowError
方法调用层次太深,内存不够新建栈帧
分析内存泄漏的工具有:Jprofiler,visualvm
java的垃圾回收
java的垃圾回收机制除了释放没用的对象,还可以清除内存记录碎片,内存碎片被整理之后,就可以用来被分配给新的对象
GC触发条件:(1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用,因为GC线程优先级最低,所以应用忙的时候不会调用
(2)当应用在运行时候,Java堆内存不足时,GC会被调用
减少GC开销的措施:
1.不要显示的调用System.gc()
2.尽量减少临时对象的使用
3.对象不用的时候最好显示置空
4.尽量使用StringBuffer,不实用String累加字符串(String的特性有关)
5.能使用基本数据类型就不要使用封装类
6.尽量减少静态对象变量的使用
java中jvm典型参数设置:
1.-Xmx3550m:设置JVM最大可用内存为3550M。
2.-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
3.-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。
持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
4.-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。
在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
什么时候触发Full GC
1、旧生代空间不足2、perm区域满的时候3、CMS GC时出现promotion failed和concurrent mode failure
4、统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
什么时候触发minor GC。
1、Eden区域满了之后
包装类型和基本类型比较问题(例如,Integer类型的变量能否==int类型变量,能否作比较,什么时候不能作比较)
一个包装对象和一个基本类型的值进行比较是比较值,而不是比较引用
关键点:当包装类型为null的情况下,不能和基本类型比较
jdk与jre的区别
1.jdk是开发工具集,里面包含javac,也就是java compiler等服务端操作
2.jre(java runtime environment),就是java程序的运行环境,java程序在经过javac编译后会形成字符行文件
这个java的中间文件,计算机是看不懂的,而这个文件是给jvm(java的虚拟机)用的
3.jdk里面包含了jre,为啥还需要提供另外一条jre单独安装,首先jdk里面有很多小工具,这些小工具也是用java编译好的文件
为了让这些小工具正常运行,所以在jdk里面也包含了jre
java反射机制:
功能:在运行时状态下,都可以构造一个类,获取里面方法属性,这样就很灵活了
核心类就是:Class,这是一个描述类的类,通过一个类可以获取到对应的Class类,通过Class类能获取到他所描述类的所有方法,字段,构造器
获取Class对象三中方式:
a,类型.class 如: String.class使用类名加“.class”的方式即会返回与该类对应的Class对象。
这个方法可以直接获得与指定类关联的Class对象,而并不需要有该类的对象存在。
b,Class.forName("类名")
该方法可以根据字符串参数所指定的类名获取与该类关联的Class对象。如果该类还没有被装入,该方法会将该类装入JVM。
forName方法的参数是类的完 整限定名(即包含包名)。通常用于在程序运行时根据类名动态的载入该类并获得与之对应的Class对象。
c, obj.getClass();所有Java对象都具备这个方法,该方法用于返回调用该方法的对象的所属类关联的Class对象
jvm内存模型和java内存模型(JMM)
jvm内存模型:java栈,pc寄存器、堆、方法区、运行常量池(方法区的一部分)、本地方法栈(运行native方法的空间)
我们要想深入了解Java并发编程,就要先理解好Java内存模型。
Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步
Java线程之间的通信采用的是过共享内存模型
线程之间的共享变量存储在主内存(main memory)中,
每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
一个本地变量如果是原始类型,那么它会被完全存储到栈区。
一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区
共享变量比较典型的就是指类的成员变量
异常:
类组成:顶级父类:Throwable,下面两个子类:exception和error
exception下面分为:运行时异常和非运行时异常
常见的非运行是异常:IOException、SQLException、FileNotFoundExcetion
运行时异常:NullPointerException、IndexOutOfBoundsException、强转出错
switch支持类型:
在java1.7之前switch,只支持byte、char、short、int和对应封装类型。
switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会 自动 转换为int类型(精精度小的向大的转化),所以它们也支持
注意,对于精度比int大的类型,比如long、float,doulble,不会自动转换为int,如果想使用,就必须强转为int,如(int)float;
jdk1.7后,整形,枚举类型,boolean,字符串都可以
为什么jdk1.7后又可以用string类型作为switch参数呢?
其实,jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode,将string转换为int从而进行判断。