zzzvvvxxxd

最快的外卖小哥


  • Home

  • About

  • Archives

【一致性协议01】 2PC

Posted on 2015-12-11 | In 分布式 |

Two-Phase Commit,二阶段提交,作为最基础的一致性协议,保证了分布式系统数据的一致性.目前绝大多数的关系型数据库都是使用2PC来完成分布式事务处理的.
利用2PC协议能够非常方便地完成所有数据库事务参与者的协调,统一决定事务的提交或回滚,从而能有效地保证分布式数据一致性.

优点

原理简单,实现方便

缺点

  • 同步阻塞

    极大的限制了系统的性能

  • 单点问题

    协调者在整个二阶段提交过程中,一旦出现故障(尤其是在阶段二),都会导致系统无法正常运行

  • 脑裂

    也就是数据不一致
    在阶段二,即执行事务提交的阶段,当协调者向所有参与者发送commit请求之后,发生了局域网络异常或者是协调者在尚未发送完commit请求之前自身崩溃,导致只有部分的参与者接收到了commit请求,这部分参与者就会进行事务的提交,没有收到commit的参与者则无法进行事务的提交。于是,整个分布式系统出现数据不一致的情况。

流程:

阶段1: 提交事务请求(投票阶段):

  1. 事务询问.

    协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应.

  2. 执行事务.

    各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志.

  3. 各参与者向协调者反馈事务询问的响应

    如果参与者成功执行了事务操作,那么就反馈给协调者Yes信息,表示事务可以执行; 如果参与者没有成功执行事务就反馈给协调者No响应,表示事务不可以执行.

这个阶段可以看成一种投票阶段,即各个参与者投票表明是否要继续执行接下去的事务提交操作.

阶段2: 执行事务提交
这个阶段,协调者会根据参与者的反馈情况来决定是否最终进行commit.一般,包含两种可能:

#1 执行事务提交:
  1. 发送提交请求

    假如协调者从所有的参与者获得的反馈都是YES,那么就执行提交操作,协调者向参与者发送commit请求

  2. 事务提交

    参与者接受到Commit请求后,会正式执行事务提交操作,并在完成后释放整个事务执行期间所占用的资源

  3. 反馈事务提交结果

    参与者在完成事务提交之后,向协调者发送Ack信息

  4. 完成事务

    协调者收到所有参与者的Ack消息后,完成事务

#2 中断事务:

如果任何一个参与者返回了No信息,或者等待超时之后,协调者尚无法接受到所有的参与者反馈信息,就会中断事务。

  1. 发送回滚请求

    协调者向所有参与者发出Rollback请求

  2. 事务回滚

    参与者接收到Rollback请求后,会利用其在阶段一种记录的Undo信息来执行回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。

  3. 反馈事务回滚结果

    参与者在完成事务回滚之后,向协调者发送Ack消息

  4. 中断事务

    协调者收到所有参与者反馈的Ack消息之后,完成事务中断

【Redis源码】zmalloc

Posted on 2015-06-14 | In blog |

【Java多线程-4】volatile

Posted on 2015-06-14 | In blog |

volatile写-读的内存语义

概念

概念 描述
缓存行 cache line 缓存中可以分配的最小存储单元,处理器填写缓存线时,回家在整个缓存线,需要使用多个内存读周期
缓存行填充 cache line fill 当处理器识别到从内存中读取操作是可缓存的,处理器读取整个缓存行到适当的缓存
缓存命中 cache hit 如果进行告诉缓存行填充操作的内存位置仍是下次处理器访问的位置,处理器从缓存中读取操作数,而不是内存
instance = new Singleton();

其对应的汇编代码为:

0x01a3de1d: movb $0x0, 0x1104800(%esi);
0x01a3de24: lock addl $0x0, (%eps);

lock前缀的指令在多核处理器会引发:将当前处理器缓存行的数据写回到系统内存, 这个写回内存的操作会使其他CPU中缓存了该内存地址的数据无效
为了优化速度,处理器不直接和内存通信,一般是将系统内存数据读取到本地内存(L1,L2或其他),但是操作并不保证何时写回到主存。如果是volatile变量,在写入时,回向处理器发送Lock指令这个变量所在的缓存行的数据会写回到系统内存。这里就有一个缓存一致性协议,每个处理器嗅探在总线上传播的数据来检查自己的缓存值是否过期。如果处理器发现自己缓存行对应的内存地址被修改,就会将当前的处理器缓存设置为无效状态。该处理器再一次对该缓存数据的操作,就会先从主存中读取。

