Java多线程之JUC

JUC

文章目录

1、什么是JUC?

Java.util.concurrent 在并发编程中使用的工具类

在这里插入图片描述

1.1、进程和线程

进程:是操作系统中正在运行的应用程序,是程序的集合,是操作系统资源分配的基本单位。一个进程往往可以包含多个线程,至少要1个。

线程:是进程的执行单元或说执行场景,用来执行具体的任务和功能,是CPU调度和分派的基本单位。

进程是操作系统调度和资源分配的最小单位,线程是CPU调度和分派的最小单位。

java默认有几个线程?至少2个:main主线程,GC垃圾回收线程

**java真的可以开启线程吗?**开不了。

它是先通过调用start()方法:public synchronized void start()

该方法里先是把该线程加入到一个线程组中:group.add(this)

然后调用了start0()方法,这是个本地方法,它是调用底层的C++程序
在这里插入图片描述

要明白一点,java是无法直接操作硬件的。而是通过这些本地方法去调用底层的c或c++程序来操作硬件。

1.2、并发&并行

并发(多线程操作同一资源)

  • CPU一核,模拟出多线程,多个线程间频繁切换,形成并发假象。

并行(同一时间点多个线程同时执行,真正的并发)

  • CPU多核,同一时间点多个线程同时执行,各自执行个的。

并发编程的本质:充分利用CPU的资源,提高程序的执行效率

java线程有几个状态?

public enum State {

        NEW,//新建状态

        RUNNABLE,//运行状态

        BLOCKED,//阻塞状态

        WAITING,//等待(死死地等)

        TIMED_WAITING,//超时等待(超过一定时间就不等了)

        TERMINATED;//终止状态
    }

在这里插入图片描述

  1. 新建状态(New):新建一个线程对象。

  2. 就绪/可运行状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获得CPU并执行程序代码。

  4. 阻塞状态(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()

1.3、wait和sleep的区别

  1. 来自不同的类:

    wait是Object的方法

    sleep是Thread的静态方法

  2. 关于锁的释放:

    wait会释放锁

    sleep不释放锁(抱着锁睡)

  3. 使用范围:

    wait必须在同步代码块中

    sleep可以在任何地方睡

2、锁(*重点)

Java中隐式锁:synchronized;显式锁:Lock

2.1、synchronized和Lock的区别
  1. 出身不同:

    synchronized是Java中的关键字,是由JVM来维护的。是JVM层面的锁。

    Lock是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。

    sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的

    而lock是通过调用对应的API方法来获取锁和释放锁的。

    概述,可以把sync理解为官二代或者是星二代,从娘胎出来自带光环的。Lock就是我们普通努力上进的人。

  2. 使用方式不同

    Sync是隐式锁。Lock是显示锁。

    所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。

    我们大家都知道,在使用sync关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当sync代码块执行完成之后,系统会自动的让程序释放占用的锁。sync是由系统维护的,如果非逻辑问题的话话,是不会出现死锁的。

    在使用Lock的时候,使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合try/finaly语句块来完成。

    用生活中的一个case来形容这个不同:官二代和普通人的你在进入机关大院的时候待遇。官二代不需要出示什么证件就可以进入,但是你需要手动出示证件才可以进入。

  3. 等待是否可中断

    sync是不可中断的。一个线程获得了锁,其他线程,必须傻傻等待该线程释放锁,不能去中断该线程,除非该线程抛出异常或者正常运行完成。其他线程才能获得锁。

    Lock可以中断的。中断方式:

    ​ 1:调用设置超时方法 tryLock(long timeout ,timeUnit unit)

    ​ 2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断

    生活中小case来理解这一区别:官二代一般不会做饭。都会去餐厅点餐等待着餐厅出餐。普通人的你既可以去餐厅等待,如果等待时间长的话,你就可以回去自己做饭了。

  4. 加锁的时候是否可以公平

    java默认的是非公平锁。为什么?因为非公平锁比较公平。通常情况下,非公平锁的吞吐量要比公平锁的吞吐量高。

    ReentrantLock实现了Lock接口,加锁和解锁都需要显式写出,注意一定要在适当时候unlock
    和synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
    ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
    
    公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
    非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
        
    ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
    

    sync:非公平锁。

    lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。

    ​ true:公平锁

    ​ false:非公平锁

    生活中小case来理解这个区别:官二代一般都不排队,喜欢插队的。普通人的你虽然也喜欢插队。但是如果遇到让排队的情况下,你还是会排队的。

  5. 锁绑定多个条件来condition

    sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。

    Lock:用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。lock的粒度更细。

  6. 从性能比较

    在这里插入图片描述

  7. 从使用锁的方式比较

    在这里插入图片描述
    在这里插入图片描述

2.2、生产者&消费者问题

/*
* 生产者消费者模式
* */
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();
        }
        
    }
}

