JUC-总结

JUC–总结

1、进程、线程概念

进程:它是操作系统动态执行的基本单元

线程:cpu将线程作为独立运行和独立调度的基本单位。

白话:

进程:就是操作系统中运行的一个程序,QQ.exe, music.exe, word.exe ,这就是多个进程

线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。

并发 / 并行是什么?

并发和并行, 都可以表示两个或多个任务一起执行

并发,是逻辑上的同时发生, 多个任务交替执行,cpu来调度

并行, 物理上的同时发生,并行的多个任务是真实的同时执行

wait / sleep 的区别

1、来自不同的类

  • 这两个方法来自不同的类

2、有没有释放锁(释放资源)

  • sleep方法没有释放锁,而wait方法释放了锁, 得其他线程可以使用同步控制块或者方法。
  • sleep有时间限制,而wait是无限期的除非用户主动notify

** 、使用范围不同**

  • sleep可以在任何地方使用,wait,notify、notifyAll只能在同步控制方法或者同步控制块里面使用

2、Lock锁

使用 juc.locks 包下的类操作 Lock 锁 + Lambda 表达式

// 启动
new Thread(()-> {
    for (int i = 1; i < 40; i++) saleTicket.saleTicket();
}, "A").start()

class Ticket{
    private Lock lock = new ReentrantLock();
    public void saleTicket(){
        lock.lock(); // 加锁
        try {
            // 业务代码
        } catch (Exception e) { 
            e.printStackTrace(); 
        } finally { 
            lock.unlock();// 解锁
        }
    }
}

Lambda 表达式,让Ticket 这个类更加纯粹!不像传统的 synchronized 需要这个去实现Runnable接口,这样就限制了这个类,耦合性变强了

synchronized 和 lock 区别
  1. synchronized是关键字,Lock是类

  2. synchronized无法判断锁的状态,Lock可以判断是否获取到锁

  3. synchronized会自动释放锁,Lock需在finally中手工释放锁

  4. synchronized的锁可重入、不可中断、非公平;而Lock锁可重入、可判断、可公平

  • // 设置 true or false 来实现(true)公平和(false)非公平锁
    private Lock lock = new ReentrantLock(true);
    
  1. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
private Lock lock = new ReentrantLock();
lock.lockInterruptibly()

void lockInterruptibly() throws InterruptedException

InterruptedException is thrown

  • 在进入该方法时设置中断状态;
  • 或在获取锁时被中断,并且支持获取锁的中断

抛出InterruptedException,并清除当前线程的中断状态

lockInterruptibly() 和 lock()的区别:

  • lockInterruptibly():中断锁,线程等待锁的过程中如果被中断,则会立刻进入该线程,响应中断异常(异常抛出的话就进入上层处理异常)
  • lock():线程在等待锁的过程中,不会响应中断(会一直等待获取锁),但是在中断点(sleep)会响应之前的中断

3、生产者和消费者

生产者和消费者 synchroinzed 版

题目:

  • 现在两个线程,可以操作初始值为0的一个变量
  • 实现一个线程对该变量 + 1,一个线程对该变量 -1
  • 实现交替10次

4个线程,两个加,两个减,生产者和消费者 synchroinzed 版 中如果使用if判断 ,会出现虚假唤醒

public synchronized void increment() throws InterruptedException { 
    // 判断该不该这个线程做 
    if (number==1){ // 如果使用if判断 ,会出现虚假唤醒
        this.wait(); 
    }
    // 干活 
    number++; 			      
    System.out.println(
        Thread.currentThread().getName()+"\t"+number
    ); 
    // 通知 
    this.notifyAll(); 
}

我的理解:锁是锁住的对该对象数据修改的那么部分代码,只允许一个线程去操作,其它的线程暂停执行!因此这里的理解是,当number==0, 两个加线程都进入increment方法,然后经过了 if(number == 1) 的判断,都决定要去number++,但是由于synchronized的存在,只能由其中某一个线程先去执行!此时number == 1了,然后,这个线程操作结束,假设cpu此时恰好调度到了另一个加线程,那么if(number == 1)因为已经在之前判断过了,那么此时线程唤醒的位置就直接从number++开始执行,因此number == 2了,这是不对的!这样就引起了线程不安全问题,虚假唤醒,因为这个后来执行的加线程,么有再次去判断number的值,因此要解决这个问题,我们需要用while来代替if,这样哪怕唤醒,while这个循环是不停判断的,也会再次判断number的值,number == 1,那么就执行 this.wait(),这个加线程不能再往下执行

太多话了,其实就是if只判断一次,那么这个线程假如正好是再判断完之后被暂停的,那么下次唤醒,就会继续往下执行,用while的话,那么会循环判断,保证什么时候唤醒,都进行重新判断

新版生产者和消费者写法

在这里插入图片描述

其实就是将sychronized 改成 lock.lock()

之前线程通信的 this.wait() 改成

private Condition condition = lock.newCondition();
condition.await();

