JUC笔记 比较杂乱

JUC并发编程

线程,继承Thread类,实现Runnable、Callable接口
Runnable无返回值,效率比Callable较低
进程,如QQ.exe,一个进程包含多个线程,线程是CPU调度的基本单位

Java默认两个线程,main线程和GC线程
Java真的能开启线程吗?不能 start方法源码里面调用start0方法是个native本地方法 C++ 无法直接操作硬件

并发VS并行
并发:多线程同时操作一个资源
并行:多线程执行 多核CPU 并发编程本质充分利用CPU资源

线程的生命周期 状态 源码6个 新生、运行、堵塞、等待(死等wait)、超时等待(sleep(时间))、终止 大致分为5个 枚举类 Thread.State
public enum State {
NEW,新生
RUNNABLE,运行
BLOCKED, 堵塞
WAITING,等待
TIMED_WAITING,超时等待
TERMINATED;终止
}
在这里插入图片描述

Sleep和wait的区别?
实际使用TimeUnit.SECONDS.sleep(1); 使用juc的工具类进行线程休眠
1、来自不同的类 sleep->Object wait->Thread
2、锁的释放 sleep抱着锁睡着了不释放 wait等待中会释放锁
3、使用的范围不同 sleep可以在任何地方 wait必须在同步代码块中 和notify唤醒配合使用
4、捕获异常 都需要捕获异常 InterruptedException 中断异常

Synchronized 关键字 队列 +锁 同步方法和同步代码

Lock锁
三个实现类
Interface Lock
所有已知实现类:
ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
可重入锁、读锁、写锁
ReentrantLock 默认非公平锁 可以插队

Synchronized关键字和lock锁的区别
1、synchronized是java内置的关键字,lock是一个接口
2、synchronized无法获取到锁状态 lock可以判断是否获取到了锁 boolean f = ock.isLocked();
3、synchronized会自动释放锁 抛了异常也会释放锁 lock必须要手动解锁,如不释放可能会死锁 自动挡和手动挡
4、synchronized线程1获得锁、堵塞;线程2等待,傻傻的等 ;
lock锁不会一直等下去 lock.tryLock(); 尝试释放锁
5、synchronized 可重入锁,不可以中断,非公平。Lock可重入锁,可以判断,可以是公平锁,构造参数设为true,公平锁效率低
6、Synchronized 适合少量的代码同步问题 lock适合大量的同步代码 官方建议防止try finally里面

生产者和消费者问题
线程之间的通信问题 等待唤醒 通知唤醒
虚假唤醒:线程可以唤醒,而不会被通知,中断或超时。
防止虚假唤醒 wait()等待在循环中 while(){ obj.wait()}
Juc中生产者和消费者问题
synchroized同步代码块中使用wait、notify
而lock中使用await、singal;通过Lock 找到 Condition
Condition 可以精确的通知和唤醒线程 如三个线程精确的执行A执行完了调用B,B执行完了调用C,C执行完调用A … …
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); // 等待
//condition.signalAll(); // 唤醒全部

Synchronized 锁的是什么
修饰普通方法 锁的是方法的调用者 即对象
修饰静态方法 锁得的是类对象 即类.class
修饰代码块 锁的是传入的对象

集合不安全类
1.List
并发环境下,ArrayList并不安全,解决?
使用Vector 底层方法被synchronized修饰
List list = Colleations.synchronizedList(list1);
使用CopyOnWriterArrayList 写入时复制
多线程调用,读的时候不加锁,写入时覆盖数据。Volatile修饰的数组
在写入的时候避免覆盖, 读写分离

什么是CopyOnWrite容器?
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
 重点容器维护的数组使用volatile修饰的,保证及时可见性!

读取的源码:
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的

 public E get(int index) {
    return get(getArray(), index);
  }

CopyOnWrite的应用场景
读多写少的情况
CopyOnWrite的缺点
1::内存占用变多,因为维护了2份相同的数组。
2:数据一致性问题,在读取的时候,可能是从旧的数组读取到的。

