分布规律
概率统计学,在随机哈希代码下,链表长度超过8的概率非常非常小
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
时间复杂度
首先只有红黑树的效率高过链表才有转换的必要
红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3
链表的平均查找长度为n/2,当长度为6时,查找长度为6/2=3 这时和红黑树效率持平
当长度一致为8时,平均查找长度为8/2=4
2.1 为什么不是6?
转化为树结构是有成本的
2.2 为什么不是7?
如果超过7则转换为数,低于7则转换为链表,就有可能导致频繁的转换,转换是有成本的
所以中间加了个缓冲,转换的临界点设定为了8和6
打开docker-注册表-设置-选择dockerhub编辑-启用注册表镜像,填入url:
https://mirror.ccs.tencentyun.com
因为文件挺大的,下载要很长时间,所以我开始下载之后第二天才进行后续的步骤
下载镜像如下:
axboy/leanote:latest //2.6.1带数据库版leanote
pch18/baota:clear //宝塔面板lastest有5个多G,clear不默认安装nginx,mysql,php等程序
选中axboy/leanote:latest
镜像,点击启动
名称随便配,点击高级设置,切换到卷标签,进行目录的一些映射,官方推荐映射以下内容
/data/db # 内置mongodb的数据目录,nodb版无此目录
/data/leanote/conf # 笔记的配置文件目录
/data/leanote/files # 笔记内上传的图片、文件存放目录
/data/leanote/public/upload # 头像上传路径
我是这样映射的
考虑不会有其他应用使用这个mongodb,所以端口删除27017
,只保留9000
的映射
其他按需修改,也可以默认
最后应用
浏览器用管理账号登录源站,在后台备份数据库,下载到本地
下载好文件上传到群晖中
ssh登陆群晖,输入以下命令(/volume1/docker/newleanote/files/是我上传backup_leanote_1610674560.tar.gz到的群晖的目录)
# 将备份压缩包cp到容器中的/tmp
sudo docker cp /volume1/docker/newleanote/files/backup_leanote_1610674560.tar.gz axboy-leanote-new:/tmp/
然后进入docker
sudo docker exec -it axboy-leanote-new bash
# 解压缩
mkdir /tmp/leanotedb
tar -zxvf /tmp/backup_leanote_1610674560.tar.gz -C /tmp/leanotedb/
# 恢复备份的数据库
mongorestore -h localhost -d leanote --dir /tmp/leanotedb/ --drop
0 failed 数据库就导入成功了
备份源站leanote目录下的files目录,放到docker映射的群晖目录(/volume1/docker/newleanote/files/)即可
/usr/bin/mongodump
/usr/bin/mongorestore
在新站备份一下数据库试试
Site's URL
以正确的展示图片leanote 初始密码:admin/abc123
这个其实用不到,我只是记录一下
/www/server/ # 面板文件
/www/backup/ # 备份目录
/www/wwwroot/ # 宝塔面板的网站根目录文件夹路径
/www/wwwlogs # 站点日志
但是我们选的这个镜像只支持backup和wwwroot目录的映射
端口只映射管理端口8888
好了
其他按需修改,也可以默认
应用并启动
使用docker创建者提供的初始的用户名密码username/password
登陆错误,通过以下方法解决
sudo docker exec -it 运行的容器ID bash
bt
5
输入新的密码
然后使用username/新密码
登陆
以前使用过宝塔的一键迁移插件,体验非常好,本来以为这次应该很容易,结果遇到了两个问题
安全组里都是开启的,我考虑映射一下22端口到本机,但是总是提示占用,修改了群晖内置的ssh端口也依旧是占用
那么就决定先把迁出端的宝塔升级到7再试一下吧
然而杯具的是我的系统是centos6.5,不能升级到7版本o(≧口≦)o
最后放弃一键迁移的打算,采用了手动在新宝塔上添加网站,然后覆盖相关文件的方案,具体很简单不细说了
]]>同步锁的本质 - 排队
同步的方式:
独享锁-单个队列窗口,共享锁-多个队列窗口
抢锁的方式:
插队抢(不公平锁)、先来后到抢锁(公平锁)
没抢到锁的处理方式:
快速尝试多次(CAS自旋锁)、阻塞等待
唤酲阻塞线程的方式(叫号器):
全部通知、通知下一个
// 自己实现(独享锁) - 常用的
public class MyLock implements Lock {
// 如何判断一个锁的状态,或者说
volatile AtomicReference<Thread> owner = new AtomicReference<>();
// 保存正在等待的线程
volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
@Override
public boolean tryLock() {
return owner.compareAndSet(null, Thread.currentThread());
}
@Override
public void lock() {
boolean addQ = true;//防止重复加入集合,这里用了最简单的方式,演示,多少有点问题,领会精神就行
while (!tryLock()) {
if (addQ) {
// 没拿到锁,加入到等待集合
waiters.offer(Thread.currentThread());
addQ = false;
} else {
// 阻塞 挂起当前线程
LockSupport.park();//伪唤醒,就是非unpark唤醒的,所以上面用while
// 后续,等待其他线程释放锁,收到通知之后继续循环
}
}
waiters.remove(Thread.currentThread());
}
@Override
public void unlock() {
// cas 修改 owner 拥有者
if (owner.compareAndSet(Thread.currentThread(), null)) {
Iterator<Thread> iterator = waiters.iterator();
while (iterator.hasNext()) {
Thread waiter = iterator.next();
LockSupport.unpark(waiter); // 唤醒线程继续 抢锁
}
}
}
...
嗯,写的很不错,查看一下java源码看看是不是差不多
public class ReentrantLock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public void unlock() {
sync.release(1);
}
嗯?源码怎么这么简洁,sync
是什么鬼?
Sync extends AbstractQueuedSynchronizer
这就是AQS
提供了对资源占用、释放,线程的等待、唤醒等等接口的定义和具体实现
可以用在各种需要控制资源争用的场景中。(ReentrantLock/ CountDownLatch/ Semphore)
acquire、 acquireShared:
定义了资源争用的逻辑,如果没拿到,则等待。
tryAcquire、 tryAcquireShared:
实际执行占用资源的操作,内容由使用者具体去实现。
release、 releaseShared:
定义释放资源的逻辑,释放之后,通知后续节点进行争抢。
tryRelease、 tryReleaseShared:
实际执行资源释放的操作,具体内容由AQS使用者去实现。
// 抽象队列同步器 - 还是独占的
// state, owner, waiters
public class MyAqs {
// 1、 如何判断一个资源的拥有者
public volatile AtomicReference<Thread> owner = new AtomicReference<>();
// 保存 正在等待的线程
public volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
public boolean tryAcquire() { // 交给使用者去实现。 模板方法设计模式
throw new UnsupportedOperationException();
}
public void acquire() {
boolean addQ = true;
while (!tryAcquire()) {
if (addQ) {
// 没拿到锁,加入到等待集合
waiters.offer(Thread.currentThread());
addQ = false;
} else {
// 阻塞 挂起当前的线程,不要继续往下跑了
LockSupport.park(); // 伪唤醒,就是非unpark唤醒的
}
}
waiters.remove(Thread.currentThread()); // 把线程移除
}
public boolean tryRelease() {
throw new UnsupportedOperationException();
}
public void release() { // 定义了 释放资源之后要做的操作
if (tryRelease()) {
// 通知等待者
Iterator<Thread> iterator = waiters.iterator();
while (iterator.hasNext()) {
Thread next = iterator.next();
LockSupport.unpark(next); // 唤醒
}
}
}
}
然后在MyLock中使用AQS
// 自己实现(独享锁) - 常用的
public class MyLock implements Lock {
// 抽象工具类AQS
MyAqs aqs = new MyAqs(){
@Override
public boolean tryAcquire() {
return owner.compareAndSet(null, Thread.currentThread());
}
@Override
public boolean tryRelease() {
// 可重入的情况下,要判断资源的占用情况(state字段保存了资源的占用次数)
return owner.compareAndSet(Thread.currentThread(), null);
}
};
@Override
public boolean tryLock() {
return aqs.tryAcquire();
}
@Override
public void lock() {
aqs.acquire();
}
@Override
public void unlock() {
aqs.release();
}
...
这样就和ReentrantLock源码差不多了,源码大体思路就是这样,只是源码AQS考虑了更多适用的情况,添加了更多的功能
比如公平锁和非公平锁的实现,公平锁是lock()直接进去资源争抢阶段acquire();非公平锁是lock()进来先尝试直接占用资源,然后才进入acquire()争抢
而且等待队列不是用的Queue而是用的链表,其中也使用了很多CAS,有兴趣的可以完整看一遍.
补充一份资源占用流程
AQS是一种很优雅的抽象,她还可以用来实现更多的操作
再说应用之前我们还要补充一下AQS中共享的资源占用的一些方法
这里增加一些共享资源争用的方法,其实就是前面提到的acquireShared,tryAcquireShared,releaseShared,tryReleaseShared
release、 releaseShared:
定义释放资源的逻辑,释放之后,通知后续节点进行争抢。
tryRelease、 tryReleaseShared:
实际执行资源释放的操作,具体内容由AQS使用者去实现
// 记录资源状态
public volatile AtomicInteger state = new AtomicInteger(0);
public AtomicInteger getState() { return state; }
public void setState(AtomicInteger state) { this.state = state; }
// 共享资源占用的逻辑,返回资源的占用情况
public int tryAcquireShared(){
throw new UnsupportedOperationException();
}
public void acquireShared(){
boolean addQ = true;
while(tryAcquireShared() < 0) {
if (addQ) {
// 没拿到锁,加入到等待集合
waiters.offer(Thread.currentThread());
addQ = false;
} else {
// 阻塞 挂起当前的线程,不要继续往下跑了
LockSupport.park(); // 伪唤醒,就是非unpark唤醒的
}
}
waiters.remove(Thread.currentThread()); // 把线程移除
}
public boolean tryReleaseShared(){
throw new UnsupportedOperationException();
}
public void releaseShared(){
if (tryReleaseShared()) {
// 通知等待者
Iterator<Thread> iterator = waiters.iterator();
while (iterator.hasNext()) {
Thread next = iterator.next();
LockSupport.unpark(next); // 唤醒
}
}
}
// 独占资源相关的代码
...
又称"信号量",控制多个线程争抢许可。
acquire:
获取一个许可,如果没有就等待release:
释放一个许可。availablePermits:
方法得到可用的许可数目使用场景示例
// 自定义的信号量实现
public class MySemaphore {
MyAqs aqs = new MyAqs() {
@Override
public int tryAcquireShared() { // 信号量获取, 数量 - 1
for(;;) {
int count = getState().get();
int n = count - 1;
if(count <= 0 || n < 0) {
return -1;
}
if(getState().compareAndSet(count, n)) {
return 1;
}
}
}
@Override
public boolean tryReleaseShared() { // state + 1
return getState().incrementAndGet() >= 0;
}
};
/** 许可数量 */
public MySemaphore(int count) {
aqs.getState().set(count); // 设置资源的状态
}
public void acquire() {
aqs.acquireShared();
} // 获取令牌
public void release() {
aqs.releaseShared();
} // 释放令牌
}
java1.5被引入的一个工具类,常被称为:倒计数器。
创建对象时,传入指定数值作为线程参与的数量;
await
:方法等待计数器值变为0,在这之前,线程进入等待状态;
countdown
:计数器数值减一,直到为0;
经常用于等待其他线程执行到某一节点,再继续执行当前线程代码
使用场景示例
类似田径运动,八个赛道运动员依次准备,等到所有人就绪,裁判才吹响口哨
// CountDownLatch 自己实现
public class MyCountDownLatch {
MyAqs myAqs = new MyAqs() {
@Override
public int tryAcquireShared() { // 如果非等于0,代表当前还有线程没准备就绪,则认为需要等待
return this.getState().get() == 0 ? 1 : -1;
}
@Override
public boolean tryReleaseShared() { // 如果非等于0,代表当前还有线程没准备就绪,则不会通知继续执行
return this.getState().decrementAndGet() == 0;
}
};
public MyCountDownLatch(int count) {
myAqs.setState(new AtomicInteger(count));
}
public void await() {
myAqs.acquireShared();
}
public void countDown() {
myAqs.releaseShared();
}
}
也是1.5加入的,又称为"线程栅栏","回环栅栏"。
创建对象时,指定栅栏线程数量。
await
:等指定数量的线程都处于等待状态时,继续执行后续代码。
barrierAction
:线程数量到了指定量之后,自动触发执行指定任务。
栅栏没有显式的使用AQS,她使用锁,每一个线程获取一把锁,且count--,当count为0时,signalAll()且重置count
使用场景示例
类似打游戏,比如吃鸡,每凑够100人就开始一场游戏
public class MyCyclicBarrier {
private int count;//计数
private int initNum;//用于重置count
private Runnable barrierAction;//barrierAction
private ReentrantLock lock = new ReentrantLock();
private Condition trip = lock.newCondition();
public MyCyclicBarrier(int initNum, Runnable barrierAction){
this.initNum = initNum;
this.count = initNum;
this.barrierAction = barrierAction;
}
public void await() throws InterruptedException {
lock.lock();
try {
if (--count == 0) { // tripped 数量够了
if (barrierAction != null)
barrierAction.run(); // 触发执行指定的任务
// 唤醒等待的线程继续执行。重新计数
trip.signalAll(); // 唤醒线程
count = initNum; // count重置
return;
}
//没到数量,进入等待
trip.await();
} finally {
lock.unlock();
}
}
}
CountDownLatch只是一次计数, CyclicBarrier对象可多次触发执行;
CountDownLatch的多个参与者只是参与计数,不会阻塞本身,后面的代码继续执行,只有等待计数归零的代码在阻塞;
而CyclicBarrier的多个参与者在执行到await()方法的时候都会被栅栏拦住,直到满足数量的参与者就绪才会开门放行
所以CountDownLatch相当于一个人在等其他人就绪后做什么,而CyclicBarrier是所有人等其他人就绪后一起开始做什么
同时,CountDownLatch需要已知总数,当总数不固定的时候没法使用
作者: 简七理财
阅读时间: 20200826~20200827
拿到掌阅f1第一天,随便找了本看起来顺眼的书开始打卡
断断续续累积两个小时读完,这个时间用的有点长了,以后这种书还是要读的更快一点
书看起来是13年的,很多东西过时了,甚至里面还推荐大家买P2P理财【瑟瑟发抖】
整个看下来发现最有用的还是自我投资这一部分,其他的看过就忘就好,对我来说没什么价值
理财不是让我们不花钱,而是让我们成为金钱的主人,通过规划去更好的更安心的生活
一个人能够取得的最终成就,不是由你的起点决定的,而是在过程中是否能有清晰的目标、保持规划并且严格的执行。所以你的生活会变得怎么样,完全取决于你愿不愿意从现在开始去规划自己的未来。
投资自己四部曲
投资自己的视野和洞察力
方式一是阅读,大量的阅读。方式二是旅行,勇敢滴旅行。
投资自己挑战的勇气和技巧
如何定义“挑战”?手到擒来那不算,要就要踮起脚尖来都够不到,必须跳起来才有机会触到的才是真正的挑战。
☆尝试一次练摊儿:比如在圣诞节晚上去商业街兜售小饰品,并与城管斗智斗勇; ☆尝试一个人旅行:从做攻略到完成旅行全程靠自己完成,如要增加难度可考虑出境游;
☆尝试在100人面前演讲:主动申请一个机会,在一群人面前演讲一个你感兴趣的题目; ☆坚持一种运动的习惯:例如坚持晨跑、或者坚持游泳三个月。
投资自己独特的眼光和方法论
除了阅读、学习、体验之外,还要会总结和提炼
投资自己的心态和情绪管理
自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。
悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)
几种重要的锁实现方式: synchronized、 ReentrantLock、 ReentrantReadWriteLock
属于最基本的线程通信机制,基于对象监视器实现的。
Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。
一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。
同步关鍵字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)
// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {
//普通方法+同步关键字,不能同步,因为锁的是对象,main里两个线程创建了两个对象
public synchronized void test1(){
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
//static方法+同步关键字,能同步,因为锁的是类
public synchronized static void test1(){
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
//synchronized (this),不能同步
static Object temp = new Object();
public void test1() {
synchronized (this) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
//synchronized(static变量),synchronized(ObjectSyncDemo1.class),能同步,两个线程争抢的是同一把锁
static Object temp = new Object();
public void test1() {
//synchronized (ObjectSyncDemo1.class) {
synchronized (temp) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
// 可重入
public class ObjectSyncDemo2 {
public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我执行结束" + arg);
}
public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
https://www.oracle.com/java/technologies/javase/6performance.html#2.1.2
锁粗化是发生在编译器级别(JIT)的一种锁优化方式。
JVM会对热点代码(不间断、高频地)中的多个锁合并成一个锁,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看,修改优化级 别为C2
public class ObjectSyncDemo3 {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
//jvm会对热点代码进行优化,变成下面这样
/*
synchronized (this) {
i++;
i++;
}*/
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}
锁消除是发生在编译器级别(JIT)的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
比如StringBuffer类的append操作,方法内部是加锁的,所以在执行一系列的append操作时JVM就会在保证线程安全的情况下消除锁
// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}
public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}
public void test1(Object arg) {
// jit 优化, 消除了锁
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}
HotSpot中,对象前面会有一个类指针和标题,储标识哈希码的标题字以及用于分代垃圾收集的年龄和标记位
参考来源:
https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
使用CAS修改mark word完毕,加锁成功。则mark word中的tag进入00
状态(01=unlocked,00=Light-Weight locked,10=Heavy-weight locked)。
解锁的过程,则是一个逆向恢复 mark word的过程
默认情况下JVM锁会经历:偏向锁-->轻量级锁-->重量级锁这四个状态
偏向标记第一次有用,出现过争用后就没用了。-XX: -UseBiasedLocking
禁用使用偏置锁定,-XX:+UseBiasedLocking
设置启用偏向锁。
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步
JVM为了少千活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作
修改mark word如果失败,会自旋CAS一定次数,该次数可以通过参数配置
超过次数,仍未抢到锁,则锁升级为重量级锁进入阻塞。
monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。
在虚拟机的源码中可以看到监视器的内容
方法 | 描述 |
---|---|
lock | 获取锁的方法,若锁被其他线程获取,则等待(阻塞) |
lockInterruptibly | 在锁的获取过程中可以中断当前线程 |
tryLock | 尝试非阻塞地获取锁,立即返回 |
unlock | 释放锁 |
根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。
独享锁;支持公平锁、非公平锁两种模式;可重入锁;
ReentrantLock
在上锁时,会根据实例化时指定的策略去获取锁,默认为非公平锁。如果上锁成功,锁状态值+1(重入,最大次数为 Integer.MAX_VALUE
),并将锁持有者设置为当前线程实例。在 Sync(AQS)
内部维护了一个队列,存放了所有上锁失败的线程。公平锁在上锁前,会检查在自己前面是否还有其他线程等待,如果有就放弃竞争,继续等待。而非公平锁会抓住每个机会,不管是否前面是否还有其它线程等待,只顾上锁
ReetrantLock
在释放锁时,将状态计数器减一(重入),当状态计数器为0时,锁可用。此时再从等待队列中寻找合适的线程唤醒,默认从队首开始,如果队列正在更新中,且未找到合适的线程,那么从队尾开始寻找。
维护一对关联锁,一个用于只读操作,一个用于写入;读锁可以由多个读线程同时持有,写锁是排他的。
适合读取线程比写入线程多的场景,改进互斥锁的性能,
示例场景:缓存组件、集合的并发线程安全性改造。
写锁是线程独占,读锁是共享,所以写->读是升级。(读->写,是不能实现的)
锁降级
指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 缓存示例
public class CacheDataDemo {
// 创建一个map用于缓存
private Map<String, Object> map = new HashMap<>();
private static ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
// 1 读取缓存里面的数据
// cache.query()
// 2 如果缓存没数据,则取数据库里面查询 database.query()
// 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
}
public Object get(String id) {
Object value = null;
// 首先开启读锁,从缓存中去取
rwl.readLock().lock();
try {
if (map.get(id) == null) {
// TODO database.query(); 全部查询数据库 ,缓存雪崩
// 必须释放读锁
rwl.readLock().unlock();
// 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
rwl.writeLock().lock(); // 所有线程在此处等待 1000 1 999 (在同步代码里面再次检查是否缓存)
try {
// 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
if (map.get(id) == null) {
// TODO value = ...如果缓存没有,就去数据库里面读取
}
rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性
} finally {
rwl.writeLock().unlock(); // 释放写锁@
}
}
//TODO cache.query();锁降级就是为了在这里保持数据可见性,避免在前面释放写锁后数据被别的线程修改
} finally {
rwl.readLock().unlock();
}
return value;
}
}
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程T获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
如果遵循锁降级的步骤,线程C在释放写锁之前获取读锁,那么线程T在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。
用于替代wait/notify
Object中的 wait(), notify(), notifyAll()方法是和 synchronized配合使用的,可以唤醒一个或者全部(单个等待集)
Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/ unpark机制);
]]>public class Demo{
public int i=0;
public void incr(){
i++
}
}
多个线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求
临界区:incr方法內部就是临界区域,关键部分代码的多线程并发执行,会对执行结果产生影响。
竞态条件:可能发生在临界区域内的特殊条件。多线程执行incr方法中的i++关键代码时,就产生了竞态条件。
如果一段代码是线程安全的,则它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。
栈封闭时,不会在线程之间共享的变量,都是线程安全的。
局部对象引用本身不共享,但是引用的对象存储在共享堆中。如果方法內创建的对象,只是在方法中传递,并且不对其他线程可用,那么也是线程安全的。
//例子
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject{
localObject.setValue("value");
}
判定规则:如果创建、使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用是线程安全的。
创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。
实例被创建,value变量就不能再被修改,这就是不可变性。
//比如没有提供set方法的私有属性
public class Demo{
private int value =0;
public Demo(int value){
this.value= value
}
public int getValue(){
return this.value;
}
}
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体是原子性的核心特征。
最开始例子里面的i++就不是一个原子操作,她包含三步骤:
存在竞态条件的情况下,线程不安全,需要转变为原子操作才能安全。
Compare and swap
比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。(类似乐观锁)
JAVA中的 sun.misc.Unsafe
类,提供了 compareAndSwaplnt()
和 compareAndSwapLong()
等几个方法实现CAS
package com.study.lock.lock;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class LockDemo1 {
volatile int value = 0;
static Unsafe unsafe; // 直接操作内存,修改对象,数组内存....强大的API
private static long valueOffset;
static {
try {
// 反射技术获取unsafe值
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
// 获取到 value 属性偏移量(用于定位value属性在内存中的具体地址)
valueOffset = unsafe.objectFieldOffset(LockDemo1.class
.getDeclaredField("value"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void add() {
// TODO xx00
// i++;// JAVA 层面三个步骤
// CAS + 循环 重试
int current;
do {
// 操作耗时的话, 那么 线程就会占用大量的CPU执行时间
current = unsafe.getIntVolatile(this, valueOffset);
} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
// 因为跨平台的问题,而我们是用反射的非正常方式获得unsafe,所以可能会失败
}
public static void main(String[] args) throws InterruptedException {
LockDemo1 ld = new LockDemo1();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
ld.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(ld.value);
}
}
其实AtomicInteger这些封装类的底层就是这么写的
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
AtomicIntegerArray:原子更新整型数组里的元素
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素
AtomiclntegerFieldUpdater:原子更新整型的字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicReference:原子更新引用类型
AtomicStampedReference:原子更新带有版本号的引用类型
AtomicMarkableReference:原子更新带有标记位的引用类型。
jdk1.8更新
更新器: DoubleAccumulator、 LongAccumulator
计数器: DoubleAdder、 LongAdder(一个使用场景是统计接口调用次数时可使用)
计数器增强版,高并发下性能更好
频繁更新但不太频繁读取的汇总统计信息时使用
分成多个操作单元,不同线程更新不同的单元只有需要汇总的时候才计算所有单元的操作
即分成多个值(Cell)减少竞争,对外暴露一个虚拟值,最后取得时候使用sum()把多个内存块的Cell相加
锁能保证可见性,是基于Happens-before原则
一个线程的unlock一定执行在另一个线程的lock前面,也就是说另一个线程不会插进该线程的执行过程(lock->op->unlock)中
]]>CAS操作存在一个ABA问题,就是在CAS之前A变成B又变回A,CAS还是能够设置成功的,
什么场景下会出现这个问题呢?查了一些资料,发现在下面的两种情况下会出现ABA问题。
这种情况在带有GC的语言中,这种情况是不可能发生的,为什么呢?
拿JAVA举例,在执行CAS操作时,A,B对象肯定生命周期内,GC不可能将其释放,那么A指向的内存是不会被释放的,B也就不可能分配到与A相同的内存地址,CAS失败。若在无GC的,A对象已经被释放了,那么B被分配了A的内存,CAS成功。
但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
比如:现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:head.compareAndSet(A,B);在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A。而对象B此时处于游离状态:此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题
在Java中,AtomicStampedReference<E>
也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,
例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败
package concur.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
]]>然而实际上因为某些原因,基本上无法实现每次迭代的所有故事都能发布.只有在分支上测试完成合并到 master上的故事才会被发布.
那么就想着能不能在分支合并到master的时候在jira上打个标签,这样在jira上一筛选就知道哪些故事是可以发布的了
好在gitlab有webhook功能,能够在触发某些操作的时候调用自定义的接口传输相关操作的信息,而jira也提供了相应的api来方便读写,那么只需要发布一个服务提供接口给webhook调用,解析传过来的参数,符合条件的情况下去调用jira的api修改标签,这个方案也就实现了.
GitLab Community Edition 11.1.4
JIRA v7.13.5
python 3.5.4
Flask 1.1.2
requests 2.24.0
在git创建分支时分支名称需要包含jiraId
我们项目组是直接以jiraId+故事标题命名分支
图快速方便所以用python写的,结果后面部署费了不少事
文档: https://docs.gitlab.com/ee/user/project/integrations/webhooks.html
参数是json形式的
主要取出其中3个参数
object_attributes.state
gitlab设置的trigger是合并事件,实际上会在申请(opened),关闭(closed)和完成合并(merged)3个情况触发,所以要判断是否是完成合并的调用
object_attributes.target_branch
target_branch 是为了确认是合并到master的请求,合并到其他分支的请求不处理
object_attributes. source_branch
source_branch 是为了取出jiraId以进行后续操作
文档: https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#editing-an-issue-examples
从source_branch利用正则截取出jiraId,拼装并调用接口地址
附三个修改标签的传参,第一个是追加,后两种是覆盖设置
#增加
{
"update" : {
"labels" : [{"add" : "标签内容"}]
}
}
#设置
{
"fields" : {
"labels": ["标签内容"]
}
}
{
"update" : {
"labels" : [{"set" : ["标签内容"]}]
}
}
建议先用postman之类的工具先调试一下jira的接口,如果报错
Field 'labels' cannot be set. It is not on the appropriate screen, or unknown.
那就需要联系jira管理员,在项目的screens(项目管理->界面)中开启相关字段的api读写权限
# -*- coding: utf-8 -*-
from flask import Flask,request
import requests
import re
baseUrl = 'http://jira.sunjianbo.com:8080/rest/api/2/issue/'
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def test():
# addTag2Jira("DB19236-2398 324扥矿赛肯")
return "hello world"
@app.route("/merged", methods=["GET", "POST"])
def merged():
# print(request.headers)
# print(request.json)
# print('---')
body = request.json
object_attributes = body['object_attributes']
state = object_attributes['state'] # opened,closed,merged
source_branch = object_attributes['source_branch']
target_branch = object_attributes['target_branch']
if state == 'merged' and target_branch == 'master':
# 已合并到master
print('是已合并到master的请求,开始修改jira标签')
addTag2Jira(source_branch)
else:
print('不是合并到master的成功请求,而是合并到['+target_branch+']的['+state+"]请求")
return ""
def addTag2Jira(source_branch):
# 取得分支jira id
matchObj = re.search(r"DB\d+-\d+", source_branch)
if matchObj:
print("匹配出的jiraId: ", matchObj.group())
jiraId = matchObj.group()
# 去相应jira故事中打一个uat标签
url = baseUrl + jiraId
messagebody = '''
{
"update" : {
"labels" : [{"add" : "UAT"}]
}
}
'''
result = fun_put(url, messagebody)
print(result)
if result.status_code == 204:
print('修改成功: ' + url)
else:
print('修改失败: ' + source_branch)
else:
print("分支名称上找不到jiraId: " + source_branch)
def fun_put(url, messagebody):
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
r = requests.put(url, data=messagebody, headers=headers, auth=('username', 'passwd'))
print(r.status_code)
return r
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
部署这里有个插曲
公司服务器是离线的,所以我先离线安装了python3,然后打算安装virtualenv时报了一个ssl的错误,明明是离线安装,为毛会涉及到ssl,咱也不知道,咱也不敢问,推测是python3安装的有点问题,依赖的openssl之类的在测试机上不太全吧
折腾了半天没搞定,考虑到是测试服务器我就瞎搞了一把
用了
pip3 install -r requirements.txt --proxy=代理服务器IP:端口号
代理走了我本地电脑在线安装了Flask和 requests,瞬间就解决了(给机智的我点个赞(◔◡◔))
settings->integrations->填写url->Trigger勾选Merge request events
->嫌麻烦我禁用了ssl,没有实际测试一下行不行->保存
)
]]>将运行模式设置为-server, 变成死循环 。 没加默认就是client模式,就是正常(可见性问题)
import java.util.concurrent.TimeUnit;
// 1、 jre/bin/server 放置hsdis动态链接库
// 测试代码 将运行模式设置为-server, 变成死循环 。 没加默认就是client模式,就是正常(可见性问题)
// 2、 通过设置JVM的参数,打印出jit编译的内容 (这里说的编译非class文件),通过可视化工具jitwatch进行查看
// -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
// 关闭jit优化-Djava.compiler=NONE
public class VisibilityDemo {
private volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo1 = new VisibilityDemo();
Thread thread1 = new Thread(new Runnable() {
public void run() {
int i = 0;
// class -> 运行时jit编译 -> 汇编指令 -> 重排序
while (demo1.flag) { // 指令重排序
i++;
}
System.out.println(i);
}
});
thread1.start();
TimeUnit.SECONDS.sleep(2);
// 设置is为false,使上面的线程结束while循环
demo1.flag = false;
System.out.println("被置为false了.");
}
}
CPU缓存在这里会导致线程读取的flag延迟一些变为false,但不会发生死循环
Java编程语言的语义允许**编译器和微处理器(JIT)**执行优化,这些优化可以与不正确的同步代码交互,从而产生看似矛盾的行为。
虽然有as-if- serial的原则,但是因为多CPU的情况就变得复杂了,每个cpu只能保证自己的重排序是没有问题的
汇编层面的重排序会将while(demo1.flag)
变为
if(demo1.flag){
while(true){
i++
}
}
它认为demo1.flag基本不变就是true
可直接参考这个操作博客
http://www.cnblogs.com/stevenczp/p/7975776.html
https://www.cnblogs.com/stevenczp/p/7978554.html
输出jit日志
工具安装
下载 https://github.com/AdoptOpenJDK/jitwatch
解压 通过maven运行
mvn clean compile exec:java
配置jitwatch
页面选择 config, 配置要调试的项目src源码路径,和class编译路径
打开jit.log
点击start
在分析的结果中,选中指定的类,再选择右侧的具体方法,则弹出jit编译结果
为了解决多线程的问题,提出了一种规范,就是内存模型
内存模型描述程序的可能行为。
Java编程语言内存模型通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。
要程序的所有执行产生的结果都可以由内存模型预测。具体的实现者任意实现,包括操作的重新排序和删除不必要的同步。
内存模型决定了在程序的每个点上可以读取什么值
比如说线程1的操作让线程2能及时看到改变,也就是说在这个点上线程2可以且必须读到它应该读取的正确的值
描述可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段、静态字段和数组元素都存储在堆内存中。
如果至少有一个访问是写的,那么对同一个变量的两次访问(读或写)是冲突的。
这句话定义在:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jis-17.4.1
程序顺序: 如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的
本规范只涉及线程间的操作
同步我理解就是对一些操作约定了固定的先后顺序,且后面能看到前面的操作
happens-before关系主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。
具体的虚拟机实现,有必要确保以下原则的成立
当程序包含两个没有被 happens-before关系排序的冲突访问时,就称存在数据争用
遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
根据JMM中规定的 happen before和同步原则:
对某个 volatile字段的写操作 happens-before每个后续对该 volatile字段的读操作。
对 volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以 volatile关键字就有这些功能:
禁止缓存:
volatile变量的访问控制符会加个 ACC VOLATILE
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#tjvms-4.5
对 volatile变量相关的指令不做重排序
f= new finalDemo();
读取到的f.x一定最新,x为final字段。而y可能是0// 官方示例,可能会读取到y的值为0
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3 肯定是3
int j = f.y; // could see 0 可能看到0
}
}
}
如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;
伪代码示例: public finalDemo(){x=1;y=x;};
y会等于1;
读取该共享对象的final成员变量之前,先要读取共享对象。
伪代码示例:r = new ReferenceObj();k=r.f;
这两个操作不能重排序
通常static final
是不可以修改的字段。然而System.in
,System.out
和 System.err
是 static final
字段,遗留原因,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段;
一个字段或元素的更新不得与任何其他字段或元素的读取或更新交互。
特别是,分别更新字节数组的相邻元素的两个线程不得干涉或交互,也不需要同步以确保顺序一致性。
有些处理器(尤其是早期的Alphas处理器)没有提供写单个字节的功能
在这样的处理器上更新byte数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。
这个问题有时候被称为**“字分裂 word tearing)”**,在单独更新单个字节有难度的处理器上,就需要寻求其它方式了。
基本不需要考虑这个,了解就好。
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4
// 官方提供的示例,检查有没有WordTearing情况
public class WordTearing extends Thread {
static final int LENGTH = 8;
static final int ITERS = 1000000;
static byte[] counts = new byte[LENGTH];
static Thread[] threads = new Thread[LENGTH];
final int id;
WordTearing(int i) {
id = i;
}
public void run() {
byte v = 0;
for (int i = 0; i < ITERS; i++) {
byte v2 = counts[id];
if (v != v2) {
System.err.println("Word-Tearing found: " +
"counts[" + id + "] = " + v2 +
", should be " + v);
return;
}
v++;
counts[id] = v;
}
}
public static void main(String[] args) {
for (int i = 0; i < LENGTH; ++i)
(threads[i] = new WordTearing(i)).start();
}
}
虚拟机规范中,写64位的 double和long分成了两次32位值的操作
由于不是原子操作,可能导致读取到某次写操作中64位的前32位,以及另外一次写操作的后32位
读写 volatile的long和 double总是原子的。读写引用也总是原子的
]]>商业JⅥM不会存在这个问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。
线程是不是越多越好?
线程池的推出,就是为了方便的控制线程数量。
用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
用于存放没有处理的任务。提供一种缓冲机制。
类型 | 名称 | 描述 |
---|---|---|
接口 | Executor | 最上层的接口,定义了执行任务的方法 execute |
接口 | ExecutorService | 继承了 Executor接口,拓展了 Callable、 Future、关闭方法 |
接口 | ScheduledExecutorService | 继承了 ExecutorService,增加了定时任务相关的方法 |
实现类 | ThreadPoolExecutor | 基础、标准的线程池实现 |
实现类 | ScheduledThreadPoolExecutor | 继承了 ThreadPoolExecutor,实现了ScheduledExecutorService中相关定时任务的方法 |
//监测ExecutorService是否已经关闭, 直到所有任务完成执行,或超时发生,或当前线程被中断
awaitTermination(long timeout, TimeUnit unit)
//执行给定的任务集合,执行完毕后,返回结果
invokeAll(Collection<? extends Callable<T>> tasks)
//执行给定的任务集合,执行完毕或者超时后,返回结果,其他任务终止
invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
//执行给定的任务,任意一个执行成功则返回结果,其他任务终止
invokeAny(Collection<? extends Callable<T>> tasks)
//执行给定的任务,任意一个执行成功或者超时后,则返回结果,其他任务终止
invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
//如果此线程池已关闭,则返回true。
isShutdown()
//如果关闭后所有任务都已完成,则返回true。
isTerminated()
//优雅关闭线程池,之前提交的任务将被执行,但是不会接受新的任务。
shutdown()
//尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表。
shutdownNow()
//提交个用于执行的Callable返回任务,并返回一个Future,用于获取Callable执行结果
submit(Callable<T> task)
//提交可运行任务以执行,并发回一个Future对象,执行结果为null
submit(Runnable task)
//提交可运行任务以执行,并返回Future, 执行结果为传入的result
submit(Runnable task, T result)
schedule(Callable<V> callable, long delay, TimeUnit unit)
创建并执行一个一次性任务,过了延迟时间就会被执行
schedule(Callable<V> callable, long delay, TimeUnit unit)
创建并执行一个一次性任务,过了延迟时间就会被执行
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
创建并执行一个周期性任务过了给定的初始延迟时间,会第一次被执行
执行过程中发生了异常,那么任务就停止
一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和scheduleWithFixedDelay
的重要区别。
此处结合代码示例进行理解即可!
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
创建并执行一个周期性任务,过了初始延迟时间,第一次被执行,后续以给定的周期时间执行
执行过程中发生了异常,那么任务就停止
一次任务执行时长超过了周期时间,下一次任务会在该次任务执行结束的时间基础上,计算执行延时。
对于超过周期的长时间处理任务的不同处理方式,这是它和scheduleAtFixedRate
的重要区别。
你可以自己实例化线程池,也可以用 Executors创建线程池的工厂类,常用方法如下:
创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数。
创建的是一个大小无界的缓冲线程池。它的任务队列是一个同步队列。
任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行。
池中的线程空闲超过60秒,将被销毁释放。线程数随任务的多少变化。
适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数= Integer.MAX_VALUE
只有一个线程来执行无界任务队列的单一线程池。
该线程池确保任务按加入的顺序一个个依次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续的任务。
与 newFixedThreadPool(1)
的区别在于,单一线程池的池大小在newSingleThreadExecutor
方法中硬编码,不能再改变的。
能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数= Integer.MAX_VALUE
是否达到核心线程数量?
没达到,创建一个工作线程来执行任务。
工作队列是否已满?
没满,则将新提交的任务存储在工作队列里。
是否达到线程池最大数量?
没达到,则创建一个新的工作线程来执行任务。
最后,执行拒绝策略来处理这个任务。
如何确定合适数量的线程?
计算型任务:
CPU数量的1-2倍
IO型任务:
相对比计算型任务,需多一些线程,要根据具体的IO阻塞时长进行考量决定。
如 tomcat中默认的最大线程数为: 200。
也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。(newCachedThreadPool
)
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** 线程池的使用 */
public class Demo9 {
/**
* 测试: 提交15个执行时间需要3秒的任务,看线程池的状况
*
* @param threadPoolExecutor 传入不同的线程池,看不同的结果
* @throws Exception
*/
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 查看线程数量,查看队列等待数量
Thread.sleep(500L);
System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
// 等待15秒,查看线程数量和队列数量(理论上,会被超出核心线程数量的线程自动销毁)
Thread.sleep(15000L);
System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
}
/**
* 标准线程池
* 1、线程池信息: 核心线程数量5,最大数量10,无界队列,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest1() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结果:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}
/**
* 有拒绝策略的
* 2、 线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest2() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
testCommon(threadPoolExecutor);
// 预计结果:
// 1、 5个任务直接分配线程开始执行
// 2、 3个任务进入等待队列
// 3、 队列不够用,临时加开5个线程来执行任务(5秒没活干就销毁)
// 4、 队列和线程池都满了,剩下2个任务,没资源了,被拒绝执行。
// 5、 任务执行,5秒后,如果无任务可执行,销毁临时创建的5个线程
}
/**
* 自定义Executors.newFixedThreadPool(int nThreads)
* 3、 线程池信息: 核心线程数量5,最大数量5,无界队列,超出核心线程数量的线程存活时间:5秒
*
* @throws Exception
*/
private void threadPoolExecutorTest3() throws Exception {
// 和Executors.newFixedThreadPool(int nThreads)一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}
/**
* 自定义Executors.newCachedThreadPool()
* 4、 线程池信息:
* 核心线程数量0,最大数量Integer.MAX_VALUE,SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒
*
* @throws Exception
*/
private void threadPoolExecutorTest4() throws Exception {
// SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。
// 在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,
// 而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,
// 那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。
// 此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。
// 和Executors.newCachedThreadPool()一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结果:
// 1、 线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行
// 2、 所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0
Thread.sleep(60000L);
System.out.println("60秒后,再看线程池中的数量:" + threadPoolExecutor.getPoolSize());
}
/**
* 延迟执行
* 5、 定时执行线程池信息:3秒后执行,一次性任务,到点就执行 <br/>
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest5() throws Exception {
// 和Executors.newScheduledThreadPool()一样的
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
threadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,现在时间:" + System.currentTimeMillis());
}
}, 3000, TimeUnit.MILLISECONDS);
System.out.println(
"定时任务,提交成功,时间是:" + System.currentTimeMillis() + ", 当前线程池中线程数量:" + threadPoolExecutor.getPoolSize());
// 预计结果:任务在3秒后被执行一次
}
/**
* 周期性执行
* 6、 定时执行线程池信息:线程固定数量5 ,<br/>
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 周期性执行某一个任务,线程池提供了两种调度方式,这里单独演示一下。测试场景一样。
// 测试场景:提交的任务需要3秒才能执行完毕。看两种不同调度方式的区别
// 效果1: 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
// 也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
// 效果2:提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒)。
// 也就是说这个代码钟的效果看到的是:4秒执行一次。 (计算方式:每次执行3秒,间隔时间1秒,执行完以后再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
/**
* 7、 终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest7() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
System.out.println("异常:" + e.getMessage());
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 1秒后终止线程池
Thread.sleep(1000L);
threadPoolExecutor.shutdown();
// 再次提交提示失败
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一个任务");
}
});
// 结果分析
// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
// 2、调用shutdown后,不接收新的任务,等待13任务执行结束
// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}
/**
* 8、 立刻终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest8() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
System.out.println("异常:" + e.getMessage());
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 1秒后终止线程池
Thread.sleep(1000L);
List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
// 再次提交提示失败
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一个任务");
}
});
System.out.println("未结束的任务有:" + shutdownNow.size());
// 结果分析
// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
// 2、调用shutdownnow后,队列中的3个线程不再执行,10个线程被终止
// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}
public static void main(String[] args) throws Exception {
new Demo9().threadPoolExecutorTest1();
// new Demo9().threadPoolExecutorTest2();
// new Demo9().threadPoolExecutorTest3();
// new Demo9().threadPoolExecutorTest4();
// new Demo9().threadPoolExecutorTest5();
// new Demo9().threadPoolExecutorTest6();
// new Demo9().threadPoolExecutorTest7();
// new Demo9().threadPoolExecutorTest8();
}
}
]]>