好文推荐

一些总结的好,不一定是要文笔有多好,或者涵盖广泛。只要是能给我提供一些想法,或者是能帮助完善自己的一些想法,这里存储下链接,做一下简要记录,希望不要像书签一样,存的多却看的少。

  • 如何让Node.js正确地日志 - 2017年5月24日11:14:46:

    虽然写的是Node.js的,但是提到日志的几个要点日志的应用范围,在其他语言中同样也是适用的,抽象出概念能帮助理解日志。
    要点:时间戳、格式、日志目标、日志级别
    应用范围:库类型、单个应用程序、分布式系统
    egg-logger的图也助于理解主流日志框架

javascript模块化方式简记

CommonJS 规范

node.js是服务端的编程,需要与操作系统、其他应用互动,需要模块化,否则无法编程,而在浏览器其复杂性有限,没有模块化也不是特别大的问题。而node.js的模块系统就是操作CommonJS规范实现的。

1
2
3
4
5
// 文件A
module.exports = {};

// 文件B
var b = require('./A');

AMD 规范

有了服务端模块化后,很自然地,大家就想要客户端模块化。而且最好能与服务端兼容,模块都不用修改,在服务器和浏览器都可以运行。但是浏览器加载模块文件是跨网络的,加载可能会造成假死,后面的代码无法运行。而服务端是在本地硬盘的,这对服务端不是问题。因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。

AMD是”Asynchronous Module Definition”,意识就是“异步模块定义”。它采用异步方式加载模块,模块的加载不影响他后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等模块加载完成后,这个回调函数才会运行。

1
2
3
4
require(['a', 'b'], function(a, b) { // 依赖前置,提前执行
a.xx();
b.xx();
});

require.jsAMD规范的一个实现。

CMD 规范

CMDSeaJS 在推广过程中对模块定义的规范化产出

1
2
3
4
5
6
7
define(function(require, exports, module) {
var a = require('./a');
a.xxx();
...
var b = require('./b'); //依赖就近书写
b.xxx();
});

静态加载与动态加载

es6之前模块加载的两种方式

  • 静态加载:在编译阶段进行,把所有需要的依赖打包到一个文件
  • 动态加载:在运行时加载

AMD标准是动态加载的代表,而CommonJS是静态加载的代表。

AMD主要用在浏览器上,是异步加载的,而NodeJS在服务端,同步加载的方式更易被接收,所以用的是CommonJS

ES6

ES6采用哪种加载机制了? ES6既希望用简单的声明来完成静态加载,有不愿放弃动态加载的特性,而这两种方式几乎不能简单的同时实现,所以ES6提供了两种独立的模块加载方法。

  1. 声明的方式

    1
    import {foo} from some_module;
  2. 通过System.import API

    1
    2
    3
    4
    5
    6
    7
    System.import('some_module)
    .then(some_module => {
    // ...
    })
    .catch(error => {
    // ...
    });

模块导出:

1
2
3
4
5
6
7
8
9
10
11
// some_module.js
export function abc() {} // export 一个命名 function
export default function() {} // export default function
export num = 123 // export 一个数值
export obj = {}
export { obj as default }

// import
import exp from 'some_module' // default export
import {default as myModule} from 'some_module' // rename
import {abc, num, obj} from 'some_module'

参考自:

字节序

其实已经有几次写到过有关字节序的问题了,这里单独拎出来再写一次,是因为又淡忘了,特意抽象其根本问题再做一个简要的记录。

抽象出的两个概念:

  1. 存储地址:内存地址“从左至右”递增,可以理解往左是低位地址,往右是高位地址
  2. 要存的值:比如1个数字 112233(十进制)、1个字符串 “112233” —(这里两处的33都是尾巴)

注意:1个xx需要多个字节表示的时候,才会有字节序的问题

面临的问题: 尾巴放到高的位置还是低的位置?

高尾端:高的位置存尾巴

低尾端:低的位置存尾巴

字节序

比拟

一个的顺序 (普遍:从左至右念,牌匾:从右至左念)

1
法语,公主,四十

一行文字中字的读取顺序

1
严管妻是本;妇顺夫为实.

解析的顺序不一样意义就大不一样咯。

其他叫法

高尾端/大端序/大尾序

低尾端/小端序/小尾序

事务及隔离级别

事务四大特性

据库具有 ACID四个特性,分别指:

Atomicity - 原子性

原子操作不可分割,内部要么全部成功,要么全部失败。

Consistency - 一致性

系统从一个一致状态转换到另一个一致状态。事务的一致性决定了一个系统设计和实现的复杂度。事务可以不同程度的一致性,下面会详细提到。

Isolation - 隔离性

事务不被其他事务干扰,相互隔离。 每个事务都感觉不到其他事务在执行,下面为详细提到事务的隔离的级别。

Durability - 持久性

事务一旦提交,那么数据的改变就是永久性的。

事务一致性

此处主要参考: 理解事务——原子性、一致性、隔离性和持久性

  • 强一致性:读操作可以立即读到提交的更新操作。
  • 弱一致性: 提交的更新操作,不一定立即会被操作读取到,此种情况会存在一个不一致窗口,指的是读操作可以读到最新值的一段时间。
  • 最终一致性: 是弱一致性的特例。事务更新一份数据,最终一致性保证在没有其他事务更新同样值的话,最终所有的事务都会读到之前事务更新的最新值。如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。
  • 单调一致性: 如果一个进程已经读到一个值,那么后续不会读到更早的值。
  • 会话一致性: 保证客户端和服务器交互的会话过程中,读操作可以读到更新操作后的最新值。

事务隔离性

如果不考虑隔离性,在事务并发操作时,可能出现的问题有:

  • 脏读一个事务读取了另一个事务未提交的数据

    例如:公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高 兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有 2000元,singo空欢喜一场。

  • 不可重复读一个事务内,单份数据多次读取结果不同,这是由于读取间隔,被别的事务修改并提交了 (事务内重复读取会有问题,不重复读则没问题,则理解为不可重复读问题)

    例如:singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在 singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为 何……

  • 幻读一个事务内,多次读取数据的结果集合不同,幻读是由于读取间隔,被别的事务插入了数据

    例如:singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction … ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出 现了幻觉,幻读就这样产生了

针对上面的问题,一般定义有以下几种事务隔离级别:

  • READ UNCOMMITED (可以读取到未提交的数据)一个事务可以读到另一个事务未提交的结果,以上的脏读不可重复读幻读问题都可能会发生。
  • READ COMMITED (可以读取到已提交的数据)只有在事务提交后,其更新结果才会被其他事务看见。可以解决上面的脏读问题
  • Repeated Read (重复读取)在同一个事务中,对于同一份数据的读取结果总是相同的,无论其他事务对这份数据是否进行操作,以及整个事务是否提交。可以解决脏读、不可重复读问题
  • Serializable (串行化) 隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题

大多数数据库的默认级别就是Read committed,如OracleSQL Server;

注:MySQL 的默认隔离级别是REPEATABLE-READ,查看命令select @@tx_isolation;

例子参考:

数据库事务隔离级别

数据库事务的四大特性以及事务的隔离级别

理解事务——原子性、一致性、隔离性和持久性

书单

书是系统性的表达,相对单篇的博客、资讯,书内容更加完整富有体系。对于一些新流行的技术,网上的资讯快餐更及时,书的内容往往会跟不上潮流,但是经典的东西往往早已沉淀在书中了。

正读

  • 《编码-隐匿在计算机背后的软硬件语言》
  • 《大话数据结构》
  • 《大话设计模式》

已读

  • 《JavaScript高级程序设计(第3版)》
  • 《Java多线程编程核心技术》
  • 《鸟哥的Linux私房菜》

  • 《把时间当作朋友》

  • 《黑客与画家》
  • 《一个人的朝圣》
  • 《影响力》
  • 《习惯的力量》

  • 《潜规则》

  • 《摆渡人》
  • 《解读量化投资:西蒙斯用公式打败市场的故事》
  • 《从你的全世界路过》
  • 《我不是潘金莲》
  • 《解忧杂货店》
  • 《智齿》
  • 《别做正常的傻瓜》
  • 《别拿村长不当干部》
  • 《老鼠仓》
  • 《为奴十二年》
  • 《追风筝的人》
  • 《人性弱点》
  • 《如何变得有思想?》

未读

  • 《编写高质量代码:改善Java程序的151个建议》
  • 《TCP-IP协议族(第4版)》

  • 《岛上书店》

  • 《无声告白》
  • 《我的晃荡青春》
  • 《白夜行》
  • 《沉默的大多数》
  • 《世界上的另一个你》
  • 《不抱怨的世界》

其他

Linux操作常用命令

记录一些常用的技巧,隔一段时间不用又给淡忘了。

ssh 免密登录

1
2
3
4
5
6
7
8
9
10
# 1. 本地生成 密钥对 , 默认生成 id_rsa , id_rsa.pub
$ ssh-keygen

# 2. 上传公钥到服务器
$ ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.22
# 服务器对应账户目录下 ~/.ssh/authorized_keys 增加了公钥的内容

# 3.测试免密登录
$ ssh root@192.168.1.22 -i ~/.ssh/id_rsa.pub
# 默认是 ~/.ssh/id_rsa.pub, 可以不指定 -i 选项

开机启动项

  1. 创建服务
1
2
3
4
5
6
7
8
9
10
vim /etc/systemd/system/disk-space-check.service

[Unit]
After=network.service

[Service]
ExecStart=/usr/local/bin/disk-space-check.sh

[Install]
WantedBy=default.target
  1. 创建脚本
1
2
3
#!/bin/bash
date > /root/disk_space_report.txt
du -sh /home/ >> /root/disk_space_report.txt
  1. 设置脚本权限

    1
    2
    $ sudo chmod 744 /usr/local/bin/disk-space-check.sh
    $ sudo chmod 664 /etc/systemd/system/disk-space-check.service
  2. 启用

    1
    2
    $ sudo systemctl daemon-reload
    $ sudo systemctl enable disk-space-check.service
  3. 重启验证下吧

打包压缩、解压

1
2
3
4
5
6
7
// .tar.bz2 包
# tar -jcv -f /root/test/etc.tar.bz2 /etc/
# tar -jxv -f /root/test/etc.tar.bz2 -C /tmp/

// .tar.gz 包
# tar -zcv -f /root/test/etc.tar.gz /etc/
# tar -zxv -f /root/test/etc.tar.gz -C /tmp/

建立链接

1
2
3
# ln -sf /usr/local/postgresql-9.2.4 /usr/local/pgsql
## -s: symbole 软链接,-f: force 如果目标链接已经存在则删除重新建立
## 如果9.2.5的版本发布了,升级只需停掉数据库在这里改下软链接地址就可以了

当前目录 各文件夹 大小 linux

1
# find $1 -maxdepth 1 | xargs du -sh

强制停止应用的两种方式

1
2
3
ps -ef|grep java|grep 'tomcat-web-api'|awk '{print $2}'|xargs kill -9

ps -ef|grep java|grep 'tomcat-web-api'|awk '{system("kill -9 " $2)}'

某次服务器中毒,删除与病毒文件同样大小字节的文件

1
find / -size 1223123c | xargs rm -rf

find类 其他

1
2
3
4
5
// 正则匹配删除
find ./ -maxdepth 1|grep "2016-12-1[2-7]"|xargs rm -rf

// 反向匹配,找出名字不含 "tar" 的文件/文件夹
find ./ -maxdepth 1|grep "tar" -v

根据进程获取进程号,打印进程的运行信息

1
# pgrep mysql | xargs -I {} ls -l /proc/{}

crontab 同步时间,

1
2
3
// crontab 分、时、日、月、周
// 10分钟同步一次
*/10 * * * * /usr/sbin/ntpdate time.windows.com && hwclock -w

输出磁盘的读写情况

1
iostat -k 2

新增磁盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// fdisk: 操作磁盘分区表
// mkfs:对磁盘分区进行文件系统格式化

// 列出磁盘装置,找到我们要操作的装置名称,便于对其进行分区
# fdisk -l

// 开始对磁盘进行分区处理,注意不要加上数字
# fdisk /dev/xvdb

// 对分区进行格式化
# mkfs -t ext4 -c /dev/xvdb1

// 建立磁盘挂载目录
# mkdir /data

// 手动挂载
# mount /dev/xvdb1 /data/

// 开机自动挂载
# echo "/dev/xvdb1 /data/ ext4 defaults 0 0" >> /etc/fstab

看你在Linux下最常用的命令是哪些?

1
history | awk '{CMD[$2]++;count++;} END { for (a in CMD )print CMD[ a ]" " CMD[ a ]/count*100 "% " a }' | grep -v "./" | column -c3 -s " " -t |sort -nr | nl | head -n10

java多线程编程核心技术

如果你认为多线程编程就是一堆的线程在运行一堆的代码,那将会一团糟。程序有时候正常,偶尔又异常,多线程的编程将变的困难。

但是如果你能将线程按需来分配,控制线程间的交互,那问题就变得简单多了。

多线程双刃剑:充分利用多核,复杂度提升,操作共享资源处理不好时会带来线程安全问题。

WEB编程普遍缺乏对多线程的理解:web容器实现,屏蔽了复杂的编程细节,多线程处理,(自己实现一款简单的web容器)

一、Java多线程技能

Skills:

  • 线程的启动
  • 如何使线程暂停
  • 如何是线程停止
  • 线程的优先级
  • 线程安全相关的问题

1.1 进程和多线程的概念及线程的优点

进程是操作系统结构的基础,它是系统进行资源分配和调度的一个独立单位。可以将windows任务管理器的.exe理解成一个进程。
线程:线程就是进程中独立的子任务,比如QQ.exe运行时,有视频的线程、下载文件线程、传输数据线程等。
优点:在多任务操作系统中效率大大提升。

1.2 使用多线程

一个进程中至少会有一个线程在运行,比如常见的main函数:

1
2
3
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}

