并发编程

创建线程的方式
    1.继承Thread
    2.实现Runnable
    3.FutureTask

sleep()
    建议:TimeUnit替换Thread的sleep方法
    1.调用sleep方法会让当前线程从Running状态进入Timed_waiting(阻塞)状态。
    2.其他线程可以使用interrupt方法打断睡眠的线程,这时sleep方法会抛出InterruptExcetpion异常
    3.睡眠结束后的线程进入Runnable状态
    
yield()
    当调用此方法会使当前线程从Running状态进入Runnable状态

线程优先级
    thread.setPriority(Thread.MIN_PRIORITY);最低为1,最高为10
    
join() join(long n)
    t1.start();
    t1.join();
    线程加入到当前线程当中,等待t1运行结束或者n秒后再向后执行。
    保护性暂停模式,一个线程等待另一个线程结束。
    
interrupt();
    正在运行的线程被打断的isinterrupt()为真,睡眠中的线程被打断的isinterrupt()为假;要结束睡眠中的线程,可以拦截打断异常,再异常处理中再进行打断,这时得到的isinterrupt()为真,就可以判断结束线程。
    
守护线程
    t1.setDaemon(true);主线程结束,守护线程会强制结束。比如JVM的GC,主线程结束了,GC线程会强制结束。
    
线程的6种状态
    NEW:创建没start()
    RUNNABLE:已经start()。
    BLOCKED:等待拿锁对象。
    WAITING:join()、wait()
    TIMED_WAITING:sleep()、wait(n);
    TERMINATED:运行结束
    


synchronized
    synchronized (对象){} 锁住这个对象
    synchronized 放在成员方法上,锁住this,当前对象
    synchronized 放在静态方法上,锁住类对象,比如 Test.clas;
    
Monitor
    最初Monitor的Owner为null
    当一个线程进入synchronized(this){},就会将Monitor的Owner设置为这个对象。Monitor只有一个Owner。
    这时,其他线程要访问synchronized中的代码。会先去这个对象对应的Monitor的Owner,如果不为null,就会进入Monitor的EntryList链表中,并进入BLOCKED阻塞状态。
    当第一线程执行完了,会将Monitor的Owner置null,并且唤醒entrylist中的线程,并进入RUNNERABLE可运行状态。

JAVA对象的组成(存在heap)

    1.对象头
        1.Mark word
            1.hashcode
            2.分代年龄(GC,到达一定的年龄,会从新生代移到老年代)
            3.lock状态(最后两位表示)
                01:正常 (hashcode、分代年龄、是否启用偏向锁(倒数第三位 0:未启用 1:已启用))
                    偏向锁   (线程ID、分代年龄 、是否启用偏向锁:1 )
                00:轻量级锁   (指向锁记录---(创建位置:在线程的栈帧上。)的指针) (锁重入:线程对同一个锁进行多次加锁。)
                        锁记录:JVM层面对象。
                            Object reference:记录对象的指针
                            lock record:记录对象的Mark word (锁记录与Mark Word 做CAS 操作) 值==null :表示有锁重入,解锁时直接去掉
                10:重量级锁   
                11:GC标记
        2.指向类的指针(Klass word) :类的元信息存在方法区
        3.数组长度(如果是数组)
    2.实例数据
    3.对齐填充字节
    
        
偏向锁:当一个线程T2对一个对象obj加锁,这时Mark word 会存入线程T2的ID。偏向锁有延时,可以设置虚拟机参数,改成不延时。
        大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头Mark Word和栈帧锁记录里存储锁偏向的线程ID,
        以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
        如果测试成功,表示线程已经获得了锁。
        如果测试失败,则测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没设置,则使用CAS竞争锁(竞争什么?);如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
            --->a线程获得锁,会在a线程的的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id.以后该线程的进入,就不需要cas操作,只需要判断是否是当前线程。
            --->a线程获取锁,不会释放锁。直到b线程也要竞争该锁时,a线程才会释放锁。
            --->偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程仍然活着,拥有偏向锁的栈会被执行。如果线程不处于活动状态,则将锁对象的MarkWord设置成无锁状态。栈帧中的lockRecord和对象头的MarkWord要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁(锁升级)。最后唤醒暂停的线程。
            --->关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。
 
        偏向锁升级:一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,
        这个线程在修改对象头MarkWord成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,
        所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,操作系统检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,
        则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
        
        
        撤销可偏向状态:
                        当一个可偏向的对象(Mark word 后三位101),执行了hashcode(),会把这个对象变成正常状态(Mark word:hashcode、age、偏向锁状态:0、state:01)。
                        一个对象被加偏向锁且线程正在执行,其他线程想获取偏向锁,会进入锁升级,变成轻量级锁。
                        wait/notify:会将锁都升级为重量级锁Monitor锁
        批量重偏向:阈值20,19次撤销后,会批量把对象的threadID更改为新的线程ID
        批量撤销:阈值40,当撤销偏向锁39次后,会把整个类的变成不可偏向。
                        

