目录
wait¬ify
今天试着写了下生产消费的同步实例,凭借着之前看视频的记忆利用wait()和notifyAll()进行阻塞唤醒时候,发生了 - IllegalState - 的异常,查了查资料才知道我没有用锁,为什么需要锁呢?
因为在生产消费模式下,彼此需要一个 - 指标 - 来判断生产与否,消费与否,也就是wait或者继续执行。这个 - 指标 - 就以锁的方式来实现,看到其他人用一些 Object obj = new Object(); 或者 String str = "lock";等等当做锁,开始我及其不理解,最后才知道这些只不过是线程间的关联变量,拿到统一的变量引用,然后在各自的锁块中进行线程行为。
总之,该异常的原因可以简单归结为,在wait和notify的使用模式下,线程间需要一个同步- 指标 -来进行阻塞唤醒,所以- 锁 -就成为了此状况下的必要条件。
组合vs聚合、表设计建议、POJO对象功能分类
组合和聚合都属于关联,即对象属性,区别就是在语义上前者为"has-a"后者为"part-of",类比细胞之于人体,轮子之于汽车。
简单看了下数据库设计原则上的问题,提炼出几点供谨记:
- 字段类型由小及大递增式适用,例如先用tinyint,不行再用smallint,以此类推;
- 删除表项逻辑删除为主,对删除项添加is_deleted等字段进行状态判断,原因是物理删除影响自增字段,不好恢复;
- 针对大数据例如text字段,可存储url或者类似链接路由至他库进行二次查询,例如存在mangodb里;
- where子句限用<>,!=,以及or,索引命中相关;
- 如果逻辑允许,优先设置NOT NULL,避免空指针;
- 添加创建时间和修改时间字段。
关于DO、VO、DTO、PO等等对象的功能划分:
数据字典的意义
数据字典的配置是为了不同表之间关联的解耦,例如对每个用户存在于一个角色字段,角色又对应着某种权限,在此条件下如果之间填写角色名字,那之后名字改动了不便于维护,因此配置一个角色字典表,在其中配置不同角色对应不同权限,在用户角色字段填写代码等语义符号即可。
一句话:字段就是表之间的字段映射,是独立出来为了降低表关联的耦合度以及方便维护设计的。
CAS
CAS = Compare And Swap, 是一种解决并发问题的方案,先分别保存目标值和操作后的目标值,然后对目标值再次读取进行判定(与第一次保存的是否一样),再决定是重新判定还是Swap,对于最后一步的比较和替换,查阅资料后才知道 - 操作系统 - 对其赋予了原子操作,因为其就是匹配存储硬件的一次原子操作;
然而对于ABA问题,即其他线程撤回修改导致引用变量不一致,AtomicStampedReference类提供了增加版本标记来记录是否被修改。
Volatile
关于Volatile,是java提供的轻量级同步机制:主要解决了多线程操作变量的不可见性,即某线程不知道即将操作的变量是不是被其他变量修改过的;往深了说,线程在操作完共享变量(被volatile修饰的变量)时,要写回至主存供其他线程“可见”。
CountDownLatch
CountDownLatch,计数门闩,用来同步多线程间的等待唤醒;
编辑了个例子:
声明两个门闩赋给工人线程和领导线程,领导线程使order门闩置零,然后等待工人线程的门闩置零;
class Boss implements Runnable{
CountDownLatch order;
CountDownLatch ifDone;
public Boss(CountDownLatch ifDone,CountDownLatch order){
this.ifDone = ifDone;
this.order = order;
}
@Override
public void run(){
long duration = 0;
try {
order.countDown();
System.out.println("Boss said: start work!");
duration = System.currentTimeMillis();
ifDone.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
duration = System.currentTimeMillis() - duration;
System.out.println("Boss said: it took our " + duration + " miliseconds, not bad!");
}
}
其次在工人线程中先等待领导线程门闩命令到位,然后开始工作,完成后使对应门闩减一,最终所有工人线程完成后门闩消失,领导线程做出审阅等收尾行为。
class Worker implements Runnable{
CountDownLatch order;
CountDownLatch ifDone;
public Worker(CountDownLatch order,CountDownLatch ifDone){
this.ifDone = ifDone;
this.order = order;
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + " standby!");
try {
order.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " start!");
System.out.println(Thread.currentThread().getName() + " almost done!");
System.out.println(Thread.currentThread().getName() + " done!");
ifDone.countDown();
}
}
注意CountDownLatch只适用于有限资源线程等的同步阻塞唤醒,而且线程间必须持有同一门闩引用;
public static void main(String[] args) {
CountDownLatch order = new CountDownLatch(1);
Scanner input = new Scanner(System.in);
System.out.println(" How may workers here, boss?");
int workersNum = input.nextInt();
CountDownLatch ifDone = new CountDownLatch(workersNum);
Thread boss = new Thread(new Boss(ifDone, order));
for (int i = 0; i < workersNum; i++) {
new Thread(new Worker(order,ifDone)).start();
}
boss.start();
}
MySQL各时间类型属性一览
类型 | 占用空间 | 日期格式 | 最小值 | 最大值 | 零值表示 |
DATETIME | 8 bytes | YYYY-MM-DD HH:MM:SS | 1000-01-01 00:00:00 | 9999-12-31 23:59:59 | 0000-00-00 00:00:00 |
TIMESTAMP | 4 bytes | YYYY-MM-DD HH:MM:SS | 19700101080001 | 2038 年的某个时刻 | 0 |
DATE | 4 bytes | YYYY-MM-DD | 1000-01-01 | 9999-12-31 | 0000-00-00 |
TIME | 3 bytes | HH:MM:SS | -838:59:59 | 838:59:59 | 0:00:00 |
YEAR | 1 bytes | YYYY | 1901 | 2155 | 0 |
可得知在定义 持续时长 属性时可依据TIME类型来选择,不仅格式规范,占用空间更是比其他类型更小;
关于浮点数的机制:
就是类比于十进制的科学计数法,使小数点的位置可以依据和阶数相乘随意浮动
而针对浮点数的表示,在32位的模型中(以float为例),是由1位的正负位和10位的指数(控制小数点浮动到哪里),以及21位的尾数(整数加小数的二进制串);
在此本人之前一直不理解的地方有两点:
- 一个是好多文章涉及到的“默认为1或者0”,如今才知道这代表的是"1.xxxx"小数点前面的部分,这就类比于十进制科学计数法 ?.xxxxxx*10e10,?肯定是1~9其中一个,0的话只有在表示0的时候才有效;并是不这样,在IEEE 754标准中,开头默认为1意思是尾数开头隐藏了一位默认为1,即上图其实是1.1001001,这样不仅规范了尾数的开头,也增加了尾数容量;
- 其二就是小数部分的转换,如上图中十进制0.125 = 二进制的0.001,如今也才知道后者是代表2的负多少次方,通过多个负权的相加,得到对浮点数尾数的近似,为什么是近似呢?因为负权是一直再除以二的,例如0.5、0.25、0.125、0.0625,他们再怎么相加,在精度有限的情况下也只能得到末尾为0或5,所以会存在系统的自动进位舍入;这里就涉及MySQL在处理精度位溢出的情况下会根据进位后是否溢出来判断是否报错。
- 对于指数,IEEE 754采用的是 Biased Exponent, 127为0, 126为-1,128为1, 这样省去了一位符号位;
- 针对尾数的范围, 这里如果用补码表示, -1为最小
Telnet&泛型
关于Telnet:
Telnet是Internet上远程登录的一种程序;它可以让您的电脑通过网络登录到网络另一端的电脑上,甚至还可以存取那台电脑上的文件;
window操作memcached需要应用telnet登录本地,命令:ip port,在提升telnet非内部命令时,在控制面板的程序打开window功能;连接至memcached后,flush_all(刷新指令)和stat(状态指令)最为常用;
聚集函数count()返回的jdbc结果是long类型,此外,long和integer是不能强转的;
转:
泛型<...>会在编译期间进行泛型擦除(泛型擦除:将原来的数据类型变为Object类型),刚好int、double等基本数据类型的父类不是Object类型(它们本身也没有父类),所以转化失败
问题:为什么Object a=1;没有报错,是因为java编译器帮你调用了Integer.valuOf(1);将int类型转化成了Integer类型;
MD5
关于MD5,其生成的密文是唯一的,因此可以用作防止篡改、鉴权等功能,数据库常存储加密后的密码也是如此,防止其他管理员恶意查看数据库的密码。
而盐值(salt value)则是用来避免暴力破解的,原理就是在明文后增加盐值字符,这样即使破解到了生成密文的明文,也得不到真正的加密对象。
MySQL索引
聚簇索引:也称主键索引,叶子结点存储着整个行数据,因此聚簇索引具有唯一性,每张表只能有一个聚簇索引;
由于其B+树的有序性,索引主键可以通过二分法快速定位至指定行数据从而加快查询,但若想构建非主键索引,就要另建二级索引,也就是辅助索引;
辅助索引(非聚簇索引):索引和表数据并不完整关联的索引,叶子结点存放的是主键值,查询非主键值则需要“回表”,即二次查询主键索引;
而对于MyISAM的索引同样是B+树结构,只不过表数据和索引树是独立的,主键索引各个节点存储主键值,辅助索引各节点存储辅助键值。
联合索引:关联多个字段作为索引值,优先按首个字段索引值排序,遇到首个字段值相同情况下才会按照第二个字段索引值进行排序,对于非首个索引值呈全局无序,局部有序;
而最左匹配原则也就体现在需要确定首个索引字段值,也就是创建索引时最左边的字段,才能确定后续索引字段值的索引有效性; 正因为如此,查询条件在遇到 ">"、"<" 的时候后面的查询条件会索引失效,因为前一个字段索引定位至一个范围,而这个范围内后续索引值都是无序的,只能挨个扫描。
B/S vs. C/S
C/S架构下,程序以客户端的形式呈现给用户,只需考虑客户端与自家服务器的访问情况,B/S架构,也就是浏览器则需要考虑与其他服务器访问的情况,也就需要规定公认的协议来明确TCP连接包的格式,避免粘包问题(数据流无边界无格式无法明确信息)。
Classpath&WEB-INF:
classpath:
就是指javac编译后的class文件所在的path...
WEB-INF:
java-web下的安全目录,外部请求无法访问,里面包含所需jar包、web.xml配置以及class文件等等。
index.jsp等欢迎页需要放在web下面,web-inf外面,与其同级,不然访问不到。
module中的web框架中的descriptor就是配置web.xml文件的位置,而下面的web resource directories就是web程序的目录。
SpringMVC启动问题:
1. db.properties要加classpath:,不然找不到;
2. Mybatis-Config.xml也是要导入applicationContext的;
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:Mybatis-Config.xml"/>
</bean>
3. db.properties中username的key值不能设置username,会优先调用默认的本机用户名。
MySQL自增不连续问题
MySQL自增逻辑:
对于InnoDB引擎,自增值是会保存在实例内存中,每次访问表都会获取这个AUTO_INCREMENT的值,然后执行加一操作再储存回去;
四个不连续场景:
1. 插入字段违反唯一约束,例如上图字段‘a’,若插入重复数值,AUTO_INCREMENT会在判断a是否重复前自增,最后导致数据插入失败,但自增值却加一的结果;
2. 事务回滚不会导致自增回滚,增到哪就是哪,原因是为了提高性能,设想在多事务情况下,a先提交b后提交,a回滚后若是自增也回滚,势必会影响到b事务的主键生成,很大可能会造成id重复,基于此原因,InnoDB采取放弃自增回滚;
3. 非默认自增步长,auto_increment_increment默认为1,在分布式数据库插入情况下,经常以奇偶id来分布自增,这时候多个库的auto_increment_offset和auto_increment_increment都会不一样;
4. 批量插入:
这里批量插入语句insert...select的自增赋值逻辑是批量分配自增id,以1、2、4、8...的增长量分配。
SQL查询存在优化写法
#### SQL写法:
SELECT 1 FROM table WHERE a = 1 AND b = 2 LIMIT 1
#### Java写法:
Integer exist = xxDao.existXxxxByXxx(params);
if ( exist != NULL ) {
//当存在时,执行这里的代码
} else {
//当不存在时,执行这里的代码
}
关于JAVA的并发锁
对于synchronized加锁:
-
给普通方法加锁时,上锁的对象是
this
-
给静态方法加锁时,锁的是class对象。
-
给代码块加锁,可以指定一个具体的对象作为锁
-
synchronized方法在多个实例下是无法做到同步的,因为其锁的是对象,而对象又不是一个,所以可以加个static,因为静态方法锁的是class对象,一个类的class对象在一个运行环境下只有一个;
synchronized在1.8的优化:
根据线程竞争情况,存在由 偏向锁 => 轻量级锁 => 重量级锁 的逐步升级,升级不可逆;
其中第一个不涉及线程竞争,第二个允许一定次数的竞争,多次失败就会退化为 系统调用 的重量锁,即线程阻塞;
可重入的概念:
涉及多个加锁方法互相调用的时候,由于锁的都是当前对象this,就不需要重新获取锁,lock和synchronized都是可重入锁;
lock:LOCK是基于AQS(AbstractQueuedSynchronizer)实现的,而AQS 是基于 volitale 和 CAS 实现的
读写锁:读操作一般不会有并发问题,适合读多写少的场景;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
public void put(K k, V v) {
writeLock.lock();
try {
map.put(k, v);
} finally {
writeLock.unlock();
}
}
锁的公平概念:
锁如果是随机分配的,那就会有饥饿的线程,那这就是一个不公平的锁;
JSONObject:
注意区分 null 和 “null”!
null即可理解为JSONObject中的key为null,也可理解为JSONObject为null,这种情况下用isNullObject判断前者,==null判断后者即可;
而针对“null”情况,只是单纯的字符串而已,equals即可解决。
MYSQL插入问题
批量插入需要注意my.ini配置的max_allow_packets的字节数,若是超过此数值会插入失败。
Junit多线程测试不兼容问题
Junit单元测试线程不会被子线程阻塞,只因内部调用了System.exit();致使子线程还未开始便被销毁,因此可以在方法入口执行测试,或者调用countDownLatch的await阻塞主线程,亦或是调用线程join方法;
12小时制转换24小时制
// 针对入参为 HH:mm AM/PM
private String handleDate(String time) {
if (time.contains("AM")) {
String[] s = time.split(" ");
int i = Integer.parseInt(s[0].split(":")[0]);
if (i == 12) {
return "00" + s[0].split(":")[1];
} else if (i > 9) {
return s[0].replaceAll(":", "");
} else {
return "0" + s[0].replaceAll(":", "");
}
} else if (time.contains("PM")) {
String[] s = time.split(" ")[0].split(":");
int i = Integer.parseInt(s[0]) + 12;
if (i == 24) {
return "12" + s[1];
}
return i + s[1];
}
return time;
}
POI行数的问题
sheet.getLastRowNum(); // 少一个
sheet.getPhysicalNumberOfRows(); // 和excel一样,比上面多一个
sheet.getRow(0).getCell(0).getStringCellValue()); //第一行,一般是标题
sheet.getRow(1).getCell(0).getStringCellValue()); // 第二行,一般是数据//所以,excel最后一行数据在POI的index为 getLastRowNum()的值
POI文件写入的问题
1、一定记得关闭流,最好把应用流封装成函数在finally中调用;
2、对于单个文件尽量不要同时打开输入流和输出流,可能会遇到棘手的问题,比如文件被损坏;
追加:以上问题是由于输出流的write方法导致,因为此前有输入流的引用占用文件,write会重写覆盖文件内容,所有在这时候打开,文件就会显示被损坏,且被清空;
这时候可以在声明输出流的时候在second argument append设置为true,表示在文件结尾处写入;
BUT=> 对于想从头修改文件的输出流只能把输出流声明在输入流操作结束之后。
以上只是网络搜罗结果,并未经过测试!
3、cell的写入在setCellValue后要依赖workbook的write写入流方法;
多线程适用注意事项
1、windows的睡眠会导致网络中断,运行程序需要网络io的话会被阻塞;
2、针对定长线程,其失效时间为0L,which means永不失效,且线程数皆为核心线程,这时候如果不手动shutdown线程池,其程序会一直运行,同时对于拥有Integer.MAX长度的任务队列,很有可能导致OOM,因此最好采用Cached线程池,其核心线程数为0,并带有失效时间(默认60s),可以一定程度上使其自动关闭线程池;
3、线程队列设置initial capacity的话很可能会导致默认的拒绝策略抛出异常,这时候最好在submit新任务的间隙添加Thread.sleep(),减缓任务添加的速率从而使任务队列能及时被消费;
4、针对shutdown和awaitTermination,搜索了许久才知道其等待任务执行完毕包括任务队列,shutdown阻止的是新任务的提交,也就意味着是程序的结束,之前一直担心的shutdown之后队列中任务会被放弃也是虚惊一场,所以shutdown就可以当做是close一样来终止程序;
5、对与shutdown、shutdownNow和awaitTermination三者最好的实践如下,实现超时终止:
6、对于准备复用的变量,例如HttpClient等,最好使用ThreadLoacl使其线程安全化,同时针对多线程请求接口很可能会出现NoHttpResponse异常,一般是接口调用频繁,Server无法处理并发导致的,这时候可以选座Thread.sleep进行释放请求压力;
POJO变量类型建议
Synchronized vs. @Transactional
当两者作用在统一方法时候,多线程并发访问SELECT可能会查询到上一个线程未提交更新的值,原因是Synchronized是以return为分割点,而@Transactional是以AOP中另一个代理方法commit为分割点,解决办法就是分层调用,外层Synchronized调用内层事务,注意不要本地调用!
关于Java中 && 与 || 的优先级问题
此处讨论 没有括号干预, 类似 表达式1 && 表达式2 || 表达式3 && 表达式4... 这样的结构, 经过测试我得出的结论为 : 没有所谓的`优先级`, 无论 || 还是 && 在前, 只要达成了短路条件, 就会行程短路, 后面的表达式不会再执行, 针对多个表达式 拼接起来的条件语句, 只要依次结合 每个 逻辑符号两侧的表达式, 作为下一次 逻辑符号 左侧的输入, 即可完成运算;
Java捕获异常的坑
线上面临多机器集群互相调用的场景, 各个服务器的代码版本可能都不一样, 这时候不仅要补充日志辅助定位请求流程, 在catch块可以增加兜底的Throwable, 我这次bug原因就是只加了Exception, 结果线上版本不一致, 来了个IllegalAccessError, 前端返回未知异常, 对第一次见到这种错得我来说只能是玄而又玄, 归根结底还是相信了部署同事的那一句 "新版已上线"...