2.3、那么锁是什么?如何判断锁的是谁?(8锁现象)

记住一点:在java多线程中,

一个对象一把锁,100个对象100把锁;(synchronized加在方法上锁的是this,即这个对象)

一个类只有一把锁; (synchronized加在静态方法上锁的是类)。

栗子

3、集合类不安全

ArrayList是非线程安全的。在多线程并发的情况会出现问题(比如ConcurrentModificationException并发修改异常),怎么变得安全呢?有一下三种方法:

第三种就是JUC方式,用List list = new CopyOnWriteArrayList();

在这里插入图片描述

CopyOnWriteArrayList比Vactor牛在哪?看源码:

  • Vactor的add方法中用的是synchronized,而synchronized是重锁,只要有synchronized的方法它的效率就会比较低。

在这里插入图片描述

  • CopyOnWriteArrayList的add方法中用的是lock锁,并且它是通过写入时赋值的方式添加元素,(即先复制一份老数组,把新元素加进来后再把整个数组set回去的方式添加元素)

在这里插入图片描述

写入时复制,这是一种 读写分离 的思想,因为由源码可知它是把原数组复制到一个新数组当中,然后把要添加的元素添加到新数组中,最后再把新数组set回去。当然这个过程进行了加锁以保证写的时候只有一个线程,而写的过程中(即添加元素的过程),其他线程就可以并发地读原来旧的数组,这就实现了读写分离。那么此时并发下就不会出错了。

内部是调用了Arrays.copyOf()方法,而Arrays.copyOf()底层又是调用了System.arraycopy(),这个方法底层调用了一个本地方法,是复制了一个数组。

4、Callable接口

因为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<V>()).start();
        // new Thread(new FutureTask<V>( 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;//返回值
    }
}
  • 优点:call方法可以有返回值,call方法可以抛异常,结果有缓存
  • 缺点:调用get()方法获取返回值时,可能会产生阻塞,导致程序执行效率降低。

5、常用的辅助类(必会)

5.1、CountDownLatch(减法计数器)

//减法计数器
//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 ,所有被阻塞的线程释放,门闩打开程序可以向下执行。

5.2、CyclicBarrier(加法计数器)

//允许一组线程全部等待彼此到达共同障碍点的同步辅助。
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();
        }
    }
}
5.3、semaphore(信号量)

计数信号量。信号量通常用于限制线程数(限流),信号量维持一组许可证,每个线程必须从信号量获取许可证,再去使用项目,当线程完成该项目后,它将返回到池中,并将许可证返回到信号量允许另一个线程获取该项目。

比如有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();
        }
    }
}

在这里插入图片描述

6、读写锁(共享锁,排他锁)

在这里插入图片描述

/*
* 独占锁(写锁)一次只能被一个线程占有
* 共享锁(读锁)多个线程可以同时占有
* */
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();//释放读锁
        }
    }
}

在这里插入图片描述

7、阻塞队列

7.1、对阻塞队列的理解

在这里插入图片描述

7.2、什么情况下我们会使用阻塞队列: 多线程并发处理,线程池!
7.3、学会使用队列

添加,移除

四组API

方式抛出异常不抛出异常阻塞等待超时等待
添加add()offer()putoffer(“a”,2,TimeUnit.SECONDS)
移除remove()polltakepoll(2,TimeUnit.SECONDS))
检测队首元素element()peek--
7.4、SynchronousQueue同步队列

没有容量:进去一个元素,必须等待取出来后,才能再往里放一个元素!

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();
    }
}

在这里插入图片描述

8、线程池(*重点)

线程池:三大方法、7大参数、4种拒绝策略

8.1、池化技术

程序的运行,本质:占用系统的资源!优化资源的使用!=>池化技术

线程池,连接池,内存池,常量池,对象池…等等。因为创建、销毁十分浪费资源,所以有了池化技术。

池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