轻量级锁:当一个线T1对一个对象obj加锁,会在线程的栈帧里创建一个锁记录对象,这个对象由两部分组成Object reference和lock record。 
          Object reference记录obj的地址,lock record 执行cas操作,lock record记录obj对象头的Mark word 中的hachcode 、age 以及锁状态。而obj的Mark Word 记录锁记录的地址和lock 变成00 :解锁 时发反向操作
          
重入锁:在轻量级锁加锁完成时,这个线程再对这个obj进行加所,会变成重入锁。会继续产生一个锁记录。只是lock record的值为null。解锁时,lock record 为null 则直接去掉。为 obj 的mark word 的值则与obj执行cas操作解锁。

锁膨胀:在轻量级锁加锁完成时,这时另一个线程T2再对这个obj进行加所,会进入锁膨胀过程。T2会对这个obj申请一个Monitor锁(重量级),让obj的指针指向Monitor,然后自己进入Monitor的EntryList,并且自己线程变成BLOCKED阻塞状态。
        解锁时,这时T1去解锁会失败,然后他会根据obj的Monitor地址,进行重量级解锁。

锁消除:
    不会被共享的锁对象,jvm编辑会进行优化,去掉加锁。-XX:+EliminateLocks

wait/notify:
    当要给线程调用wait方法,这时,这个线程会释放自己的锁对象,也就是Monitor的owner设为空,然后进入Monitor的WaitSet中,自己进入WAITING、TIMED_WAITING状态。
    当一个线程调用notity方法,会唤醒Monitor中的WaitSet中的线程。
    
    wait vs  sleep
        sleep是Thread方法,wait是所有对象都有的方法。
        wait调用必须获得Monitor的锁标记,sleep可以直接使用
        wait被调用会释放Monitor,sleep不会。
    使用:synchronized(lock){
            while(判断不成立){
                lock.wait();
            }
            //工作
        }
        //另一个线程
        synchronized(lock){
            lock.notifyAll();
        } 
        
LockSupport
    park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
    park和unpark的使用不会出现死锁的情况
    blocker的作用是在dump线程的时候看到阻塞对象的信息
 
保护性暂停模式:Guarded Suspension 一个线程等待另一个线程的执行结果。
    join()、Future也是这个模式.
        

如何找到死锁:等待对方释放锁资源
    命令形式:
        1.jps:找到所有的运行的java程序并拿到线程ID
        2.jstack 线程ID Found one Java-level deadlock:会列出死锁的信息。
    工具 jconsole:
        1.打开jconsole链接到某一给java进程。
        2.选择线程栏,点击检测死锁。会出现所有死锁的线程信息。
    工具 jvisualvm:
        1.打开工具,点击java进程,选择线程栏,如果有死锁,会有红色字体提示。
    解决死锁:
        按照相同的加锁顺序。

活锁:改变对方的结束条件。增加睡眠时间解决活锁。

饥饿:拿到锁的几率很低。。ReentrantLock(可重入锁)可以解决


ReentrantLock:
    可中段
    可以设置超时时间
    可以设置为公平锁
    支持多个条件变量
    与synchronized一样支持可重入
    
    
    lock.lock();加锁
    lock.lockInterruptibly();可打断
    boolean b = lock.tryLock();锁超时,尝试获得锁
    ReentrantLock lock = new ReentrantLock(true);创建公平锁,解决饥饿问题。性能低
        await前必须先获得锁,
        执行await后会释放锁
        await被唤醒或者打断会重新竞争锁
        竞争成功从await后开始执行
        
共享变量的可见性        
    volatile 关键字:易变,修饰成员变量和静态成员变量。线程每次都会从主内存中读取而不会从工作缓存中读取。    
        实现两阶段优雅退出:定义成员属性,通过成员属性判断是否退出,另一个线程修改这个成员属性。
        可以防止volatile修饰的属性之前的代码重排序
        解决:有序性和可见性
        同一个线程,被volatile修饰的属性之前的代码都会同步到主存中
    synchronized也可以解决    
    System.out.println();也能解决可见性
    线程start()前对变量的修改,在线程是可见的
    线程结束了,对变量的修改,在其他线程是可见的