1.2.1 继承Thread类

实现多线程的主要方式有两种:一种是继承Thread类,另一种是实现Runnable接口。
前者与后者工作时的性质是一样的,继承Thread最大的局限就是不支持多继承.
继承实现的方式就是继承Thread类,重写run方法,然后创建线程类实例,调用其start方法即启动了一个线程
如果多次调用start()方法,则会出现异常 IllegalThreadStateException ..

1.2.2 实现Runnable接口

实现方式:实现Runnable接口并实现run方法

1.2.3 实例变量与线程安全

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyThread extends Thread {

private int count = 5; //实例变量(非static)

public MyThread() {
super();
}

public MyThread(String name) {
this.setName(name);
}

@Override
public void run() {
count--;
System.out.println("由 " + Thread.currentThread().getName() + " 计算, count=" + count);
}
}

运行方式一,不共享数据,运行类Run:

1
2
3
4
5
6
7
8
9
10
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}

这里共创建了3个线程,每个线程都有自己的count变量,不存在访问同一个实例变量的情况。

运行方式二,共享数据,运行了ShareRun:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShareRun {

public static void main(String[] args) {
MyThread threadRun = new MyThread(); // 使用同一个Runnable实现类的实例

Thread a = new Thread(threadRun, "A"); // 再通过Thread包装
Thread b = new Thread(threadRun, "B");
Thread c = new Thread(threadRun, "C");
Thread d = new Thread(threadRun, "D");
Thread e = new Thread(threadRun, "E");

a.start();
b.start();
c.start();
d.start();
e.start();
}
}

jvm的i–分3个步骤:

  1. 取i值
  2. 计算i-1
  3. 赋值i

此时多个线程访问threadRun实例的变量出现非线程安全问题,这就是典型的线程问题了。这时通过在run方法前添加synchronized可以解决这种问题。
synchronized可以在任意对象及方法上加锁。
多个线程执行run时,以排队的方式执行,先判断run方法有没有上锁,如果有上锁说明其他线程正在执行,等待其他线程执行。线程尝试拿锁,如果没有
拿所锁,这个线程就会不断的尝试拿这把锁,直到拿到为止。

1.2.4 注意count–与System.out.println()异常

将上面代码里面count-- 放置到System.out.println()中,如下:

1
System.out.println("由 " + Thread.currentThread().getName() + " 计算, count=" + (count--));

我们注意下在jdk的源码中System.out.println()这个方法内部是有synchronized的,说明这个方法是线程安全。
但是上面的代码也会发生非线程安全问题,因为count--这个计算是在println方法前执行的,这个有点类似scala的求值策略的Call by Value
先会将形参计算出来 ,再进入方法中。

1.3 currentThread()方法

currentThread()方法可以返回代的段正被哪个线程所执行,可以在线程类的run方法打印执行线程的名称,然后在外部分别使用线程的startrun方法执行对比查看。

1
System.out.println(Thread.currentThread().getName());

1.4 isAlive()方法

isAlive() 方法是用来判断线程是否属于活动状态,活动状态即线程已经启动且位尚未终止

1.5 sleep()方法

sleep()方法是用来让线程暂停指定的时间。

1.6 getId()方法

getId()方法用来获取线程的唯一标识。

1.7 停止线程

停止线程是在多线程开发时重要的技术点,它不想循环中的break简单粗暴,需要一些技巧性的处理,处理好一些身后事(关闭资源连接、事务)才停止线程。
Thread.stop()可以停止一个线程,但不建议使用它,这个方法是不安全的。
大多数停止一个线程的操作使用的是Thread.interrupt(),尽管方法名称是终止、停止的意思,但这个方法不会终止一个正在运行的线程,需要加入一些判断才可以完成线程的停止。
3种终止正在运行的线程的方法:

  1. 使用退出标志,使程序正常退出,也就是当run方法完成后线程终止
  2. 使用stop方法强行终止,但是不推荐使用,stop、suspend、resume都是作废过期的方法,使用它们可能会产生意料不到的结果。(强制性停止可能使一些清理工作得不到完成。)
  3. 使用interrupt方法,程序判断isInterrupted,使用throw/return来中断线程,推荐“抛异常”方式

1.7.1 停止不了的线程