之前线程通信this.notifyAll() 改成 condition.signalAll();

那么为什么有了 老版的 synchronized , this.wait 和 this.notify 这些方法还需要

使用新的lock 锁 以及 condition 类的 await 和 signalAll 这些方法呢?

自然是因为第一 lock锁性能更好!第二condition类还可以完成精确通知顺序访问这个功能。

什么是精确通知顺序访问?

就是实现多线程之间按顺序调用

题目:多线程之间按顺序调用,实现 A->B->C

// 资源类  属性,方法
class Data3{
    private int num = 1; // A1   B2   C3
    // 定义锁
    Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition(); //3个判断,交替执行 A--B--C--A
    private Condition condition2 = lock.newCondition(); //3个判断,交替执行 A--B--C--A
    private Condition condition3 = lock.newCondition(); //3个判断,交替执行 A--B--C--A

    // 3个方法、作业,合3为1
    // +1
    public void print5(){
        // 加锁
        lock.lock();
        try {
            //判断
            while (num!=1){
                condition1.await(); //等待
            }
            // 干活
            for (int i = 1; i <=5 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }

            // 第一个线程通知第二个线程,第二个线程通知第三个....  计数器
            num=2;
            // 通知第二个线程干活,指定谁干活
            condition2.signal();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    public void print10() {
        // 加锁
        lock.lock();
        try {
            //判断
            while (num!=2){
                condition2.await(); //等待
            }
            // 干活
            for (int i = 1; i <=10 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            // 第一个线程通知第二个线程,第二个线程通知第三个....  计数器
            num=3;
            // 通知第二个线程干活,指定谁干活
            condition3.signal();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    public void print15() {
        // 加锁
        lock.lock();
        try {
            //判断
            while (num!=3){
                condition3.await(); //等待
            }
            // 干活 = 业务代码
            for (int i = 1; i <=15 ; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }

            num=1;
            condition1.signal();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }


}

不同的condition实例,放在不同的线程当中,标记着不同的线程的通知信号!

利用标志位num,来决定哪些condition实例对应的线程来等待,

condition1.await(); //等待

condition2.await(); //等待

condition3.await(); //等待

通过通知不同的condition实例condition1.singal()|condition2.singal()|condition3.singal()

来唤醒对应等待的condition1|condition2|condition3所在的线程继续工作!

由此,我们来定义通知不同的condition实例的顺序,也就达到了多线程之间按顺序调用的自定义的方式!这就是精确顺序通知!

4、8锁的现象

其实我觉得,你需要能分的清楚,锁的对象是谁,就可以算是没太大问题了!

  • 被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。
  • 普通方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待
  • 被synchronized修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
  • 被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。

总结: 看这里就好了!

  • 对于普通同步方法,锁的是当前实例对象
  • 对于静态同步方法,锁的是当前的Class对象。
  • 因此你只要确定了,每个线程锁的是不是同一个对象,就能知道,线程间执行的是否会产生阻塞了!就能理解锁的问题

5、集合类不安全

集合不安全,因为多线程同时去修改集合数据,自然是不安全的

list解决方案:

/**
 * 善于总结:
 * 1、 故障现象: ConcurrentModificationException
 * 2、 导致原因: 多线程操作集合类不安全
 * 3、 解决方案:
 *      List<String> list = new Vector<>(); // Vector 是一个线程安全的类,效率低下  50
 *      List<String> list = Collections.synchronizedList(new ArrayList<>()); // 60
 *      List<String> list = new CopyOnWriteArrayList<>(); // JUC 100 推荐使用
 */

写入时复制(CopyOnWrite)思想

读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

也就是读的时候不上锁,写的时候上锁

这也是为什么CopyOnWriteArrayList为什么并发安全且性能比前两种解决方案好的原因!

同理 set解决方案:

/**
 * 善于总结:
 * 1、 故障现象: ConcurrentModificationException 并发修改异常!
 * 2、 导致原因: 并发下 HashSet 存在安全的问题
 * 3、 解决方案:
 *     Set<String> set = Collections.synchronizedSet(new HashSet<>());  60
 *     Set<String> set =new CopyOnWriteArraySet<>();  // 100
 *
 */

同理 map解决方案:但是名字有变化ConcurrentHashMap

// 解决方案:Map<String, String> map = new ConcurrentHashMap<>();

6、Callable

多线程中,第3种获得多线程的方式,Callable。它与Runnable有什么区别呢?

  1. Callable 有返回值
  2. Callable 抛出异常

如果你不使用线程池的方式去使用实现了Callable接口的类创建新线程,而是想要new Thread(Runnable) 的方式去创建新线程,我们就要用到 FutureTask这个类,

因为new Thread(Runnable)只接收实现了Runnable接口的类的实例,因此我们需要通过 new FutureTask(Callable接口实现的类的实例)适配一下,你可以这么理解,等于就是变成了Runnable模式了。

public class Test1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Thread(Runnable)
        // Thread(RunnableFuture)
        // Thread(FutureTask)

        MyThread myThread = new MyThread();
        FutureTask task = new FutureTask(myThread); // 适配类

        // 会打印几次 end
        new Thread(task,"A").start(); // 执行线程
        new Thread(task,"B").start(); // 执行线程。细节1:结果缓存!效率提高N倍

        System.out.println(task.get());// 获取返回值, get()

        // 细节2:task.get() 获取值的方法一般放到最后,保证程序平稳运行的效率,因为他会阻塞等待结果产生!
        // 线程是一个耗时的线程,不重要!

    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("end");

        TimeUnit.SECONDS.sleep(3);

        return 1024;
    }

}

在这里插入图片描述

创建FutureTask,在运行时执行给定的Callable。

FutureTask 实现了 Runnable接口,又可以接收 Callabe,因此可以在这里用来当作适配类

7、常用辅助类

  • CountDownLatch
  • CyclicBarrier
  • Semaphore

CountDownLatch你可以把它称作减法计数器

通过new CountDownLatch(6); 规定计数器要完成任务的数量

通过 new CountDownLatch(6).countDown()需要完成任务数量-1

通过new CountDownLatch(6).await()来阻塞其它线程的执行,任务数量为0,则唤醒刚阻塞的线程继续执行

package com.coding.demo03;

import java.util.concurrent.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");
                // 出去一个人计数器就 -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        countDownLatch.await(); // 阻塞等待计数器归零
        // 阻塞的操作 : 计数器  num++
        System.out.println(Thread.currentThread().getName()+"===END");

    }

}

CyclicBarrier 也可称作加法计数器

package com.coding.demo03;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

// CyclicBarrier 栅栏 加法计数器
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 集齐7个龙珠召唤神龙 ++ 1

        //  public CyclicBarrier(int parties, Runnable barrierAction)
        // 等待cyclicBarrier计数器满,就执行后面的Runnable,不满就阻塞
        CyclicBarrier cyclicBarrier = new CyclicBarrier(8, new Runnable() {
            @Override
            public void run() {
                System.out.println("神龙召唤成功!");
            }
        });

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

        }


    }
}