2.Set
Set set =Collections.synchronizedSet(set1);
CopyOnWriterArraySet

HashSet底层是hashMap的key

3.Map
Hashtable是线程安全的,但效率低
HashMap是线程不安全的,但效率高
ConcurrentHashMap 线程安全 兼顾效率
Collections.synchronizedMap(),工具类提供了同步包装器的方法,来返回具有线程安全的集合对象,性能依然有问题,只是普通的加锁进行同步包装

new Hashtable() 等价于 new HashMap<>(16, 0.75);

ConcurrentHashMap
jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响 ,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment
get 方法无需加锁,volatile保证。

jdk8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS CAS效率最高
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写
操作、并发扩容
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组被volatile修饰,保证扩容时被读线程感知

Callable接口
可以有返回值 可以抛异常 方法不同 run()/call()
效率比Runnable效率跟高
Callable接口与Runnable接口相比,还有一个很大的不同:
Callable接口的实例不能作为Thread线程实例的target来使用;
而Runnable接口实例可以作为Thread线程实例的target构造参数,开启一个Thread线程。

借助适配类 FutureTask 执行线程 调用futureTask的get方法 获取返回值

MyThread thread = new MyThread();
FutureTask futureTask = new FutureTask(thread); // 适配类
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); // 结果会被缓存,效率高
Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到最后 或者使用异步通信来处理!
System.out.println(o);

细节:
1、有缓存
2、结果可能需要等待,会阻塞!

常用的辅助类
CountDownLatch

· countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。
· 存在于java.util.cucurrent包下。

CountDownLatch 这个类使一个线程直到等其他线程执行完毕之后再继续执行。
是基于计数器实现的,计数器的初始值是线程的数量。每当一个线程执行完后,线程的数量-1,当计数器为0,表示执行所有的线程执行完,然后在闭锁上等待执行的线程就恢复工作了。

源码 countDownLatch类中只提供了一个构造器:
//参数count为计数值
public CountDownLatch(int count) { };

三个方法
await() await(x,y) countDown()

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };

详细描述

CyclicBarrier 可重复使用的栅栏!

在这里插入图片描述

现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。

在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。

CyclicBarrier 和 CountDownLatch 很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

首先,CyclicBarrier 的源码实现和 CountDownLatch 大同小异,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。

在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理,下面我们先看看CyclicBarrier有哪些成员变量。
//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//换代前执行的任务
private final Runnable barrierCommand;
//表示栅栏的当前代
private Generation generation = new Generation();
//计数器
private int count;
//静态内部类Generation
private static class Generation {
boolean broken = false;
}

*CountDownLatch和CyclicBarrier区别:
1.countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
2.CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

详细描述

Semaphore 信号量

一个计数信号量,信号量维持一组许可证,如有必要,每个acquire()都会堵塞,知道许可证可用,才能使用。
Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。
去餐厅打过饭,假如有3个窗口可以打饭,同一时刻也只能有3名同学打饭。第四个人来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了。

Semaphore好像和synchronized关键字没什么区别,都可以实现同步,semaphone只是限制了访问某些资源的线程数,其实并没有实现同步!

对于Semaphore来说,我们需要记住的其实是资源的互斥而不是资源的同步,在同一时刻是无法保证同步的,但是却可以保证资源的互斥。
semaphore.acquire(); // 得到许可证 获取锁
semaphore.release(); // release() 释放 放finally中

详细描述

读写锁 ReadWriteLock
读的时候可以被多线程同时访问 共享锁
写的时候只能有一个线程去写 独占锁
读-读 可以共存!
读-写 不能共存!
写-写 不能共存!

