JavaEE12
一、多线程案例四:定时器
定时器相当于"闹钟“,在代码中,也常常使用”闹钟“机制。在java标准库中,也提供了现成的定时器。
1、定时器的实现
class MyTimerTask implements Comparable<MyTimerTask>{//实现比较接口
private Runnable runnable;
private long time;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<MyTimerTask>();
public MyTimer(){
Thread t=new Thread(()->{
while(true) {
synchronized (this){
//这里涉及队列的修改,是一个线程,因此要加锁
if(queue.isEmpty()) {
//如果任务队列为空,先不执行任务
continue;
}
MyTimerTask current = queue.peek();
if (System.currentTimeMillis() >= current.getTime()) {
//执行过的任务要删除
current.run();
queue.poll();
} else {
//未到指定时间,先不执行任务
continue;
}
}
}
});
t.start();
}
//这里涉及队列的修改,是一个线程,因此要加锁
public void schedule(Runnable runnable,long delay){
synchronized (this){
MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
}
}
}
public class Demo18 {
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(()->{
System.out.println("hello 3000");
},3000);
myTimer.schedule(()->{
System.out.println("hello 2000");
},2000);
myTimer.schedule(()->{
System.out.println("hello 1000");
},1000);
}
}
这种情况下:
如果初始队列为空,程序先给这个线程1加锁,然后发现队列为空,继续进入循环,继续给线程1加锁,导致线程2无法进行加入任务操作,那么这是无意义的。因此要使用wait……notify……
2、代码改进
class MyTimerTask implements Comparable<MyTimerTask>{//实现比较接口
private Runnable runnable;
private long time;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<MyTimerTask>();
public MyTimer(){
Thread t=new Thread(()->{
while(true) {
synchronized (this){
//这里涉及队列的修改,是一个线程,因此要加锁
while(queue.isEmpty()) {
//如果任务队列为空,先不执行任务
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
MyTimerTask current = queue.peek();
if (System.currentTimeMillis() >= current.getTime()) {
//执行过的任务要删除
current.run();
queue.poll();
} else {
//未到指定时间,先不执行任务
try {
//线程阻塞会释放cpu等资源
this.wait(current.getTime()-System.currentTimeMillis());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//不能使用sleep,原因是,如果此时插入一个更早时间的任务,我们会错过这个任务
//而使用wait,来了一个更早的任务,会唤醒该线程,重新计算wait时间
//sleep不会释放cpu
}
}
}
});
t.start();
}
//这里涉及队列的修改,是一个线程,因此要加锁
public void schedule(Runnable runnable,long delay){
synchronized (this){
MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
this.notify();
}
}
}
二、常见的锁策略
1、乐观锁and悲观锁
1)乐观锁:加锁的时候,假设出现锁冲突的概率不大,因此接下来围绕加锁的工作就会做的很少
2)悲观锁:加锁的时候,假设出现锁冲突的概率很大,因此接下来围绕加锁的工作就会做的很多
2、重量级锁and轻量级锁
1)重量级锁:加锁的开销比较大,要做更多的工作
2)轻量级锁:加锁的开销比较小,要做的工作少
JavaEE13
3、挂起等待锁and自旋锁
1)挂机等待锁:就是 悲观锁 / 重量级锁 的一种典型实现
2)自旋锁:就是 乐观锁 / 轻量级锁 的一种典型实现
举个例子来说明这两种锁的区别:
设想一个场景:你去追你的女神。你向女神表白(尝试对女神加锁),女神表示,自己已经有对象了(女神表示别人已经对她这个线程加锁了),此时你还是放不下自己对她的喜欢,因此你选择等待,但是等待分为两种:
1)比如,你选择每天还特别关注她的生活,对她嘘寒问暖,这里的情况类称为“自旋锁”
(特点:可以及时了解女神的感情状态,但是会比较消耗自己,在计算机中表现为不释放cpu资源,适用于锁冲突小的情况)
2)比如,你可以选择不关注她的生活,回归自己的生活,做自己的事情,若干年从别人那里听说她分手了,你再去联系她,这种情况称为“挂起等待锁”
(特点:无法及时了解女神的感情状态,但是不会很消耗自己,在计算机中表现为主动释放cpu资源,让cpu去做别的任务,适用于锁冲突大的情况)
4、公平锁and非公平锁
在多个线程竞争锁的时候,究竟该哪个线程获取锁,这就涉及锁的类型是公平锁还是非公平锁。
1)计算机对公平锁的定义是:会按照先来后到的顺序决定哪个线程可以优先获得锁,多个线程不能同时竞争锁
2)计算机对非公平锁的定义是:不会按照先来后到的顺序决定哪个线程可以优先获得锁,而是,多个线程可以同时竞争锁
举个例子:一个女生有多个追求对象A(追了一年)、B(追了一个月)、C(追了三天),但是,此时女生已经有对象了,这三个追求者不想放弃,都在等待。有一天,女生分手了,想要人安慰:
公平情况:A由于追的时间最长,因此他来安慰女生
非公平情况:三个追求者都可以同时去安慰女生,但是最后是谁去安慰取决于他们的努力
而我们常用的synchronized锁就是典型的非公平锁
5、可重入锁and不可重入锁
如果一个线程针对一把锁,连续加两次,就可能出现死锁情况,如果把锁设定成“可重入”就可以避免死锁了
6、读加锁
所谓的读加锁,就是把“加锁操作“分为两种情况:
1)读加锁
2)写加锁
这个锁将读和写分别进行加锁操作,依赖ReentrantReadWriteLock类中的内部类:
1)ReentrantReadWrite.ReadLock
2)ReentrantReadWrite.WriteLock
这两个类中有各自的lock、unlock方法
一、深入理解synchronized锁
1、锁升级
使用synchronized加锁的时候,会经历锁升的过程!
刚开始使用synchronized加锁,首先锁会处于”偏向锁“的状态,遇到线程之间的锁竞争,升级到”轻量级锁“,进一步统计竞争出现的频次,达到一定的程度之后,升级到”重量级锁“
synchronized加锁的时候,会经历 无锁-->偏向锁-->轻量级锁-->重量级锁的过程
偏向锁:偏向锁只是做个标记,不是真的加锁,当它发现出现锁竞争,才会真的加锁,此时偏向锁就升级为轻量级锁(加锁会产生开销,偏向锁减少了不必要的开销)
锁升级是不可逆的,只能升级,不能降级!
2、锁消除
锁消除是编译器的一种优化策略,编译器会根据synchronize代码做出判定,判定这个地方是否真的需要加锁,如果没有必要,它就会把锁消除掉!
3、锁粗化
锁粗化也是编译器的一种优化策略,所谓粗不粗,和锁代码的”粒度“有关!代码越多,粒度越粗,代码越少,粒度越细。
当一次加锁、解锁的代码粒度过细,且类似的细粒度加锁、解锁操作过多,编译器就会粗化其代码,使其合并为一次加锁,解锁操作!
比如,领导安排你做三个工作,你已经完成并且准备打电话汇报领导!此时,你可以选择两种汇报方式:
1)分三次汇报,每次汇报一个工作已经完成
2)一次汇报三个工作已经完成
很明显,第一种方式效率很低,而第二种方式效率很高!
二、CAS操作
1、什么是CAS
CAS全称Compare and swap,字面意思是“交换和比较”。一个CAS操作包括以下操作:
1)比较A和V是否相同(比较)
2)如果比较相同,将B写入V(交换)此处虽然叫做“交换”,实际达到的效果是“赋值”
3)返回操作是否成功
特点:虽然一个CAS操作包含3个具体操作,但真实的CAS是一个原子的cpu指令完成的。这样的指令是线程安全的!
2、CAS的具体应用场景1:基于CAS实现“原子类”
实际上,int / long 类型的数据在进行++ 或 -- 的时候,都不是原子的……基于CAS实现的原子类,对int / long 等这些类型进行了封装,从而可以原子的完成 ++ --等操作。
该原子类,在标准库中也有实现。
由于int 类型的++操作不是原子的,因此两个线程对count同时进行读写操作,会出现线程安全问题!那么可以使用原子类!
如何通过CAS实现原子类?
class AtomicInerger{
private int value;//相当于内存数据
public int getAndIncrement(){
int oldValue=value;
while(CAS(value,oldValue,oldValue+1)!=true){
oldValue=value;
//如果while循环条件判定为true,说明在CAS的过程中,没有其他线程穿插修改value
//此时线程是安全的
}
return oldValue;
}
}
JavaEE14
一、JUC(java.util.concurrent)的常见类
1、Callable类:
Callable和Runable相对,都是描述一个任务,Runnable描述的是一个不带返回值的任务,Callable描述的是一个带有返回值(Integer)的任务。
Callable通常需要搭配FutureTask来使用,FutureTask用来保存Callable的返回值。如果不理解二者的关系,可以想象你去吃麻辣烫,当你点好菜之后,工作人员会递给你一张小牌子,Callable相当于你的”麻辣烫“,FutureTask相当于”号码牌“。你可以凭借号码牌去取你的麻辣烫。
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int a=0;
for(int i=0;i<5000;i++){
a++;
}
return a;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
//getf方法在call方法还没执行结束前发生阻塞
System.out.println(futureTask.get());
}
}
2、ReentrantLock:可重入锁
ReentantLock与synchronized的区别:
1)synchronized在申请锁失败的时候会死等,ReentrantLock可以通过trylock等待一段时间后放弃
2)synchronized是非公平锁,ReentrantLock默认是非公平锁,但也可以在其构造方法中传入true开启公平锁模式
3)synchronized的wait……notify……只能随机唤醒多个线程中的某一个线程,ReentrantLock可以具体唤醒某一个线程
3、Semaphore:信号量
Semaphone相当于一个资源计数器。就好像记录产品个数的变量count,当产品被售出时,count--,当产品被产出时,count++。
案例一:
案例二:
案例三:当成锁使用(获取不到资源会阻塞)
public class Demo1 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(1);
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
4、CountDownLatch
CountDownLatch一般是搭配线程池使用,主要应用场景是将一个大任务拆分为多个子任务,可以使用CountDownLatch衡量出当前任务是否整体执行结束!
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(20);//拆分为20个子任务
ExecutorService executorService= Executors.newFixedThreadPool(4);
for(int i=0;i<20;i++) {
int id = i;
executorService.submit(() -> {
System.out.println("下载任务 " + id + " 开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("下载任务 " + id + " 结束执行");
countDownLatch.countDown();//完毕、over
});
}
//当countDownLatch收到20个“完成”,所有任务就完成了
countDownLatch.await();
System.out.println("任务执行结束");
}
}
JavaEE15
一、集合类的线程安全问题
1、list类
关于ArrayList类和LinkedList类,可以使用“读写拷贝”,意思就是在写的时候,将链表拷贝一份,写操作在新链表中进行!如果在写的同时也有其他线程正在读取链表,那么让它们读取旧链表的数据,等到写操作结束,将旧链表的引用指向新链表即可。
不过,这种情况只能适用于多个线程读,一个线程写的情况,无法应对多个线程写的情况!
2、哈希表
Hashtable的加锁,就是直接给put 、 get等方法加上synchronized,任何一个针对哈希表的操作,都会触发锁竞争,因此锁竞争冲突大。为了优化这种情况,推出了ConcurrentHashMap,它是给每个hash表中的“链表”进行加锁。
1)当两个线程修改哈希表上不同的元素,相当于修改不同的变量,这样是不会发生线程堵塞的,只有出现极端情况,两个线程同时修改同一个元素,才会引发锁冲突!
2)ConcurrentHashMap引入了CAS原子操作,针对修改size的操作,因此也不需要加锁
3)ConcurrentHashMap针对读操作,通过volatile以及一些精巧的设计,确保读操作不会读到“修改一半的数据”
4) 针对哈希表的扩容,进行了特殊处理,普通哈希表的扩容,需要创建新的哈希表,将全部的元素搬运过去,这样效率是非常慢的。ConcurrentHashMap进行了“化整为零”,不会在一次操作中,进行所有的数据搬运,而是一次只搬运一部分
比如,在还不需要扩容的时候,ConcurrentHashMap的哈希表就已经创建好了一个比旧哈希表大的新哈希表,每次put元素的时候,都会将旧表中一部分数据拷贝到新表中,当新旧表内容相同的时候,再次插入元素,就只会插入到新表中
另外,对于查询、修改、删除,新旧表都要进行同步的操作!