下面的例子演示了interrupt停止不了线程的现象:
线程类:

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {

@Override
public void run() {
for (int i = 0; i< 500000; i++) {
System.out.println("i=" + (i+1));
}
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Run {
public static void main(String[] args) {
try {
MyThread t = new MyThread();
t.start();
Thread.sleep(2000);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

最终控制台还是打印了50万行,没有停止住线程。

1.7.2 判断线程是否是停止状态

我们来看下如何判断线程状态不是停止的。Thread提供了两种方法:

  1. Thread.interrupted: 测试当前线程是否已经中断,执行后将状态标识清除为false的功能。
  2. this.isInterrupted: 测试当前线程是否已经中断,但不清除状态标志。

可以参考: interrupt、interrupted 、isInterrupted 区别

1.7.3 能停止的线程——异常法

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 500000; i++) {
if (Thread.interrupted()) { //每次获取标识判断
System.out.println("已经有了停止标识,我要退出了!");
break; //手动break
}
System.out.println("i=" + (i + 1));
}
System.out.println("for 循环后面的代码被输出,线程其实未被停止。");
}
}

上面的线程其实未真正停止,下面在接着改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread {

@Override
public void run() {
try {
for (int i = 0; i < 500000; i++) {
if (Thread.interrupted()) { //每次获取标识判断
System.out.println("已经有了停止标识,我要退出了!");
throw new InterruptedException(); // 改为 抛出异常的方式
}
System.out.println("i=" + (i + 1));
}
System.out.println("for 循环后面的代码被输出,线程其实未被停止。--不会再输出");
} catch (InterruptedException e) {
System.out.println("进入线程类的catch,线程终止");
e.printStackTrace();
}
}
}

1.8 暂停、恢复线程

使用suspend()方法暂停线程,使用resume()方法恢复线程执行。

1.8.1 测试

演示功能效果,起到暂停、恢复效果,测试类:SuspendResumeTest.java

1.8.2 suspend与resume方法的缺点——独占

线程类内部锁,独占,此类的其他线程实例也被阻塞,测试类:SuspendResumeDealLock.java

公共锁同步对象被独占,造成主线程阻塞,例如println方法,测试类:SuspendResumeLockStop.java
注释掉除线程内部的println方法后面的代码即恢复执行。

1.8.3 suspend与resume方法的缺点——不同步

使用suspendresume方法时容易出现因为线程的暂停而导致数据不同步的情况。

值不同步的情况,测试类:SuspendResumeNoSameValue.java

1.9 yiled 方法

yield()方法的作用是放弃当前CPU资源,放弃时间不确定,有可能刚放弃,马上又获得了CPU时间片。

去除注释前后对比测试下吧,测试类:YieldTest.java

1.10 线程优先级

线程可以划分优先级,优先级高的线程得到的CPU资源角度,也就是CPU优先执行优先级较高的线程对象中的任务。java中优先级设置中定义的几个常量:MIN_PRIORITY=1NORM_PRIORITY=5MAX_PRIORITY=10,设置小于1或者大于10将会抛出IllegalArgumentException

1.10.1 优先级的继承性

java中,线程的优先级具有有继承性,比如A线程启动创建了B线程,则B线程的优先级与A线程是一样的。

继承特性,上面写道了创建了,可以将创建 t1调换值至设置main优先级之前测试下,测试类:PriorityInheritanceTest.java

1.10.1 优先级具有规则性

高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部执行完。

1.11 守护线程

Java进程中有两种线程,一种是用户线程,一种是守护线程。

守护线程的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程。

守护进程,测试类: DaemonTest.java , 理解:用户线程 main都结束了,守护线程们已经没有什么可守护的了,就结束了

对象及变量的并发访问

Skills:

  • synchronized监视Object的使用
  • synchronized监视Class的使用
  • 非线程安全是如何出现的
  • 关键字volatile的主要作用
  • 关键字volatilesynchronized的区别及使用情况

2.1 synchronized同步方法

多线程并发访问共享资源,很可能发生“非线程安全”题,产生的后果就是”脏读”。而“线程安全”就是获得的资源是经过同步处理的,不会出现脏读现象。

2.1.1 方法内的变量为线程安全

方法内的变量不存在线程安全问题,永远都是线程安全的,这是方法内部的变量私有的特性造成的。

2.1.2 实例变量非线程安全

多个线程访问一个实例中变量发生线程安全问题, 测试类: ThreadSafetyProblem.java

2.1.3 多个对象多个锁

关键字synchronized取的锁都是对象锁,而不是把一段代码或方法当作锁

两个线程访问同一个类的的不同实例的相同同步方法,因为创建了不同的实例,系统将根据实例个数产生锁。测试类: TwoObjectTwoLock.java

2.1.4 synchronized 方法与锁对象

  • A线程先持有object对象的Lock锁,B线程可以调用object对象中的非synchronized类型的方法。
  • A线程先持有object对象的Lock锁,B线程调用object对象中的synchronized类型的方法则需等待,也就是同步。

测试类:SynchronizedMethodLockObject.java

2.1.5 脏读

脏读出现在不同线程“争抢”实例变量的结果,即2.1.4中非同步方法随时可取共享资源就会造成脏读。

2.1.6 synchronized 锁重入

线程进入synchronized方法调用本对象的另一个synchronized方法时,是永远可以得到锁的。即进入synchronized方法后可以无阻塞畅游本实例的所有方法了(当然这里不包括访问其他实例的synchronized方法)

2.1.7 出现异常,锁自动释放

当一个线程执行代码出现异常时,其所持有的锁会自动释放。

测试类:ExceptionAutoReleaseLock.java

2.1.8 同步不具有继承性

子类重写父类的synchronized方法,如果该方法不添加synchronized标识,此方法将不再具有同步的特性。

测试类:SyncNotExtends.java

2.2 synchronized 同步语句块

多个线程访问同一个对象中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,其他线程需要等待当前线程完成这个代码块的执行。我们来对比下,同步方法与同步块

同步方法,测试类:SyncMethod.java

我们分析可以发现doLongTimeTask方法里面,只是对getData1getData2赋值做了对共享资源的操作,之前部分的耗时操作不依赖共享资源,这部分代码完全可以非同步率先执行,所以修改方法,可以让方法内部达到一般异步,一半同步的效果。

同步块,测试类:SyncBlcok.java

完成同样的效果,后者花费的时间缩短了50%;

2.2.1 synchronized 方法的弊端

2.2.2 synchronized 同步代码块的使用

2.2.3 用同步方法解决同步方法的弊端

2.2.4 一半异步,一半同步

其实上面提到的几点,上面的例子都已经体现了

同步方法弊端:也就是方法内部的代码,眉毛胡子一把抓,不区分“必须同步”与“可以异步”的代码将其“同步”的方式来执行,这样方法持锁时间更长,耗时更久。

同步块代码的使用:分析代码,区分必须同步可以非同步,将同步代码加上synchronized

2.2.5 synchronized 代码块的同步性

使用synchronized(this)代码块时,当一个线程访问object的一个synchronized(this)方法时,其他线程访问对object其他所有的synchronized(this)方法访问将会阻塞。

2.2.6 synchronized(this) 代码是锁定当前对象的

2.2.7 将任意对象作为对象监视器

synchronized方法与synchronized(this)都具有:

  1. 对其他synchronized方法与synchronized(this)同步块调用呈阻塞状态
  2. 同一时间只有一个线程执行当前synchronized方法块或synchronized(this)中的代码

java中支持对任意对象(注意此处是对象,基础的值类型可是不行的哦)作为“对象监视器”来实现同步功能,使用格式 synchronized(非this)

测试类:SyncBlockString.java

2.2.8 细化验证3个结论

synchronized(非this对象x)即意味着将对象x作为对象监视器,可以得出以下3个结论:

  1. 多个线程同时执行synchronized(x)代码块时呈同步效果
  2. 其他线程执行x对象中的synchronized同步方法时呈同步效果
  3. 其他线程执行x对象中的synchronized(this)代码块时呈同步效果 (2、3点是x对象中本身还有相关的同步方法)

测试类:SyncLockObjInsideSyncMethod.java

2.2.9 静态同步synchronized方法与synchronized(class)代码块

synchronized可以应用在static静态方法上,这样表示对当前的XX.java文件对应的Class类进行持锁,等同于synchronized(XX.class),会从class级别全局阻塞class锁,但不会阻塞实例的同步方法(非静态同步方法)。

测试类:SyncStaticMethod.java

2.2.10 数据类型String的常量池特性

JVM具有String常量池缓存的功能,所以使用String作为监控锁对象不小心时可能会带来一些意外。所以一般synchronized代码块不使用String,改用其他如new Object()

测试类:StringConstantTrait.java

2.2.11 同步sychronized方法无限等待与解决

同步方法容易造成死循环,让其他线程得不到运行机会。

测试类:TwoStop.java

这个其实还得与具体的业务分析,不同的同步方法,是否涉及同一个资源访问的读写,如果不涉及则可以使用不同的”监控锁对象”(以上举例的测试类,在真正的业务场景中基本不会这样)

比如按业务不同,定义不同的锁对象,测试类:TwoStopMultiLockObj.java

2.2.12 多线程的死锁

java线程死锁是一个经典的问题,造成的原因是不同的线程在等待不可能被释放的锁。

这里我自己脑补了一个来帮助理解死锁的例子:

测试类:DeadLockTest.java

死锁是因为阿毛先锁坑位,再锁纸不走寻常人的路,阿毛他爹阿毛麻麻是:先锁纸,再锁坑位,导致阿毛全家都不能正常上厕所了。 如果注释掉阿毛,其他人都能正常排队的上厕所,反之注释掉其他人,阿毛一个人也玩的转,他们同时来就会发生死锁。

避免死锁:对多个资源(数据库表、对象)同时加锁时,需要保持一致的加锁顺序

运行程序后可以看到应用假死了,这是我们可以用jps 查看下运行的java进程的信息,得到pid,然后使用 jstack -l pid,可以看出线程在等待哪个对象的锁,而这把锁现在正被哪个线程锁持有,可以看出这里的死锁就是两个线程互相持有了对方等待的锁。

1
2
3
4
5
6
7
8
$ jps 
-q 只显示pid,不显示class名称,jar文件名和传递给main 方法的参数
-m 输出传递给main 方法的参数,在嵌入式jvm上可能是null
-l 输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名
-v : 详细信息

$ jstack
-l

2.2.13 内置类与静态内置类

内部类依赖其外部类实例来实例化:new External().new InnerClass(), 静态内部类则可以直接:new InnerStaticClass(),实例化时不依赖外部类实例

测试类:ExternalClass.javaRunTestExternalClass.java

1
2
3
4
5
// 内部类, 把类比喻成一个鸡蛋,内部类是蛋黄,外部类是蛋壳
// 普通内部类(非static) 那相当于生鸡蛋,没有鸡蛋壳(外部类没有实例化),蛋黄也就不复存在 ----- 生鸡蛋: 壳之不存,黄之焉附
// 内部静态类 , 相当于熟鸡蛋,没有鸡蛋壳,蛋黄也可以是完好的(可以实例化), ----- 熟鸡蛋:唇亡齿寒 ,照样可以嚼东西(没有蛋壳,蛋黄可以用来做卤蛋呀...)

// 内部类没有 `public`标识时,只有在同一个包的其他类可访问、实例化

2.2.14 内置类与同步:实验1

测试类:OutClzSyncTest.javaRunOutClzSyncTest.java

这里很好理解,由于method1method2持有不同的“对象监视器”,所以他们是异步非阻塞,打印结果是乱序的。

2.2.15 内置类与同步:实验2

测试类:OutClzSyncTest2.javaRunOutClzSyncTest2.java

这里用我们上面学到知识就可以理解了,A1、A2 使用了不同锁,非阻塞异步执行,A1、B1争抢一把锁,A1释放锁后B1才可拿锁执行。

2.2.16 锁对象的改变

测试类:ChangeLockString.java

从测试类发现:A、B线程锁对象lock,就算值发生了改变,他们持有的锁都是“123”,还是起到了同步的效果,C、D线程进一步验证了只要对象不变,即使对象的属性被改变,其运行结果还是同步的。

2.3 volatile 关键字

volatile关键字的主要作用是使变量在多个线程间可见。强制从公共堆栈中取得变量值,而不是从线程私有数据取得变量的值

测试类:VolatileCompare.java

测试类输出结果可以看出,没有volatile标识的变量,threadA根本不理会主线程对这个变量的修改,线程会一直运行;而依赖volatile修饰变量运行的线程,可以得到主线程的修改,线程得以正常退出。

-server为了线程效率,从私有堆栈中取值

volate强制从公共堆栈中取值

2.3.1 关键字volatile与死循环–单线程死循环,下一步的停止标识设置没有机会执行

2.3.2 解决同步死循环–(多线程解决,新启线程来设置停止标识)

2.3.3 解决异步死循环–(-server 不读取主堆栈问题,使用volatile解决,强制读主堆栈内存)

2.3.4 volatile 非原子的特性

虽然volatile虽然实现了共享资源在多个线程间的可见性,但它却不具备同步性,那么也就不具备原子性(个人理解:变量本身没有原子性,对变量的操作才有原子性一说)。

测试类:CounterVolatileUnsafe.java

volatile只是增加了多线程间共享资源的透明度,上面的执行结果有可能出现的是你的期望值10000,这只是提高了他出现的几率,这也体现了线程安全问题的难以测试和问题偶发性。

2.3.5 使用原子类进行i++ 操作

从jdk1.5起开始提供了AtomicXX的一些原子类,这些类是乐观锁的一种CAS(Compare And Swap)的实现,其利用JNI调用CPU指令实现。

主要提供了:AtomicIntegerAtomicBooleanAtomicLong供基础数据类型的操作,AtomicReference<V>对象数据操作,AtomicStampedRefrence<V>来操作对象并解决ABA的问题

AtomicInteger完成i++, 测试类:AtomicIntegerTest.java
AtomicReference<V>模拟栈,测试类:AtomicStack.java
AtomicStampedRefrence<V>解决ABA问题,测试类:ABA.java

2.3.6 原子类也并不完全安全

这里其实主要说明的是多个原子类方法间是不安全的,单个原子类方法没有问题。

2.3.7 synchronized 代码块有volatile同步的功能

关键字synchronized可以使多个线程访问同一个资源具有同步性,而且还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

线程间通信

Skills:

  • 使用wait/notify 实现线程间的通信
  • 生产者/消费者模式的实现
  • 方法join的使用
  • ThreadLocal的使用

3.1 等待/通知机制

线程与线程之间不是独立的个体,他们彼此之间可以互相通信和协作。

3.1.1 不使用等待/通知机制实现线程间通信

测试类:TwoThreadTransData.java

线程thread-b循环中不断检测一个条件,轮循时间小,会造成浪费CPU资源,轮循间隔大时,响应不会实时。 所以要出现了wait/notify机制。

3.1.2 什么是等待/通知机制

比如:厨师完成一道菜的时间不确定,服务员需要将这道菜,送给就餐者。

如果不是“等待/通知”机制:服务员不断的询问厨师菜完成了没…

有了“等待/通知”机制择时:服务员坐等(wait),厨师完成菜品即告诉(notify)服务员

3.1.3 等待/通知机制的实现

wait使线程暂停执行,而notify唤醒其他线程继续执行

wait()方法使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法将当前线程置入“预执行队列中”,并且在wait()所在代码处停止,直到接到通知或被中断。

1
调用`wait()`方法前必须获得该对象实例的锁,即只能再锁对象实例的同步代码内中调用`wait()`方法,否则将抛出`IllegalMonitorStateException`,`wait()`方法后,当前线程释放该对象实例的锁。

notify()方法也需要或得对象实例的锁,该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则有线程规划器随机挑出一个呈wait状态的线程,对其发出通知notify。

1
执行`notify()`方法后,当前线程不会马上释放该对象的锁,呈wait状态的锁不会马上获得锁,而是要等执行`notify()`方法所在同步块执行完。

简单体现wait/notify, 测试类:TestWaitNotify.java

实现之前提到的当公共变量==5时退出一个线程, 测试类:WaitNotifyWhen5.java

`运行–就绪–等待 (用单核cpu的方式去立即:同一时刻只有一个线程被执行,所以有了就绪队列)`:
每个锁对象都有两个队列,一个是就绪队列(竞争锁),一个是阻塞队列(wait待唤醒)。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU调度;反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒

关于就绪队列阻塞队列顺序相关

  1. 就绪队列,进入方法的顺序与竞争得到锁的顺序,测试类:CompeteOrder.java
  2. 阻塞队列,wait触发的顺序与被唤醒的顺序,测试类:WaitOrder.java

3.1.4 方法wait()锁释放与notify()方法锁不释放

当方法wait()被执行后,锁自动释放,但执行完notify()方法,锁是不自动释放的,还有在同步代码块内sleep()方法也是不会释放锁的。 这些其实在上面的例子中都已经有体现了。

3.1.5 当interrup方法遇到wait方法

当线程呈wait状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。

3.1.6 只通知一个线程

调用notify()方法一次只随机(wait队列poll出一个)通知一个线程进行唤醒。

测试类:NotifyOne.java

3.1.7 唤醒所有线程

调用notifyAll()方法将唤醒wait队列中的所有线程。

3.1.8 方法wait(long)的使用

带一个参数wait(long)方法的功能是等待指定的时间,如果指定时间内没有被notify将自动苏醒。

无人唤醒,自动苏醒,测试类:WaitHasParamMethod.java

3.1.9 通知过早

“服务员”还没过来等待,“厨师”做完菜就发出了通知。也就是 一个线程notify 发生在另外一个线程wait之前。解决这种问题是在调用wait方法前判断,如果先通知了,则wait方法就没必要执行了。

3.1.10 等待wait的条件发生变化

要注意wait所依赖的条件变化,多个线程在wait,有可能条件检验已经过期,测试类:WaitOld.java

3.1.11 生产者/消费者模式实现

生产者消费者:互相通知,互相等待

  1. 一生产者与一消费者:操作值,测试类:ProducerConsumerTest.java
  2. 多生产者与多消费:操作值-假死,测试类:ProducerConsumerAllWait.java
  3. 多生产者与多消费:操作值,将上面中的notify改为notifyAll,唤醒所有。
  4. 一生产与一消费:操作栈,测试类:ProducerConsumerStack.java
  5. 一生产与多消费:操作栈,测试类:ProducerMulConsumerStack.java

3.1.12 通过管道进行线程间通信:字节流

Java语言中提供了各种各样的输入/输出Stream,能方便的对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。

字节流通信,测试类:PipedInputOutput.java

3.1.13 通过管道进行线程间通信:字符流

字符流通信,测试类:PipedReaderWriter.java

3.1.14 实战:等待/通知之交叉备份

创建20个线程,其中10个线程将数据备份至A数据库,另外10个线程将数据备份至B数据库,并且备份A数据库和备份B数据库交叉进行。

测试类:WaitNotifyInsert.java

3.2 方法join的使用

在很多情况下,主线程创建并启动子线程,如果子线程耗时较久,主线程往往将早于子线程结束。这这时如果主线程想等待子线程完成之后做操作,就可以使用join()方法了。

3.2.1 学习方法join前的铺垫

比如我们需要 T1、T2、T3 三个线程按先后顺序执行,没有join方法可以尝试这么做:

1
2
3
T1.start();  T1.sleep(xxx);
T2.start(); T2.sleep(xxx);
T3.start();

问题就在于 上面的xxx时间我们无法确定,因为每个线程运行多久我们不能确定, 设值长了,程序会浪费等待时间,设值短了,可能出现顺序不对。

3.2.2 用join方法解决

1
2
3
T1.start();  T1.join();
T2.start(); T2.join();
T3.start();

这样就能保证三个线程依次执行了。

另外注意:如果T1内部启动了新的线程,T1.join()方法后面的代码不会等待T1新启的线程

3.2.3 方法join与异常

3.2.4 方法join(long)的使用

1
2
3
childThread.join(1000); //执行这个方法所在的线程最多等待 1000ms,
Thread.sleep(1000); //这个方法会无论如何等待 1000ms
如果子线程执行的时间超过1000ms,那么他们所看起来的效果是一致的。

3.2.5 方法 join(long) 与 sleep(long)的异同

join(long)方法内部是使用wait(long)实现的,所以join(long)方法也具有释放锁的特点, 而sleep(long)方法不会释放锁。

相同:

  1. 调用sleepjoin方法来达到阻塞当前线程的目的

不同:

  1. sleep(long)Thread类static方法,join(long)Thread实例的方法,故需要注意他们作用于的线程区别
  2. 作用于普通的非同步方法中区别就是:sleep(long)等待固定时间、join(long)最多等待这么久的时间

具体深入对比可以查看: Thread类join方法中的 wait(0) 能用 sleep(0) 来替代模拟吗%20%E8%83%BD%E7%94%A8%20sleep(0)%20%E6%9D%A5%E6%9B%BF%E4%BB%A3%E6%A8%A1%E6%8B%9F%E5%90%97/)

3.3 类ThreadLocal的使用

类变量的共享可以采用public static形式,所有线程都使用同一个public static变量。 如果想要实现每个线程都有自己的共享变量呢? JDK提供的类ThreadLocal正是为了解决这个问题的。

  • 局部变量:方法内,不同享,与实例和线程都无关。
  • 全局变量:类内,共享实例变量,在不同的线程、或方法间达到共享
  • 全局静态:类内,共享静态变量,不同线程间访问达到共享,静态与实例无关。

3.3.1 方法 get() 与 null

3.3.2 验证变量的隔离性

3.3.3 解决 get() 返回 null问题

3.3.4 再次验证线程变量的隔离性

测试类:VerifyIsolation.java

3.4 类 InheritableThreadLocal 的使用

使用InheritableThreadLocal可以在子线程取得父线程继承下来的值。

3.4.1 值继承

3.4.2 值继承再修改

测试类:InheritableThreadLocalTest.java

4. Lock的使用

Skills:

  • ReentrantLock类的使用
  • ReentrantReadWriteLock类的使用

4.1 使用 ReentrantLock类

java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但在jdk1.5中新增了ReentrantLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如有嗅探锁定、多路分支通知等功能,使用上比synchronized更加灵活。

4.1.1 使用 ReentrantLock类实现同步

测试类:ReentrantLockTest.java

4.1.2 使用 ReentrantLock类实现同步: 测试2

测试类:ReentrantLockTest2.java

4.1.3 使用Condition 实现等待/通知:错误用法与解决

关键字 synchronizedwait()notify()/notifyAll()方法结合可以实现等待/通知模式,类ReentrantLock实现同样的功能是借助于Condition对象。Condition是JDK5中出现的技术,使用它有更好的灵活性,比如实现多路通知的功能,也就是在一个Lock对象中可以创建多个Condition对象实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

测试类:UseConditionWaitNotifyError.java

4.1.4 正确使用Condition实现等待/通知

测试类:UseConditionWaitNotifyOK.java

成功的实现了等待/通知模式。

  • Object类中的wait()相当于Condition类中的await()方法, 线程进入WAITING状态。
  • Object类中的wait(long timeout)相当于Condition类中的await(long time, TimeUnit unit)方法, 线程进入TIMED_WAITING状态。
  • Object类中的notify()相当于Condition类中的signal()方法
  • Object类中的notifyAll()相当于Condition类中的signalAll()方法

4.1.5 使用Condition实现通知部分线程:错误用法

测试类:MustUseMoreConditionError.java

两个方法共用一个Condition,不能体现区别唤醒,thread-Athread-B两个线程启动分别调用了同一个conditionawait()方法,线程都进入了WAITING状态,最后主线程同时唤醒的是两个线程。

4.1.6 使用多个Condition实现通知部分线程:正确用法

测试类:MustUseMoreConditionOK.java

此时两个方法分别使用了conditionAconditionB,主线程调用了conditoinA.signalAll()达到了只唤醒thread-A的效果,thread-B继续WAITING

4.1.7 实现生产者/消费者:一对一交替打印

测试类:ConditionTest.java

4.1.8 实现生产者/消费者:多对多交替打印

测试类:ConditionTestManyToMany.java

类似Objectnotify()方法,signal()方法同样也会造成假死的现象,这是因为生产者消费者使用的是同一个Conditionsignal()方法在我们期望通知消费者时,可能通知到的是另一个消费者,反之消费者发出的通知也是一样的。所以这里也采取了signalAll()方法发出信号一并唤醒。

4.1.9 公平锁与非公平锁

公平与非公平锁:锁Lock分为公平锁非公平锁, 公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出的顺序。 而非公平锁就是一种获取锁的抢占机制,是随机获得锁的。

测试类:FairNoFairTest.java

  • 公平锁 ,开始运行与得锁顺序呈有序
  • 非公平锁, 开始运行与得锁顺序基本上是乱序的

4.1.10 方法getHoldCount()、getQueueLength()、getWaitQueueLength()

  • int getHoldCount() 方法是查询当前线程保持此锁的个数,也就是调用 lock()方法的次数。测试类:LockMethodHoldCount.java
  • int getQueueLength() 方法是返回等待获取此锁的线程估计数 测试类:LockMethodQueueLength.java
  • int getWaitQueueLength(Condition condition) 方法是返回等待此锁相关条件Condition的线程估计数 测试类:LockMethodWaitQueueLength.java

这里的体现其实与之前synchronized内部锁达一致:锁的两个队列,getQueueLength() 取的是针对此锁 在BLOCKED 就绪阻塞线程,getWaitQueueLength(Condition condition) 则是WAITING睡眠等待唤醒的线程

4.1.11 方法 hasQueuedThread()、hasQueuedThreads() 和 hasWaiters()的测试

  • boolean hasQueuedThread(Thread thread) 查询指定的线程是否等待获取此锁。
  • boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁。

测试类:LockMethodHasQueued.java

  • boolean hasWaiters(Condition condition) 查询是否有线程正在等待此锁有关的condition条件,测试类:LockMethodHasWaiters.java

4.1.12 方法 isFair()、isHeldByCurrentThread() 和 isLocked() 的测试

  • boolean isFair() 判断锁是不是公平锁,默认情况下ReentrantLock非公平锁
  • boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定。
  • boolean isLocked() 查询此锁是否有线程保持锁定。

测试类:LockMethodIsHeldByCurrentThread.java

4.1.13 方法 lockInterruptibly()、tryLock() 和 tryLock(long timeout, TiemUnit unit) 的测试

  • void lockInterruptibly() :取锁,如果当前线程未被中断,则获取锁定,如果已经被终端则出现异常。测试类:LockMethodInterruptiblyTest.java
  • boolean tryLock() : 取锁,尝试取锁,如未取得则返回false。
  • boolean tryLock(long timeout, TimeUnit unit):取锁,指定时间内如未取得锁,取锁失败返回false

测试类:LockMethodTryLock.java

4.1.14 方法 awaitUninterruptibly() 的使用

测试类:LockMethodAwaitUninterruptibly.java

awaitUninterruptibly()方法不同于await(),前者将不理会interrupt()动作,继续执行,而await()在线程触发interrupt()动作时将正常抛出异常。

4.1.15 方法 awaitUntil() 的使用

测试类:LockMethodAwaitUntil.java

4.1.16 使用 Condition 实现顺序执行

测试类:ConditionABC.java

4.2 使用 ReentrantReadWriteLock 类

ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后的任务。这样虽然保证了实例变量的线程安全性,但效率确实低下的。所以JDK提供了一种读写锁ReentrantReadWriteLock类,使他可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock来提升该方法的代码运行速度。

读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。

多个读锁之间不互斥,读写与写锁互斥,写锁与写锁互斥。 在没有线程进行写操作时,进行读取操作的多个线程都可以获取读锁,而写操作的线程只有在获取写锁后才能进行写操作。

即多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写操作。

4.2.1 类 ReentrantLockReadWriteLock 的使用:读读共享

可以看出多个线程都获得了读锁readLock, 测试类:ReadWriteLockBegin1.java

4.2.1 类 ReentrantLockReadWriteLock 的使用:写写互斥

同意时刻,只有一个线程获得锁,写锁阻塞等待前一个线程释放锁, 测试类:ReadWriteLockBegin2.java

4.2.3 类 ReentrantLockReadWriteLock 的使用:读写互斥

4.2.4 类 ReentrantLockReadWriteLock 的使用:写读互斥

获得读锁未释放,写锁也会被阻塞,获得写锁未释放,读锁也会被阻塞:测试类:ReadWriteLockBegin3.java

4.3 Lock 本章总结

完全可以使用Lock对象将synchronized关键字替换掉,而且其具有的功能是synchronized不具有的,Locksynchronized的进阶。

5 定时器 Timer

Skills:

  • 如何实现指定时间执行任务
  • 如何实现按指定周期执行任务

5.1 定时器 Timer 的使用

JDKTimer类主要负责计划任务的功能,也就是在指定时间开始执行某一任务。Timer的作用是设置计划任务,但是封装任务的类是TimerTask抽象类,所以具体要计划执行的任务继承TimerTask类即可。

5.1.1 方法 schedule(TimeTask task, Date time)

指定的日期的时间执行一次任务。

  1. 执行任务的时间晚于当前时间:在未来的某个时间点执行 — timer内部的TimerThread 在实例化时start,默认非守护线程,意味任务完成后,即使没有其他线程,程序不会结束,如果设定为守护线程,如果任务运行之前,其他非守护都已经结束,那么有可能任务还未执行,程序就已经结束。
  2. 计划时间早于当前时间:设定的时间点是已经过去 – 时间点是过去,则会立即执行task任务

    1,2 测试类:TimerTest1.java

  3. 一个Timer中多个TimerTask 任务及延时的测试 – 以计划执行的时间排队成队列,前者执行完后者再执行,当前者执行时间较长时会阻塞后面队列中的任务。
    3 测试类:TimerMultiTask.java

5.1.2 方法 schedule(TimerTask task, Date firstTime, long period)

指定日期的时间后,按指定间隔周期性的执行某一任务。

  1. 计划时间晚于当前时间:在未来某个时间点开始
  2. 计划时间早于当前时间:设定的开始时间是已经过去的 – 立即开始周期任务
  3. 任务执行时间被延迟 – 当间隔时间小于任务单次执行所要的时间时,后面的任务都被延时堆压,会越积越多,但还是一个一个顺序执行

    1,2,3 测试类:TimerTest2.java

  4. TimerTask类的cancel方法 – 将自身任务从Timer任务队列中移除,其他任务不受影响
    测试类:TimerTaskCancel.java

  5. Timer类的cancel方法 – timer中的任务全部清除,timer内部线程销毁,程序退出
    测试类:TimerCancel.java
  6. Timercancel() 方法注意事项 – 调用cancel() 方法不一定会停止任务,当cancel()方法没有竞争到内部的queue锁时。

5.1.3 方法 schedule(TimerTask task, long delay)

以当前时间为基准,延迟指定的毫秒数执行某一任务。
测试类:TimerTest3.java

5.1.4 方法 schedule(TimerTask task, long delay, long period)

以当前时间为基准,延迟指定的毫秒数开始周期性的执行某一任务。
当前时间往后推3秒开始执行, 每3秒 执行一次myTask ,测试类:TimerTest4.java

5.1.5 方法 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

“单次执行任务的时间”一般小于”间隔时间period“,比如你完成一次仰卧起坐的时间是2秒,那么你就不会制定一个每隔1秒做1个仰卧起坐执行计划,你有可能制定一个:每隔3秒做1个仰卧起做执行计划,基于这个计划我们来对比下各种情况,schedule/scheduleAtFixedRate的表现:

情况1:中途出现4秒完成一个的情况 – 认定为意外

此时schedule/scheduleAtFixedRate都会认为这是一个意外,为了不让这个意外继续扩散,他们都会在这个情况出现后马上开始下一次。

情况2:昨天忘记按这个计划执行了 – 区别在这里

schedule: 按period周期,不追赶;马上开始做仰卧起做,按period周期做。

scheduleAtFixedRate: 追赶执行;马上开始做仰卧起坐,做完一个紧接着做下一个,直到赶上,完成昨天的量。

测试类:TimerTest5.java

6. 单例模式与多线程

通过单例与多线程技术的结合,在这个过程中发现很多以前为考虑的问题,学习如何使单例模式遇到多线程时安全的。

6.1 立即加载/“饿汉模式”

在使用类之前类的对象就已经创建好了,中文语境来看,就是饿的迫不及待,有着急迫切的含义。实现的一般做法是:私有化构造方法,声明类全局静态变量,并实例化

1
private static MyObject myObject = new MyObject();

测试类:SigletonTest1.java

注意看下上面例子不单是单例相关的演示,而且包括:静态资源初始化的问题,会有一个奇怪的现象,多线程访问类的普通静态方法,不是立马返回结果,而是线程被“阻塞了”

  • 静态资源初始化本身就是 单线程的(同步阻塞),在类内部资源被初次访问时,触发静态初始化,初始化的顺序 是从上往下.
  • 被静态初始化“阻塞”的方法,不是阻塞”BLOCKD”状态,而是 “RUNNABLE” 状态

其实这里应该也是JVM的一个编译优化,类如果被使用到,其静态的资源也不会被初始化加载。

6.2 延迟加载/懒汉模式

只有在get()时才被创建,从语境上看是“缓慢”、“不急迫”的含义。

测试类:SigletonTest2.java

以上代码帮助理解”懒汉模式” + “DCL” 方式实现的单例,应对的绝大多数场景:高并发取单例,低并发初始化实例,巧妙的避免了synchronized的阻塞,又使用synchronized来保证单次实例化。

进阶理解,对比测试:SyncDclMethodCompare.java

剧情再次反转,在我的上面的测试环境,就用 synchronized 方法就好,不会有什么性能影响

6.3 静态内置类实现单例

静态内部类实现 SigletonTest3.java

6.4 序列化反序列化实现单例

反序列化生成对象时,不通过对象的构造方法,所以会造成有另外的实例被生成,出现非单例的情况,但是反序列化内部会判断对象是否有readResolve方法,有就会自动调用,来达到单例的目的。

测试类: SigletonTest4.java

6.5 使用 static 代码块实现单例

其实这种和饿汉模式类似,都类被访问,静态资源自动初始化。

测试类: SigletonTest5.java

6.6 使用枚举 enum 数据类型实现单例

利用枚举类中枚举元素自动实例化的特点,定义几个枚举元素,产生几个实例,定义一个枚举元素,就是单个实例。

测试类: SigletonTest6.java

6.7 完善使用 enum 枚举实现单例模式

上面6.6的例子中违反了“职责单一原则”,完善测试类: SigletonTest7.java

7. 拾遗增补

Skills:

  • 线程组的使用
  • 如何切换线程状态
  • SimpleDateFormat 类与多线程的解决办法
  • 如何处理线程异常

7.1 线程的状态

线程状态枚举类:Thread.State

7.1.1 验证 NEW、RUNNABLE、TERMINATED

7.1.2 验证 TIMED_WAITING

7.1.3 验证 BLOCKED

7.1.4 验证 WAITING

演示以上1-4状态的例子

7.2 线程组

可以把线程归属到某一个线程组中,线程组中可以有线程对象、也可以有线程组,组中还可以有线程。就类似于一颗节点树,树分支是线程组,叶子节点就是线程,树分支上可以有更小的树分支。

线程组的作用是批量管理线程或线程组对象,有效的对线程或线程组对象进行组织。

7.2.1 线程对象管理线程组: 1级关联

测试类: GroupAddThread.java

7.2.2 线程对象管理线程组: 多级关联

多级分组, 测试类:GroupAddThreadMoreLevel.java

7.2.3 线程组自动归属特性

自动归属就是自动归到当前线程组中。
测试类:AutoAddGroup.java

7.2.4 获取根线程组

通过线程getThreadGroup().getParent()获取线程所在组的父级线程组,得到为null时则已经是最根级别的组了。

测试类:GetParentGroup.java

7.2.5 线程组里加线程组

利用ThreadGroup构造函数显示指定父线程组,测试类:GroupAddGroup.java

7.2.6 组内的线程批量停止

测试类:GroupInnerStop.java

7.2.7 递归与非递归取的组内对象

  • getThreadGroup().enumerate(putList, isRecurse) 可以指定是否递归子孙组
  • activeGroupCount() 取的数量是包括子孙组的

测试类:GroupRecurse.java

7.3 使线程具有有序性

正常情况下,多个线程执行任务的时机是无序的。可通过改造代码使他们具有有序性。

测试类:ThreadRunSync.java

这里的顺序控制逻辑其实可以利用其它方式,如指定nextFlag,或者使用ReentrantLock的多个Condition来指定唤醒执行。

7.4 SimpleDateFormat 非线程安全

SimpleDateFormat主要负责日期的转换和格式化,但在多线程环境中容易误用,比如全局静态化实例、全局实例多线程访问造成转换不准确。

7.4.1 出现异常

发生异常的原因:跟踪SimpleDateFormat 源码可以发现 内部 存储了全局变量: Calendar,也就是单个实例,多线程 都会访问操作 这个Calendar,造成混乱,最终转换错误或出现转换异常
测试类:FormatError.java

7.4.2 解决异常方法1

7.4.3 解决异常方法2

其实都是同理,不可避免每次调用都需要新的实例。一般做法是 封装工具类,实现静态方法内部实例化SimpleDateFormat。或者使用现有的三方工具类。

7.5 线程中出现异常的处理

Thread实例方法:setUncaughtExceptionHandler(UncaughtExceptionHandler eh),与Thread静态方法:setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)