java 指令重排序
    int a=10;
    int b=20; 被重排序后可能顺序变了,变成先赋值b,再复制a。多线程下,可能会出现问题:一个线程依赖另一个线程的数据赋值。
    
java 单例 反序列化会破坏单例模式,可以
    public Object readResovle(){
        return singleTon;
    }
    反序列化会掉这个方法把单例返回回去。

推荐使用静态内部类实现单例

JMM
    可见性:由JVM缓存优化引起
    有序性:由JVM指令重排序优化引起
    
    
    
CAS:
    结合CAS和volatile可实现无锁的并发、适用线程数少,多核CPU的情况。
    CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我再重试呗。
    synchronized是基于悲观锁的思想,得防着其他线程来修改变量,我上了锁,你们都别想改,我改完了,解了锁,你们才有机会。
    CAS是体现的无锁并发、无阻塞并发
        因为没有synchronized,所以线程不会进入阻塞,提升效率。
        但如果竞争激烈,可以想象的到重试频繁发生,反而效率会受影响。

原子类型数据:    (基于Unsafe类实现的)
    AtomicXXX
        compareAndSet(old,now):比较和设置,每次每次set值前都会把old的值与对象实际的值比较,如果相同,则设值,不相同返回false
        
        AtomicReference<Bigdecimal>: 
        AtomicStampedReference<Bigdecimal> :有修改的版本好,当前线程可以知道值的修改,A -> B -> A 这种情况是能够知道。  
    
原子数组
    AtomicXXXXXArray :

原子更新器:
    AtomicReferenceFieldUpdater:(修饰的属性必须与 volatile结合使用)
   //第一个参数:修改的类  第二个参数:修改的字段类型  第二个参数:字段名字
    private AtomicReferenceFieldUpdater fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Account.class,String.class,"name");
    
    private AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Account.class,"balance");
    AtomicLongFieldUpdater

原子累加器:
    LongAdder:increment():累加时,设置多个累加单元Cell,每一个Cell独立累加,最后将结果来汇总,减少CAS的重试次数,有点像并发累加,reduce。


Supplier<T>  无中生有 ()->结果
Function<T>     函数 一个参数要给结果 (参数)->结果
Consumer<T>  消费者 一个参数没有结果 (参数)->void


不可变类
    DateTimeFormatter
    
享元模式
    不可变类基本都实现了享元模式:String,包装类,:像连接池一样,对象可以重复使用。
    
阻塞队列:Blocking Queue


线程池ThreadPoolExecutor:
    
    状态:用一个整数的高三位表示状态,后29位表示线程数量
          111 RUNNING  正在运行
          000 SHUTDOWN 关闭会等正在运行和队列中的任务都执行完才会关闭,但是不会接收新的任务
          001 STOP        直接强制关闭:打断正在运行的线程,
          010 TIDYING    执行完所有的任务,即将进入结束状态
          011 TERMINATED 结束
    构造方法:最全参数:
        public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {...}
            参数1:核心线程数量
            参数2:最大线程数
            参数3:救急线程存活时间。(救急线程:当线程用完,任务队列装满,这时添加任务,就会创建一个救急线程。救急线程的最大数量=最大线程数-核心线程数),救急线程有存货时间。
                   救急线程必须配合有界队列使用,不然不会生成救急线程。
            参数4:存货时间单位。
            参数5:队列,用于存任务。
            参数6:线程工厂,可以对线程做一些修改,比如取名字
            参数7:

固定大小线程池:1.核心线程数=最大线程数,没有救急线程。因此也不需要超时时间。
                2.可以放任意数量的任务
                适用:任务量已知,但相对耗时的任务。
                Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
                参数1:核心线程数
                参数2:线程工厂
                
带缓冲的线程池:核心线程数 = 0 ,最大线程时Integer.MAX_VALUE,救急线程存活60秒
                    全都时救急线程,(60秒回收)
                    可以无线创建
                    队列:SynchronousQueue,没有容量。没有线程来取,是放不进去的(一手交钱啊、一手交货)。
                    
                Executors.newCachedThreadPool(ThreadFactory threadFactory)
                参数1:线程工厂
单线程线程池:    希望多个线程排队执行,线程数固定1,没有救急线程。任务完成不会释放,会等待新的线程。
                Executors.newSingleThreadExecutor()
                