new CyclicBarrier(8, ()->{})更加类似于一种回调的方式,第一个参数放置需要完成的任务数量。然后在多个线程中放置 cyclicBarrier.await(),当然要放在业务代码的下方,因为这个方法是等待阻塞的。,然后等待完成任务的数量到达8,就执行第二参数里面的内容!

Semaphore 信号量

可以用抢车位例子来描述它的作用

Semaphore semaphore = new Semaphore(3);// 模拟资源类,有3个空车位

semaphore.acquire(); // acquire 得到 多个线程中通过这个来抢车位,没抢到的,则会阻塞下面代码的执行,直到有空车位可以抢,并且抢到,才继续执行下去

semaphore.release(); // 释放这个位置 抢到车位的人,可以通过这个方法释放车位的位置,好让其它没有抢到车位的人有机会入住

// 抢车位
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 模拟6个车,只有3个车位
        Semaphore semaphore = new Semaphore(3); // 3个位置

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                // 得到车位
                try {
                    semaphore.acquire(); // 得到
                    System.out.println(Thread.currentThread().getName()+"抢到了车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放位置
                }

            },String.valueOf(i)).start();
        }

    }
}

信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

8、读写锁

Interface ReadWriteLock

实现了这个接口的类 ReentrantReadWriteLock

独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLockSynchronized 而言都是独占锁。

共享锁(读锁):该锁可被多个线程所持有。

为什么要这个锁的出现呢?自然是为了提高性能, ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的

往往我们实际上修改共享资源,担心的就是修改写入的时候,多线程引发的不安全。但是读取这个资源,其实没有关系,ReentrantLocksychronized 就是不管三七二十一,读写全给你用独占锁锁上,性能自然不如ReentrantReadWriteLock

public class ReadWriteDemo {
    public static void main(String[] args) {

        MyCache2 myCache = new MyCache2();

        // 多个线程同时进行读写
        // 五个线程在写  线程是CPU调度的
        for (int i = 1; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.put(temp+"",temp+"");
            },String.valueOf(i)).start();
        }

        // 五个线程在读
        for (int i = 1; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.get(temp+"");
            },String.valueOf(i)).start();
        }


    }
}

// 线程操作资源类,存在问题的
class MyCache2{

    private volatile Map<String,Object> map = new HashMap<>();

    // ReadWriteLock --> ReentrantReadWriteLock   lock不能区分读和写
    // ReentrantReadWriteLock 可以区分读和写,实现更加精确的控制
    // 读写锁
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 写。独占
    public void put(String key,String value){
        // lock.lock 加锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            // 存在别的线程插队
            System.out.println(Thread.currentThread().getName()+"写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock(); // lck.unlock();
        }
    }

    // 多线程下尽量加锁!