堵塞队列 BlockingQueue
写入:如果队列满了,就必须堵塞等待
取:如果队列是空的,必须堵塞等待生产
BlockingQueue继承自Queue,Queue继承自Collection接口
实现类有AbstractQueue、ArrayBlockingQueue、LinkedBlockingQueue、SynchonousQueue同步队列
AbstractQueue 非堵塞队列、BlockingQueue堵塞队列、Deque双端队列
在这里插入图片描述

堵塞队列使用:多线程并发处理,线程池
四组API
在这里插入图片描述

Add remove 抛出异常
Offer poll 有返回值 没异常
Put take 等待 堵塞
Offer poll加时间参数
// blockingQueue.offer(“d”,2,TimeUnit.SECONDS); // 等待超过2秒就退出
// blockingQueue.poll(2,TimeUnit.SECONDS); // 等待超过2秒就退出

SynchronousQueue 同步队列
没有容量 进去一个元素 必须等待取出来之后,才能往里放一个元素。

线程池
在这里插入图片描述

池化思想:线程池、字符串常量池、数据库连接池
提高资源的利用率
1、手动创建线程对象,2、执行任务,3、执行完成,释放线程对象
预先创建好线程对象放到线程池里面,从线程池中拿线程执行,执行完成后放回去。
优点:提供线程的利用率,提高程序的响应速度,便于统一管理线程对象,可以控制最大的并发数

Java线程池的四种创建方式
Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool 创建一个可缓存线程池,可伸缩的,遇强则强,遇弱则弱 线程可以缓存和重复利用 回收默认时间 1分钟
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newSingleThreadScheduledExecutor 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

线程池的好处:
降低资源的消耗,提高响应速度,方便管理。
线程复用,控制最大并发数,管理线程。
结合异步调用complateableFuture来使用

一般不使用Executors创建,使用ThreadPoolExcutors的方式,避免资源消耗,本质上也是调用的ThreadPoolExcutors。
FixedThreadPool和SingleThreadPool 运行请求的队列长度是Integer.MAX_VAlue 约21
亿 容易造成OOM

CachedThreadPool和ScheduledThreadPool 运行创建的线程数量最大值是Integer.MAX_VAlue 约21
亿 容易造成OOM

拒绝策略:

  • new ThreadPoolExecutor.AbortPolicy() // 抛出异常
  • new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!线程池拒绝,并由调用线程池的线程执行任务
  • new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常! *
  • newThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会 抛出异常!
    抛弃队列最前面的任务,然后尝试重新执行

队列
一般使用堵塞队列的ArrayBlockingQueue

队列从有界无界上分 常见的有界队列为
ArrayBlockingQueue 基于数组实现的阻塞队列
LinkedBlockingQueue 其实也是有界队列,但是不设置大小时就是无界的。ArrayBlockingQueue 与 LinkedBlockingQueue 对比一哈ArrayBlockingQueue 实现简单,表现稳定,添加和删除使用同一个锁,通常性能不如后者LinkedBlockingQueue 添加和删除两把锁是分开的,所以竞争会小一些常见的无界队列
ConcurrentLinkedQueue 无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常PriorityBlockingQueue 具有优先级的阻塞队列
DelayedQueue 延时队列,使用场景缓存:清掉缓存中超时的缓存数据

池的最大的大小如何去设置!
了解:IO密集型,cpu核数 CPU密集型:(调优) CPU数量*2
01:一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
02:如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。
https://www.cnblogs.com/liangbaolong/p/13201403.html

4大函数式接口

Lambda表达式
链式编程
函数式接口
Stream流式计算

函数式接口 只有一个方法的接口
在这里插入图片描述

@FunctionInterface
Public INTERFACE Runnable{
Public abstract void run();
}
foreach(消费者类的函数式接口)

4大函数式接口

  • Function 函数是接口 有一个输入参数,有一个输出
  • Predicate 断定型接口:有一个输入参数,返回值只能是 布尔值!
  • Consumer 消费型接口 只有输入,无返回值
  • Supplier 供给型接口 没有参数 只有返回值

只要是 函数型接口 可以 用 lambda表达式简化