任务调度线程池:
            ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
            service.schedule(()->{},1,TimeUnit.SECONDS);//延时1秒执行
            service.scheduleAtFixedRate(()->{},10,10,TimeUnit.SECONDS);//以一定的速率执行。但是假如一个任务执行完需要2秒,周期1秒,这时会等第一个任务执行完了,才会接着执行,(任务不会重叠执行)
                参数1:Runnable 任务
                参数2:延时时间
                参数3:周期、间隔
                参数4:时间单位
            service.scheduleWithFixedDelay(()->{},10,10,TimeUnit.SECONDS);//会在上一个任务完成后再计算延迟时间。
            

提交任务到线程池:
    
    1.void execute(Runnable runnable);//没有返回
    
    2.Future<T> submit();//有返回 Future.get()可以拿到结果
    
    3.List<Future<T>> invokeAll(Collection tasks,long timeout,TimeUnit timeunit);//批量提交任务,带有超时
    
    4.<T> T invokeAny(tasks);//提交tasks所有任务,哪个任务先执行完毕就返回哪个任务的结果,然后取消其他任务。
    
关闭线程池:
    1.shutdowm(); //把线程池状态设置为SHUTDOWN,不会接收新的任务。执行完已有的和队列中的任务。结束。不会阻塞掉用线程代码的执行。
    2.shutdownNow();// 把线程池状态设置为STOP,不会接收新的任务,会将队列中的任务返回,并用interrupt的方式打断正在执行的任务。
    
    
饥饿问题:由于线程不足导致的饥饿问题,应该把线程按照不同的线程做不同的事情,可以解决。

创建多大的线程池合适?
    CPU密集型运算:通常采用cpu核数+1 能够实现CPU的最大利用率。+1是保障线程故障等问题。
    I/O密集型运算:经验公式:线程数=核数*期望CPU利用率*总时间(CPU计算时间+等待时间)/CPU计算时间
    
线程池中的异常处理:
    1.try{}catch(){}
    2.Future 会有返回值,如果有异常,Future.get()会获得异常信息。

Fork/Join:
    jdk1.7加入的新的线程池实现,体现分治思想,适用于能够任务拆分的CPU密集型运算。
    在分治的基础上加入了多线程,可以把任务分解合并交给不同的线程来完成,进一步提升了运算效率。
    默认创建与cpu核数相同的线程池。
    
    
AQS:全称:AbstractQueuedSynchronizer 阻塞式锁和相关同步器工具的框架
    1.用state来表示资源的状态。子类需要定义如何维护和控制获取和释放锁。(分独占模式和共享模式)
        独占模式:只有一个线程能够访问资源。
        共享模式:可以允许多个资源访问资源。
    2.提供了FIFO,先进先出的等待队列。
    3.条件变量来实现等待、唤醒的机制。类似Monitor的WaitSet。
    子类主要实现:
        1.tryAcquire()
        2.tryRelease()
        3.tryAcquireShared()
        4.tryReleaseShared()
        5.isHeldExclusively()
        
读写锁:
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        Lock writeLock = readWriteLock.writeLock();
        Lock readLock = readWriteLock.readLock();
        注意:
            1.读锁不支持条件变量
            2.重入时,升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待。
            3.重入时降级支持:即持有写锁的情况下去获取读锁。
        应用:缓存
        
        场景:适合读多写少的场景,如果写操作比较多,以上实现效率比较低
              没有考虑缓存容量
              没考虑缓存过期
              只适合单机
              并发性能低,目前只会用一种锁
              更新方法简单粗暴,清空了所有的缓存。
        原理:
            第一次成功上锁,与ReentrantLock一致,不同之处是,写锁使用state的低16位,读锁使用state的高16位
            
        
        StampedLock:JDK8加入,优化读性能,特点:使用读锁、写锁都必须配合戳使用。
                    读锁:
                        long stamp = lock.readLock();
                        lock.unlockRead(stamp);
                    写锁:
                        long stamp = lock.writeLock();
                        lock.unlockWrite(stamp);
                    乐观读:
                            StampedLock支持tryOptimisticRead方法,读取完毕后需要一次校验戳,如果戳校验通过,表示这期间没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁
                            保证数据安全。
                            long stamp = lock.tryOptimisticRead();
                            if(!lock.validate(stamp)){//验戳
                                //锁升级
                            }
                    缺点:不支持条件变量
                          不支持可重入

