一、什么是JUC
是java.util.concurrent包下的。我们原本多线程普通的是用Thread类,Runable类和Callable类。而JUC是并发包,用来提高效率。
二、线程和进程
进程:运行起来的程序,一个进程往往可以包含多个线程至少包含一个。Java至少包含2个线程,main线程和gc线程(资源调度的最小单位)。
线程:CPU调度和执行的最小单位。
Java并不能真正的开启一个线程。因为底层是通过本地方法来调用底层的C++来开启线程。
进程与线程的区别:
1、进程是资源分配的最小单位,线程是资源调度的最小单位
2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
4、多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
此上摘自https://www.jianshu.com/p/2dc01727be45
并发和并行
并发:多线程操作一个资源。单核CPU模拟出多条线程。想充分利用cpu的资源。
并行:CPU多核,多个线程同时执行。线程池。
获取当前系统的cpu数:
public class Test1 {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
守护线程:守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。(GC就是守护线程,我们自己的线程结束了GC也自动结束,如果不算守护线程,那么就将一直进行)
将线程设置为守护线程:setDaemon(true)
线程有多少个状态:实际上是6个–新生、运行、阻塞、等待( 死死的等)、超时等待(有超时时间现在)、终止。
wait()和sleep()区别:1. 来自不同的类:wait来自Object类 ,而sleep 是Thread类。 2.wait会释放锁,sleep不会释放锁。 3. 使用的范围是不同的,wait只能在同步代码块中使用,而sleep可以在任何地方使用。
三、Lock锁
传统的锁:synchronized
JUC中用Lock接口:主要方法:lock()加锁,unlock()解锁。
实现类:ReentrantLock 可重入锁,ReentrantReadWriteLock.ReadLock 读锁,ReentrantReadWriteLock.WriteLock写锁
ReentrantLock的构造器有两种,默认为非公平锁,也可以传入布尔值指定公平锁和非公平锁。
公平锁:先来后到。 非公平锁:可以插队。
一般使用:先new一个锁,之后加锁,之后再trycatch中写业务代码,finally中解锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test1 {
Lock lock=new ReentrantLock();
private int number=30;
public void test(){
lock.lock();
try {
//业务代码
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
lock锁和synchronized的区别:
- synchronized是内置的Java关键字,而Lock是一个Java类
- synchronized无法判断获取锁的状态,Lock可以去判断是否获取到了锁
- synchronized会自动释放锁,而Lock锁必须释放锁。如果不释放则会死锁。
- synchronized 当线程1阻塞时候,线程2会一直等下去。而Lock锁由于存在tryLock方法,所以不一定会等待下去。
- synchronized 是可重入锁,不可中断,是非公平锁。而Lock锁 可重入锁,可以判断锁,可以自己设置是公平锁还是非公平锁。
- synchronized时候锁少量的代码同步问题,而Lock锁适合锁大量的同步代码。
四、生产者和消费之问题
原来解决:用synchronized wait和notify
public class Test1 {
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.del();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
class Data{
private int num=0;
public synchronized void add() throws InterruptedException {
while(num!=0){
this.wait();
}
num++;
this.notifyAll();
}
public synchronized void del() throws InterruptedException {
while (num==0){
this.wait();
}
num--;
this.notifyAll();
}
}
注意:while不能换成if,否则会造成虚假唤醒(当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功)。因为if只会判断一次,执行完之后会继续往下进行,而while会直到条件满足才会执行下面的代码。
用JUC来实现生产者消费者
Condition类中的方法可以来替代Object类中的wait和notify方法。此外 condition还可以指定唤醒那个线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test1 {
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printA();
}
}).start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printB();
}
}).start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printC();
}
}).start();
}
}
class Data{
private Lock lock=new ReentrantLock();
private int num=1;
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
public void printA(){
lock.lock();
try {
while (num!=1){
condition1.await();
}
num=2;
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num!=2){
condition2.await();
}
num=3;
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num!=3){
condition3.await();
}
num=1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
五、锁的深度理解
1.synchronized锁的是方法的调用者,当多个方法用的是一个锁,谁先拿到锁谁就先执行。一定要保证锁的唯一性!
2. 如果是静态方法,默认锁的是class对象,与调用锁的对象无关。
六、安全的集合
1.ArrayList线程不安全,如果想安全,方法1:用Vector,但是不建议。方法2:Collecitons.synchronizedList(new ArrayList()); 方法3:JUC中的CopyOnWriteArrayList();
CopyOnWriteArrayList()读操作不加锁,所有线程都不会阻塞。写操作加锁,线程会阻塞。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。底层是用lock锁实现的。
CopyOnWriteArrayList比Vector好在哪里
- Set线程不安全解决方法1:Collections.synchronizedSet(new HashSet<>()); 方法2:Set set=new CopyOnWriteArraySet<>();
- HashMap不安全解决方法1:Collections.synchronizedMap(new HashMap());
2:Map map=new ConcurrentHashMap();
ConcurrentHashMap()底层JDK1.7采用分段锁,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
坏处是这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长。
好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。数据结构采用:数组+链表+红黑树。JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并且其中用了volatile 修饰,保证了可见性。
其put方式是对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述:
1.如果没有初始化就先调用initTable()方法来进行初始化过程
2.如果没有hash冲突就直接CAS插入
3.如果还在进行扩容操作就先进行扩容
4.如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
5.最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
6.如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
七、Callable
Callable和Runnable一样都是为了另一个线程而设计的接口。与Runnable区别:Callable由返回值,需要抛出异常,调用时调用call方法。
Callable使用步骤:1. 类实现Callable接口并重写call方法 2. 实例化该类 3. 将实现的Callable接口丢入到FutureTask的构造器中,实例化FutureTask 4. FutureTask实例化对象丢入Thread构造器中 5. 调用start方法
如果想获得返回结果使用 FutureTask实例化对象.get() 方法。 但是这个方法可能会造成阻塞,因为需要等待最后返回,一般把这个放到最后,或者使用异步ajax。此外,结果存在缓存来增加效率。
八、常用辅助类
8.1 CountDownLatch
这是一个减法计数器。有两个方法countDownLatch.countDown();//计数器-1 countDownLatch.await();//等待计数器归零后继续往下执行
import java.util.concurrent.CountDownLatch;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName());
countDownLatch.countDown();//计数器-1
}).start();
}
countDownLatch.await();//等待计数器归零后继续往下执行
System.out.println("finish");
}
}
8.2 CyclicBarrier
可以理解为加法计数器。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("集齐龙珠");
});//计数器达到后调用另一线程
for (int i = 0; i < 7; i++) {
final int tem=i;//JDK1.8的lambda表达式要使用final来传参,final可省由默认加
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了"+tem+"个龙珠");
try {
cyclicBarrier.await();//本线程执行完等待计数器唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3 Semaphore
信号量,可以理解为多把锁或者限制线程数量。
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
semaphore.acquire();//获取
System.out.println(Thread.currentThread().getName()+"获得");
TimeUnit.SECONDS.sleep(2);//休眠2S
System.out.println(Thread.currentThread().getName()+"释放");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
九、读写锁(共享锁与独占锁)
读锁可以让多个线程同时读但不让写,写锁只能让一个线程写。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final int tem=i;
new Thread(()->{
myCache.put(tem+"",tem);
}).start();
}
for (int i = 0; i < 5; i++) {
final int tem=i;
new Thread(()->{
myCache.get(tem+"");
}).start();
}
}
}
class MyCache{
private volatile Map<String,Object> map=new HashMap<>();
ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try{
map.put(key,value);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try{
Object o=map.get(key);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
十、阻塞队列BlockingQueue
BlockingQueue是一个接口,实现类–>ArrayBlockQueue,DelayQueue,LinkedBlockingQueue,LinkedTransferQueue,PriorityBlockingQueue,SynchronousQueue.
FIFO,先进先出队列。写入时,如果队列已满则必须阻塞等待。读取时如果队列是空的,必须阻塞等待生产
ArrayBlockingQueue的构造方法必须指定队列大小和LinkedBlockingQueue构造方法可以不指定队列大小默认为Integer.MAX也可指定队列大小。
他们通用的方法:
| 方法 | 抛出异常 | 有返回值 | 阻塞等待(会造成一直阻塞) | 超时等待(超过指定时间就不等了) |
|---|---|---|---|---|
| 添加 | add | offer | put | offer(element,2,TimeUnit.SECONDS) |
| 删除 | remove | poll | take | poll(element,2,TimeUnit.SECONDS) |
| 检测队首元素 | element | peek | - | - |
SynchronousQueue同步队列没有容量,进去一个元素必须等待出来了才能往里面放一个元素。放入方法:put,移出方法:take
十一、线程池
好处:1.降低资源消耗 2.提高相应速度 3.方便管理(线程复用、可以控制最大并发数、管理线程)
线程池创建的三大方法
import java.util.concurrent.*;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadpool = Executors.newSingleThreadExecutor();//单个线程
//ExecutorService service =Executors.newFixedThreadPool(5);//固定线程池大小
//ExecutorService service =Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
try{
for (int i = 0; i < 10; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();//最后一定要关闭线程池
}
}
}
三者底层都是new ThreadPoolExecutor(), 这个构造器可以有七大参数
int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime,//存活时间
TimeUnit unit,//存活时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略
四种拒绝策略:线程池和阻塞队列都满了,还有来调用线程的
ThreadPoolExecutor.AbortPolicy()不处理这个任务并抛出异常
ThreadPoolExecutor.CallerRunsPolicy() 拒绝后哪个线程调用的哪个线程处理
ThreadPoolExecutor.DiscardOldestPolicy()放弃阻塞队列中最早的任务,将当前任务添加到队列
ThreadPoolExecutor.DiscardPolicy()丢掉任务但不会抛出异常
根据阿里手册,不建议使用这三种方法来创建而是直接用ThreadPoolExecutor()来创建
import java.util.concurrent.*;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadpool=new ThreadPoolExecutor(
2,5,3,TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()
);
try{
for (int i = 0; i < 10; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();//最后一定要关闭线程池
}
}
}
最大线程数改如何定义:
1.CPU密集型 CPU支持的最多线程数(虚拟处理器) 在构造ThreadPoolExecutor时,最大线程数写为
Runtime.getRuntime().availableProcessors //获取系统的最大线程数
2.IO密集型 大于任务中十分耗IO的线程数量。
线程池有5种状态:
Running
线程池可以接收新任务并对任务进行处理。线程池一旦创建就进入该状态
Shutdown
当线程池调用shutdown方法时进入该状态,此时不接受新任务,但仍可处理已添加任务
Stop:
当调用shutdownNow方法时进入该状态,该状态不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
Tidying
在Shutdown状态下,阻塞队列为空且线程池中任务也为空时,就会由 Shutdown -> Tidying。
当线程池在Stop状态下,线程池中执行的任务为空时,就会由Stop -> Tidying。当线程池变为Tidying状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为Tidying时,进行相应的处理;可以通过重载terminated()函数来实现。
Terminated
线程池彻底终止,就变成TERMINATED状态。线程池处在Tidying状态执行完terminated()之后进入Terminated。
线程池执行线程:pool.excute(线程)(excute没有得到的参数)无返回值
或者
pool.submit(线程)(submit可以得到Future类)有返回值可以用得到的Future类.get() 由于submit可以得到Future类,所以可以去捕获异常,将Future类.get() 放到try中。
线程池的submit和excutor区别:
1、接收的参数不一样
都可以是Runnable,submit 也可以是Callable
2、submit有返回值,而execute没有
返回值是Future
3、submit方便Exception处理
十二、四大函数式接口
函数型接口:只有一个方法的接口。可以用lambda表达式简化。
Function函数型接口:有一个输入参数一个输出。(有两个泛型,第一个指输入的类型,第二个指输出的类型)
Predicate断定型接口:一个输入参数,返回值只能说布尔值。(只有一个泛型为输入类型)
Consumer消费型接口:只有输入没有返回值(一个泛型指输入的类型)
Supplier供给型接口:没有输入只有返回值(一个泛型指输出的类型)
十三、Stream流式计算
得到流,用流来进行计算。
import java.util.Arrays;
import java.util.List;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
User u1=new User(1,"a",21);
User u2=new User(2,"b",22);
User u3=new User(3,"c",23);
User u4=new User(4,"d",24);
User u5=new User(5,"e",25);
//存储
List<User> list=Arrays.asList(u1,u2,u3,u4,u5);
//交给流计算 链式编程
list.stream().filter(u->{return u.getId()%2==0;})//获取id为偶数
.filter(u->{return u.getAge()>23;})//获取年龄大于23
.map(u->{return u.getName().toUpperCase();})//用户名转为大写
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//逆序排序 传入的是comparator接口实现
.limit(1);//分页,只选一个
}
}
十四、ForkJoin
ForkJoin是JDK1.7后出来的,用来并行执行任务,提高效率。 说白了就是分治的思想,大项目分成各个小任务,之后每个小任务得到的结果组合成最终的结果。
ForkJoin特点:工作窃取–两个线程分别处理两个双端队列,B线程先完成自己的所有任务,之后B线程从另一方向去解决A线程未完成的任务。
使用方法:1. 继承RecursiveAction(无返回值)或RecursiveTask(有返回值)类 2.重写computer方法 3. 其他类新建ForkJoinPool,之后新建ForkJoinTask任务(即刚刚写的继承子类),丢入到ForkJoinPool中,之后ForkJoinPool调用submit方法。
更快的做法是用并行流。
十五、异步回调
一个线程一直往下执行,任务1,任务2…当任务1会阻塞时候(需要一些实验才能拿到结果),这时候可以异步调用(可以理解为线程间的ajax)。
简单的讲就是另启一个线程来完成调用中的部分计算,使调用继续运行或返回,而不需要等待计算结果。但调用者仍需要取线程的计算结果。
用Future的实现类CompletableFuture
无返回值用法:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Void> completableFuture=CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("222");
});
System.out.println("111");
completableFuture.get();
}
}
有返回值做法:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class Test1 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
return 1024;
});
completableFuture.whenComplete((t,u)->{})//完成时做法,t为正常时返回的参数,u为错误信息
.exceptionally((e)->{return 233;})//可以获取到错误的参数
.get();
}
}
十六、JMM
JMM:Java内存模型
关于JMM的一些约定:
1.线程加锁前,必须读取主存中的最新值到线程的工作内存中。
2. 线程解锁前,必须将共享变量立刻刷回主存。
3. 加锁和解锁必须的同一把锁。
JMM有8种操作:先从主存中read变量,之后load到线程的工作内存中,之后该线程执行引擎use这个变量后assign回工作内存种,之后store传入主存中,最后writer放入到主存的变量中。以上六种和lock ,unlock。
JMM对这八种指令做了如下规则:
1.不允许read和load、store和write之一单独存在。
2.不允许线程丢弃最近assign操作,即工作变量数据改变后必须告知主存。
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须从主存中得到,不允许工作内存直接使用一个未被初始化的变量。
5.一个变量同一时间只能有一个线程对其进行lock。多次lock后必须同步次数unlock才能解锁。
6.如果对一个变量进行lock操作,会清光所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个其他线程的锁住的变量。
8.对一个变量进行unlock操作之前,必须把此变量同步回主存。
锁是加在store和write处
这里存在一个问题:线程不知道主内存中的值已经被修改过了。
import java.util.concurrent.TimeUnit;
public class Test1 {
private static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(1);
}
}
十七、Volatile
Volatile是Java虚拟机提供的轻量级的同步机制。 主要为了解决指令重排带来的线程安全问题。
1.保证可见性 (在主存修改了之后会线程会知道) 加上volatile可以解决这个问题。
2.不保证原子性 (原子性:不可分割 线程A在执行任务的时候不能被打扰也不能被分割。要么同时成功要么同时失败)
public class Test1 {
private static int num=0;
private static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(num);
}
}
不用synchronized和lock如何解决原子性呢?
i++不是原子性,因为字节码文件中是3步。先获得这个值,之后+1,之后写回这个值。将int改为AtomicInteger ,并将++改为getAndIncrement方法。底层用的是CAS,和操作系统挂钩。
- 禁止指令重排
我们写的代码要经过编译器的优化重排,指令并行也可能会重排,内存系统也会造成重排,最后才是执行。处理器在进行指令重排的时候会考虑数据之间的依赖性。
比如当我们执行a,b,x,y都为0时 A线程:x=a, b=1; B线程 y=b, a=2 我们得到的结果x=0,y=0。 而指令重排后会使得a和b的值先改变了得到x=2,y=1。
而volatile可以避免指令重排。在内存中在voliate上下层加内存屏障。防止指令重排。
十八、CAS
什么是CAS。如果期望的值达到了就更新并返回true,没达到就不更新返回false。这是上层的CAS
CAS是CPU的并发原因。
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.compareAndSet(2020,2021);
AtomicInteger的getAndIncrement方法底层调用的Unsage类。Java不能直接操作内存而是需要调用底层的C++来操作内存。而Unsafe类可以理解为是Java留的后门即native。在底层用的CAS就是操作内存。
比如说getAndInt的底层使用的是自旋锁+CAS。
底层的CAS就是比较当前内存中的值和主存中的值,如果这个值是相等的那么就执行操作。如果不是就阻塞。因为底层用的是dowhile自旋锁。
缺点:循环会耗时。一次性只能保证一个共享变量的原子性。会存在ABA问题。
ABA问题(狸猫换太子):A线程cas(1,2),B线程cas(1,3)后又cas(3,1) 由于B线程速度快所以先执行了这两步,而对于A来说一直都是1. 这就是ABA问题。
解决ABA问题一般用版本号(或时间戳)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test1 {
static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference(2020,1);
public static void main(String[] args) {
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1,2,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(2,1,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(atomicStampedReference.getStamp());
}).start();
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1,6,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(atomicStampedReference.getStamp());
}).start();
}
}
要小心包装类一般要equals。
十九、乐观锁和悲观锁
悲观锁:悲观锁具有强烈的独占和排他特性。就是在修改数据之前先锁定,再修改的方式。悲观锁主要分为共享锁或排他锁。就是我们MySQL的读锁和写锁。
乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
如update items set quantity=quantity-1 where id =1 andquantity-1>0; 但这种方法存在ABA问题最好加个版本号解决ABA问题。
二十、各种锁
1.公平锁和非公平锁:
公平锁不能插队。非公平锁能插队。synchronized和lock锁都默认是非公平锁。
2.可重入锁(递归锁)
拿到外面的锁就自动获得里面的锁。比如synchronized 方法A调用 synchronized 方法B,自动获得B的锁。
3.自旋锁
AtomicInteger下的getAndAdd下调用的Unsafe类的方法用的就是自旋锁。说白了就是不算的循环直到得到锁。(底层用的就是while+cas)。
比如A线程进来了将用CAS将值1换为了值2,B也来了一直等待值1但是此时为值2,只有当A解锁将值2重新CAS为值1之后,B线程才能往下走。
4.死锁
A线程持有A锁,B线程持有B锁,A想获得B的锁,B想获得A的锁。
解决方法:
- 命令行使用jps -l定位进程号 命令行输入 jps -l
- 使用jstack 查看进程的堆栈信息 命令行输入 jstack 进程号
下面信息会出现 Found 1deadlock
二十一、AQS
AQS是AbstractQueuedSynchronizer抽象队列同步器,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的。
它内部提供了一个FIFO的等待队列(双向链表实现)(但是AQS也是无法保证fair的,也就是说先入队列的线程不一定先获取到锁),用于多个线程等待一个事件(锁)。有一个head结点和一个tail结点,head节点是一个哨兵节点,不存放实际的“线程”节点(使用Node的无参构造函数),tail指向链表的最后一个节点。当新增节点时,将新节点作为当前tail的下一个节点,通过CAS设置成功后,将新节点设为新的tail节点。
链表的每个结点有一个重要的状态标志——state,该属性是一个int值,表示对象的当前状态(如0表示lock,1表示unlock)。AQS提供了三个protected final的方法来改变state的值,分别是:getState、setState(int)(直接写入新的)、compareAndSetState(int, int)(修改原来的)。根据修饰符,它们是不可以被子类重写的,但可以在子类中进行调用,这也就意味着子类可以根据自己的逻辑来决定如何使用state值。
AQS的head、tail节点,内部类Node的waitStatus、next属性均使用unsafe对象,通过偏移地址来进行CAS操作。
AQS类方法中
tryAcquire(int) 尝试获取state
tryRelease(int) 尝试释放state
tryAcquireShared(int) 共享的方式尝试获取
tryReleaseShared(int) 共享的方式尝试释放
isHeldExclusively() 判断当前是否为独占锁
AQS需要子类复写的方法均没有声明为abstract,目的是避免子类需要强制性覆写多个方法,因为一般自定义同步器要么是独占方法,要么是共享方法,只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。
总的来说AQS提供了三个功能:
实现独占锁(ReentrantLock、ReentrantReadWriteLock.WriteLock)
实现共享锁(ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore)
实现Condition模型(ConditionObject,它实现了Condition接口,可以用于await/signal。采用CLH队列的算法,唤醒当前线程的下一个节点对应的线程,而signalAll唤醒所有线程。)
本文深入探讨Java并发编程的关键概念和技术,包括线程与进程的区别、JUC包下的并发工具、锁的深度解析、线程池的高效使用、以及AQS抽象队列同步器的工作原理。涵盖生产者消费者模式、安全的集合操作、Callable与Runnable接口对比、阻塞队列的运用、ForkJoin框架、异步回调机制、JMM内存模型、Volatile关键字、CAS算法、乐观锁与悲观锁的区别、各类锁的特性和死锁的解决方法。
576

被折叠的 条评论
为什么被折叠?