volatile实现原则

  • Lock前缀指令会引发处理器缓存写回到内存
  • 一个处理器的缓存回写到内存会使得其他处理的缓存无效

也就是:

当写一个volatile变量时,JMM会把该线程对应的本地内存中(本地内存就是上文中的当前处理器缓存)的共享变量(实际应当是volatile变量所在的缓存行)刷新到主内存。所以,很多时候一些非volatile变量的写操作发生在volatile变量之前修改,因为这里的刷新机制,所以也无意中实现了可见性(很多blog中的代码都有这个问题…所以没有拿到理想的输出,例如:http://blog.csdn.net/ns_code/article/details/17101369)


简述

volatile是Java提供的一种弱同步机制

在Java1.2之前,Java内存模型总是从主存(即共享内存)中读取变量,也就是不同线程读取的是同一块内存。而后来随着JVM优化,在多线程环境下volatile关键字的作用越来越明显。
当前的JMM模型下,线程可以把变量保存在本地内存,而不是直接在主存中读写。
后果就是:

一个线程在主存中修改了变量的值,另一个线程还在使用其拷贝到寄存器中的值,数据就产生了不一致性.

volatile就指示JVM改变量是不稳定的,每次操作都要在内存读取。在多线程环境下,任何多任务共享的变量,都应该是volatile的。新值可以立即同步到主存(即共享内存)。 每次使用前,从主存刷新。在任何时刻,两个不同的线程总是看到某个成员变量的同一个值在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 说白了就是:禁止线程私有拷贝

volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制

综合来说:

  • volatile 变量对所有线程是立即可见的,对 volatile 变量所有的写操作都能立即反应到其他线程之中
  • 也就是说volatile变量在各个线程中是一致的
  • 但是基于volatile的运算仍旧不算是线程安全的

在遇到多个线程共享的变量时都加上volatile,但是不要将其看成是线程安全的

【深入理解Java虚拟机3-笔记】GC算法和垃圾收集器

Posted on 2015-06-12 | In JVM |

垃圾回收算法

1. 标记回收算法

  • 标记
  • 回收

标记出所有需要回收的对象,在标记完成后 统一回收所有标记的对象

不足:

  1. 效率问题。标记和清楚两个过程效率都不高
  2. 空间问题。会产生大量内存碎片,导致后续要分配较大对象时,无法找到足够的内存而不得不提前触发垃圾收集动作。

2. 复制算法 Copying

为了解决效率问题,复制算法将可用内存按容量划分为大小相同的两块,每次只使用其中一块,当这一块内存用完,就将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。不需要考虑内存碎片的情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

  • Eden
  • Survivor0
  • Survivor1

HotSpot中默认的Eden和Survivor的比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%。当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。

3. 标记-压缩算法 Mark-Compact

适用于老年代。
标记过程和标记-清楚算法一致,后续让所有存活对象都向一端移动,然后直接清理掉端边界意外的内存。

垃圾收集器

总览

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明可以搭配使用

Serial收集器

最基本、发展历史最悠久的收集器。是一个单线程的收集器,它只会使用一个CPU或一条线程来完成垃圾收集的工作,同时还必须暂停其他的所有工作线程,知道收集结束。比较适合client模式下的客户端或者单核的场景。
Serial收集器

ParNew收集器

本质就是Serial收集器的多线程版本
ParNew收集器
目前只有Serial和ParNew可以和老年代的CMS收集器配合工作,而后者目前是最多使用的收集器。自然ParNew也成了Server模式下首选的新生代收集器。
ParNew是使用-XX:+UseConcMarkSweepGC的默认新生代收集器,也可以通过-XX:+UseParNewGC来明确指定它。
如果要设置ParNew的线程数,可以使用-XX:ParallelGCThreads来限制。

Parallel Scavenge收集器

使用复制算法的新生代收集器。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量(Throughput)。前者可以有更好的响应时间和用户体验,而后者则可以高效率地利用CPU,尽快完成计算密集型任务。

吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

相关参数:

1
2
3
-XX:MaxGCPauseMillis # 设置最大垃圾收集时间
-XX:GCTimeRatio # 直接设置吞吐量,默认值为99,即为允许最多1%的GC时间
-XX:+UseAdaptiveSizePolicy # Parallel Scavenge支持自适应调节 P80

Serial Old收集器

Serial收集器的老年代版本,使用标记-整理算法

Parallel Old收集器

Parallel Scavenge的老年代版本,使用多线程标记-整理算法

CMS

CMS收集器
是一个以获取最短停顿时间为目标的收集器,基于标记-清除算法。

  • 初始标记(CMS initial mark)

    仅仅标记一下GC Roots能直接关联到的对象

  • 并发标记(CMS concurrent mark) –> stop the world

    进行GC Roots Tracing过程

  • 重新标记(CMS remark)

    修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,停顿时间稍长于初始标记,远小于并发标记

  • 并发清除(CMS concurrent sweep) –> stop the world

明显缺点:

CMS收集器对CPU资源非常敏感

其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。

CMS无法处理浮动垃圾(Floating Garbage),可能会导致Concurrent ModeFailure失败而导致另一次Full GC

由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。
CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发收集时的使用,可以通过参数-XX:CMSInitiatingOccupancyFraction来设置年老代空间达到多少的百分比时触发CMS进行垃圾收集,默认是68%。
如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败,此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。

CMS收集器是基于标记-清除算法,因此不可避免会产生大量不连续的内存碎片

如果无法找到一块足够大的连续内存存放对象时,将会触发因此Full GC。CMS提供一个开关参数-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。

【Java多线程-2】线程挂起、恢复、终止

Posted on 2015-06-11 | In blog |

类似suspend()方法的挂起方式,已经被java丢弃, 较为合理的方式是设置挂起的标识位

首先在Runnable对象中设置标志位suspended

private volatile boolean suspended;

volatile变量告诉JVM,这个变量将要被外部的线程修改,具体的参考下一篇笔记吧
然后设置置位和消除suspended状态方法

public void suspendRequest() {  
    suspended = true;  
}  

public void resumeRequest() {  
    suspended = false;  
}

还有一个挂起时用于等待的方法:

private void waitWhileSuspended() throws InterruptedException {  
    //这是一个“繁忙等待”技术的示例。  
    //它是非等待条件改变的最佳途径,因为它会不断请求处理器周期地执行检查,   
    //更佳的技术是:使用Java的内置“通知-等待”机制  
    while ( suspended ) {  
        Thread.sleep(200);  
    }  
}

接下来在Runnable对象的run()方法所要保护起来的代码段前后添加waitWhileSuspended方法来确保进入和退出代码块的时候,都可以检测到suspend信号,从而可以挂起

public void run() {
    while(true) {
        waitWhileSuspended(); 

        // do sth ...
        waitWhileSuspended();
    }
}

而产生suspend信号的线程只需要在需要线程挂起的时候调用suspendRequest()方法,解除suspend怎使用resumeRequest()方法而Runnable对象中被waitWhileSuspended();包围的代码段不会被suspend信号终止

终止

在Thread的run()方法中return,线程会自然消亡

如果使用stop()线程突然终止,很少有机会进行清理工作会释放当前所持有的锁,而锁的突然解除,会对数据一致性产生危险,甚至程序崩溃解决方案还是使用标志位,然后在程序中使用while来检测标志位

【Java多线程-1】中断线程

Posted on 2015-06-10 | In blog |

中断

Thread.interrupt()

当一个线程执行时,另一个线程可以调用Thread对象的interrupt()方法来中断它

interrupt()方法

在目标线程中设置一个标志,表示它已经被中断。类似sleep这样的方法,会检查该标志位,如果被置位会抛出InterruptedException异常
目标线程可以catch到InterruptedException,但是并不会中断该线程try-catch之后的代码执行,如果catch语句块中没有类似return的语句,则会在catch之后继续顺序执行。

isInterrupted()方法

判断中断状态
可以在Thread对象上调用isInterrupted()方法来检查任何线程的中断状态。

注意:

线程一旦被中断,isInterrupted()方法便会返回true,而一旦sleep()方法抛出异常,它将清空中断标志,此时isInterrupted()方法将返回false。

// 中断该线程
// 除了是线程自己interrupt自己,否则都要使用checkAccess函数进行权限检查
// 可能会引起SecurityException()
// 如果线程被wait() join() sleep()函数阻塞住,那么其interrupt的状态会被清空
// 并且会抛出InterruptedException
// 如果线程被阻塞在InterruptibleChannel上的nio操作,那么该channel会被关闭
// 线程的interrupt状态会被置位,线程也会收到ClosedByInterruptException异常
// 如果线程在nio.channels.Selector上阻塞,则线程的interrupt状态会被置位
// 并且会从选定的操作中立即返回...
// 如果不是上述的情况,那么线程的interrupt状态将被设置
// 对非alive的线程,使用该方法,没有任何作用
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();
    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

Thread.interrupted()方法判断中断状态

可以使用Thread.interrupted()方法来检查当前线程的中断状态(并隐式重置为false)。又由于它是静态方法,因此不能在特定的线程上使用,而只能报告调用它的线程的中断状态,如果线程被中断,而且中断状态尚不清楚,那么,这个方法返回true。
与isInterrupted()不同,它将自动重置中断状态为false,第二次调用Thread.interrupted()方法,总是返回false,除非中断了线程。
建议使用isInterrupted()方法

示例代码:

public class ThreadTest implements Runnable{
       public void run() {
              try{
                     System.out.println("in run() - sleep about 20s ...");
                     Thread.sleep(20000);
                     System.out.println("in run() - wake up!");
              } catch (InterruptedException e) {
                     System.out.println("in run() - interrupted while sleeping!");
                     return;        //返回run调用处
              }
              System.out.println("in run() - leaving normally");
       }

       public static void main(String[] args) throws Exception{
              ThreadTest r = new ThreadTest();
              Thread t = new Thread(r);
              t.start();
              Thread.sleep(2000);
              System.out.println("in main() - interrupting!");
              t.interrupt();
              System.out.println("in main() - leaving");
       }
// END_OF_CLASS
}

输出:
run()方法sleep大于2s之后,被中断,并且立即返回
同样是2s被终端了try中的操作,但是继续顺序执行之后的代码,并不是立即返回

待决中断

sleep()方法的实现检查到休眠线程被中断,它会相当友好地终止线程,并抛出InterruptedException异常。另外一种情况,如果线程在调用sleep()方法前被中断,那么该中断称为【待决中断】
它会在遇到sleep())方法时,立即抛出InterruptedException异常。

