JUC

只是个大概学习框架,都需要具体学的…
这个来自狂神的JUC视频
视频资料pdf链接:
链接:https://pan.baidu.com/s/1szA37eDsReakkIJKpfQ8Mg
提取码:vytr

MyProblemList

  1. 为什么一个类一定要有构造方法
  2. CopyOnWriteArrayList是不是只是把Vector中的synchronized锁换成了Lock而已

1.什么是JUC

在这里插入图片描述就是java.util.concurrent在并发编程中使用的工具类

2.线程和进程

进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程? 2 个 mian、GC线程
开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言,线程的实现主要通过三种方式:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了

// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();

获得多线程的方法有几种?
传统的是继承thread类和实现runnable接口,
java5以后又有实现callable接口java的线程池 获得

并发、并行

并发编程:并发、并行
并发(多线程操作同一个资源)

  • CPU 一核,模拟出来多条线程,天下武功,唯快不破,快速交替

并行(多个人一起行走)

  • CPU 多核,多个线程可以同时执行;线程池
public class Test1 {
	public static void main(String[] args) {
 	// 获取cpu的核数
 	// CPU 密集型,IO密集型
 	System.out.println(Runtime.getRuntime().availableProcessors()); 
 	}
 }

并发编程的本质:充分利用CPU资源

线程有几个状态

  • 6个状态, 可以通过Thread.State点进去查看
 public enum State {
        //新生
        NEW,
        //运行
        RUNNABLE,
        //阻塞
        BLOCKED,
        //等待,死死地等
        WAITING,
        //超时等待
        TIMED_WAITING,
        //终止
        TERMINATED;
    }

wait和sleep的区别

  1. 来自不同的类
    wait=>object
    sleep=>Thread

    为什么wait是Object的?
    因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object

  2. 关于锁的释放
    wait会释放锁,sleep抱着锁睡觉,不会释放锁

  3. 使用的范围是不同的
    wait必须在同步代码块中
    sleep可以在任何地方睡觉

  4. 是否需要捕获异常(现在wait和sleep都要捕获异常InterruptedException)

3. Lock锁

  • Lock是个接口,有三个实现类
    在这里插入图片描述
  • RetreenLock默认非公平的

Synchronized 和 Lock 区别

  1. Synchronized 是一个内置的Java关键字,Lock是一个接口
  2. Synchronized 无法判断锁的状态,Lock可以判断是否获取到了锁
  3. Synchronized 可以自动释放锁,lock必须手动释放锁,如果不释放,会死锁
  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;(tryLock()尝试获得锁)
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,非公平(可以自己设置);
  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码

锁的是谁,如何判断锁对象

生产者消费者和虚假唤醒问题

虚假唤醒

什么是虚假唤醒? 当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功

  • 比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁
    在这里插入图片描述

生产者消费者Lock写法

public class Goods {
    private int goods = 5;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void producer() {
        lock.lock();
        try {
            while (goods > 0) {
                condition.await();
            }
            ++goods;
            System.out.println("生产者,还剩下"+goods+"个");
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumer() {
        lock.lock();
        try {
            while (goods <= 0) {
                condition.await();
            }
            --goods;
            System.out.println("消费者,还剩下"+goods+"个");
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Goods goods = new Goods();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                goods.consumer();
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                goods.producer();
            }
        },"B").start();
    }
}

syschronized原理

TimeUnit

8锁现象

一个类中存在一个synchronized修饰的方法和一个普通的方法,不同线程同时访问这两个方法,会出现什么情况?如果这两个方法都是同步方法又会出现什么现象?

总结:一个线程持有对象锁,另一个线程可以以异步的方式调用对象里面的非synchronized方法,输出结果是不按照顺序的

一个线程持有对象锁,另一个线程可以以同步的方式调用对象里面的synchronized方法,需要等待上一个线程释放资源,也就是同步。

一个类中存在一个synchronized修饰的静态方法和一个synchronized修饰的普通方法,不同线程同时访问这两个方法,会出现什么情况?

总结:修饰static的那个锁,锁住的是一个类模板,没有static的那个锁,锁住的是调用这个方法的那个对象。这两个不是同一把锁。所以互不干扰,没有static的那个方法不需要等有static的那个方法执行完,就可以执行