Stream流式计算
Stream(流)是一个来自数据源的元素队列并支持聚合操作
元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
生成流
在 Java 8 中, 集合接口有两个方法来生成流:
stream() − 为集合创建串行流。
parallelStream() − 为集合创建并行流。

串行流的执行机制是基于pipeline(管道)
在这里插入图片描述

流的特性
stream不存储数据
stream不改变数据源
stream不可重复使用
stream串行执行
上个节点会影响下个节点
流的节点
中间节点(懒加载): filter、distinct
结束节点:toArray、foreach

并行流
API:parallelStream()
利用多线程去处理一批数据,各个线程处理完的结果最后

Stream三个步骤

  1. 创建Stream
    一个数据源(如:集合、数组),获取一个流。
  2. 中间操作
    一个中间操作链,对数据源的数据进行处理。
  3. 终止操作
    一个终止操作,执行中间操作链,并产生结果。

ForkJoin
ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量!
多线并发处理框架,ForkJoin的框架的基本思想是分而治之。使用ForkJoin将相同的计算任务通过多线程的进行执行。从而能提高数据的计算速度。
大数据:Map Reduce (把大任务拆分为小任务
分而治之就是将一个复杂的计算,按照设定的阈值进行分解成多个计算,然后将各个计算结果进行汇总。相应的ForkJoin将复杂的计算当做一个任务。而分解的多个计算则是当做一个子任务。

使用ForkJoin框架,需要创建一个ForkJoin的任务。因为ForkJoin框架为我们提供了RecursiveAction和RecursiveTask。
我们只需要继承ForkJoin为我们提供的抽象类的其中一个并且实现compute方法。
RecursiveTask在进行exec之后会使用一个result的变量进行接受返回的结果。而RecursiveAction在exec后是不会保存返回结果。
在这里插入图片描述

ForkJoin 特点:工作窃取 基于双端队列

task要通过ForkJoinPool来执行,分割的子任务也会添加到当前工作线程的双端队列中,
进入队列的头部。当一个工作线程中没有任务时,会从其他工作线程的队列尾部获取一个任务(工作窃取)。
工作窃取(work-stealing)
任务进行分解成多个子任务的时候,每个子任务的处理时间都不一样。
例如分别有子任务A和B。如果子任务A的1ms的时候已经执行,子任务B还在执行。那么如果子任务A的线程等待子任务B完毕后在进行汇总,那么子任务A线程就会在浪费执行时间,最终的执行时间就以最耗时的子任务为准。
而如果子任务A执行完毕后,处理子任务B的任务,并且执行完毕后将任务归还给子任务B。这样就可以提高执行效率,这就是工作窃取。
https://zhuanlan.zhihu.com/p/190170283

// 使用ForkJoin 
public static void test2() throws ExecutionException, InterruptedException { 
	long start = System.currentTimeMillis(); 
	ForkJoinPool forkJoinPool = new ForkJoinPool(); 
	ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L); 
	ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务 
	Long sum = submit.get(); 
	long end = System.currentTimeMillis(); 
	System.out.println("sum="+sum+" 时间:"+(end-start)); 
}
public static void test3(){ 
	long start = System.currentTimeMillis(); 
	// Stream并行流
	long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0,Long::sum); 
	long end = System.currentTimeMillis();
	System.out.println("sum="+"时间:"+(end-start)); 
} 

普通线程池+countDownLatch的缺点在于,每个线程执行完成之后调用countDownLatch.await()方法会被阻塞。而forkAndJoin就不同了,该线程池使用的是工作窃取机制,每个线程执行完自己的任务之后会从队列的末尾(该线程池将任务放在队列中)获取任务来执行。比阻塞线程效率理论上来的高(注意:仅仅是理论上)。

异步回用 Future
Future设计目的: 对将来的某个事件的结果进行建模
ComplateableFuture 实现 Future接口

没有返回值的 runAsync 异步回调
有返回值的 supplyAsync 异步回调
在这里插入图片描述