    // 读
    public void get(String key){
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取结果:"+result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

}

当然这里有个问题,关于读锁不加和加上,有什么区别吗?

不加的话,读的线程和写的线程,就会交替执行,加上,则会表现为互斥,所有读的线程先执行,然后执行所有写的线程!这样做的好处,有待大佬告知

9、阻塞队列

当队列是空的,从队列中获取元素的操作将会被阻塞

当队列是满的,从队列中添加元素的操作将会被阻塞

阻塞队列的好处:

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都给你一手包办了。

在这里插入图片描述

加粗部分阻塞队列优先学习

ArrayBlockingQueue由数组结构组成的有界阻塞队列。

LinkedBlockingQueue由链表结构组成的有界(默认值为:integer.MAX_VALUE)阻塞队列。

SynchronousQueue不存储元素的阻塞队列,也即单个元素的队列

PriorityBlockingQueue:支持优先级排序的无界阻塞队列

DelayQueue:使用优先级队列实现的延迟无界阻塞队列。

LinkedTransferQueue:由链表组成的无界阻塞队列。

LinkedBlockingDeque:由链表组成的双向阻塞队列。

阻塞队列API 的使用

在这里插入图片描述

在这里插入图片描述

尽量按组匹配使用

ArrayBlockingQueue 同步队列
// 队列大小 ,有容量的阻塞队列
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
SynchronousQueue 同步队列

SynchronousQueue 没有容量。

与其他的 BlockingQueue 不同,SynchronousQueue是一个不存储元素的 BlockingQueue 。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

/ 同步队列
// 每一个 put 操作。必须等待一个take。否则无法继续添加元素!
public class Test5 {
    public static void main(String[] args) {
        // 不用写参数!
        SynchronousQueue<String> queue = new SynchronousQueue<>();


        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"put 1");
                queue.put("1");
                System.out.println(Thread.currentThread().getName()+"put 2");
                queue.put("2");
                System.out.println(Thread.currentThread().getName()+"put 3");
                queue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();


    }
}

10、线程池

池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销

线程池的优势:

它的主要特点为:线程复用,控制最大并发数,管理线程。

  • 第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低统的稳定性,使用线程池可以进行统一分配,调优和监控。

Java中的线程池是通过 Executor 框架实现的, 该框架中用到了 ExecutorExecutors

ExecutorServiceThreadPoolExecutor 这几个类

三大方法

1、Executors.newFixedThreadPool(int)

执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程

// 池子大小为5
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
    for (int i = 1; i <= 10; i++) threadPool.execute(Runnable);  // 这个来执行线程的任务
} catch(Exception) {
    e.printStackTrace();
} finally {
    // 用完一定要记得关闭
	threadPool.shutdown
}

2、Executors.newSingleThreadExecutor()

只有一个线程

// 不管有多少任务,始终只有一个线程去办理
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
    for (int i = 1; i <= 10; i++) threadPool.execute(Runnable);  // 这个来执行线程的任务
} catch(Exception) {
    e.printStackTrace();
} finally {
    // 用完一定要记得关闭
	threadPool.shutdown
}

3、Executors.newCachedThreadPool();

执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。

可扩容,遇强则强

// 一池N线程,可扩容伸缩
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
    for (int i = 1; i <= 10; i++) threadPool.execute(Runnable);  // 这个来执行线程的任务
} catch(Exception) {
    e.printStackTrace();
} finally {
    // 用完一定要记得关闭
	threadPool.shutdown
}
ThreadPoolExecutor 七大参数

操作:查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )

我们平常使用的时候,也不要去使用executor的三大方法!直接使用这个来创建线程池!

在这里插入图片描述

public ThreadPoolExecutor(
    int corePoolSize, 
    int maximumPoolSize, 
    long keepAliveTime, 
    TimeUnit unit, 
    BlockingQueue<Runnable> workQueue, 
    ThreadFactory threadFactory, 
    RejectedExecutionHandler handler)
{
    // 内部代码
}
  1. corePollSize::核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime :空闲的线程保留的时间。
  4. TimeUnit :空闲线程的保留时间单位。
  5. BlockingQueue< Runnable>:阻塞队列,存储等待执行的任务
  6. ThreadFactory :线程工厂,用来创建线程,一般默认即可
  7. RejectedExecutionHandler ::队列已满,而且任务量大于最大线程的异常处理策略(4大策略)
    1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务
    4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

在这里插入图片描述

用银行办理业务模型来理解这些参数:

  • 1,2窗口是原本就打开的窗口 对应的就是 corePollSize参数 :核心线程数
  • 候客区的大小 对应 BlockingQueue< Runnable>参数 :阻塞队列
  • 3,4,5对应的就是如果候客区满了,能够打开的最大窗口数 也就是 maximumPoolSize参数:最大线程数
  • 全部都满了,也就是窗口和候客区都满了,再进来人,就对应 RejectedExecutionHandler :拒绝策略

思考题:线程是否越多越好?

一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了