测试类:ThreadExceptionHandler.java,测试中在这里发现了另外一个神奇的地方:new Thread(existsThread)

7.6 线程组内异常

新建MyThreadGroup 重写其uncaughtException(Thread t, Throwable e)方法
测试类:ThreadGroupInnerException.java

7.7 线程异常处理的传递

前面介绍涉及了3中异常处理的方式,如果将这些方式一起用上,会有什么效果呢?

测试类:ThreadExceptionMultiHandler.java

8. 其他

8.1 1. Synchronized底层优化(偏向锁、轻量级锁、重量级锁、自旋锁)

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。参考:Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

  • 偏向锁: 适用于低竞争情况,其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。可以理解为:当只有一个线程操作带有同步方法的Vector对象的时候,此时对Vector的操作就转变成了对ArrayList的操作。jvm参数:-XX:+UseBiasedLocking
  • 重量级锁:对象监视锁(monitor)实现,依赖于操作系统Mutex Lock实现,操作系统线程间状态切换相对耗时长,这就是synchronized效率低的原因。
  • 自旋锁: 空转避免线程进入BLOCKED,适用于锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,对于竞争激烈、单线程持锁时间长的不仅仅浪费CPU,最终避免不了进入BLOCKED状态。在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。

总览

Thread

  • synchronized ,内部锁, 锁对象, Object() 类方法:截图 wait()/wait(long)、notify()、notifyAll()