集合类不安全

ArrayList:

  • ArrayList底层一般可以说是初始容量为10的Object数组
  • ArrayList扩容,一般扩容到原来的1.5倍。然后采用Arrays.copyOf()把旧的数组搬到新的里面去
  • ArrayLIst线程不安全

List不安全:

  1. 故障现象:
  • java.util.ConcurrentModificationException 并发修改异常!(只要是并发,经常遇到这个错误)
  1. 导致原因
  2. 解决方法
    3.1 使用Vector,他的基本上方法都加了Synchronized
    3.2 使用工具类,Collections.synchronizedList(new ArrayList<>());
    3.3 使用CopyOnWriteArrayList()

CopyOnWrite

写的蛮好的链接:https://www.jianshu.com/p/4f594a84f2dd

  • 顾名思义就是写时复制,JDK中一共有两个类CopyOnWriteArrayList和CopyOnWriteArraySet。
  • 下面是CopyOnWriteArrayList的add方法的源码,可以看到add方法里面使用的是RetreenLock锁。
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

适用场景:

copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作。

HashSet的底层是什么

  • 说白了,HashSet就是限制了功能的HashMap,所以了解HashMap的实现原理,这个HashSet自然就通
  • 对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性
  • 虽说是Set是对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了(这个Set的put方法的返回值真有意思)
  • HashSet没有提供get()方法,愿意是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得
    在这里插入图片描述
  • HashMap的put方法返回值问题:调用put方法时,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value;如果是新的一个key,则返回的是null;

ConcurrentHashMap & HashTable详解

原博客链接:https://blog.youkuaiyun.com/qq_35190492/article/details/103589011

Callable

Callable和Runnable对比

在这里插入图片描述相同点:

  1. 都是接口
  2. 都可以编写多线程程序
  3. 都采用Thread.start()启动线程

不同点:

  1. Runnable没有返回值,Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callable接口的call方法允许抛出异常,Runnable的run()方法异常只能在内部消化,不能继续往上抛

注意:

Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续向下执行,如果不调用不会阻塞

Thread的构造方法如下图:
在这里插入图片描述

  • 由Thread的构造方法可知,Callable不能直接调用Thread的start方法。But,Callable就是通过Thread().start启动线程的。那说明Callable是通过Runnable的子类实现调用的。Runnable拥有的实现类如下
    在这里插入图片描述
  • 顺着实现类往下找,点开FutureTask,你会发现他的构造方法正是我们要找的Callable。这样我们就可以把Callable对象放到Future里面,然后把Future放到Thread里面实现Thread.start()方法了
    在这里插入图片描述

代码示例如下:

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer>futureTask=new FutureTask<Integer>(new MyThread());
        new Thread(futureTask,"A").start();
        System.out.println(futureTask.get()); //用get获取call()的返回值
    }
}

class MyThread implements Callable<Integer>
{
    @Override
    public Integer call() throws Exception {
        System.out.println("HelloHello");
        return 1024;
    }
}

细节: futureTask.get()尽可能的往后放,因为如果futureTask没有计算完,但是你又要获得值,就会一直阻塞到futureTask计算完了,你能get到为止

关于FutureTask详解

相关类的关系:
在这里插入图片描述
Future只是一个接口,FutureTask是实现了RunnableFuture。RunnableFuture实现了Future和Runnable接口。

认识Runnable、Future、FutureTask

Runnable:Runnable仅仅表示这个对象是可执行的。是一个函数式接口,仅有一个Run方法

ExecutorService

常用的辅助类

1. CountDownLatch

参考链接:https://www.cnblogs.com/Lee_xy_z/p/10470181.html
原理:
CountDownLatch主要有两个方法await和countDown,当一个或多个线程调用await 方法时,这些线程会阻塞。

其他线程调用countDown 方法会将计数器减1(调用countDown方法的线程不会阻塞)

当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行

CountDownLatch(倒计时计算器)使用说明:

1. public void countDown()

递减锁存器的计数,如果计数到达0,则释放所有因await等待的线程。如果当前计数大于0,则将计数减少。