Complateable.get();可以获取堵塞执行结果
结合线程池使用

在这里插入图片描述

如有异常,可以通过whenComplate(t,u)
t是正确结构 u是异常
exceptionlly来捕获异常
在这里插入图片描述

JMM
Java内存模型,不存在的,概念 约定
分为工作内存和主内存

voaltile是JVM提供的轻量级的同步机制
保证可见性 不保证原子性 防止指令重排

同步的约定
线程释放锁之前,必须把共享变量刷新回主内存
线程加锁之前,必须读取主内存的数据到工作内存
加锁和解锁是同一把锁

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
l

lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中

JMM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须
write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量
实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解

如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,
必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存

Volatile
1、保证可见性

public class JMMDemo { 
// 不加 volatile 程序就会死循环! 
// 加 volatile 可以保证可见性 
private volatile static int num = 0;public static void main(String[] args) { 
// main 
	new Thread(()->{ // 线程 1 对主内存的变化不知道的 
		while (num==0){ 
	  } 
	}).start(); 
	try {
	TimeUnit.SECONDS.sleep(1); 
	} catch (InterruptedException e) { 
	e.printStackTrace(); 
	}
	num = 1; 
	System.out.println(num); 
	} 
}

2、不保证原子性
原子性 : 不可分割
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。

如果不加 lock 和 synchronized ,怎么样保证原子性
使用原子类,解决 原子性问题

这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类

3、防止指令重排
什么是 指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
volatile可以避免指令重排:
内存屏障。CPU指令。作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)

Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

DCL懒汉式

public class SingleLazyMan {

    private static boolean flag = false;
    public SingleLazyMan(){
        if (flag == false){
            flag =true;
        }else {
            throw new RuntimeException("不要试图用反射破坏单例");
        }
    }

    private volatile static SingleLazyMan lazyMan;

    public SingleLazyMan getLazyMan() {
        if (lazyMan == null) {
            synchronized (SingleLazyMan.class) {
                if (lazyMan == null) {
                    return new SingleLazyMan();
                }
            }
        }
        return lazyMan;
    }
}

1、分配内存空间
2、执行构造方法,初始化对象
3、把这个对象指向这个空间
123–》132
在这里插入图片描述

CAS
compareAndSet 比较并替换

AtomicInteger atomicInteger = new AtomicInteger(2020); 
// 期望、更新 
// public final boolean compareAndSet(int expect, int update) 
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语! 
System.out.println(atomicInteger.compareAndSet(2020, 2021)); 
System.out.println(atomicInteger.get()); 
atomicInteger.getAndIncrement() 
System.out.println(atomicInteger.compareAndSet(2020, 2021)); 
System.out.println(atomicInteger.get()); 

Unsafe 类 通过这个类操作内存 Java无法直接操作内存 通过调用C++ native

在这里插入图片描述
自旋锁

CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就
一直循环!
缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
CAS : ABA 问题

模拟ABA问题:

AtomicInteger atomicInteger = new AtomicInteger(2020);
System.out.println(atomicInteger.get());
// 期望、更新 // public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!

// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());

// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());

**解决ABA问题
引用原子引用 乐观锁 版本号 **

/AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题

// 正常在业务操作,这里面比较的都是一个个对象 
static AtomicStampedReference<Integer> atomicStampedReference = new 
AtomicStampedReference<>(1,1); 
// CAS compareAndSet : 比较并交换! 
public static void main(String[] args) { 
new Thread(()->{ 
int stamp = atomicStampedReference.getStamp(); // 获得版本号 
System.out.println("a1=>"+stamp); 

各种锁

  1. 公平锁、非公平锁
    公平锁: 非常公平, 不能够插队,必须先来后到!
    非公平锁:非常不公平,可以插队 (默认都是非公平)
  2. 可重入锁
    可重入锁(递归锁)
    public ReentrantLock() {
    sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    }

3、自旋锁
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值