Object

  • Lock接口方法,ReentrantLock类方法,ReentrantReadWriteLock类方法 读、写锁特性

ReentrantLock

Condition

  • Timer类方法

Timer

实验碰到的几个问题

1. 类内部的public static xxMethod() {} 有可能被阻塞吗?

静态资源初始化造成“阻塞”,但是线程其实是呈RUNNABLE状态的,静态资源初始化本身就是 单线程的(同步阻塞),在类内部资源被初次访问时,触发静态初始化,初始化的顺序 是从上往下. 测试类:SigletonTest1.java

2. thead.join()方法内的wait(0)可以用sleep(0)代替实现吗?

具体查看:Thread类join方法中的 wait(0) 能用 sleep(0) 来替代模拟吗

3. thread.join() 如果thread内部启动了新的线程,那么thread.join()后的代码会等待thread内部线程再执行吗?

thread.join()方法不会理会,thread新启动的线程,只会根据threadisAlive返回来判断。可以利用线程组ThreadGroup来判断thread内部新建立的线程是否都已经运行完毕。

4. 线程基础 Object.wait()Object.notify() 都是干什么用的,怎么用的?

线程同步中用到,执行wait/notify时都需要已经持有该对象的监视锁,wait()方法使线程释放锁并进入WAITING状态,notify()方法是唤醒该锁阻塞队列中的一个线程,notify()方法不会释放锁,同步代码块结束后才释放锁。

