JUC
并发编程3个包:
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
线程的六种状态与转换:
wait()与sleep()的区别:
- 来自不同的类:wait()来自Object类,sleep()来自Thread类
- 有无释放锁资源:sleep()不释放锁,wait()释放锁,
- 使用范围不同:sleep()可以在任何地方使用,wait()只能在同步方法或同步块中使用
- 是否需要捕获异常:sleep()必须捕获异常,wait()不需要捕获异常
synchronized与Lock区别:
- synchronized是java内置关键字,Lock是类
- synchronized无法判断是否获取到锁,Lock可以
- synchronized会自动释放锁,Lock需要手动释放锁
- synchronized等待不可中断,Lock可以中断
- synchronized是非公平锁,Lock锁默认是非公平,可以通过构造方法传入boolean值true来设置为公平锁(公平锁就是按申请锁的顺序分配锁,非公平锁就是不按顺序分配)
8锁现象:
1.标准访问 2.使邮件sleep4秒 都是先调用先执行
public class Lock8 { public static void main(String[] args) throws InterruptedException { Phone phone=new Phone(); new Thread(()->{ try{ phone.sendEmail();; }catch (Exception e){ e.printStackTrace(); } },"A").start(); Thread.sleep(2000); new Thread(()->{ try{ phone.sendSMS(); }catch (Exception e){ e.printStackTrace(); } },"B").start(); } } public class Phone { public synchronized void sendEmail() throws Exception{ System.out.println("SendEmail"); } public synchronized void sendSMS() throws Exception{ System.out.println("sendSMS"); } }
谁先调用谁先执行,因为synchronized修饰的方法,锁的对象是方法的调用者,因为两个方法的调用者是同一个,所以连个方法用的是同一个锁,先调用的先执行
3.未同步的方法与同步方法谁先执行
未同步的方法不受锁的影响,无需等待先执行
4.两部手机,谁先执行
两部手机两个锁对象,所以第二部手机无需等待,先执行
5.一个手机两个静态同步方法,谁先执行
被sychronized和static修饰的方法锁的对象是类的class对象,两个方法用的还是同一把锁,先调用先执行
6.两个手机,两个静态同步方法,谁先执行
被sychronized和static修饰的方法锁的对象是类的class对象,两个方法用的还是同一把锁,先调用先执行
7.一部手机,一个普通同步方法,一个静态同步方法,谁先执行
普通同步方法锁的对象是方法的调用者,静态同步方法锁的是类的Class对象,两个方法用的不是同一个锁,后调用的方法无需等待先调用的方法
8.两部手机,一个普通同步方法,一个静态同步方法,谁先执行
普通同步方法锁的对象是方法的调用者,静态同步方法锁的是类的Class对象,两个方法用的不是同一个锁,后调用的方法无需等待先调用的方法
synchronized具体表现:
- 普通同步方法:锁的是当前实例对象
- 静态同步方法:锁的是当前Class对象
- 同步方法块:锁的是synchronized括号的配置对象
集合类不安全
在多线程下使用多个线程向List里add值会抛出异常主要原因是add方法没有加锁
public class UnSafeList { public static void main(String[] args) { List<String> list=new ArrayList<>(); for(int i=1;i<=30;i++){ new Thread(()->{ list.add("12"); System.out.println(list); },String.valueOf(i)).start(); } } }
使用CopyOnWriteArrayList就不会抛出异常
public class UnSafeList { public static void main(String[] args) { List<String> list=new CopyOnWriteArrayList<>(); for(int i=1;i<=30;i++){ new Thread(()->{ list.add("12"); System.out.println(list); },String.valueOf(i)).start(); } } }
写入时复制的思想(CopyOnWrite)
当多个调用者同时请求相同资源,只是进行读取操作时共享该资源;当有调用者要修改资源时,就将该资源的副本给调用者,其他调用者见到的最初资源仍然保存不变,直到修改完成后才将复制的资源赋值给最初资源。
CopyOnWriteArrayList和Vector
Vector是在增删改查方法上都加了锁,而CopyOnWriteArrayList只是在增删改上加了锁,所以CopyOnWrite在读方面的性能好于Vector
CopyOnWriteArrayset()与ConcurrentHashMap()同理
Callable接口
实现Callable接口重写call方法时第三种实现多线程的方式,其与Runnable接口的区别是:
- 是否有返回值
- 是否抛出异常
- 方法不一样,一个call()一个run()
public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable myCallable=new MyCallable(); FutureTask futureTask=new FutureTask(myCallable);//适配类 Thread thread=new Thread(futureTask); thread.start(); Integer integer=(Integer)futureTask.get(); System.out.println(integer); } }
public class MyCallable implements Callable { @Override public Integer call() throws Exception { System.out.println("调用call()方法"); return 1024; } }
常用的辅助类
CountDownLatch
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 计数器 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"Start"); countDownLatch.countDown(); // 计数器-1 },String.valueOf(i)).start(); } //阻塞等待计数器归零 countDownLatch.await(); System.out.println(Thread.currentThread().getName()+" End"); } }
CountDownLatch是一个计数器,当线程调用其countDown()时计数器-1;当线程调用await()方法时,线程就会阻塞,等到计数器变为0时,await()阻塞的线程就会被唤醒继续执行。
CyclicBarrier
栅栏类,阻塞一组线程直到某一事件发生;所有的线程必须同时达到栅栏位置才能继续执行,构造方法CyclicBarrier(int parties, Runnable barrierAction),parties表示拦截线程数,barrierAction线程都到达后执行事件
public static void main(String[] args) { // CyclicBarrier(int parties, Runnable barrierAction) CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{ System.out.println("召唤神龙成功"); }); for (int i = 1; i <= 7; i++) { final int tempInt = i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集了第"+ tempInt +"颗龙珠"); try { cyclicBarrier.await(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } }
Semaphore
信号量
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore=new Semaphore(3); for (int i=1;i<=6;i++){ new Thread(()->{ try { semaphore.acquire(); // acquire 得到 System.out.println(Thread.currentThread().getName()+" 抢到了车位"); TimeUnit.SECONDS.sleep(3); // 停3秒钟 System.out.println(Thread.currentThread().getName()+" 离开了车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放这个位置 } },String.valueOf(i)).start(); } } }
acquire():线程调用acquire()时。要么成功获取到了信号量(此时信号量-1),要么一直等待获取
release():释放信号量(此时信号量+1),唤醒在等待的线程
读写锁
独占锁:此类型的锁一次只能被一个线程持有,ReentrantLock和Synchronized都是独占锁
共享锁:此类型的锁可以被多个线程持有
读写锁(ReentrantReadWriteLock):其读锁是共享锁,写锁是独占锁,保证了在并发读时的高效,读锁readLock();写锁writeLock()
阻塞队列
阻塞队列是一个队列,当队列是空的时,往队列中获取元素的操作会被阻塞,当队列时满的时,往队列添加元素的操作会被阻塞。当满足条件后,被挂起的线程会被自动唤起
常用:ArrayBlockingQueue
线程池
线程池:控制运行的线程数量,将任务放入队列,在线程创建后启动这些任务,如果线程数超过了最大数量,超出的线程排队等候,其他线程执行完毕后再冲队列中取出任务来执行。主要特点是线程复用,管理线程,控制最大并发数。
线程池是通过Executor框架实现的
三大方法:
- Executors.newFixedThreadPool(int n):创建一个有n个线程的线程池
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newFixedThreadPool(5); try { // 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 办理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完记得关闭 } }
- Executors.newSingleThreadExecutor():只有一个线程
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newSingleThreadExecutor(); try { // 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 办理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完记得关闭 } } }
- Executors.newCachedThreadPool():根据需要创建线程,再先构建的线程可用时会重用。
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newCachedThreadPool(); try { // 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 办理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完记得关闭 } } }
七大参数
- corePoolSize:核心线程数,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当 中。
- maxumumPoolSize:最大线程数,线程池中最多能创建的线程数,必须大于等于1
- keepAliveTime:空闲线程保留时间
- TimeUnit:空闲线程保留时间单位
- BlockingQueue workQueue:存储等待执行的任务
- ThreadFactory threadFactory:线程工厂,用于创建线程
- RejectedExecutionHandler:队列已满且任务量大于最大线程数的异常处理策略
ThreadPoolExecutor工作原理
CPU密集型程序(计算为主):线程数等于CPU数最好(频繁切换线程会消耗时间)
IO密集型(输入输出为主):IO任务数等于线程数最好
四大函数式接口
public class FunctionDemo { public static void main(String[] args) { //消费型接口 Consumer<String> consumer=(s)->{ System.out.println(s); }; consumer.accept("dwx"); //供给型接口 Supplier<String> supplier=()->{ return "dwx"; }; System.out.println(supplier.get()); //函数型接口 Function<String,String> function=s->{ return s; }; System.out.println(function.apply("dwx")); //断定型接口 Predicate<String> predicate=s->{ if(s.equals("dwx")){ return true; }else{ return false; } }; System.out.println(predicate.test("dwx")); } }
Stream流式计算
集合等是用来存储数据,Stream流用来计算数据
一个流式处理首先调用stream()将其转换成流,然后流有两个操作:
- 中间操作:执行对集合的一些操作
- 终端操作:将结果进行封装,返回需要的形式
类似车间流水线
public class StreamDemo { public static void main(String[] args) { User user1=new User(11,"dwx1",22); User user2=new User(12,"dwx2",23); User user3=new User(13,"dwx3",24); User user4=new User(14,"dwx4",25); List<User> list= Arrays.asList(user1,user2,user3,user4); //1.将list转换未流list.stream() //2.然后过滤流,过滤符合条件的元素filter(Predicate函数接口) //3.map()实现映射 //4.foeEach()输出forEach(s->{System.out.println(s);}),等价于forEach(System.out::println) list.stream() .filter(s->{return s.getId()%2==0;}) .map(s->{return s.getUserName().toUpperCase();}) .forEach(System.out::println); } }
分支合并
jdk1.7后,Java提供Fork/Join框架用于并行执行任务,思想是将大任务分割成若干个小任务,最后把各个小任务的结果汇总得到大任务的结果
该模型概念:线程池的每一个线程有自己的工作队列,当自己队列中的任务完成后,会去其他线程的工作队列中偷取任务来执行,这样就可以充分的利用资源
工作窃取
工作窃取算法是指某个线程自己的任务完成后去他窃取其他线程的任务来执行
线程里的任务队列采用双端队列,来减少窃取任务的线程和被窃取任务线程之间的竞争,被窃取的任务从双端队列的头部拿任务,而窃取任务的队列从双端对列的尾部拿任务
异步回调
让被调用者立即返回一个引用,让其在后台慢慢处理。此时调用者可以先处理其他任务,在真正需要数据的场合再去获取数据
JMM(Java内存模型)
谈谈对volatie的理解
volatile是java虚拟机的轻量级同步机制。有三大特性:
- 保证可见性
- 禁止指令重排
- 不保证原子性
JMM抽象的概率,描述的是一组规则或者规范
JMM规定了内存主要分为主内存和工作内存,主内存是硬件的物理地址,工作内存是寄存器和高速缓存
Java线程在每次读取和写入操作都去访问主内存会影响性能,因此JMM规定每条线程拥有各自的工作内存,工作内存中的变量是主内存变量中的拷贝。线程对工作内存的操作其他线程不可见,为了保证线程间同步,使用Volatile关键字即可,实现了一下两个规则:
- 线程对变量进行修改后要立刻写入到主内存中
- 线程对变量读取时要从主内存去读
内存交互操作
内存交互操作有8种,每个操作都是原子的,不可再分的
- lock锁定:作用于主内存的变量,把一个变量标识为线程独占状态
- unlock解锁:作用于主内存的变量,把锁定的变量解锁
- read读取:作用于主内存的变量,把一个变量的值从主内存传输到工作内存
- load载入:作用于工作内存的变量,把read操作的变量从主内存放入工作内存
- use使用:作用于工作内存的变量,把工作内存中的变量传输个执行引擎
- assigin赋值:作用于工作内存的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store存储:作用于与主内存的变量,将工作内存中的变量值传输到主内存
- write:作用于主内存的变量,将store操作的变量放入到主内存的变量中
JMM对这八种指令制定了如下规则
- read和load,store和write必须成对使用
- 线程不能丢弃最近的assign操作,工作内存变量数据改变了必须告知主内存
- 不允许线程将没有assigin的数据从工作内存同步到主内存
- 不允许工作变量使用未初始化的变量。新的变量必须在主内存中诞生
- 一个变量同一时间只有一个线程对其lock
- 对一个变量进行lock操作会清空所有工作内存的此变量的值,在执行引擎要使用该变量前,必须重写load或assign
- 一个变量没被lock就不能进行unlock
- 对一个变量进行unlock操作之前,必须把此变量同步回内存中
volatile
volatile保证可见性
public class VolatileDemo { //不加volatile就会陷入死循环 private volatile static int num=0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while(num==0){ } }).start(); Thread.sleep(1000); num=1; System.out.println(num); } }
volatile不保证原子性
public class VolatileDemo { private volatile static int num=0; public static void add(){ num++; } public static void main(String[] args) throws InterruptedException { for(int i=1;i<=20;i++){ new Thread(()->{ for(int j=1;j<=1000;j++){ add(); } },String.valueOf(i)).start(); } while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+""+num); } }
一部分值被覆盖了
使用atomic包下的原子类实现原子性,num++是不安全的,使用getAndIncrement()替代
public class VolatileDemo { private volatile static AtomicInteger num=new AtomicInteger(); public static void add(){ num.getAndIncrement(); } public static void main(String[] args) throws InterruptedException { for(int i=1;i<=20;i++){ new Thread(()->{ for(int j=1;j<=1000;j++){ add(); } },String.valueOf(i)).start(); } while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+""+num); } }
指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排
在多线程环境中,由于线程是交替执行的,编译器优化的指令重排会使两个线程中使用的变量是否保证一致性无法确定,结果无法预测
volatile实现了禁止指令重排优化
内存屏障(内存栅栏)是一个CPU指令,有两个作用:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(volatile就是利用了该特性)
在指令键插入一条内存屏障指令就会告诉CPU和编译器不管什么指令都不能和这条内存屏障指令重排。即禁止内存屏障前后的指令执行重排优化。该指令还会强制刷出CPU缓存数据,所以线程读取到的都是最新值
基于保守策略的内存屏障策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的前面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
单例模式
饿汉式
public class Hungry { private int[] data1=new int[10]; private int[] data2=new int[10]; privare Hungry(){} private final static Hungry hungry=new Hungry(); public static Hungry getInstance(){ return hungry; } }
当代码一运行就会生成一个Hungry实例,并且data1和data2会被放入内存,如果长时间不用就会造成内存浪费
懒汉式
public class Lazy { private Lazy(){} private static Lazy lazy; public static Lazy getInstance(){ if (lazy == null) { lazy = new Lazy(); } return lazy; } }
在多线程下,一些线程的单例会失效,且由于指令重排,会发生一些错误导致单例不完整。
使用DCL单例模式加volalite,可以避免问题
public class Lazy { private Lazy(){} private volatile static Lazy lazy; public static Lazy getInstance(){ if (lazy == null) { synchronized(Lazy.class) { if(lazy==null) { lazy = new Lazy(); } } } return lazy; } }
CAS
CAS比较并交换
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger=new AtomicInteger(5); //第一个参数为期望值,第二个参数为修改值,如果期望值=实际值,就将实际值修改,不等于就不改 System.out.println(atomicInteger.compareAndSet(5,2021)); System.out.println(atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(12,2020)); System.out.println(atomicInteger.get()); } }
UnSafe类
UnSafe类是java的核心类,类中所有方法都是native修饰,即其方法都是直接调用操作系统底层资源执行任务的。
CAS(Compare And Swap)
CAS是一条CPU并发语句,用来判断内存的某个位置的值是否是期望值,如果是则将其更改为新的值,该过程是原子的
CAS有三个操作数,内存值V,预期值A,和修改值B,当预期值A与内存值V相同时就将内存V修改为B,否则不断重试或放弃。
CAS缺点
- 时间开销大,while循环
- 只能保证一个共享变量的原子操作
- 导致ABA问题(ABA问题:另一个线程修改了内存值V为V1,然后又将其改回原来的值V,当前线程无法分辨内存值是否发生过改变)
解决ABA问题使用原子版本号引用AtomicStampedReference,相当于添加了一个版本号,当修改内存值是版本号就会发生改变,类似乐观锁
Java锁的类别
公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序来获取锁,即排队
非公平锁:多个线程不是按照申请锁的顺序获取锁
可重入锁
可重入锁(递归锁):程序外层获取了锁,内层也可以获得该锁(避免死锁)ReentrantLock和Synchronized都是可重入锁
自旋锁
自旋锁:尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,减少线程上下文切换消耗