Java.util.concurrent 在并发编程中使用的工具类
进程:是操作系统中正在运行的应用程序,是程序的集合,是操作系统资源分配的基本单位。一个进程往往可以包含多个线程,至少要1个。
线程:是进程的执行单元或说执行场景,用来执行具体的任务和功能,是CPU调度和分派的基本单位。
进程是操作系统调度和资源分配的最小单位,线程是CPU调度和分派的最小单位。
java默认有几个线程?至少2个:main主线程,GC垃圾回收线程
**java真的可以开启线程吗?**开不了。
它是先通过调用start()方法:public synchronized void start()
该方法里先是把该线程加入到一个线程组中:group.add(this)
然后调用了start0()方法,这是个本地方法,它是调用底层的C++程序
要明白一点,java是无法直接操作硬件的。而是通过这些本地方法去调用底层的c或c++程序来操作硬件。
并发(多线程操作同一资源)
并行(同一时间点多个线程同时执行,真正的并发)
并发编程的本质:充分利用CPU的资源,提高程序的执行效率
java线程有几个状态?
public enum State {
NEW,//新建状态
RUNNABLE,//运行状态
BLOCKED,//阻塞状态
WAITING,//等待(死死地等)
TIMED_WAITING,//超时等待(超过一定时间就不等了)
TERMINATED;//终止状态
}
新建状态(New):新建一个线程对象。
就绪/可运行状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
运行状态(Running):就绪状态的线程获得CPU并执行程序代码。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep的状态超时、join等待线程终止或者超时、以及I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完成或者因异常退出run方法,该线程结束生命周期。
**wait() 与 notify() **
wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
notify()和notifyAll()的区别:
① notify()随机唤醒等待队列中一个线程,notifyAll()唤醒正在等待队列中全部线程。
② notify()可能引发异常,比如一个生成者多个消费者情况下,消费者消费完随即唤醒一个线程,恰好也是消费线程,就会引发异常。所以建议使用notifyAll()
来自不同的类:
wait是Object的方法
sleep是Thread的静态方法
关于锁的释放:
wait会释放锁
sleep不释放锁(抱着锁睡)
使用范围:
wait必须在同步代码块中
sleep可以在任何地方睡
Java中隐式锁:synchronized;显式锁:Lock
出身不同:
synchronized是Java中的关键字,是由JVM来维护的。是JVM层面的锁。
Lock是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。
sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。
而lock是通过调用对应的API方法来获取锁和释放锁的。
概述,可以把sync理解为官二代或者是星二代,从娘胎出来自带光环的。Lock就是我们普通努力上进的人。
使用方式不同
Sync是隐式锁。Lock是显示锁。
所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
我们大家都知道,在使用sync关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当sync代码块执行完成之后,系统会自动的让程序释放占用的锁。sync是由系统维护的,如果非逻辑问题的话话,是不会出现死锁的。
在使用Lock的时候,使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合try/finaly语句块来完成。
用生活中的一个case来形容这个不同:官二代和普通人的你在进入机关大院的时候待遇。官二代不需要出示什么证件就可以进入,但是你需要手动出示证件才可以进入。
等待是否可中断
sync是不可中断的。一个线程获得了锁,其他线程,必须傻傻等待该线程释放锁,不能去中断该线程,除非该线程抛出异常或者正常运行完成。其他线程才能获得锁。
Lock可以中断的。中断方式:
1:调用设置超时方法 tryLock(long timeout ,timeUnit unit)
2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
生活中小case来理解这一区别:官二代一般不会做饭。都会去餐厅点餐等待着餐厅出餐。普通人的你既可以去餐厅等待,如果等待时间长的话,你就可以回去自己做饭了。
加锁的时候是否可以公平
java默认的是非公平锁。为什么?因为非公平锁比较公平。通常情况下,非公平锁的吞吐量要比公平锁的吞吐量高。
ReentrantLock实现了Lock接口,加锁和解锁都需要显式写出,注意一定要在适当时候unlock
和synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
sync:非公平锁。
lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。
true:公平锁
false:非公平锁
生活中小case来理解这个区别:官二代一般都不排队,喜欢插队的。普通人的你虽然也喜欢插队。但是如果遇到让排队的情况下,你还是会排队的。
锁绑定多个条件来condition
sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。
Lock:用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。lock的粒度更细。
从性能比较
从使用锁的方式比较
/*
* 生产者消费者模式
* */
public class A {
public static void main(String[] args) {
Data data = new Data();
// 生产线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// 消费线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//资源类
class Data{
private int number = 0;
//+1
public synchronized void increase() throws InterruptedException {
while (number!=0){
//这里必须用while循环。来防范虚假唤醒的情况。不能用if!因为一个生产一个消费时if还不会出现问题,但是多个生产,多个消费时,if就会出现问题,因为if只进行一次判断,
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知来消费
this.notify();
}
//-1
public synchronized void decrease() throws InterruptedException {
while (number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知生产
this.notify();
}
}
/*
* 生产者消费者模式
*想实现的效果就是,A执行完B执行,B执行完C执行,C执行完D执行,D执行完A执行
* */
public class A {
public static void main(String[] args) {
Data data = new Data();
// 生产线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increaseA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// 消费线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decreaseB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
// 生产线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increaseC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
// 消费线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decreaseD();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//资源类
class Data{
private int number = 0;
Lock lock = new ReentrantLock();
//如何精准唤醒?就是通过new多个condition监视器,每个condition监视一个线程
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
Condition conditionD = lock.newCondition();
public void increaseA() throws InterruptedException {
lock.lock();
try{
while (number!=0){
conditionA.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知B来消费
conditionB.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void decreaseB() throws InterruptedException {
lock.lock();
try{
while (number == 0){
conditionB.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知C来生产
conditionC.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void increaseC() throws InterruptedException {
lock.lock();
try{
while (number!=0){
conditionC.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知D来消费
conditionD.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
//-1
public void decreaseD() throws InterruptedException {
lock.lock();
try{
while (number == 0){
conditionD.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知A来生产
conditionA.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
记住一点:在java多线程中,
一个对象一把锁,100个对象100把锁;(synchronized加在方法上锁的是this,即这个对象)
一个类只有一把锁; (synchronized加在静态方法上锁的是类)。
栗子
ArrayList是非线程安全的。在多线程并发的情况会出现问题(比如ConcurrentModificationException并发修改异常),怎么变得安全呢?有一下三种方法:
第三种就是JUC方式,用List list = new CopyOnWriteArrayList();
CopyOnWriteArrayList比Vactor牛在哪?看源码:
写入时复制,这是一种 读写分离 的思想,因为由源码可知它是把原数组复制到一个新数组当中,然后把要添加的元素添加到新数组中,最后再把新数组set回去。当然这个过程进行了加锁以保证写的时候只有一个线程,而写的过程中(即添加元素的过程),其他线程就可以并发地读原来旧的数组,这就实现了读写分离。那么此时并发下就不会出错了。
内部是调用了Arrays.copyOf()方法,而Arrays.copyOf()底层又是调用了System.arraycopy(),这个方法底层调用了一个本地方法,是复制了一个数组。
因为Thread只接受Runnable,Callable怎么让Thread接受自己从而启动线程呢?
当然只能通过Runnable,那又怎么和Runnable搭上关系呢?
通过Runnable的实现类FutureTask(相当于适配器类)
public class TestCallable {
public static void main(String[] args) throws ExcutionException,InterruptedException{
// new Thread(new Runnable()).start();
// new Thread(new FutureTask()).start();
// new Thread(new FutureTask( Callable )).start();
myThread thread = new myThread();
FutureTask futureTask = new FutureTask(thread)//适配器类,把Callable接口实现类实例包装一下
new Thread(futureTask,"A").start();
Integer o = (Integer)futureTask.get();//获取返回值,这个方法可能会阻塞
System.out.println(o);
}
}
//Callable接口实现类
class myThread implements Callable<Integer>{
public Integer call(){//返回类型要与接口泛型一致
System.out.println("call...");
return 1024;//返回值
}
}
//减法计数器
//CountDownLatch是一种通用的同步工具
public class CountDownLatch_test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);//设置计数
for (int i = 0; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown();//数量-1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数归零,再向下执行(计数没归零之前,任何线程都过不去这道门槛。大家一起过这道门)
System.out.println("Close Door");
}
}
原理:
countDownlatch.countDown(); //数量 -1
countDownlatch.await(); //等待计数归零,然后再向下执行(相当于一个门闩)
每次有线程调用countDown()时 计数 -1,其他线程阻塞谁也无法通过await这道门闩,当计数器为0 ,所有被阻塞的线程释放,门闩打开程序可以向下执行。
//允许一组线程全部等待彼此到达共同障碍点的同步辅助。
public class CyclicBarrier_test {
public static void main(String[] args) {
//集齐7颗龙珠召唤神龙
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("神龙召唤成功!");//只有计数达到7才会执行该操作
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了第"+temp+"颗龙珠");
try{
cyclicBarrier.await();//等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
计数信号量。信号量通常用于限制线程数(限流),信号量维持一组许可证,每个线程必须从信号量获取许可证,再去使用项目,当线程完成该项目后,它将返回到池中,并将许可证返回到信号量允许另一个线程获取该项目。
比如有6辆车(线程),目前只有3个车位(信号量)
/*
比如有6辆车(线程),目前只有3个车位(信号量)
只能同时供3辆车停放,其余3辆等待,走一辆进去一辆,交替停车。
* */
public class Semaphore_test {
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(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//release()释放
}
},String.valueOf(i)).start();
}
}
}
/*
* 独占锁(写锁)一次只能被一个线程占有
* 共享锁(读锁)多个线程可以同时占有
* */
public class ReadWriteLock_test {
public static void main(String[] args) {
MyCacheLock myCacheLock = new MyCacheLock();
for (int i = 1; i <= 5 ; i++){//开启5个线程写入
final int temp = i;
new Thread(()->{
myCacheLock.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5 ; i++){//开启5个线程读取
final int temp = i;
new Thread(()->{
myCacheLock.get(String.valueOf(temp));
},String.valueOf(i)).start();
}
}
}
//资源类
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁,更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock();//加写锁
try{
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);//写入
System.out.println(Thread.currentThread().getName()+"写入OK");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();//释放写锁
}
}
//取,读所有线程都可以读
public void get(String key){
readWriteLock.readLock().lock();//加读锁
try{
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);//读取
System.out.println(Thread.currentThread().getName()+"读取OK");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();//释放读锁
}
}
}
添加,移除
四组API
方式 | 抛出异常 | 不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put | offer(“a”,2,TimeUnit.SECONDS) |
移除 | remove() | poll | take | poll(2,TimeUnit.SECONDS)) |
检测队首元素 | element() | peek | - | - |
没有容量:进去一个元素,必须等待取出来后,才能再往里放一个元素!
put(),take()
/*
* 同步队列和其他BlockingQueue不一样,SynchronousQueue不存过多元素只存一个。
* put进队列了一个元素,必须要take出来,否则不能再put进去。
* */
public class SynchronousQueue_Demo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();//同步队列
//开两个线程,一个线程存一个线程取。
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+" put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+" put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);//等待3秒
System.out.println(Thread.currentThread().getName()+" take "+blockingQueue.take());
TimeUnit.SECONDS.sleep(2);//等待3秒
System.out.println(Thread.currentThread().getName()+" take "+blockingQueue.take());
TimeUnit.SECONDS.sleep(2);//等待3秒
System.out.println(Thread.currentThread().getName()+" take "+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
线程池:三大方法、7大参数、4种拒绝策略
程序的运行,本质:占用系统的资源!优化资源的使用!=>池化技术
线程池,连接池,内存池,常量池,对象池…等等。因为创建、销毁十分浪费资源,所以有了池化技术。
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池就是线程复用、可以控制最大并发数
使用 **Executors 工具类**去创建线程池的三大方法:
public class Demo1 {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
ExecutorService threadPool = 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();
}
}
}
三大方法的源码分析:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//最大约等于21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//可见三者底层实际调用的都是ThreadPoolExecute()方法
//ThreadPoolExecute()中有7大参数
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大核心线程池大小
long keepAliveTime,//超时了没人调用就会释放
TimeUnit unit,//超时单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂,是创建线程的,一般不用动。
RejectedExecutionHandler handler) {//拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
来看看阿里巴巴关于线程池的规范:
比如银行办理业务。假设1,2窗口是开的,3,4, 5窗口是关的。候客区相当于一个阻塞队列。此时,又来三个人办理业务,而候客区又满了,就会触发3,4,5窗口开放营业接客。
这种情况下:1,2就相当于corePoolSize核心线程池大小;候客区相当于阻塞队列;3,4,5相当于maximumPoolSize最大核心线程池大小;如果这时候再来一个人,就会有拒绝策略来处理他。
超过8时就会执行拒绝策略,在这里会抛出异常。
自定义拒绝策略
使用RejectedExecutionHandler
接口,重写rejectedExecution方法。
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(10),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { //r:请求执行的任务 executor:当前的线程池
//打印丢失的任务
System.out.println(r.toString() + " is discard");
}
});
两种情况:IO密集型,CPU密集型
CUP密集型
看电脑是几核cpu,java并发是一个线程占用一个CPU,几核CPU就支持几个线程同时执行,这样能充分发挥cpu性能
查看CPU核数也很容易:1、任务管理器->性能:可见我的是12个
2、打开计算机管理->设备管理器:
但是每个用户的电脑情况不一样,我们肯定不能把这个值写死,怎么办呢?通过代码获取:
max = Runtime.getRuntime().availableProcessors();//获取CPU的核数
IO密集型
IO十分耗时又占用资源的!程序中有几个IO任务就相当于有几个大型任务。
所以设置最大核心线程池大小的时候,我们就要大于大型任务的个数,以保证有多余的线程去执行其他的任务。
比如我们程序中有 15 IO任务,那 最大核心线程池大小 > 15
新时代java程序员必会:lambda表达式、链式编程、函数式接口、Steam流式计算
例如比较典型的就是Runnable接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//java中函数式接口超级多FunctionalInterface
//它能简化编程模型,在新版本的框架底层大量应用。
//forEach(消费者类型的函数式接口)
/*
* Function 函数接口,有一个输入参数,返回类型是第二个泛型参数
* */
public class Function_test {
public static void main(String[] args) {
Function<String,String> function = new Function<String, String>() {//匿名内部类方式实现
@Override
public String apply(String s) {
return s;
}
};
System.out.println(function.apply("abc"));
//只要是函数式接口都可以用lambda表达式简化
Function<String,String> function2 = (s)->{return s;};
System.out.println(function2.apply("abc"));
}
}
/*
* Predicate 断定接口,有一个输入参数,返回结果是布尔值
* */
public class Function_test {
public static void main(String[] args) {
Predicate<String> predicate = new Predicate<String>() {//匿名内部类方式实现
@Override
public boolean test(String s) {
return s.isEmpty();
}
};
System.out.println(predicate.test(""));//True
//只要是函数式接口都可以用lambda表达式简化
Predicate<String> predicate2 = (s)->{return s.isEmpty();};
System.out.println(function2.test(""));
}
}
/*
* Consumer 消费型接口,有一个输入参数,无返回类型;
* */
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("abcdefg");
//只要是函数式接口都可以用lambda表达式简化
Consumer<String> consumer2 = (s)->{System.out.println(s);};
consumer2.accept("abcdefg");
/*
* Supplier 供给型接口,无输入参数,返回类型指定泛型;
* */
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
return 1024;
}
};
System.out.println(supplier.get());
//只要是函数式接口都可以用lambda表达式简化
Supplier<Integer> supplier2 = ()->{return 1024;};
System.out.println(supplier2.get());
可见消费型接口,只消费不返回;供给型接口,只返回不消费。
函数式接口哪里用得到?Stream流式计算!
大数据的计算模式主要分为批量计算(batch computing)、流式计算(stream computing)、交互计算(interactive computing)、图计算(graph computing)等。其中,流式计算和批量计算是两种主要的大数据计算模式,分别适用于不同的大数据应用场景。
流数据(或数据流)是指在时间分布和数量上无限的一系列动态数据集合体,数据的价值随着时间的流逝而降低,因此必须实时计算给出秒级响应。流式计算,顾名思义,就是对数据流进行处理,是实时计算。批量计算则统一收集数据,存储到数据库中,然后对数据进行批量处理的数据计算方式。主要体现在以下几个方面:
1、数据时效性不同:流式计算实时、低延迟, 批量计算非实时、高延迟。
2、数据特征不同:流式计算的数据一般是动态的、没有边界的,而批处理的数据一般则是静态数据。
3、应用场景不同:流式计算应用在实时场景,时效性要求比较高的场景,如实时推荐、业务监控…批量计算一般说批处理,应用在实时性要求不高、离线计算的场景下,数据分析、离线报表等。
4、运行方式不同,流式计算的任务持续进行的,批量计算的任务则一次性完成。
大数据:存储+计算
存储:集合、数据库等
计算:交给流来操作!
/*
* 题目要求:一分钟完成此题,只能用一行代码实现!
* 现有5个用户!筛选
* 1、ID必须是偶数!
* 2、年龄必须大于23!
* 3、用户名转为大写字母!
* 4、用户名字母倒着排序!
* 5、只输出一个用户!
* */
public class Demo1 {
public static void main(String[] args) {
User u1 = new User(1,"zhangsan",21);
User u2 = new User(2,"lisi",23);
User u3 = new User(3,"wanger",26);
User u4 = new User(4,"maliu",25);
User u5 = new User(5,"zhaoqi",35);
User u6 = new User(6,"zhaoqi",29);
//集合就是存储元素的
List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6);
//交给Stream流来计算
//一个例子涵盖了:lambda表达式、函数式接口、链式编程、Steam流式计算
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);})//用户名字母倒着排序!
.limit(1)//只输出一个用户!
.forEach(System.out::println);
}
}
//这几个方法的参数分别是哪种类型的函数式接口?
//filter(Predicate断定型的函数式接口),map(Function函数型接口),sorted(Compartor比较型接口),forEach(消费者类型的函数式接口)
并行执行任务!提高效率。必须在大数据量下才会使用!
把大任务拆分为多个子任务。 把子任务的结果合并成最初问题的结果(分治策略)
空闲线程主动去执行高负载线程的任务。从而提高效率。
怎么提高效率的?它内部实际上维护的是双端队列。如下图A可以从上面取,B可以从下面取。两端可以同时取。
ForkJoinPool.execute() 或者 ForkJoinPool.submit()
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
/*
* 计算1~10亿的求和任务
*初级(for循环) ,中级(ForkJoin),高级(Stream并行流)
* 如何使用ForkJoin?
* 1、ForkJoinPool,用它来执行.
* 2、计算任务 ForkJoinPool.execute(ForkJoinTask task)
* 3、计算类要继承ForkJoinTask
* */
public class ForkJoin_Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
//普通程序员
public static void test1(){
Long sum = 0L;
long begin = System.currentTimeMillis();
for (Long i = 1L; i <= 10_0000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum= "+sum+" 时间: "+(end-begin));
}
//中级程序员使用ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
long begin = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoin_task task = new ForkJoin_task(0L, 10_0000_0000L);//创建一个ForkTask任务
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum= "+sum+" 时间: "+(end-begin));
}
//高级程序员Stream并行流
public static void test3(){
long begin = System.currentTimeMillis();
//一行代码搞定; rangeClosed是(]左开右闭;parallel并行;reduce累加求和;:: JDK8中的关键字 方法引用
Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum= "+sum+" 时间: "+(end-begin));
}
}
//定义一个ForkJoinTask任务
class ForkJoin_task extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long boundary = 10000L;//临界值,超过临界值就分成两个任务
public ForkJoin_task(Long start, Long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
if ((end-start) < boundary){
//正常使用for循环计算
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{
//使用ForkJoin分支合并计算(递归)
Long middle = (start+end)/2;//中间值
//拆分成两个子任务
ForkJoin_task task1 = new ForkJoin_task(start, middle);
task1.fork();//把拆分的子任务压入线程的工作任务队列(这个队列是个双端队列)
ForkJoin_task task2 = new ForkJoin_task(middle+1, end);
task2.fork();//把拆分的子任务压入线程的工作任务队列(这个队列是个双端队列)
return task1.join() + task2.join();//合并结果
}
}
}
8种操作
深入理解volatile关键字:https://blog.csdn.net/weixin_43587472/article/details/106342353
我们知道synchronized和Lock都能保证原子性,而volatile不能保证原子性。但是有没有什么方法可以既不使用synchronized和Lock
又能保证原子性呢?
使用原子类,解决原子性问题
这些类的底层都是调用的本地方法和操作系统之间挂钩,所以效率很高效!在内存中修改值!
饿汉式;懒汉式
//饿汉式单例
//浪费空间,无论你用不用类一加载加创建好对象了。
public class Hungry {
private Hungry(){}//私有化构造函数
private final static Hungry HUNGRY = new Hungry();//私有化实例对象
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式:
//懒汉式单例
//需要用到的时候在实例化对象
public class LazyMan {
private LazyMan(){}//私有化构造函数
private static LazyMan lazyMan;//私有化实例对象
public static LazyMan getInstance(){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
DCL懒汉式:双重检测锁模式的懒汉单例
//懒汉式单例
//需要用到的时候在实例化对象
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}//私有化构造函数
private volatile static LazyMan lazyMan;//私有化实例对象
//双重检测锁模式的懒汉单例 DCL双重锁定检查(Double Check Lock)懒汉式
public static LazyMan getInstance(){
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();//不是原子性操作。
/*
* 这行代码会经过三个步骤:
* 1,分配内存空间
* 2,执行构造方法初始化对象
* 3,把这个对象指向这个空间
*
* 如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。
* 如此,在程序真正运行时以上指令执行顺序可能是这样的:
* (a)给instance实例分配内存;
* (b)将instance对象指向分配的内存空间;
* (c)初始化instance的构造器;
* 这时候,当线程一执行(b)完毕,在执行(c)之前,线程二走到第一个if(此时还没开始抢锁),这时候instance判断为非空,
* 此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
* 具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),
* 但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
* 根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量。
* */
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
静态内部类方式
//静态内部类方式
public class Holder {
private Holder(){
System.out.println(Thread.currentThread().getName());
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();//创建实例
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Holder.getInstance();
},String.valueOf(i)).start();
}
}
}
枚举方式
//枚举本身就是一个单例类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance(){
return INSTANCE;
}
}
单例不安全,因为反射可以破坏私有
LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//打破私有
LazyMan instance2 = declaredConstructor.newInstance();//反射方式构造实例对象
System.out.println(instance1);
System.out.println(instance2);
CAS:Compare And Swap,即比较再交换。是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization).
jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的 JVM 支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。
由此可见,AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。
Java中AtomicInteger中incrementAndGet和getAndIncrement的区别
//从源码分析
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
通过代码可以看出:
getAndIncrement返回的是当前值;
incrementAndGet返回的是加1后的值。
我们知道AtomicInteger有个getAndIncrement方法。该方法底层调用的是Unsafe类的getAndAddInt方法,而getAndAddInt内部是一个自旋锁
CAS:比较当前工作内存中的值和主存中的值,如果这个值是期望值,则执行操作!如果不是会一直循环!
缺点:
1,循环会耗时
2,一次性只能保证一个共享变量的原子性
3,会存在ABA问题
什么是ABA问题?
假设原来是A,先修改成B,再修改回成A
假如有两个线程A,B。假如A,B都读到了a=1;这时候B先对这个a进行了CAS操作,第一次cas(1,3)第二次cas(3,1)。对于线程A来说,a表面上并没有变,而实际上线程B已经对a做了手脚。
像这样的现象是我们不希望的,我们希望谁动了这个线程一定要告诉我。如何解决这个问题呢?原子引用。
CAS可以引发ABA问题,怎么解决?——>原子引用(AtomicStampedReference)在比较并交换的过程加上时间戳。
public class ASR {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(1,1);
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获取版本号(时间戳)
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行成功会输出true否则false
//执行后时间戳加1
System.out.println(atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
},"a").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("b1=>" + atomicStampedReference.getStamp());
},"b").start();
}
}
顾名思义它是悲观的,它总是假设最坏的情况,当每次要对数据进行修改的时候都认为会发成并发冲突,别人会修改,所以为了避免冲突同时被其他人修改,每次拿数据的时候都会上锁以防止并发。这样别人想拿到这个数据就会阻塞直到释放锁后拿到这个锁。JAVA中synchronized和Lock都是采用的悲观锁。(具有强烈的独占和排他特性)
共享锁 【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个线程或事务对于同一数据可以共享一把锁,(相当于对于同一把门,它拥有多个钥匙一样)都能访问到数据,但是只能读不能修改和删除。一个线程或事务对数据A加上锁后,其他线程或事务只能对数据A加共享锁,不能加排它锁。共享锁下,其他线程可以并发读取查询数据,不能修改、增加、删除数据。(资源共享)
排他锁【Exclusive lock】又称为写锁或独占锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果线程T获取了数据A排他锁,线程T就独占数据A,只允许线程T对数据A进行读取和修改,其他线程不能再获取数据A的任何锁,包括共享锁和排他锁,直到线程T释放该锁。(独占资源)
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
顾名思义它是乐观的,它采取了更加宽松的加锁机制。它总是假设最好的情况。每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是更新的时候会判断一下在此期间别人有没有去更新这个数据。可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下的原子类就是使用了乐观锁的一种实现方式CAS算法实现的。
开销对比:
悲观锁的原始开销大于乐观锁。
适用场景:
乐观锁,并发写入少,大量读操作;
悲观锁,并发写入多的情况,临界区代码复杂,竞争激烈等;
公平锁:是指多个线程按照申请锁的顺序来获取锁,先来后到,不能插队!非常公平。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。非常不公平,可以插队!(java默认是非公平锁,因为实际上非公平锁相对公平些)
Lock lock = new ReentrantLock();//默认非公平锁
Lock lock = new ReentrantLock(true);//改成公平锁
可重入锁(递归锁):**可重入锁指的是可以重复调用、可以递归调用的锁,并且不发生死锁。**在外层使用锁之后,在内层仍然可以使用,获取到外层锁自动获取到内层锁,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。
synchronized版:
Lock版:
补充:什么是AQS?
AQS:AbstractQueuedSynchronizer抽象队列式同步器。是除了java自带的synchronized关键字之外的锁机制。AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS的核心思想:如果被请求的共享资源空闲,则将当前请求线程设为有效工作线程,并将共享资源设为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功。
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
什么是自旋锁:
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁与互斥锁的异同点:
相同点:其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
不同点:但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态(即会阻塞)。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
自定义一个锁
public class ZiXuanSuo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==>"+"myLock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread));
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==>"+"myUnLock");
atomicReference.compareAndSet(thread,null);
}
}
测试
public class Test {
public static void main(String[] args) {
ZiXuanSuo lock = new ZiXuanSuo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T1").start();
try {
TimeUnit.SECONDS.sleep(1);//保证T1先执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T2").start();
}
}
死锁是什么?
各自占有自己的锁,又试图去获取对方的锁,这种现象就会造成死锁。
public class TestDeadLock {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new myThread(lockA,lockB),"T1").start();
new Thread(new myThread(lockB,lockA),"T2").start();
}
}
class myThread implements Runnable{
private String lock1;
private String lock2;
public myThread(String lockA, String lockB) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
//死锁就是嵌套锁
synchronized(lock1){
System.out.println(Thread.currentThread().getName()+"lock:"+lock1+"=>get"+lock2);
try{//让线程休眠一下保证线程都各自先拿到一把锁
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"lock:"+lock2+"=>get"+lock1);
}
}
}
}
解决死锁问题
在diea终端
jps -l
定位进程号面试或者工作中排查问题