5. 怎么看待单例模式中懒汉模式结合DCL的方式解决线程安全问题?

初期理解,以高并发初次实例化的场景看待问题,觉得存在几处浪费工作,代码结构更加不清晰。

不过后来发现,其实应该“乐观”看待,高并发初始化场景少,高并发取实例场景多

理解代码:SigletonTest2.java

6. 发生线程安全问题一般有哪几种情况? 方法内定义的变量,可能引起线程安全问题吗?

容易出现问题的一般有这几中情况:

  1. 全局的静态变量, 问题的发生点在于全部线程都能访问这个静态变量,少有的web下应用也常见的场景:定义一个全局静态的map来缓存数据 public static HashMap<String, String> cacheMap = new HashMap<String, String>();、定义一个全局
  2. 全局的实例变量, 问题的发生点就在于单个实例会被多个线程访问到,web下其实遇到的较少,一般不同的线程会建立不同的实例,但是也有单个实例被多个线程访问到的情况,比如网上看到的一个利用i++全局变量作为sessionId 链接
  3. 方法内的”局部”变量,这种在实际中发生的较少,问题发生在:方法内定义变量,方法内部再启动多个Thread,此时"局部"变量就成了新启动的几个线程的共享变量了,测试类:MethodVaribleSecurity.java
7. 变量到底怎样的规则存储堆、栈中?