8.2、线程池的好处
  1. 降低资源的消耗
  2. 提高响应的速度
  3. 因为线程放在了池子里,方便管理

线程池就是线程复用、可以控制最大并发数

8.1、三大方法

使用 **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();
        }
    }
}
8.2、七大参数

三大方法的源码分析:

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.3、手动创建一个线程池
image-20200822172923560

​ 超过8时就会执行拒绝策略,在这里会抛出异常。

8.4、四种拒绝策略(*)
  • AbortPolicy (默认): (银行满了)再有新任务来,不处理,直接抛RejectExecutionException异常阻止系统正常运行。
  • CallerRunsPolicy:“调用者运行的一种机制”。该策略不会抛弃任务,也不会抛出异常,而是将新来的任务回退给调用者,让调用者去处理。
  • DiscaredPolicy:队列满了,该策略默默地丢弃新来的任务,不予任何处理,也不抛出异常。(如果允许任务丢失,这是最好的一种策略。)
  • DiscaredOldestPolicy:队列满了,当新来任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的任务,再尝试把这个新任务添加进去。也不会抛出异常。

自定义拒绝策略
使用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");
		       }
        });
8.5、 最大核心线程池大小设置多大合适?(调优)

两种情况:IO密集型,CPU密集型

  • CUP密集型

    看电脑是几核cpu,java并发是一个线程占用一个CPU,几核CPU就支持几个线程同时执行,这样能充分发挥cpu性能

    查看CPU核数也很容易:1、任务管理器->性能:可见我的是12个

在这里插入图片描述

2、打开计算机管理->设备管理器:

在这里插入图片描述

但是每个用户的电脑情况不一样,我们肯定不能把这个值写死,怎么办呢?通过代码获取:

max = Runtime.getRuntime().availableProcessors();//获取CPU的核数
  • IO密集型

    IO十分耗时又占用资源的!程序中有几个IO任务就相当于有几个大型任务。

    所以设置最大核心线程池大小的时候,我们就要大于大型任务的个数,以保证有多余的线程去执行其他的任务。

    比如我们程序中有 15 IO任务,那 最大核心线程池大小 > 15

9、四大函数式接口(必需掌握)

新时代java程序员必会:lambda表达式、链式编程、函数式接口、Steam流式计算

9.1、什么是函数式接口?(只有一个方法的接口

例如比较典型的就是Runnable接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
} 
//java中函数式接口超级多FunctionalInterface
//它能简化编程模型,在新版本的框架底层大量应用。
//forEach(消费者类型的函数式接口)

在这里插入图片描述

9.2、Function 函数式接口

在这里插入图片描述