我目前的理解就是,Thread.interrupt()只是改变了Thread内部的中断标识,不会终止程序的运行
当运行到sleep方法时,会检查中断标识,并抛出异常,然后就是用户处理的部分了

【深入理解Java虚拟机2-笔记】可达性分析&引用

Posted on 2015-06-07 | In JVM |

引用计数法

Java并没有使用传统的引用计数法,理由很简单,该算法无法很好的解决对象间相互循环引用的问题。

可达性分析算法

Java(甚至是C#和Lisp)的主流实现中,都是使用可达性分析(Reachability Chain)来判定对象是否存活的。

基本思路:通过一系列的成为GC Roots的对象作为起点,从这些起点开始向下搜索,搜索过程的路径称为Reference Chain,当一个对象没有任何引用链相连时,则证明此对象是不可用的。所以即便有些对象相互引用,但是因为和GC Roots之间不可达,依然会被GC掉。

作为GC Roots的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用

无论是通过引用计数法还是可达性分析来判断对象是否存活,都和“引用”相关。

  • 强引用(Strong Reference)

    类似Object obj = new Object();这类引用,只要强引用还在,GC就不会回收

  • 软引用(Soft Reference)

    用来描述一些还有用,但并非必须的对象。对于软引用关联的对象,在OOM之前,将会把这些对象进行二次回收。如果之后还是没有足够内存,就会跑出内存溢出异常。

  • 弱引用(Weak Reference)

    同样用来描述非必须对象,但是强度低于弱引用。在下一次GC时一定会被回收。

  • 虚引用(Phantom Reference)

    一个对象是否有虚引用,完全不会对其生存时间造成影响,也无法通过虚引用获得一个对象实例。唯一目的就是可以在这个对象呗收集器回收时收到一个系统通知。
    一旦GC决定一个“obj”是虚可达的,它(指PhantomReference)将会被入队到对应的队列,但是它的指代并没有被清除。也就是说,虚引用的引用队列一定要明确地被一些应用程序代码所处理。
    虚引用在实现一个对象被回收之前必须做清理操作是很有用的。有时候,他们比finalize()方法更灵活。

补充:

  1. 软引用可以用来实现Caching和pooling
  2. 可以使用弱引用来实现对HashMap/HashSet中对象回收,但是要看使用场景。

    1
    2
    3
    4
    5
    6
    7
    class Registry {
    private Set registeredObjects = new HashSet();
    public void register(Object object) {
    registeredObjects.add( new WeakReference(object) ); //如果不适用WeakReference则HashSet中的对象永远不会回收
    }
    }
  3. 虚引用可以用来替代finalize()做一些回收之前的操作。例如,page缓存在gc之前需要被刷回磁盘。

参考

1. Java 如何有效地避免OOM:善于利用软引用和弱引用

【深入理解Java虚拟机1-笔记】HotSpot对象探秘

Posted on 2015-06-06 | In JVM |

1. 对象的创建

虚拟机遇到一个new指令时

  1. 检查这个指令的参数是否能在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则必须先执行相应的类加载过程。
  2. 对新生对象分配内存
    1. 内存是绝对规整的: Bump the Pointer指针碰撞。就是仅仅把内存分界指针挪动一段与对象大小相等的距离。
    2. 内存不是规整的: Free List通过空闲列表中找到足够大的空闲内存
  3. 将分配到的内存空间初始化为0(不包括对象头)
  4. 虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些都存储在对象头(Object Head)中

关于内存分配

GC收集器 内存分配算法
Serial、ParNew(带Compact过程的收集器) 指针碰撞
CMS(基于Mark-Sweep) Free List

问题:对象创建在虚拟机中是非常频繁的,即使是仅仅修改一个指针,在并发的情况下也不一定是安全的。
解决:

  • 同步处理:CAS
  • 将内存分配的操作划分在不同的空间中,即每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

虚拟机对TLAB使用-XX:+/-UseTLAB参数来配置

2. 对象的内存布局

  • 对象头(Object Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

对象的访问定位

建立对象是为了使用,我们在Java程序中使用栈上的reference数据来操作堆上的具体对象。Java在虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的具体位置,所以这是取决于虚拟机实现而定的。目前主流方式有两种:

  • 句柄

    Java堆中将会划分中一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的地址信息。好处是移动对象时,只需要移动句柄,而不是reference。

  • 直接指针

    堆中对象实例包含了类型数据的指针信息,reference中存储的就是对象实例地址。

【深入理解Java虚拟机0-笔记】Java内存结构

Posted on 2015-06-02 | In JVM |
  • 方法区
  • 堆
  • 虚拟机栈
  • 本地方法栈(Native Method Stack)
  • 程序计数器

【Java多线程-0】综述

Posted on 2015-05-29 | In 多线程系列 |

综述

并发编程的挑战:

  1. 上下文切换
    可能出现并行程序性能不能最优化,甚至慢与串行程序
  2. 死锁
  3. 资源限制的挑战
    受限与资源的限制,可能将并行程序最终按照了串行方式执行(例如,某一资源同时只允许一个线程访问)

线程:
程序中单独顺序的控制流,本身依靠程序进行运行
进程:
执行中的程序

线程状态

创建状态:new
就绪状态:调用了start()等待调度
运行状态:执行run()
阻塞状态:暂停执行,可能在等待IO
终止状态:线程销毁

线程的优先级

1 : MIN_PRIORITY
10: MAX_PRIORITY
5 : NORM_PRIORITY(默认)

通过setPriority()方法设置

实现方式

两种实现线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
1
2
3
class ClassName extends Thread{
Run()
}

线程调度器来分别调用所有线程的run()方法

Thread 常用方法

new Thread()    //创建
new Thread(String name)
new Thread(Runnable target)
new Thread(
    Runnable target,
    String name
)

void start()    //启动线程

static void sleep(long millis)    //线程休眠
static void sleep(
    long millis,
    int nanos
)

void join()    //使其他线程等待当前线程终止
void join(long millis)
void join(
    long millis,
    int nanos
)

stativ void yield()    //当前运行线程释放处理器资源
//获取线程引用    
static Thread currentThread()    //返回当前运行的线程引用
//是否启动
boolean isAlive()    //是否启动
//优先级    
void setPriority(int newPriority)    //设置线程优先级

继承Thread和实现Runnable接口的区别

(1)适合多个相同程序代码的线程去处理同一资源的情况

把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。

(2)可以避免由于Java的单继承特性带来的局限。

我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。

(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。

当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。

12

zzzvvvxxxd

30 posts
12 categories
15 tags
RSS
© 2017 zzzvvvxxxd
Powered by Hexo
|
Theme — NexT.Muse v5.1.3