2. public boolean await(long timeout,TimeUnit unit) throws InterruptedException

使当前线程在锁存器倒计数为0之前一直等待,除非线程被中断或者超出了指定的等待时间。如果当前计数为0,则此方法立刻返回true值

如果当前计数大于0,则处于线程调度的目的,禁用当前线程,且在发生以下三种情况之一前,该线程将一直处于休眠状态:

  1. 计数器到达0,则该方法返回true值
  2. 如果当前线程,在进入此方法时已经设置了该线程的中断状态;或者在等待时被中断,则抛出InterruptedException,并且清除当前线程的已中断状态。
  3. 如果超出了指定的等待时间,则返回值为false。如果该时间小于等于零,则该方法根本不会等待。

CountDownLatch的两个典例:

  1. 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

代码示例1:

public class Test01 {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch=new CountDownLatch(6);
        for(int i=1;i<=6;i++)
        {
            new Thread(()->{
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName()+"离开了");
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); //countDown计数器没有减到0,main线程会一直阻塞
        System.out.println("main"+"离开了");
    }

}

运行结果:
在这里插入图片描述

代码示例2:

public class Test01 {
    public static void main(String[] args) throws Exception {
        CountDownLatch cdl1=new CountDownLatch(4); //四个运动员
        CountDownLatch cdl2=new CountDownLatch(1); //相当于那把发令枪
        for(int i=1;i<=4;i++)
        {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"准备就绪");
                try {
                    cdl2.await();
                    System.out.println(Thread.currentThread().getName()+"听到指令");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"到达终点");
                cdl1.countDown();
            },String.valueOf(i)).start();
        }
        System.out.println("各运动员准备,裁判即将发布口令");
        cdl2.countDown(); //相当于发令枪发出命令
        cdl1.await();
        System.out.println("运动员全部到达终点,结束");

    }

}

运行结果
在这里插入图片描述

2. CyclicBarrier

参考链接:https://www.cnblogs.com/williamjie/p/9456746.html
原理:
CyclicBarrier大致是可循环利用的的屏障。顾名思义,这个名字也将这个类的特点给明确的表示出来了。首先,便是可重复利用,说明该类创建的对象可以复用。其次,屏障则体现了该类的原理:每个线程执行时都会遇到一个屏障,直到所有线程执行结束,然后屏障会打开,使所有线程往下执行.

这里介绍CyclicBarrier的两个构造函数: CyclicBarrier(int parties)和CyclicBarrier(int parties,Runnable barrierAction):前者只需要申明需要拦截的线程数即可,后者还需要定义一个等待所有线程到达屏障优先执行的Runnable对象

实现原理: 在CyclicBarrier 的内部定义了一个Lock对象,每当一个线程调用await方法时,将拦截的线程数减1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中所有线程放入锁等待队列中,这些线程会一次的获取锁、释放锁

代码示例:

public class Test01 {
    public static void main(String[] args) throws Exception {
        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
            System.out.println("集齐七颗龙珠,召唤神龙");
        });
        for(int i=1;i<=7;i++)
        {
            final int temp=i;
            new Thread(()-> {
                try {
                    System.out.println("收集到第"+temp+"颗龙珠");
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}

在这里插入图片描述

3. Semaphore

原理:
信号灯,类似于抢车位。多个线程争夺多个资源
信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
常用方法说明:

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。(将信号量减1)
​
acquire(int permits)  
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
    
acquireUninterruptibly() 
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
    
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
​
tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
​
release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。(将信号量加1)
​
hasQueuedThreads()
等待队列里是否还存在等待线程。
​
getQueueLength()
获取等待队列里阻塞的线程数。
​
drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。
​
availablePermits()
返回可用的令牌数量。

代码示例:

public class Test01 {
    public static void main(String[] args) throws Exception {
        /*
            1. 两个目的之一的并发线程数量的控制,通过这句话实现
            2. 在这里模拟三个停车位
         */
        Semaphore semaphore=new Semaphore(3);

        for(int i=1;i<=6;i++) //模拟6辆汽车
        {
            final int temp=i;
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println("第"+temp+"抢到了停车位");
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println("第"+temp+"已离开");
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }

}

运行结果:
在这里插入图片描述

读写锁

在这里插入图片描述

  • 读写互斥,写的时候是不能读的。(所以读也得加锁,为了读和写互斥)
  • 写写互斥
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
            //暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }

    }

    public Object get(String key) {
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(num + "");
            }, String.valueOf(i)).start();
        }
    }
}