Semaphore:
        信号量,用来限制能同时访问共享资源的线程上限。
        应用:
            限流,在访问高峰,让请求线程阻塞,高峰过去再释放线程。他只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如链接数,对比Tomcat LimitLatch的实现)
            实现简单的连接池,对比享元模式的实现(用wait notify),性能和可读性会更好。
        原理:
        
CountDownLatch:倒计时锁:线程同步协作
        应用:1.王者荣耀10个玩家信息加载完毕才开始游戏
              2.分布式访问多个微服务,一个一个调用时穿行,很费时间,应该采用多线程并行执行,用countdownlatch等待所有程序返回结果(无返回结果时使用,有返回加过时应该使用Future,线程池的submit方法)。    
        
CyclicBarrier:循环栅栏 线程同步协作    ,等待线程完成某个计数,构造时设置计数个数。每个线程执行到需要的同步的时刻,调用await(),当等待的线程数满足某个计数时,继续执行。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
        //当两个线程执行完毕后,执行参数后面的任务
        CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2,()->{});
        注意:要实现循环执行的效果,必须要线程池的数量和CyclicBarrier的参数的数值一致。
        
JUC:
    Blocking:大部分实现基于锁(ReentrantLock),并提供用来阻塞的方法
    CopyOnWrite:之类容器开销相对交重。
    Concurrent:
            1.内部很多操作使用cas优化,一般可以提高交高的吞吐量
            2.弱一致性
                1.遍历时弱一致性,当迭代器遍历时,数据发生修改,遍历还会继续,但是,读取的数据都是旧的。
                2.求大小弱一致性,size操作未必是100%
                3.读取弱一致。
        ConcurrentHashMap: K V 都不允许为null
            computeIfAbsent():map.computeIfAbsent("key",(key)->"value");//如果缺少一个Key,则计算生成一个value,然后将k-v放入map。(报证get、put的原子操作)
            原理:JDK8
                1.属性、内部类、方法
                    1.volatile sizeCtl:默认0,初始化为-1,扩容时为-(1+扩容线程数),初始化或者扩容完成后为下次扩容的阈值的大小(容量的3/4),
                    2.Node<K,V> 链表结构
                    3.volatile Node<K,V>[] table K-V数组,每一个K-V,可能是链表或者红黑数
                    4.volatile Node<K,V>[] nextTable 
                    5.ForwardingNode<K,V> 扩容时,如果某个bin迁移完毕,则用 ForwardingNode 作为旧table bin的头节点
                    6.ReservationNode<K,V> 用在compute和computeIfAbsent时,用来占位,计算完成之后替换位普通的node
                    7.TreeBin<K,V>,作为TreeBin的头节点,存储root和first
                    8.TreeNode<K,V>,作为reebin的节点,存储parent,left,right
                2.get()
                    1.没有加锁
                    2.调用h=spread(key.hashCode)方法,会产生一个正整数,先判断tab是否位空或者长度是否大于0,如果有值,然后会调用tabAt(tab,(n-1) & h)方法,找到数组的下标。
                      然后比较头节点的hash是否==h 和 equals,如果相等,然后取出值,如果<0,-1则表示在扩容当中,-2表示扩容完毕(正在扩容,但是当前节点扩容完毕),前两种都不是,就会在红黑树中查找。
                3.put()
                    put()调用putVal(),
                    开始为null判断。调用spread()计算出hashcode,然后进入死循环,在里面先判断tab是否为空,为空就初始化容量initTab()(用cas保证只有要给cas能够操作成功)。;然后判断头节点是否为空
                    如果是空,就直接放入(cas放入),    然后判断当前桶的头节点的hash是否==-1,如果是,则说明在扩容,然后会帮忙扩容。最后的else,说明桶下标冲入,然后当前桶下标的头节点加synchronized,分两种清空,1,是链表,2。是红黑树。最后判断是否需要扩容。然后addCouunt进行size大小计数。
                4.initTable()
                    死循环(while)先判断table是否有没有被初始化,如果没有,先把尝试(cas操作)sizeCtl设置为-1,表示正在初始话,成功就进入初始化工作,创建Node<K,V> 数据,然后计算阈值大小赋值给sizeCtl
                5.transfer(tab,nextTab)扩容
                    先判断nextTab是否为空,为空就两倍初始话容量,不为空,就开始从尾到头的转移数据,转移时:如果节点为null,然后用cas把他改成fwd。如果hashcode==-2,
                    则他退出当前节点的转移进入上一个数组下标的转移,如果不是前面两种情况,就会对当前node加锁synchronized开始数据转移,转移时分两种情况,1.普通节点 2:红黑数节点
                    
                    
                    
        

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值