如果是一个磁盘或网络为主的程序(IO密集型),此时 线程数等于IO任务数是最佳的。甚至是两倍于任务数

11、四大函数式接口

java.util.function , Java 内置核心四大函数式接口,可以使用lambda表达式

  1. 函数型接口,有一个输入,有一个输出

    • public class Demo01 {
          public static void main(String[] args) {
              // new Runnable(); ()-> {}
      //
      //        Function<String,Integer> function = new Function<String,Integer>() {
                  @Override // 传入一个参数,返回一个结果
                  public Integer apply(String o) {
                      System.out.println("into");
                      return 1024;
                  }
      //        };
              // 链式编程、流式计算、lambda表达式
              Function<String,Integer> function = s->{return s.length();};
              System.out.println(function.apply("abc"));
      
          }
      }
      
  2. 断定型接口,有一个输入参数,返回只有布尔值

    • public class Demo02 {
          public static void main(String[] args) {
      //        Predicate<String> predicate = new Predicate<String>(){
      //            @Override
      //            public boolean test(String o) {
      //                if (o.equals("abc")){
      //                    return true;
      //                }
      //                return false;
      //            }
      //        };
      
              Predicate<String> predicate = s->{return s.isEmpty();};
              System.out.println(predicate.test("abced"));
          }
      }
      
  3. 消费型接口,有一个输入参数,没有返回值

    • public class Demo03 {
          public static void main(String[] args) {
              // 没有返回值,只能传递参数  消费者
      //        Consumer<String> consumer = new Consumer<String> () {
      //            @Override
      //            public void accept(String o) {
      //                System.out.println(o);
      //            }
      //        };
              Consumer<String> consumer =s->{System.out.println(s);};
              consumer.accept("123");
              // 供给型接口  只有返回值,没有参数  生产者
          }
      }
      
  4. 供给型接口,没有输入参数,只有返回参数

    • public class Demo04 {
          public static void main(String[] args) {
      //        Supplier<String> supplier =  new Supplier<String>() {
      //            @Override
      //            public String get() {
      //                return "aaa";
      //            }
      //        };
      
              Supplier<String> supplier = ()->{return "aaa";};
              System.out.println(supplier.get());
          }
      }
      

函数接口最大的好处是啥?就是可以使用lamda表达式,代替这些接口的写法,从而大大降低代码量!

12、Stream流式计算

流(Stream)到底是什么呢?

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

就是操作集合的数据的方法

特点:

  • Stream 自己不会存储元素。
  • Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。
  • Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

在这里插入图片描述

list.stream()
    .filter(u->{return u.getId()%2==0;})
    .filter(u->{return u.getAge()>24;})
    .map(u->{return u.getUsername().toUpperCase();})
    .sorted((o1,o2)->{return o2.compareTo(o1);})
    .limit(1)
    .forEach(System.out::println);

13、分支合并

思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

主要有两步:第一、任务切分;第二、结果合并

线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

工作窃取

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:

在这里插入图片描述

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。

  1. 核心类**ForkJoinPool**
  2. -> ForkJoinPool拥有一个内部类 workQueue (拥有工作窃取功能)
  3. -> workQueue 有一个ForkJoinWorkerThreadowner 线程,加一个这个线程需要处理的任务ForkJoinTask<?>[] array的任务队列,那么新任务,就是加入到这个队列当中!
  4. -> ForkJoinTask代表运行在ForkJoinPool中的任务。主要有三个方法,两个子类
    1. 三个方法
      • fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务
      • join() 当任务完成的时候返回计算结果。
      • invoke() 开始执行任务,如果必要,等待计算完成。
    2. 两个子类
      • RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
      • RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
ublic class ForkJoinDemo extends RecursiveTask<Long> {

    private Long start;
    private Long end;
    private static final Long temp = 10000L; // 临界值

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    // 计算
    @Override
    protected Long compute() {
        // 如果这个数 超过中间值,就分任务计算!
        if (end-start<=temp){ // 正常计算
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }else {
            // 获取中间值
            long middle = (end + start) / 2;
            ForkJoinDemo right = new ForkJoinDemo(start, middle);// 第一个任务
            right.fork();
            ForkJoinDemo left = new ForkJoinDemo(middle+1, end);// 第一个任务
            left.fork();

            // 合并结果
            return right.join() + left.join();
        }
    }
}
public class ForkJoinTest {
    public static void main(String[] args) {
        // test1();  // 10582 ms    60
        // test2();  // 9965 ms    90
        // test3();  // 158 ms   101
    }

    // 正常测试
    public static void test1(){
        long start =  System.currentTimeMillis();

        Long sum = 0L;
        for (Long i = 0L; i <= 10_0000__0000 ; i++) {
            sum +=i;
        }
        long end =  System.currentTimeMillis();
        System.out.println("time:"+(end-start)+" sum:"+sum);
    }


