并发编程及JUC
本文是对Java基础的一些个人总结,适合小白在进行基本的学习后所复习的总结文档,授人以渔不如授之以渔,在Java的学习中需要多敲多理解,所以本文中代码数量占少,所需要理解的地方居多。本文为个人理解,如有需要订正之处,敬请指出,本人邮箱1209110434@qq.com,在之后也会更新Spring系列等个人总结。最后,希望各位在Java之路越走越远。Java之路道阻且长,最后一句话:Java之路为者长存,行者长至–送给你也送给我
1.多线程的创建
继承Thread类
缺点:不利于扩展
class myThread extends Thread{
public static void main(String[] args) {
Thread mythread = new Thread();
mythread.start();
System.out.println("主线程");
}
@Override
public void run() {
System.out.println("线程启动成功");
}
}
实现runnable接口
- 定义Runnable接口实现类MyRunnable,并重写run()方法
- 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
- 调用线程对象的start()方法
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
Thread thread = new Thread(() ->System.out.println("ss"));
thread.start();
}
缺点:重写的run方法没有返回值
实现 Callable 接口
-
创建实现Callable接口的类myCallable
-
以myCallable为参数创建FutureTask对象
-
将FutureTask作为参数创建Thread对象
-
调用线程对象的start()方法
-
public class CallableTest {
public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回结果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); }
}
使用 Executors 工具类创建线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续详细介绍这四种线程池
实现线程同步
方法一:同步代码块
锁对象只要对于同时操作的锁唯一(静态方法用类目.class,实例方法用this)
ctrl alt t
方法二:在方法上加上synchronized+ 操作共享资源的代码
方法三:lock锁
线程池:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}
//线程中的常用方法
/*
1.start --启动一个新线程,让线程进入就绪状态等待jvm的调用
2.启动后调用run方法(不start 直接调用会在主线程中跑,不会发挥多线程的作用)
3.join --等待线程结束 (可设置等待时间)
4.set,getname
5.get set priority (1-10)设置优先级
6 getstate--获取线程运行的状态
7.isinterrupted ---布尔值判断是否被打断
interrupt 打断线程(如果线程正在执行其他操作如sleep wait join 则会抛出异常,正常打断会设置打断标记)
8.isalive --判断线程是否还存活
9.currentthread --获取当前正在运行的线程(静态)
10.sleep 使线程休眠让出cpu 的使用权(静态)(睡眠被打断会抛出异常,睡眠结束后未必会立刻执行)
11。 yield --提示让出cpu主要是为了测试和调试
@Test
void contextLoads() throws Exception {
//线程创建方式一
Thread thread1 = new Thread("t1") {
@Override
public void run() {
System.out.println("线程一被启动");
}
};
thread1.start();
System.out.println(thread1.getState());
//线程创建方式二
Thread thread2 = new Thread(() -> System.out.println("线程二被执行"));
thread2.start();
//1.第一种睡眠
thread2.sleep(20);
//第二种
SECONDS.sleep(1);
//线程创建方式三
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("线程三被执行");
return 11;
}
});
Thread thread3 = new Thread(integerFutureTask);
thread3.start();
System.out.println(integerFutureTask.get()); //线程创建方式四:利用线程池创建线程
/*
1.所有线程类都直接或者间接继承ExecutorServise但是顶级接口都继承于Executor
2.线程池有两种创建方式1.通过工具类Executors创建
2.1newFixedthreadpool(int nThread )线程池中含有固定数目的线程,空线程会一直保留,等待队列是无限的linkedMQ
2.2newSingleThreadExecutor--线程池中只有一个线程,它依次执行某些任务,任务排队是无限的
2.3Executors.newCachedThreadPool();,核心线程是0有任务时才创建线程,且全部是救急线程,并可以无限创建空闲线程被保留60秒
2.4Executors.newScheduledThreadPool();线程池能够按照计划执行任务 corepoolsize是线程池中的最小数目,任务较多时会创建更多的工作线程来执行任务
2.5newSingleThreadScheduledExecutor--线程池中只有一个任务,而且会按照时间来执行任务
3.手动创建线程池对象(推荐)方便自己定义参数
int corePoolSize,--核心线程数
int maximumPoolSize,--最大线程数
long keepAliveTime,--救急线程存活时间
TimeUnit unit,--救急线程时间单位
BlockingQueue<Runnable> workQueue,--任务队列
1.ArrayBlockingQueue基于数组结构的有界阻塞队列(先进先出)
2.LinkedArrayBlockingQueue--基于链表的有界阻塞队列吞吐量高于第一个,先进先出---上fixedThreadpool运用了此策略
3.priorityArrayBlockingQueue---具有优先级别的队列
4.synchronousQueue--一个不存储元素的阻塞队列,每次插入操作必须等待另一个线程操作,可以视为只有一个元素的队
ThreadFactory threadFactory,--用于创建线程的工厂
RejectedExecutionHandler handler--饱和策略
Abortpolicy:默认策略,线程池满了抛出异常
DiscardOldestpolicy:丢弃队列里执行最久的
CallerRunsPolicy:线程池满了会丢弃新加入的任务并且不会报异常
*/
ExecutorService executorService1 = Executors.newFixedThreadPool(5);
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
ExecutorService executorService3 = Executors.newCachedThreadPool();
ExecutorService executorService4 = Executors.newScheduledThreadPool(5);
ExecutorService executorService5 = Executors.newSingleThreadScheduledExecutor();
//ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 10, 10, SECONDS, 10);/*
线程运行原理:
线程在启动后栈区会分配内存给线程
线程的上下切换,1.使用cpu的时间片用完了2.垃圾回收暂停所有线程3.有更高级的线程需要运行4.线程自己调用了sleep, yield,join,park ,synchronize,lock 等方法
利用程序计数器记录下一条JVM指令的执行地址,并且是线程私有的
线程的生命周期
1.new 初始状态--值、指线程的创建,创建线程后jvm 会为其分配空间
2.runnable可运行状态--对象调用strat方法后进入就绪状态,虚拟机会为其创建程序计数器和本地方法栈
3.running 运行状态--处于执行状态指该线程获得了cpu开始执行run方法,该线程则处于运行状态
4.block:阻塞状态 指因为某种原因放弃了cpu使用,让出cpu 暂停运行
阻塞分为三种
4.1.等待阻塞--运行执行线程中的o.wait方法 ,jvm会把该线程放在等待队列
4.2.同步阻塞
5.deda 死亡状态 线程已经执行完毕,不会再转为其他状态
*/
多线程进阶:
1.多个线程去读取共享资源,一个代码块内对多个共享资源多线程进行读写操作叫做临界区
2.由于代码的执行序列不同导致结果无法预测,叫做境界条件
解决多线程临界区的竞态条件产生的方案
阻塞式方案 1 .synchronize保护临界区
1.1 即使锁内的第一个线程时间片用完了被踢出去但是们还是锁住的2线程同样进不来,等到了1线程的时间片才继续运行执行完后锁才会让出来
1.2关键字加在成员方法上 锁住的是this 加在静态方法上锁住的是类对象
2.lock
线程安全分析:
成员变量和静态变量安全性:
- 没有被共享的变量是线程安全的线程安全
- 基本类型的访问是线程安全的,对象在创建时会创造栈帧,
- 引用类型的是不安全的,因为运行中访问的是同一个引用对象
被共享了变量只有读操作线程安全,如果有读写操作则需要考虑线程安全性问题
局部变量: 局部变量是线程安全的,引用类型的对象局部变量是安全的(对象没有逃离方法范围,Return 后线程不安全)
继承后线程共享资源局部变量引用会造成多线程问题(可以用final ,priviate 增强线程的安全性)
常见的线程安全类
线程安全多个线程调用他们同一个实例的某个方法时是线程安全的
String ,包装类,Stringbuffer,random,vector,hashtable ,java concurrent 下的包,他们的每个方法都是原子的,但是组合在一起 不一定是线程安全的(只是指方法内的执行是线程安全的)
String 底层是对原有的对象进行一层复制再进行其他的操作,只是创建了新的字符串对象,再赋值给原字符串对象
实现线程安全成员变量条件:成员变量不可变则是线程安全的
synchronize底层:
Java对象底层结构:synchronize关键字绑定锁的对象结构
synchronize(lock)//lock结构包括klassword(指类对象)和markword
关键字使用
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
Monitor
Moniter又名监视器和管程 ,加锁时该对象会关联一个moniter对象通过markword创建指针指向monitor
markword格式
状态
01 默认值,没有关联Monitor对象
00 轻量级锁被启用
10 重量级锁 对象markword通过指针关联moniter后 存储锁住对象的地址
Moniter结构
获得锁进入ownner ,没有获得到锁是进入entrylist进入阻塞状态后等获得锁线程释放锁时进行竞争,对象总是与锁关联
注意:synchronize 必须是进入同一个对象才有以上效果,加synchronize才会关联监视器
弊端:这种锁耗费资源过大
synchronize优化:
轻量级锁:适用访问时间错开没有竞争,在加入synchronize修饰时初始锁位轻量级锁
轻量级锁原理:
线程在运行栈帧时会加载锁记录,锁记录里lockrecord会与锁对象mark word进行一次(CAS操作)
缺点:每次重入都需要进行一次CAS 操作)
- CAS交换成功则设置对象头中记录为00
- CAS交换失败则判断是否有锁竞争,有竞争则膨胀为重量级锁
- CAS交换失败如果是锁重入则增加一条lock record
解锁时利用CAS 将mark word依次值恢复给对象头
锁膨胀:
CAS操作无法成功会进行锁膨胀,将轻量级变为重量级1.申请moniter 锁后两位变成10后同上
自旋优化:
重量级锁让线程2多进行几次自循循环避免线程进行阻塞(适合多核CPU)
自旋失败则会进入阻塞
偏向锁:
只有第一次将线程ID设置到mark word头,同意一个线程多次获取相同的锁,且访问时不用重新CAS该对象归该线程所有即为偏向锁
偏向锁:如果开启了偏向锁,对象创建后三位为101(biased_lock为1),它的thread,epoch,age都是0,加锁时会加入进去偏向锁默认时是延迟的(可以对vm配置进行修改)
撤销可偏向:对象竞争或有第二个线程尝试获取锁
禁用偏向级锁,在测试代码机上VM参数 –usebiaselocking
可偏向状态调用hashcode后会撤撤销偏向状态
批量重偏向:偏向锁的偏向可以改变,当撤销偏向锁阀超过20次后,会重新进行偏向
批量撤销:当撤销偏向锁阀值超过40次时,会撤销偏向状态变成不可偏向
设计模式:保护性暂停:
public class guardobject {
private Object response;
public Object getResponse(long timeout) {
long begin= System.currentTimeMillis();
long passtime = 0;
synchronized (this){
while (response==null){
if (passtime >= timeout){
break;}
try {
this.wait(timeout - passtime);//避免虚假唤醒时等待的时长
} catch (InterruptedException e) {
e.printStackTrace();} }
passtime= System.currentTimeMillis()-begin;
return response;} }
public void compeletget(Object response){
synchronized (this){
this.response = response;
this.notify();}}}
wait -notify原理:
wait¬ify是Object类的方法且调用wait 方法时会释放锁
wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用,必须先获得锁对象,再使用锁对象调用
- 获得owner(锁)对象的线程如果发现条件不满足则调用Wait方法则会进入Monitor中的waitset变为waiting状态,不占用CPU时间片
- Monitor中的entrylist为阻塞队列为BLOCK状态且不占用CPU时间片
- BOLCK会在OWNER线程释放锁后惊醒竞争唤醒
- waitting状态的线程需要等待notify唤醒后不会立即获得锁会重新进入entrylist等待并重新竞争
synchronize(lock){
wait()
wait(long time)
lock.notify//(随机唤醒一个正在等待的线程)
lock.notifyAll//(唤醒所有在waitset的线程重新进入entrylist)
}
注意:使用wait方法需要避免错误警报和虚假唤醒,可利用死循环判断条件是否成立
synchronized (lock) {
// 判断条件谓词是否得到满足
while(!false) {
// 等待唤醒
monitor.wait();
}
// 处理其他的业务逻辑
}
sleep和wait区别
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
join 原理:
指一个线程等待一个线程的结束 ,应用保护性暂停设计模式。(等待谁就调用谁)
生产者消费者模式(异步(会有一定的延时))
class mesageque {
private LinkedList<message> list = new LinkedList();
private int capacity;
public message take (){
synchronized (list) {
while (list.isEmpty()) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message message = list.removeFirst();
list.notifyAll();
return message;
}
}
//存入消息
public void put(message msg){
synchronized (list){
while (list.size() == capacity){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.addLast(msg);
list.notify();
}
}
}
final class message{
private int id;
private Object value ;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public message(int id, Object value) {
this.id = id;
this.value = value;
}
@Override
public String toString() {
return "message{" +
"id=" + id +
", value=" + value +
'}';
}
}
park&unpark
是使用locksupport类中的方法locksupport.park暂停线程,unpark恢复暂停线程对象
特点:
- park和unpark不需要获得锁
- park和unpark是以线程为单位阻塞和唤醒线程
- unpark可以再park前用,也可以在park 后用
底层原理:每个线程都有自己的一个parker对象由_counter,_cond,_mute组成
counter状态有:0和1,0代表线程不可park,1代表线程可park
调用park对象:
- 如果counter为0则线程进入cond
- 如果counter为1则线程不会被阻断
调用unpark
- 如果线程正在cond中则被唤醒
- 如果线程正在运行则会补充counter为1,且多次调用只会补充一次
线程状态以及状态切换:
线程状态:
-
新建(new):新创建了一个线程对象。
-
可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
-
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
- 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
- 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
-
死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程状态切换
- new==>runnable
线程调用start方法时
- Runnable<===>waiting
线程用synchronized(obj) 获取了对象锁后
- 调用obj.wait()方法时,t线程从RUINABLE ===> WAITING
- 调用obj.notify(), obj.notifyA11(), t.interrupt()时会WAITING ==>RUINABLE
- 竞争锁成功,t线程从WAITING ==> RUINABLE
- 竞争锁失败,t线程从WAITING ===> BLOCKED
- RUNNABLE <====> WAITING当
- 当前前线程调用t.join()方法时,当前线程从RUNNABLE ===> WAITING注意是当前线程在线程对象的监视器上等待
- t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING ==> RUINNABLE
- 线程调用LockSuport.park会让当前线程从RUNNABLE ===> WAITING
- 线程调用LockSuport.unpark,或调用了当前线程的interrupt()时,当前线程从WAITING ==> RUINNABLE
- RUNNABLE <===> TIMED_ WAITING
t线程用synchronized(obj) 获取了对象锁后
-
调用obj.wait(1ong n)方法时,t 线程从RUNNABLE ==> TIMED_ WAITING
-
t线程等待时间超过了n毫秒,或调用obj .notify(), obj. notifyA1l(), t. interrupt()时
- 竞争锁成功,t线程从TIMED_ WAITING==>RUNNABLE
- 竞争锁失败,t 线程从TIMED_ WAITING --> BLOCKED
-
当前线程调用t.join(1ong n)方法时,当前线程从RUNNABLE ==> TIMED_ WAITING注意是当前线程在线程对象的监视器上等待
-
当前线程等待时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_ WAITING ==> RUNNABLE
-
当前线程调用Thread.sleep(1ong n),当前线程从RUNNABLE => TIMED_ WAITING
-
当前线程等待时间超过了n毫秒,当前线程从TIMED_ WAITING --> RUNNABLE
-
线程调用了LockSupport.parknaos(long naos)或LockSupport.parkuntil(long millis)t 线程从RUNNABLE ==> TIMED_ WAITING
-
调用unpark或者线程的interrupt方法或者超时会使线程变成TIMED_ WAITING --> RUNNABLE
线程死锁
在并发中可以设置多把锁来提高并发量,但是容易发生死锁
**死锁概念:**当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的条件:
-
互斥条件:所谓互斥就是进程在某一时间内独占资源。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
防止死锁:
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
补充:
定位死锁–利用jconsole ,利用jps, jstack进程ID
活锁:两个线程互相改变对方的结束条件,导致循环得不到结束,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
饥饿:线程优先级太低,始终得不到CPU调度执行
交替输出实验:
public class park {
static Thread t1;static Thread t2;static Thread t3;
public static void main(String[] args) {
parkunpark p = new parkunpark(5);
t1 = new Thread(()->{
p.print("a",t2); });
t2 = new Thread(()->{
p.print("b",t3); });
t3 = new Thread(()->{
p.print("c",t1); });
t1.start();t2.start();t3.start();
LockSupport.unpark(t1); }}
class parkunpark{
private int loopnumber;
public parkunpark(int loopnumber){
this.loopnumber = loopnumber; }
public void print(String str, Thread next){
for (int i = 0; i < loopnumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next); }}}
运用reentred
public class test12 {
public static void main(String[] args) throws InterruptedException {
reentred re = new reentred(5);
Condition condition1 = re.newCondition();
Condition condition2 = re.newCondition();
Condition condition3 = re.newCondition();
new Thread(() -> {
re.print("a", condition1, condition2);
}).start();
new Thread(() -> {
re.print("b", condition2, condition3);
}).start();
new Thread(() -> {
re.print("c", condition3, condition3);
}).start();
Thread.sleep(1000);
re.lock();
try {
condition1.signal();
}finally {
re.unlock();
}
}
}
class reentred extends ReentrantLock {
private int numbers;
public void print( String str,Condition conn ,Condition nextcondition){
for (int i = 0; i < numbers; i++) {
try {
conn.await();
System.out.print(str);
nextcondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
unlock();
}
}
lock();
}
public reentred(int numbers) {
this.numbers = numbers;
}
}
volatile
-
volatile可以修饰成员变量和静态成员变量,他可以避免线程从自己工作的缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile 都是直接操作主存。synchronize 也可以保证此效果。
-
volatile不能保证原子性,适合一个或多个线程是读取变量,另一个线程是修改变量,不适合于指令交错的场景。
-
jvm会在不影响正确性的前提下,可以调整语句的执行顺序会自动进行指令重排序,加volatile关键字会避免在多线程下的指令重排序,可以保证在之前的变量不会重排序
底层原理:volatile 底层原理是内存屏障
对volatile变量的写指令会加入写屏障–保证在该屏障之前,对共享变量的改动,都同步到主内存中–保证可见性,保证之间的代码不会进行指令重排–保证有序性
对volatile 变量的读指令会加入读屏障,在读取操作之后的代码,都会同步更新到主内存–保证可见性,在读屏障之后的情况都不会读到屏障之前。
dcl(double-check-locking)问题分析:
最初始:两次加锁 解决:–在变量上加volatile
happens- before规则
synchronize–在区域范围内保证原子性,可见性,有序性
volatile–被修饰的变量写会同步到主存中,读操作才可见
线程start之前操作的写,对该线程操作之后的读可见
线程结束前对变量的写入,对其他线程得知它结束后的读可见
线程一打断线程二前对变量的写,对于其他线程得知线程二被打断后的变量的的读可见。
对变量默认值的写,对其他线程对该变量的读可见
共享模型之无锁—乐观锁
-
乐观指不怕别人修改,改了再重试
-
CAS(compare and set//compare and swap)–保证原子性
-
每次操作线程时都会进行比较,比较成功后才会进行设置
-
底层为了保证原子性和可见性底层运用了volatile
性能分析:synchronize于无锁性能分析:synchronize容易进行上下文切换,无锁会减少上下文切换,锁效率会高(线程数小于cpu时)
CAS 工具包
原子整数:AutomaticInteger(加减法)–getAndadd,inctrmentandget
updateandget(x->x*10)–更新同时设置
原子引用:
AtomicReference
AtomicMarkbleReference(判断是否被更改过)
AtomicStampeReference(判断版本号可以解决ABA问题)
解决ABA问题
(线程修改没有被线程察觉到)–AtomicStampeReference(根据版本号判断是否被修改过)AtomicMarkbleReference–只关注有没有被更改过
原子数组:
atomicIntegerarray
atomiclongarray
atomicreferencearray;(保护数组的元素)
字段更新器:
是基于反射的工具类,用来将指定类型的指定的volatile引用字段进行原子更新,对应的原子引用字段不能是private的。通常一个类volatile成员属性获取值、设定为某个值两个操作时非原子的,若想将其变为原子的,则可通过AtomicReferenceFieldUpdater来实现
atomicreferencefieldupdate,
atomicintegerfieldupdate,
atomiclongfieldupdate保护对象里的属性,保护成员变量的安全性(属性必须是volatile)
public class AtomicReferTest {
public static void main(String[] args) throws Exception
{
AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dry.class,String.class,"name");
Dry dry=new Dry();
System.out.println(updater.compareAndSet(dry,"dry","compareAndSet"));
System.out.println(dry.name);
System.out.println(updater.getAndSet(dry, "getAndSet"));
System.out.println(dry1.name);
}
}
class Dog
{
volatile String name="dry1";
}
原子累加器:
jdk1.8 longadder(notremmber)–重要
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。–获取最终结果通过 sum 方法,将各个累加单元的值加起来就得到了总的结果
不可变类设计
不可变类设计:
成员变量都是不可变的,类对象也需要加上final,保证不可以被继承。String等不可变类都是保护性拷贝(底层赋值)。
tips:单个方法是不可变的
享元设计模式(适用重用数量有限的同一类对象)包装类
final变量原理:final在设置之后会加入写屏障
tomcat线程池:
Fork/Join线程池
ForkJoin是JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进-步提升了运算效率
Fork/Join默认会创建与cpu核心数大小相同的线程池
使用:需要继承Recursivetask(有结果),RecursiveAction
1.创建线程池对象(forkjoin线程池)
2.执行任务
AQS原理
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
特点:
用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何
获取锁和释放锁 getState -获取state状态 setState .设置state状态
compareAndSetState - cas机制设置state状态
独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源提供了基于FIFO的等待队列,类似于Monitor的EntyList条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
- 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何
- 获取锁和释放锁
- getState -获取state状态
- etState .设置state状态
- ”compareAndSetState - cas机制设置state状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于FIFO的等待队列,类似于Monitor的EntyList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
AQS 对资源的共享方式
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
Reentrentlock
跟synchronize相比:
- 可重入
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronize一样,都支持可重入(可重入是指同一个线程可对同一个对象反复加锁)
用法: 创建对象
可打断:其他线程可以用interrupt打断该线程(指打断无止境的等待锁)
锁超时:获取锁的线程等待不到超过了时间则放弃锁等待trylock
公平性:reentrantlock默认时不公平锁(可设置为公平锁)设置fair性 先入先得
条件变量:支持多个条件变量,有多个休息室
Condition condition1 = reentrantLock.newCondition();
Condition condition2 = reentrantLock.newCondition();
reentrantLock.lockInterruptibly();
try{
condition1.await();//进入休息室
condition1.signal();//唤醒
condition1.signalAll();//唤醒所有
}
finally {
reentrantLock.unlock();
}
/*await前需要获得锁
await执行后, 会释放锁,进入conditionObject等待
await的线程被唤醒(或打断、或超时)取重新竞争lock锁
竞争lock锁成功后,从await后继续执行*/
原理:
加锁
通过CAS操作将state从0改到1并获得线程对象(exclusiveOwnerThread)
竞争出现时
- 先通过CAS获取资源失败后尝试
- tryAcquire获取连接,如果state已经是1,则会失败
- 进入add waiter逻辑,构造node(在header后)队列
- 图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态
Node的创建是懒惰的 - 其中第一个Node称为Dummy (哑元)或哨兵,用来占位,并不关联线程
- 请求失败后进入acquireQueued逻辑
- acquireQueued会在一个死循环中不断尝试获得锁, 失败后进入park阻塞
- 如果自己是紧邻着head (排第二位),那么再次tryAcquire尝试获取锁,当然这时state仍为1,失败
- 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为1,这次返回false
- shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire
尝试获取锁,当然这时state仍为1,失败 - 当再次进入shouldParkAferFailedAcquire 时,这时因为其前驱node 的
waitStatus已经是-1,这次返回true - 进入parkAndCheckInterupt,Thread-park (灰色表示)
释放锁:
释放锁进入进入TryReleaser如果成功
- 设置exclusiveOwnerThread为null,state=0
- 找到离队列中head最近的一个node(没被取消的),unpark恢复运行
- 唤醒node线程的acquirequeque流程,如果加锁成功则会设置,如果有其他锁竞争则重新进入阻塞
可重入原理:同步器会判断该线程是否是已获得锁的线程
可重入后释放:多次释放
可打断原理(默认不可被打断)
- 打断是指会用interrupt标记自己被打断过,但是不会立即响应,等获得到锁时再进行打断
非公平锁:不会去检查AQS队列,直接抢夺线程
条件变量原理:
await原理
持有锁线程调用await,进入ConditionObject的addConditionwait的双向链表
创建新的Node并设置为-2
进入fullyrelease释放同步器上的锁
unpark AQS队列中的下一个节点,竞争锁
singal
- 判断该线程是否是锁的持有者
- 进入ConditionObject的dosingnal
- 执行transferforsignal流程,将该Node加入AQS队列尾部,并将原线程的waitstatus设置为-2
ReentrantReadandWritelock
读读不互斥可并发,读写,写写互斥
注意事项:
- 读写不支持条件变量
- 重入时不支持升级,及有读锁的情况下去获取写锁,会导致写锁永久等待
- 重入时降级支持
原理(后看)
stampedlock
进一步优化性能特点是在使用读写锁是都必须配合戳使用
乐观读(tryOptimistcRead)在读取完毕之后进行一次戳校验(校验是否有写操作,失败后锁会升级)
semaphore
信号量,用来限制能够同时访问共享资源的线程数
原理:(tryRelease)
- 将参数设置给AQS的state
- 获得Acquire后利用CAS更行state
- state为0时执行CAS流程创建Node
释放:(tryrelease)
- 利用CAS将state由0改到1
- 后用AQS方法进行唤醒
原理:
Countdownlatch:
用来进行线程同步协作,等待其他 线程完成倒计时(等待线程运行结束)
await用来等待计数归零,countDown()用来让计数减一
cyclicbarrier
循环栅栏,用完后会立即恢复。线程池线程数要跟cyclicbarrier
JUC
Blocking类:大部分实现基于锁,并提供用来阻塞的方法
Copyonwrite:采用拷贝方式(底层拷贝)适用读多写少的形式
concurrent:内部应用CAS优化,提高吞吐量
遍历时弱一致性
ConCurrentHashMap
为什么使用CurrentHashMap。
-
在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMap形成环形链表,即链表的一个节点的next节点永不为null,就会产生死循环。这时,CPU的利用率接近100%,所以并发情况下不能使用HashMap。
-
HashTable通过使用synchronized保证线程安全,但在线程竞争激烈的情况下效率低下。因为当一个线程访问HashTable的同步方法时,其他线程只能阻塞等待占用线程操作完毕。
-
ConcurrentHashMap使用分段锁的思想,对于不同的数据段使用不同的锁,可以支持多个线程同时访问不同的数据段,这样线程之间就不存在锁竞争,从而提高了并发效率。
原理:
sizeCtl:默认值为0,初始化时值为-1,扩容时为-(1+扩容数)
Node:本质是一个map,是哈希链表结构的最小单元,包括键值,哈希值,next
ForwardingNode:表示在扩容中表示该链表被处理过即处理完毕的节点
TreeBIn:作为红黑树的头节点储存root和first(如果哈希表长度大于64时)
TreeNode:作为红黑树的节点(储存parent,left,right)
构造器:
jdk8时实现懒惰初始化,等到真正用时才去创建
get:
- spread方法保证哈希值为正整数
- TabAt方法哈希值与数组长度减一进行按位与运算确定下标值
- 判断哈希值与equals方法
- 头节点hash值为负数表示正在扩容
- 负数表示为红黑树结构,调用find方法寻找
put:
- 调用put方法
- 判断键和值是否为空 ConcurrentHashMap不能存储空的键值
- 求哈希码进入死循环判断是否有Nodetable(懒惰利用CAS操作创造避免线程安全问题)
- 判断是否有头节点
- 利用CAS创建头节点
- 判断头节点是否是负数
- 对链表加锁(synchronize)
- 找到相同的Key覆盖旧值
- 没有相同Key创建新节点
- 若为红黑树则调用红黑树Putval方法
- 判断是否有头节点
- 判断bincount是否需要转换为红黑树
扩容 计数addcount(利用Long adder)
jdk7:
- 它维护了一个segment数组,每个segment对应一把锁
- 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与jdlk8中是类似的
- 缺点: Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
移位计算segement(继承rentrantlock)下表
put:
- 计算hash码和segement数组(懒惰初始化)
- segment继承reentrantlock,尝试加锁(trylock)
- 计算索引后同上(头插法)
**get:**用unsafe方法保证可见性
- 先定位segment 方法
CopyOnWriteArrayList
CopyOnWriteArrayList 是一个并发容器,非复合场景下操作它是线程安全的。
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。
在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。合适读多写少的场景。
缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
CopyOnWriteArrayList 的设计思想
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突
结语
感谢在学习途中乐于分享的每一位码员,有了你们的分享才让学习者变得更轻松
断是否有头节点
- 利用CAS创建头节点
- 判断头节点是否是负数
- 对链表加锁(synchronize)
- 找到相同的Key覆盖旧值
- 没有相同Key创建新节点
- 若为红黑树则调用红黑树Putval方法
- 判断bincount是否需要转换为红黑树
扩容 计数addcount(利用Long adder)
jdk7:
- 它维护了一个segment数组,每个segment对应一把锁
- 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与jdlk8中是类似的
- 缺点: Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
移位计算segement(继承rentrantlock)下表
[外链图片转存中…(img-eZlodthb-1647538724745)]
put:
- 计算hash码和segement数组(懒惰初始化)
- segment继承reentrantlock,尝试加锁(trylock)
- 计算索引后同上(头插法)
**get:**用unsafe方法保证可见性
- 先定位segment 方法
CopyOnWriteArrayList
CopyOnWriteArrayList 是一个并发容器,非复合场景下操作它是线程安全的。
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。
在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。合适读多写少的场景。
缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
CopyOnWriteArrayList 的设计思想
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突
结语
感谢在学习途中乐于分享的每一位码员,有了你们的分享才让学习者变得更轻松
作者:Yang11😊