/*
* 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流式计算!

10、Stream流式计算

10.1、什么是流式计算?

大数据的计算模式主要分为批量计算(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(消费者类型的函数式接口)

11、ForkJoin(分支合并)

11.1、什么是ForkJoin?

并行执行任务!提高效率。必须在大数据量下才会使用!

把大任务拆分为多个子任务。 把子任务的结果合并成最初问题的结果(分治策略)

在这里插入图片描述

11.2、ForkJoin的特点:工作窃取

空闲线程主动去执行高负载线程的任务。从而提高效率。

怎么提高效率的?它内部实际上维护的是双端队列。如下图A可以从上面取,B可以从下面取。两端可以同时取。
在这里插入图片描述

11.3、怎么使用ForkJoin呢?

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();//合并结果
        }
    }
}

在这里插入图片描述

12、异步回调

在这里插入图片描述

13、JMM

什么是JMM?

在这里插入图片描述

8种操作

在这里插入图片描述

14、Volatile(轻量级的同步机制)

  • 保证可见性
  • 禁止指令重排(保证有序性)
  • 不保证原子性

深入理解volatile关键字:https://blog.csdn.net/weixin_43587472/article/details/106342353

在这里插入图片描述

我们知道synchronized和Lock都能保证原子性,而volatile不能保证原子性。但是有没有什么方法可以既不使用synchronized和Lock

又能保证原子性呢?

使用原子类,解决原子性问题

在这里插入图片描述

这些类的底层都是调用的本地方法和操作系统之间挂钩,所以效率很高效!在内存中修改值!

15、彻底玩转单例模式

饿汉式;懒汉式

//饿汉式单例
//浪费空间,无论你用不用类一加载加创建好对象了。
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);

在这里插入图片描述

16、深入理解CAS

16.1、什么是CAS ?

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后的值。

在这里插入图片描述

16.2、Unsafe类

在这里插入图片描述

我们知道AtomicInteger有个getAndIncrement方法。该方法底层调用的是Unsafe类的getAndAddInt方法,而getAndAddInt内部是一个自旋锁

在这里插入图片描述

CAS:比较当前工作内存中的值和主存中的值,如果这个值是期望值,则执行操作!如果不是会一直循环!

缺点:

1,循环会耗时

2,一次性只能保证一个共享变量的原子性

3,会存在ABA问题

16.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)在比较并交换的过程加上时间戳。

17、原子引用

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();
    }
}

在这里插入图片描述

18、各种锁的理解

在这里插入图片描述

18.1、乐观锁&悲观锁
18.1.1、悲观锁(互斥同步锁)

顾名思义它是悲观的,它总是假设最坏的情况,当每次要对数据进行修改的时候都认为会发成并发冲突,别人会修改,所以为了避免冲突同时被其他人修改,每次拿数据的时候都会上锁以防止并发。这样别人想拿到这个数据就会阻塞直到释放锁后拿到这个锁。JAVA中synchronized和Lock都是采用的悲观锁。(具有强烈的独占和排他特性)

18.1.2、悲观锁主要分为共享锁和排他锁
  • 共享锁 【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个线程或事务对于同一数据可以共享一把锁,(相当于对于同一把门,它拥有多个钥匙一样)都能访问到数据,但是只能读不能修改和删除。一个线程或事务对数据A加上锁后,其他线程或事务只能对数据A加共享锁,不能加排它锁。共享锁下,其他线程可以并发读取查询数据,不能修改、增加、删除数据(资源共享)

  • 排他锁【Exclusive lock】又称为写锁或独占锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果线程T获取了数据A排他锁,线程T就独占数据A,只允许线程T对数据A进行读取和修改,其他线程不能再获取数据A的任何锁,包括共享锁和排他锁,直到线程T释放该锁。(独占资源)

18.1.3、悲观锁有哪些劣势?
  1. 阻塞、唤醒。因为阻塞唤醒会涉及到排队和时间,从而导致性能下降。(性能劣势)
  2. 永久阻塞或死锁。比如现在持有锁的线程被永久阻塞了,比如说是无限循环,这会导致这个持有锁的线程永远不会释放锁
  3. 优先级。阻塞是优先级越高,持有锁的优先级就越低。从而导致优先级翻转。

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

18.1.3、乐观锁(非互斥同步锁)

顾名思义它是乐观的,它采取了更加宽松的加锁机制。它总是假设最好的情况。每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是更新的时候会判断一下在此期间别人有没有去更新这个数据。可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下的原子类就是使用了乐观锁的一种实现方式CAS算法实现的。

18.1.4、乐观锁悲观锁对比
  • 开销对比:

    悲观锁的原始开销大于乐观锁。

  • 适用场景:

    乐观锁,并发写入少,大量读操作;

    悲观锁,并发写入多的情况,临界区代码复杂,竞争激烈等;

18.2、公平锁、非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,先来后到,不能插队!非常公平。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。非常不公平,可以插队!(java默认是非公平锁,因为实际上非公平锁相对公平些)

Lock lock = new ReentrantLock();//默认非公平锁
Lock lock = new ReentrantLock(true);//改成公平锁
    

在这里插入图片描述

18.3、可重入锁

可重入锁(递归锁):**可重入锁指的是可以重复调用、可以递归调用的锁,并且不发生死锁。**在外层使用锁之后,在内层仍然可以使用,获取到外层锁自动获取到内层锁,并且不发生死锁(前提得是同一个对象或者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的衍生物

在这里插入图片描述

18.4、自旋锁

什么是自旋锁:

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁与互斥锁的异同点:

相同点:其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

不同点:但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态(即会阻塞)。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

自定义一个锁

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();
    }
}

在这里插入图片描述

18.5、死锁

死锁是什么?

各自占有自己的锁,又试图去获取对方的锁,这种现象就会造成死锁。

在这里插入图片描述

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终端

  1. 使用jps -l定位进程号

在这里插入图片描述

  1. 使用jstack 进程号查看堆栈信息

    在这里插入图片描述

面试或者工作中排查问题

  1. 查看日志
  2. 查看堆栈信息
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值