    // ForkJoin测试
    public static void test2(){
        long start =  System.currentTimeMillis();

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L,10_0000__0000L);
        Long sum = forkJoinPool.invoke(forkJoinDemo);

        long end =  System.currentTimeMillis();
        System.out.println("time:"+(end-start)+" sum:"+sum);
    }



    // Stream并行流测试
    public static void test3(){
        long start =  System.currentTimeMillis();

        long sum = LongStream.rangeClosed(0, 10_0000__0000).parallel().reduce(0, Long::sum);

        long end =  System.currentTimeMillis();
        System.out.println("time:"+(end-start)+" sum:"+sum);
    }

}

大数据处理下, 并行流自然是最快的,第二是ForkJoin,第三是普通循环

但是小数据下,普通循环才是最快的!

14、异步回调

这块不太理解,先放例子把!

Future它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中出发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要等待耗时的操作完成。

CompletableFuturerunAsync只是简单的异步执行一个线程,但是它将返回一个CompletableFuture,有了这个CompletableFuture,可以重新组装和调配,这是和一个普通Runnable不同之处。

package com.coding.stream;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

// CompletableFuture 异步回调, 对将来的结果进行结果,ajax就是一种异步回调!
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        // 多线程也可以异步回调
//
//        // 没有返回结果,任务执行完了就完毕了! 新增~
//        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
//            // 插入数据,修改数据
//            System.out.println(Thread.currentThread().getName() + " 没有返回值!");
//        });
//
//        System.out.println(voidCompletableFuture.get());

        // 有返回结果  ajax。 成功或者失败!
        CompletableFuture<Integer> uCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 有返回值!");
            // int i = 10/0;
            return 1024;
        });

        // 有一些任务不紧急,但是可以给时间做!占用主线程!假设这个任务需要返回结果!

        System.out.println(uCompletableFuture.whenComplete((t, u) -> { // 正常编译完成!
            System.out.println("=t==" + t); // 正常结果
            System.out.println("=u==" + u); // 信息错误!
        }).exceptionally(e -> { // 异常!
            System.out.println("getMessage=>" + e.getMessage());
            return 555; // 异常返回结果
        }).get());

    }
}

public class Future {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> vcompletableFuture = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "没有返回, update mysql ok");
        });
        vcompletableFuture.get();
        System.out.println("111111");

        CompletableFuture firstFuture = CompletableFuture.supplyAsync(()->{
            System.out.println("start to execute supplyAsync");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end to execute supplyAsync");
            return "firstFuture";

        }).thenAccept(otxGroupData -> {
            System.out.println("start to execute thenAccept=====>"+otxGroupData);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end to execute thenAccept");
        }).exceptionally((e) -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e1) {
                e.printStackTrace();
            }
            return null;
        });

        try {
            firstFuture.get();
            System.out.println("main end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

15、JMM

JMM即为JAVA 内存模型(java memory model),JMM规定了内存主要划分为主内存工作内存两种。

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所

每条线程拥有各自的工作内存工作内存中的变量是主内存中的一份拷贝线程对变量的读取和写

入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个

线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了

一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

JMM 关于同步的规定:

1、线程解锁前,必须把共享变量的值刷新回主内存

2、线程加锁前,必须读取主内存的最新值到自己的工作内存

3、加锁解锁是同一把锁

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类

型的变量来说,load、store、read和write操作在某些平台上允许例外)

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

在这里插入图片描述

线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用

Volatile关键字即可!volatile 保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,

在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则 :

  • 线程对变量进行修改之后,要立刻回写到主内存。
  • 线程对变量读取的时候,要从主内存中读,而不是缓存。

16、volatile

volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:

1、保证可见性

2、不保证原子性

3、禁止指令重排

代码验证可见性:

public class Test1 {
   // volatile 不加volatile没有可见性
    // 不加 volatile 就会死循环,这里给大家将主要是为了面试,可以避免指令重排
    // volatile 读取的时候去主内存中读取在最新值!
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException { // Main线程

        new Thread(()->{ // 线程A 一秒后会停止!  0
            while (num==0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        num = 1;
        System.out.println(num);

    }

}

验证 volatile 不保证原子性

原子性理解:不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。

在这里插入图片描述

public class JMMVolatileDemo02 { 
    private volatile static int num = 0; 
    public static void add(){ 
        // 因为这一步操作,实际上底层是有很多步操作的,由于这一步的非原子性操作(上图所示),因此下面
        // 多线程执行的时候,会出现线程不安全情况!
        num++;  
    }
    // 结果应该是 num 为 2万,测试看结果 
    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(); 
        } 
        // 需要等待上面20个线程都全部计算完毕,看最终结果 
        while (Thread.activeCount()>2){ 
            // 默认一个 main线程 一个 gc 线程 
            Thread.yield(); 
        }
        System.out.println(Thread.currentThread().getName()+" "+num); 
    } 
}

num++ 在多线程下是非线程安全的,如何不加 synchronized解决?

// 遇到问题不要着急,要思考如何去做!
public class Test2 {
	// 为解决上面num++的非原子性操作,我们采用AtomicInteger来进行操作
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void  add(){
        num.getAndIncrement(); // 等价于 num++
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add();  // 20 * 1000 = 20000
                }
            },String.valueOf(i)).start();
        }

        // main线程等待上面执行完成,判断线程存活数   2
        while (Thread.activeCount()>2){ // main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+" "+num);

    }
}