一般的说法:基础值类型存栈中,对象在堆中。 不管怎样 基础类型、对象不都是定义在class内么,整个实例化后也是对象,那岂不是都在堆中了?
比如:Person类,有属性:int ageString nameArray<Person> friends,分析下这个类的实例时如何存储的吧。
java中数据的5种存储位置(堆与栈)
《深入理解Java虚拟机》-Java内存区域

8. Thread threaNew = new Thread(existsThread)existsThread 的状态 对threadNew有什么影响?

new Thread(existsThread)

Thread类join方法中的 wait(0) 能用 sleep(0) 来替代模拟吗

Thread 线程类 join方法实现如下

都知道join() 经常用于等待前一个线程执行完毕,再执行后面的任务,那join()方法是如何做到这个”等待”的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1 public final synchronized void join(long millis)                                                                                                                        
2 throws InterruptedException {
3 | long base = System.currentTimeMillis();
4 | long now = 0;
5
6 | if (millis < 0) {
7 | | throw new IllegalArgumentException("timeout value is negative");
8 | }
9
10 | if (millis == 0) {
11 | | while (isAlive()) {
12 | | | wait(0);
13 | | }
14 | } else {
15 | | while (isAlive()) {
16 | | | long delay = millis - now;
17 | | | if (delay <= 0) {
18 | | | | break;
19 | | | }
20 | | | wait(delay);
21 | | | now = System.currentTimeMillis() - base;
22 | | }
23 | }
24 }

分析如下:

  1. 方法无static修饰,说明其为线程实例的方法
  2. 方法有syncrhonized修饰,意味着可能需要竞争锁才能进入方法,进入这个方法后将锁住线程实例这个对象
  3. 10if代码块内的即是join()的实现
  4. 逻辑:判断 线程实例 isAlive()返回的是 true,则 wait(0), 然后再次进入前面的判断,否则退出,也就是在目标线程执行完毕前,这里会不断循环取状态判断、wait(0), 这里的不断即起到了当前线程等待目标线程执行完毕的作用了

这里的做法看起来就像这个思路,比如需要等某人做完了一些事,我们再去做另外一件事,那么我们可不断的去问这个人他是不是做完了那些事

那么我们能将join方法中的wait(0)sleep(0)替代吗?

实验一下吧:

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
 1 public class TestSyncMethodLockAlive {                                                                                                                                  
2 public static void main(String[] args) throws InterruptedException {
3 Thread targetThread = new Thread(() -> {
4 System.out.println("target begin run state=" + Thread.currentThread().getState());
5 sleep(1000); // 模拟目标线程做某些事情,比如查询数据库
6 System.out.println("target end run state=" + Thread.currentThread().getState());
7 }, "target-thread");
8
9 // 定时输出 目标线程的状态 与 isAlive 值
10 new Thread(() -> {
11 while (true) {
12 System.out.printf("### log ### targetThread: state=%s, isAlvie=%s\n", targetThread.getState(), targetThread.isAlive());
13 if (!targetThread.isAlive() && !targetThread.getState().equals(Thread.State.NEW)) {
14 break;
15 }
16 sleep(500);
17 }
18 System.out.printf("### log ### targetThread: state=%s, isAlvie=%s\n", targetThread.getState(), targetThread.isAlive());
19 }).start();
20 targetThread.start();
21
22 // 模拟 Thread 的 join 方法
23 synchronized (targetThread) {
24 while (targetThread.isAlive()) { // 取线程状态
25 targetThread.wait(0);
26 }
27 System.out.println("synchronized anlog join over.");
28 }
29 }
30
31 public static void sleep(long millis) {
32 try {
33 Thread.sleep(millis);
34 } catch (InterruptedException e) {
35 e.printStackTrace();
36 }
37 }
38 }

这里我们用23-28wait(0)的模式模拟了Threadjoin方法,运行测试下,没有问题。 下面我们尝试把25行替换为Thread.sleep(0);测试下,发现控制台打印没有停止,输出以下信息:

1
2
3
4
5
6
7
target begin run state=RUNNABLE
### log ### targetThread: state=RUNNABLE, isAlvie=true
### log ### targetThread: state=TIMED_WAITING, isAlvie=true
target end run state=RUNNABLE
### log ### targetThread: state=BLOCKED, isAlvie=true
### log ### targetThread: state=BLOCKED, isAlvie=true
....

分析下:

  1. taget end run 的输出意味着线程内部的代码已经运行结束了,但是目标线程状态的isAlvie()返回的true,所以我们期盼的27行也不会打印
  2. 为什么线程内部代码执行完了,还是alive的呢? 我们发现:线程状态由RUNNABLE转为BLOCKED状态,而不是期望值TERMINATED
  3. 状态为BLOCKED 意味着线程阻塞,线程等待某个锁 ,死锁了? 我们一起来分析下堆栈内存
分析堆栈内存
1
2
3
4
$ jps # 拿到进程 pid
$ jstack -l pid # 直接查看下 堆栈信息,可以发现:"target-thread" BLOCKED ,但是好像没有发现 "deadlock" 的信息,没有发现死锁那为什么还会一直等待呢?
# jconsole.exe 也可以查看
# $ jmap -dump:live,format=b,file=heap.bin pid # 将堆栈信息导出至文件,离线分析下