运行结果:
在这里插入图片描述

阻塞队列

阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列,在数据结构中起的作用如下图:
 线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素 线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素

阻塞队列的用处:

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

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

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

种类分析:

在这里插入图片描述

BlockingQueue的核心方法:

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

代码示例:

/**
 * 阻塞队列
 */
public class BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {

//        List list = new ArrayList();

        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //第一组
//        System.out.println(blockingQueue.add("a"));
//        System.out.println(blockingQueue.add("b"));
//        System.out.println(blockingQueue.add("c"));
//        System.out.println(blockingQueue.element());

        //System.out.println(blockingQueue.add("x"));
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//    第二组
//        System.out.println(blockingQueue.offer("a"));
//        System.out.println(blockingQueue.offer("b"));
//        System.out.println(blockingQueue.offer("c"));
//        System.out.println(blockingQueue.offer("x"));
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//    第三组        
//         blockingQueue.put("a");
//         blockingQueue.put("b");
//         blockingQueue.put("c");
//         //blockingQueue.put("x");
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
        
//    第四组        
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));

    }
}

线程池

讲的不错的链接:https://www.jianshu.com/p/210eab345423
三大方法,七大参数,四种拒绝策略
Executor相当于Collection

Executor相当于Collections,是线程池的工具类

ExecutorService相当于List,是一个接口,所以不让new、在线程池里面一般使用工具类Collections来用它

线程池主要使用的实现类是ThreadPoolExecutor

代码示例如下:


四大函数式接口

  • lambda表达式就是为了解决内部类冗余的问题,能使用lambda的前提是这是一个函数式接口
  • lambda表达式就是为了简化编程模型的
  • 函数式编程,因为一个函数里面只有一个抽象方法,所以甚至可以省略方法名,直接()->{},就知道实现的是那个唯一的方法;

注意: Java7之前要求接口里面只能有抽象方法,Java8及之后允许在接口中实现部分static和default方法。需要注意的是,此处的静态方法只能被public修饰(或者省略不写),不能是private或者protected。

关键字: default 在接口中方法前面加上修饰符default 编译器就会认为该方法
并非抽象方法,可以在接口中写实现。

补充:java泛型中T、E、K、V、?等含义

E -Element 在集合中使用,因为集合中存放的是元素。E是对各方法中的泛型类型进行限制,以保证同一个对象调用不同的方法时,操作的类型必定是相同的。E可以用其它任意字母代替
T - Type(Java类 ),T代表在调用指定类型时,会进行类型推断
KV 键值对中的key,value
N - Number 数值类型
- 表示不确定的java类型,是类型通配符,代表所有类型。?不会进行类型推断
S、U、V - 2nd、3rd、4th types
这几个本质上没有什么区别,只是一种约定。随便用哪个字符都是可以的

实现接口,方法名冲突问题

我们知道Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法(default)在Java8中的引入,有可能出现一个类继承了多个签名一样的方法。这种情况下,类会选择使用哪一个函数呢?

为解决这种多继承关系,Java8提供了下面三条规则:

  1. 类中的方法优先级最高,类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。

  2. 如果第一条无法判断,那么子接口的优先级更高:方法签名相同时,优先选择拥有最具体实现的默认方法的接口, 即如果B继承了A,那么B就比A更加具体。

  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法, 显式地选择使用哪一个默认方法的实现。

Stream流式计算

  • Java8之后,list和Stream可以互换,list.Stream

ForkJoin

异步回调

JMM

Volatile

内存屏障避免指令重排

  • 内存屏障在单例模式使用的最多

单例模式

在这里插入图片描述

  • 利用反射可以破坏单例
  • 反射不能破坏枚举的单例模式

Enum

单例模式的作用

深入理解CAS

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值