指令重排

volatile 实现了禁止指令重排优化,从而避免 多线程环境下程序出现乱序执行的现象。

17、深入单例模式

1、饿汉式

饿汉式是最简单的单例模式的写法,保证了线程的安全。

但是在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如

果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?

public class Hungry {
	// 这种单例模式的缺点就是,浪费内存空间,相较于那种用到才去创建实例的模式
    private byte[] data1 = new  byte[10240];
    private byte[] data2 = new  byte[10240];
    private byte[] data3 = new  byte[10240];
    private byte[] data4 = new  byte[10240];

    // 单例模式核心思想,构造器私有!
    private Hungry(){

    }
	// 就是不管三七二十一直接在内部创建实例
    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

2、懒汉式

加一个判断,多线程操作会有问题!

public class LazyMan { 
    private LazyMan() { 
        System.out.println(Thread.currentThread().getName()+"Start"); 
    }
    private static LazyMan lazyMan; 
    public static LazyMan getInstance() { 
        if (lazyMan == null) { 
            lazyMan = new LazyMan(); 
        }
        return lazyMan; 
    }// 测试并发环境,发现单例失效 
    public static void main(String[] args) { 
        for (int i = 0; i < 10; i++) { 
            new Thread(()->{ 
                LazyMan.getInstance();
            }).start(); 
        } 
    } 
}

进行加锁操作,可避免多线程操作问题!

public class LazyMan { 
    private LazyMan() { }
    private static LazyMan lazyMan; 
    public static LazyMan getInstance() { 
        if (lazyMan == null) { // 这一层if 决定时候要不要加锁,如果实例都有了,还加锁干嘛
            // 多加一层锁,避免多线程操作问题!
            synchronized (LazyMan.class) { 
                if (lazyMan == null) { // 这一层if 才是去判断是否存在LazyMan实例
                    // 但是这个new LazyMan()的操作又是非原子性的,又会有问题出现
                	lazyMan = new LazyMan(); 
            	} 
            } 
        }
        return lazyMan; 
    } 
}

lazyMan = new LazyMan();

不是原子性操作,至少会经过三个步骤:

  1. 分配对象内存空间
  2. 执行构造方法初始化对象
  3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第

二步),此时线程B又进来了,发现lazyMan已经不为空了直接返回了lazyMan,并且后面使用了返回

的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错

误,所以就有了下面一种单例模式

// 这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排
private volatile static LazyMan lazyMan;

    public static  LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan(); // 可能存在指令重排!
                    /*
                    A:  1    3    2
                    B:  lazyMan = null ;
                    1. 分配对象的内存空间
                    2. 执行构造方法初始化对象
                    3. 设置实例对象指向刚分配的内存的地址, instance = 0xfffff;
                     */
                }
            }
        }
        return lazyMan;
    }

3、静态内部类

还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。

public class Holder { 
    private Holder() { }
    public static Holder getInstance() { 
        return InnerClass.holder; 
    }
    private static class InnerClass { 
        private static final Holder holder = new Holder(); 
    } 
}

4、万恶的反射

前面三种模式,都可以被反射破坏,因此还要针对反射特殊处理,

public class LazyMan {
	
    // 增加标志位
    private static boolean flag = false;

    private LazyMan(){
        // 防止反射破坏
        synchronized (LazyMan.class){
            if (flag==false){
                flag = true;
            }else {
                throw new RuntimeException("不要试图使用反射破坏单例模式");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static  LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan(); // 可能存在指令重排!
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {

        //LazyMan instance1 = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructors.setAccessible(true); // 无视 private 关键字

        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);

        LazyMan instance1 = declaredConstructors.newInstance();

        flag.set(instance1,false);


        LazyMan instance2 = declaredConstructors.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

增加的标志位 flag来防止反射,但是如果flag名字被人知道,直接反射修改flag,还是不安全,因此来到了枚举的方案

5、枚举

枚举天然带了防止反射破坏单例模式的功能,源码就不看了!

public enum EnumSingleton { 
    INSTANCE; 
    public EnumSingleton getInstance(){ 
        return INSTANCE; 
    } 
}

枚举是目前最推荐的单例模式的写法,因为足够简单,不需要开发自己保证线程的安全,同时又可以有

效的防止反射破坏我们的单例模式

18、深入理解CAS

CAS : 比较并交换

一句话:真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!

/* 
* public final boolean compareAndSet(int expect, int update) { 
*   return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
* }
* 底层其实是调用了unsafe类的方法,unsafe的方法都是native方法,用来直接操作内存的,是原子指令
* 不会造成数据不一致的问题
*/
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        // compareAndSet  简称 CAS 比较并交换!
        // compareAndSet(int expect, int update)  我期望原来的值是什么,如果是,就更新

        //  a
        System.out.println(atomicInteger.compareAndSet(5, 2020)+"=>"+atomicInteger.get());

        // c  偷偷的改动
        System.out.println(atomicInteger.compareAndSet(2020, 2021)+"=>"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021, 5)+"=>"+atomicInteger.get());


        //  b
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"=>"+atomicInteger.get());

    }
}