可以看以下信息(截取了部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 1 "target-thread" #10 prio=5 os_prio=0 tid=0x000000001d5f4000 nid=0x24140 waiting for monitor entry [0x0000000000000000]                                                  
2 java.lang.Thread.State: BLOCKED (on object monitor)
3 "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001bf06800 nid=0x21a24 in Object.wait() [0x000000001d15f000]
4 java.lang.Thread.State: WAITING (on object monitor)
5 at java.lang.Object.wait(Native Method)
6 - waiting on <0x000000076b406f58> (a java.lang.ref.ReferenceQueue$Lock)
7 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
8 - locked <0x000000076b406f58> (a java.lang.ref.ReferenceQueue$Lock)
9 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
10 at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
11 "main" #1 prio=5 os_prio=0 tid=0x0000000002458000 nid=0x244ec runnable [0x000000000293f000]
12 java.lang.Thread.State: RUNNABLE
13 at java.lang.Thread.sleep(Native Method)
14 at commu.TestSyncMethodLockAlive.main(TestSyncMethodLockAlive.java:31)
15 - locked <0x000000076b4b1c40> (a java.lang.Thread)

内容分析

  1. 1-2行: target-thread waiting for monitor entry ` 意味“在等待进入一个临界区, ”,这里的信息没有期望的”-waiting to lock<0xx>“ 不能看出来到底是被哪个锁阻塞。 (jconsole中都看不到此线程了)
  2. 11-15行: main方法运行的主线程,主线程一直处于RUNNABLE状态,并且持有锁- locked <0x764b1c40>`
  3. 3-10行:daemon类型,一般为jvm守护线程,这里的Finalizer线程主要是给执行完run的线程处理一些身后事,比如将线程移除引用队列。可以看出这里等待的锁,不是直接main线程持有的锁,而是线程本身在此前locked的一个锁,只是这里在等待外部调用某个方法来notify唤醒线程,而间接的在外部依赖了main一直持有的锁,而这个外部可能是在native中而无法跟踪了。

思考弯路:

  1. synchronized 不是一直持有 线程对象的锁么, 为何 “target-thread”线程在等待锁? —-> synchronized是main方法持有了锁, “target-thread” 是线程内部需要这个锁

总结:

  1. 创建的线程运行完run方法后,线程调度器对其还有身后事要处理的,并且间接同步的使用到“线程实例”,并且这个”间接”不好追踪,所以最好不要再线程实例外部来将线程实例作为同步条件来使用。
  2. 线程内做的操作影响线程本身的状态
  3. 将“线程实例”作为锁与“普通对象”作为锁本质一样,将其普通看待不要受到干扰,所以不要把targetThread.wait()方法看的特殊了,理解为lock.wait()就好

回到之前的问题“我们能将wait(0)sleep(0)替代吗?”

其实从控制台打印信息,可以看出isAlive返回的一直为true,因为"身后事"的处理被阻塞,所以线程还是alive的,但是线程状态已经由"RUNNABLE"转变为"BLOCKED"了.

那么我们可以将我们的while (targetThread.isAlive()) 的判断条件修改下while (!targetThread.getState().equals(Thread.State.BLOCKED)) 修改好了接着测试看下控制台的输出吧:

1
2
3
### log ### targetThread: state=TIMED_WAITING, isAlvie=true
target end run state=RUNNABLE
synchronized anlog join over.

bingo! 按我们期盼的顺序输出了

高兴的有些早了,多运行几次发现有时候会是这样:

1
2
synchronized anlog join over.BLOCKED
target begin run state=RUNNABLE

main先结束,target才开始,再次证明多线程问题的偶发性,一不小心就以为万事大吉,其实已经暗藏危机了。其实我们这里的根本原因是忽略了除了NEW/TERMINATED状态,其它的几个线程状态都有可能随意切换。 我们这里单纯认为BLOCKED就是由处理“身后事”造成的,是不严谨的,这里多种其他的情况情况要处理比如:

  1. targetThread.start()synchronized方法,如果它比mainsynchronized块后拿到锁,会造成线程BLOCKED
  2. run方法内部的System.out.println方法为同步方法也有可能造成线程 BLOCKED [这也是为什么在打包的代码里最好不要出现System.out.println的原因]
  3. run方法内部的Thread.sleep(1000)native的,经测试这里暂时不会造成BLOCKED,但是这里是模拟的业务,如果真实业务访问数据库、读写文件等操作其他资源都有可能造成BLOCKED

现有情况的解决方式:

  1. 基于1的问题,可以在targetThread.start()后再 sleep(50)
  2. 2的问题,将打印信息的方式改为logger方式输出,比如java.util.logging.Logger.getGlobal().info非阻塞来替代
  3. 3如果只是基于我们现有的模拟任务,不用修改可以暂时满足。

结论:基于上面的测试代码,可以用sleep(0)代替wait(0)的方式来达到join()的效果。

我们根据日志结果做了不科学的事情:利用判断 thread.getState() 尝试代替 thread.isAlvie() 。只有状态NEWTERMINATED时isAlive()为false,其他状态对应的都是true;

不科学的使用,将给你带来各种意外,需要步步跟踪测试才能做出严谨的判断,除非你只是本着探索的心在学习,否则不建议这样使用

技能点:

  1. 线程状态 - 帮助理解线程状态的例子
    • NEW 创建了未 start
    • RUNNABLE 有可能正在运行(RUNNING),也有可能在就绪队列中(Ready),这个取决于 线程调度器
    • TIMED_WAITING 挂起,等待指定的时间后自动恢复
    • WATING 挂起,被动恢复,依赖其他线程的操作才会唤醒
    • BLOCKED 阻塞,被动恢复执行,依赖其他线程释放相关资源的锁
    • TERMINATED 执行完毕的线程
  2. wait()wait(long)
    • wait()释放锁,线程进入WAITING状态,无限期等待另一个线程执行某一操作,如在锁对象上执行notify()/notifyAll()
    • wait(long)释放锁,线程进入TIMED_WAITING,等待指定的时间自动结束指定
  3. sleep(long)
    • sleep(long) 如果在同步块内,线程将不会释放锁,一直持有,线程进入TIMED_WAITING,等待指定的时间自动往下执行
  4. java dump 分析:

vim 技巧手记

  1. 根据文件内容查找文件:vimgrep

    1
    2
    3
    4
    :vimgrep /Exception/ *.log    #查找目录下  所有含Exception 的 .log文件
    :vimgrep /Exception/ */*.log # 遍历子目录
    #搜索到的文件列表会加入到quickfix中去,执行:
    :copen
  2. 排序,删除重复行

    1
    2
    3
    4
    :sort   //可以直接排序,这个太好用了
    :g/^\(.*\)$\n\1$/d #去除重复行
    :g/\%(^\1$\n\)\@<=\(.*\)$/d #功能同上,也是去除重复行
    :g/\%(^\1\>.*$\n\)\@<=\(\k\+\).*$/d #功能同上,也是去除重复行
  3. 查找忽略大小写

    1
    2
    :set ic  #忽略大小写 set ignorecase
    :set noic #不忽略大小写
  4. 标记复制、粘贴

    1
    2
    "ay
    "ap
  5. 数字递增

    1
    2
    3
    插入模式下:Ctrl+r  ===> =(等号) ===> range(1,100)  回车
    # 格式化补齐 4 位,0001,0002....
    =map(range(1,100), 'printf(''%04d'', v:val)')
  6. 多文件内容查找替换

    1
    2
    :args *.jsp  #第一步:择定范围 *.jsp
    :argdo %s#<title>光大保德信</title>#<title>光大保德信基金</title>#gc|update #替换内容“|update”执行替换后并且存盘。
  7. 删除/保留匹配的行

    1
    2
    :g/pattern/d # 用于删除带有指定搜索内容的行
    :g!/pattern/d 用于删除不带指定搜索内容的行
  8. 删除空白行

    1
    :g/^\s*$/d
  9. html标签选中、内容选中

    1
    2
    vat #整个标签
    vit #标签的内容
  10. 正则替换

    1
    2
    3
    4
    5
    :%s#("\([A-Z_]\+\)")#'\1'#g
    -- 功能将 ("BIT") 替换为 'BIT',将两边的括号去除了,将双引号改为了单引号
    -- %s 表示在所有行中搜索替换,若要指定范围可以写成:1,10s、10s、1,$s 分别表示:1-10行、只在第10行、1-最后一行
    -- # 跟在上面的s后面,表示使用其作为分隔符,因为/分隔符经常会用于替换内容,使用到就得转义比较麻烦
    -- \(、\)、\+、\1 这些是正则表达式的内容,前面两个表示分组捕获,\+ 表示表达式的匹配次数还有*、?等等,\1 则是取前面捕获的第一个分组的内容
  11. 粘贴时取消小自动缩进

    1
    2
    :set paste # 开启paste模式
    :set nopaste # 粘贴完毕关闭paste模式
  12. BOM标记相关

    1
    2
    3
    4
    :set nobomb #去掉BOM标记
    :set bomb #加上BOM标记
    :set bomb? # 查询当前是否有BOM标记
    :%!xxd #以16进制模式打开文件,可以看到具体的大端标记或小端标记
  13. 返回符合查找条件的个数(下面返回yes出现的次数)

    1
    :%s/yes//gn
  14. 大小写替换 (动作,操作符,文本对象)

    1
    2
    3
    4
    5
    6
    7
    ~ # swap case: Upercase/lowercase
    U # Upcase
    u # lowercase
    gU$ # go Upcase line end
    gu$ # go lowercase line end
    gUU # 类似 dd, UU, uu, ~~ 都具有“双击”整行生效的特性
    # all operation see: :h operation

Read Later

建立这个Read Later的初衷:一些好的资料没有吸收完全,用其它闲暇的时间来继续吸收,手机、其他PC都可访问。


2017年8月4日09:03:44

  1. java Bean Validation

2017年7月5日13:51:38

  1. 幂等性的应用

2017年5月23日15:30:51

  1. js之 函数式编程、柯里化、组合等
  2. react、flux-看我们3天 hackday 都干了些什么
  3. 看漫画,学 Redux

2017年5月3日10:17:04

  1. 线程池文章:
    1. Java 线程池框架核心代码分析
    2. Java并发编程与技术内幕:线程池深入理解

2017年4月14日15:05:05

  1. 旧文重读-有哪些生活小习惯,慢慢地可以改变一个人的性格或者生活
  2. 非技术-战隼的学习探索
  3. 知乎-每天一本书的人-经典回答

2017年4月1日09:07:00

  1. javap 反汇编器,查看java编译器生成的字节码
    1. javap -c命令详解
    2. JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)
    3. 读《深入理解Java虚拟机》-类文件结构
  2. 有什么相见恨晚的小知识?
  3. 你有什么相见恨晚的英语学习方法?

2017年3月31日13:24:12

  1. Java的Spi机制研究

2017年3月24日09:59:55

  1. oauth2简单原理描述

2017年3月17日10:56:38 (java性能调优)

  1. jps、jstack等工具排查问题、微信收藏java 排查问题文章的整理
    1. JVM调优之jstack找出最耗cpu的线程并定位代码
    2. Java自带的性能监测工具用法简介——jstack、jconsole、jinfo、jmap、jdb、jsta、jvisualvm
  2. javap 查看编译版本信息,主要用于Bad version错误,使用javap命令查看编译版本信息
  3. 编码最佳实践
    1. 编码最佳实践(5)–小心!这只是冰山一角 - 意想不到的保留了原始对象的引用
  4. java线程安全总结 其中有有序性相关,待消化
  5. 好文sun的java编译器对string常量表达式的处理和优化 - 关键字:编译器优化、运行时、基本类型、常量优化、final、final 方法
  6. 浅谈spring和依赖注入的价值 - 关键字:解耦、抽象、分离、provider,扩展思考:依赖注入–对比–内部接口实现、容器IOC
  7. java常见面试题

2017年3月14日09:27:57(构师-类图-设计)

  1. 架构师之UML类别图,顺序图,用例图,活动图

2017年3月13日13:56:26

  1. 乐观锁的一种实现方式——CAS
  2. 深入理解Java虚拟机笔记—原子性、可见性、有序性
  3. Java 理论与实践: 流行的原子
    Q1: 乐观锁的CAS Atomic类的应用
    Q2: jdk 5.0 由锁方案改为 原子变量实现 和 需要时进行锁定
    Q3: 原子性、可见性、有序性的理解

2017年3月10日16:31:43

  1. java堆、栈、堆栈的区别
  2. java中数据的5种存储位置(堆与栈)-生命周期
  3. git reset revert 回退回滚取消提交返回上一版本 整理入=> git常用命令