CAS 的缺点

1、循环时间长开销很大。

可以看到源码中存在 一个 do…while 操作,如果CAS失败就会一直进行尝试。

2、只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:

对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。

3、引出来 ABA 问题???

19、原子引用

ABA问题怎么产生的?

CAS算法实现一个重要前提:需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间

差内会导致数据的变化

比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进

行了一些操作将值变成了B,然后线程two又将 V位置的数据变成A,这时候线程one进行CAS操作发现内

存中仍然是A,然后线程one操作成功。

要解决ABA问题,我们就需要加一个版本号

利用AtomicReference (也就是原子引用)类加版本号

package com.coding.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.ReentrantLock;

/**
 * AtomicReference 原子引用
 * AtomicStampedReference 加了时间戳  类似于乐观锁! 通过版本号
 */
public class CASDemo2 {
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        new Thread(()->{
            //1 、 获得版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println("T1 stamp 01=>"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(100,101,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

            System.out.println("T1 stamp 02=>"+atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101,100,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

            System.out.println("T1 stamp 03=>"+atomicStampedReference.getStamp());

        },"T1").start();


        new Thread(()->{
            // GIT  看到数据被动过了!

            //1 、 获得版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println("T1 stamp 01=>"+stamp);

            // 保证上面的线程先执行完毕!
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean b = atomicStampedReference.compareAndSet(100, 2019,
                    stamp, stamp + 1);
            System.out.println("T2 是否修改成功:"+b);
            System.out.println("T2 最新的stamp:"+stamp);
            System.out.println("T2 当前的最新值:"+atomicStampedReference.getReference());
        },"T2").start();

    }


}

20、Java锁

1、公平锁非公平锁
  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

并发包中的 ReentrantLock 的创建可以指定构造函数 的 boolean类型来得到公平锁或者非公平锁,默认是非公平锁!对于Synchronized而言,也是一种非公平锁

2、可重入锁

可重入锁最大的作用就是避免死锁

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可以进入里面的房间了

public class RTLock {

    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendSMS();
        },"T1").start();

        new Thread(()->{
            phone.sendMail();
        },"T2").start();
    }

}

class Phone {

    public synchronized void sendSMS(){ // 外面的锁
        System.out.println(Thread.currentThread().getName()+" sendSMS");
        sendMail();  // 这个方法本来也是被锁的,但是由于获得了外面的锁,所以这个锁也获得了!
    }
    public synchronized void sendMail(){
        System.out.println(Thread.currentThread().getName()+" sendMail");
    }
}
3、自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下

文切换的消耗,缺点是循环会消耗CPU

compareAndSwapInt 就是采用了自旋锁的方式

自定义一个自旋锁:

// coding自己定义的自旋锁!
public class MyLock {

    // 原子引用 CAS
    AtomicReference<Thread> atomicReference = new AtomicReference<>();


    // 加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"==mylock");

        // 期望是空的没有加锁, thread         // 自旋,(循环!)
        while (atomicReference.compareAndSet(null,thread)){// cas

        }

    }

    // 解锁
    public void myUnlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"==myUnlock");
    }


}

这里进行测试自己自定义的自旋锁!

package com.coding.lock;

import java.util.concurrent.TimeUnit;

public class SpinLockDemo {

    public static void main(String[] args) {
        MyLock myLock = new MyLock();

        new Thread(()->{
            myLock.myLock();

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            myLock.myUnlock();

        },"T1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            myLock.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myLock.myUnlock();
        },"T2").start();

    }

}

4、死锁

在这里插入图片描述

产生死锁主要原因:

1、系统资源不足

2、进程运行推进的顺序不合适

3、资源分配不当

产生死锁的例子:

package com.coding.lock;
import java.util.concurrent.TimeUnit;
public class DeadLock {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new HoldLockThread(lockA,lockB),"T1").start();
        new Thread(new HoldLockThread(lockB,lockA),"T2").start();
    }
}


class HoldLockThread implements  Runnable{

    private String lockA;
    private String lockB;

    public HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        // A 想要拿B
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // B想要拿到A
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get" + lockA);
            }
        }
    }
}

平常开发中如何分析出哪里产生了死锁

  • 1、查看JDK目录的bin目录
  • 2、使用 jps -l 命令定位进程号
  • 3、使用 jstack 进程号 找到死锁查看, 分析堆栈信息

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值