首页 文章 精选 留言 我的

精选列表

搜索[全模态理解],共10000篇文章
优秀的个人博客,低调大师

深入理解线程通信

前言 开发中不免会遇到需要所有子线程执行完毕通知主线程处理某些逻辑的场景。 或者是线程 A 在执行到某个条件通知线程 B 执行某个操作。 可以通过以下几种方式实现: 等待通知机制 等待通知模式是 Java 中比较经典的线程通信方式。 两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯。 如两个线程交替打印奇偶数: publicclassTwoThreadWaitNotify{ privateintstart=1; privatebooleanflag=false; publicstaticvoidmain(String[]args){ TwoThreadWaitNotifytwoThread=newTwoThreadWaitNotify(); Threadt1=newThread(newOuNum(twoThread)); t1.setName("A"); Threadt2=newThread(newJiNum(twoThread)); t2.setName("B"); t1.start(); t2.start(); } /** *偶数线程 */ publicstaticclassOuNumimplementsRunnable{ privateTwoThreadWaitNotifynumber; publicOuNum(TwoThreadWaitNotifynumber){ this.number=number; } @Override publicvoidrun(){ while(number.start<=100){ synchronized(TwoThreadWaitNotify.class){ System.out.println("偶数线程抢到锁了"); if(number.flag){ System.out.println(Thread.currentThread().getName()+"+-+偶数"+number.start); number.start++; number.flag=false; TwoThreadWaitNotify.class.notify(); }else{ try{ TwoThreadWaitNotify.class.wait(); }catch(InterruptedExceptione){ e.printStackTrace(); } } } } } } /** *奇数线程 */ publicstaticclassJiNumimplementsRunnable{ privateTwoThreadWaitNotifynumber; publicJiNum(TwoThreadWaitNotifynumber){ this.number=number; } @Override publicvoidrun(){ while(number.start<=100){ synchronized(TwoThreadWaitNotify.class){ System.out.println("奇数线程抢到锁了"); if(!number.flag){ System.out.println(Thread.currentThread().getName()+"+-+奇数"+number.start); number.start++; number.flag=true; TwoThreadWaitNotify.class.notify(); }else{ try{ TwoThreadWaitNotify.class.wait(); }catch(InterruptedExceptione){ e.printStackTrace(); } } } } } } } 输出结果: t2+-+奇数93 t1+-+偶数94 t2+-+奇数95 t1+-+偶数96 t2+-+奇数97 t1+-+偶数98 t2+-+奇数99 t1+-+偶数100 这里的线程 A 和线程 B 都对同一个对象 TwoThreadWaitNotify.class 获取锁,A 线程调用了同步对象的 wait() 方法释放了锁并进入 WAITING 状态。 B 线程调用了 notify() 方法,这样 A 线程收到通知之后就可以从 wait() 方法中返回。 这里利用了 TwoThreadWaitNotify.class 对象完成了通信。 有一些需要注意: wait() 、nofify() 、nofityAll() 调用的前提都是获得了对象的锁(也可称为对象监视器)。 调用 wait() 方法后线程会释放锁,进入 WAITING 状态,该线程也会被移动到等待队列中。 调用 notify() 方法会将等待队列中的线程移动到同步队列中,线程状态也会更新为 BLOCKED 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。 等待通知有着一个经典范式: 线程 A 作为消费者: 获取对象的锁。 进入 while(判断条件),并调用 wait() 方法。 当条件满足跳出循环执行具体处理逻辑。 线程 B 作为生产者: 获取对象锁。 更改与线程 A 共用的判断条件。 调用 notify() 方法。 伪代码如下: //ThreadA synchronized(Object){ while(条件){ Object.wait(); } //dosomething } //ThreadB synchronized(Object){ 条件=false;//改变条件 Object.notify(); } join() 方法 privatestaticvoidjoin()throwsInterruptedException{ Threadt1=newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("running"); try{ Thread.sleep(3000); }catch(InterruptedExceptione){ e.printStackTrace(); } } }); Threadt2=newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("running2"); try{ Thread.sleep(4000); }catch(InterruptedExceptione){ e.printStackTrace(); } } }); t1.start(); t2.start(); //等待线程1终止 t1.join(); //等待线程2终止 t2.join(); LOGGER.info("mainover"); } 输出结果: 2018-03-1620:21:30.967[Thread-1]INFOc.c.actual.ThreadCommunication-running2 2018-03-1620:21:30.967[Thread-0]INFOc.c.actual.ThreadCommunication-running 2018-03-1620:21:34.972[main]INFOc.c.actual.ThreadCommunication-mainover 在 t1.join() 时会一直阻塞到 t1 执行完毕,所以最终主线程会等待 t1 和 t2 线程执行完毕。 其实从源码可以看出,join() 也是利用的等待通知机制: 核心逻辑: while(isAlive()){ wait(0); } 在 join 线程完成后会调用 notifyAll() 方法,是在 JVM 实现中调用,所以这里看不出来。 volatile 共享内存 因为 Java 是采用共享内存的方式进行线程通信的,所以可以采用以下方式用主线程关闭 A 线程: publicclassVolatileimplementsRunnable{ privatestaticvolatilebooleanflag=true; @Override publicvoidrun(){ while(flag){ System.out.println(Thread.currentThread().getName()+"正在运行。。。"); } System.out.println(Thread.currentThread().getName()+"执行完毕"); } publicstaticvoidmain(String[]args)throwsInterruptedException{ VolatileaVolatile=newVolatile(); newThread(aVolatile,"threadA").start(); System.out.println("main线程正在运行"); TimeUnit.MILLISECONDS.sleep(100); aVolatile.stopThread(); } privatevoidstopThread(){ flag=false; } } 输出结果: threadA正在运行。。。 threadA正在运行。。。 threadA正在运行。。。 threadA正在运行。。。 threadA执行完毕 这里的 flag 存放于主内存中,所以主线程和线程 A 都可以看到。 flag 采用 volatile 修饰主要是为了内存可见性,更多内容可以查看这里。 CountDownLatch 并发工具 CountDownLatch 可以实现 join 相同的功能,但是更加的灵活。 privatestaticvoidcountDownLatch()throwsException{ intthread=3; longstart=System.currentTimeMillis(); finalCountDownLatchcountDown=newCountDownLatch(thread); for(inti=0;i<thread;i++){ newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("threadrun"); try{ Thread.sleep(2000); countDown.countDown(); LOGGER.info("threadend"); }catch(InterruptedExceptione){ e.printStackTrace(); } } }).start(); } countDown.await(); longstop=System.currentTimeMillis(); LOGGER.info("mainovertotaltime={}",stop-start); } 输出结果: 2018-03-1620:19:44.126[Thread-0]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1620:19:44.126[Thread-2]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1620:19:44.126[Thread-1]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1620:19:46.136[Thread-2]INFOc.c.actual.ThreadCommunication-threadend 2018-03-1620:19:46.136[Thread-1]INFOc.c.actual.ThreadCommunication-threadend 2018-03-1620:19:46.136[Thread-0]INFOc.c.actual.ThreadCommunication-threadend 2018-03-1620:19:46.136[main]INFOc.c.actual.ThreadCommunication-mainovertotaltime=2012 CountDownLatch 也是基于 AQS(AbstractQueuedSynchronizer) 实现的,更多实现参考 ReentrantLock 实现原理 初始化一个 CountDownLatch 时告诉并发的线程,然后在每个线程处理完毕之后调用 countDown() 方法。 该方法会将 AQS 内置的一个 state 状态 -1 。 最终在主线程调用 await() 方法,它会阻塞直到 state == 0 的时候返回。 CyclicBarrier 并发工具 privatestaticvoidcyclicBarrier()throwsException{ CyclicBarriercyclicBarrier=newCyclicBarrier(3); newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("threadrun"); try{ cyclicBarrier.await(); }catch(Exceptione){ e.printStackTrace(); } LOGGER.info("threadenddosomething"); } }).start(); newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("threadrun"); try{ cyclicBarrier.await(); }catch(Exceptione){ e.printStackTrace(); } LOGGER.info("threadenddosomething"); } }).start(); newThread(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("threadrun"); try{ Thread.sleep(5000); cyclicBarrier.await(); }catch(Exceptione){ e.printStackTrace(); } LOGGER.info("threadenddosomething"); } }).start(); LOGGER.info("mainthread"); } CyclicBarrier 中文名叫做屏障或者是栅栏,也可以用于线程间通信。 它可以等待 N 个线程都达到某个状态后继续运行的效果。 首先初始化线程参与者。 调用 await() 将会在所有参与者线程都调用之前等待。 直到所有参与者都调用了 await() 后,所有线程从 await() 返回继续后续逻辑。 运行结果: 2018-03-1822:40:00.731[Thread-0]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1822:40:00.731[Thread-1]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1822:40:00.731[Thread-2]INFOc.c.actual.ThreadCommunication-threadrun 2018-03-1822:40:00.731[main]INFOc.c.actual.ThreadCommunication-mainthread 2018-03-1822:40:05.741[Thread-0]INFOc.c.actual.ThreadCommunication-threadenddosomething 2018-03-1822:40:05.741[Thread-1]INFOc.c.actual.ThreadCommunication-threadenddosomething 2018-03-1822:40:05.741[Thread-2]INFOc.c.actual.ThreadCommunication-threadenddosomething 可以看出由于其中一个线程休眠了五秒,所有其余所有的线程都得等待这个线程调用 await() 。 该工具可以实现 CountDownLatch 同样的功能,但是要更加灵活。甚至可以调用 reset() 方法重置 CyclicBarrier (需要自行捕获 BrokenBarrierException 处理) 然后重新执行。 线程响应中断 publicclassStopThreadimplementsRunnable{ @Override publicvoidrun(){ while(!Thread.currentThread().isInterrupted()){ //线程执行具体逻辑 System.out.println(Thread.currentThread().getName()+"运行中。。"); } System.out.println(Thread.currentThread().getName()+"退出。。"); } publicstaticvoidmain(String[]args)throwsInterruptedException{ Threadthread=newThread(newStopThread(),"threadA"); thread.start(); System.out.println("main线程正在运行"); TimeUnit.MILLISECONDS.sleep(10); thread.interrupt(); } } 输出结果: threadA运行中。。 threadA运行中。。 threadA退出。。 可以采用中断线程的方式来通信,调用了 thread.interrupt() 方法其实就是将 thread 中的一个标志属性置为了 true。 并不是说调用了该方法就可以中断线程,如果不对这个标志进行响应其实是没有什么作用(这里对这个标志进行了判断)。 但是如果抛出了 InterruptedException 异常,该标志就会被 JVM 重置为 false。 线程池 awaitTermination() 方法 如果是用线程池来管理线程,可以使用以下方式来让主线程等待线程池中所有任务执行完毕: privatestaticvoidexecutorService()throwsException{ BlockingQueue<Runnable>queue=newLinkedBlockingQueue<>(10); ThreadPoolExecutorpoolExecutor=newThreadPoolExecutor(5,5,1,TimeUnit.MILLISECONDS,queue); poolExecutor.execute(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("running"); try{ Thread.sleep(3000); }catch(InterruptedExceptione){ e.printStackTrace(); } } }); poolExecutor.execute(newRunnable(){ @Override publicvoidrun(){ LOGGER.info("running2"); try{ Thread.sleep(2000); }catch(InterruptedExceptione){ e.printStackTrace(); } } }); poolExecutor.shutdown(); while(!poolExecutor.awaitTermination(1,TimeUnit.SECONDS)){ LOGGER.info("线程还在执行。。。"); } LOGGER.info("mainover"); } 输出结果: 2018-03-1620:18:01.273[pool-1-thread-2]INFOc.c.actual.ThreadCommunication-running2 2018-03-1620:18:01.273[pool-1-thread-1]INFOc.c.actual.ThreadCommunication-running 2018-03-1620:18:02.273[main]INFOc.c.actual.ThreadCommunication-线程还在执行。。。 2018-03-1620:18:03.278[main]INFOc.c.actual.ThreadCommunication-线程还在执行。。。 2018-03-1620:18:04.278[main]INFOc.c.actual.ThreadCommunication-mainover 使用这个 awaitTermination() 方法的前提需要关闭线程池,如调用了 shutdown() 方法。 调用了 shutdown() 之后线程池会停止接受新任务,并且会平滑的关闭线程池中现有的任务。 管道通信 publicstaticvoidpiped()throwsIOException{//面向于字符PipedInputStream面向于字节 PipedWriterwriter=newPipedWriter(); PipedReaderreader=newPipedReader();//输入输出流建立连接 writer.connect(reader); Threadt1=newThread(newRunnable(){@Override publicvoidrun(){ LOGGER.info("running");try{for(inti=0;i<10;i++){ writer.write(i+""); Thread.sleep(10); } }catch(Exceptione){ }finally{try{ writer.close(); }catch(IOExceptione){ e.printStackTrace(); } } } }); Threadt2=newThread(newRunnable(){@Override publicvoidrun(){ LOGGER.info("running2");intmsg=0;try{while((msg=reader.read())!=-1){ LOGGER.info("msg={}",(char)msg); } }catch(Exceptione){ } } }); t1.start(); t2.start(); } 输出结果: 2018-03-1619:56:43.014[Thread-0]INFOc.c.actual.ThreadCommunication-running 2018-03-1619:56:43.014[Thread-1]INFOc.c.actual.ThreadCommunication-running2 2018-03-1619:56:43.130[Thread-1]INFOc.c.actual.ThreadCommunication-msg=0 2018-03-1619:56:43.132[Thread-1]INFOc.c.actual.ThreadCommunication-msg=1 2018-03-1619:56:43.132[Thread-1]INFOc.c.actual.ThreadCommunication-msg=2 2018-03-1619:56:43.133[Thread-1]INFOc.c.actual.ThreadCommunication-msg=3 2018-03-1619:56:43.133[Thread-1]INFOc.c.actual.ThreadCommunication-msg=4 2018-03-1619:56:43.133[Thread-1]INFOc.c.actual.ThreadCommunication-msg=5 2018-03-1619:56:43.133[Thread-1]INFOc.c.actual.ThreadCommunication-msg=6 2018-03-1619:56:43.134[Thread-1]INFOc.c.actual.ThreadCommunication-msg=7 2018-03-1619:56:43.134[Thread-1]INFOc.c.actual.ThreadCommunication-msg=8 2018-03-1619:56:43.134[Thread-1]INFOc.c.actual.ThreadCommunication-msg=9 Java 虽说是基于内存通信的,但也可以使用管道通信。 需要注意的是,输入流和输出流需要首先建立连接。这样线程 B 就可以收到线程 A 发出的消息了。 实际开发中可以灵活根据需求选择最适合的线程通信方式。 号外 最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。 地址: https://github.com/crossoverJie/Java-Interview java线程与并发相关内容推荐阅读:http://www.roncoo.com/course/list.html?courseName=java

优秀的个人博客,低调大师

8 张图理解 Java

一图胜千言,如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟。 1、字符串不变性 下面这张图展示了这段代码做了什么 String s = "abcd"; s = s.concat("ef"); 2、equals()方法、hashCode()方法的区别 HashCode被设计用来提高性能。equals()方法与hashCode()方法的区别在于: 如果两个对象相等(equal),那么他们一定有相同的哈希值。 如果两个对象的哈希值相同,但他们未必相等(equal)。 JAVA高级架构群:https://jq.qq.com/?_wv=1027&k=5gMDouY 3、Java异常类的层次结构 图中红色部分为受检查异常。它们必须被捕获,或者在函数中声明为抛出该异常。 4、集合类的层次结构 注意Collections和Collection的区别。(Collections包含有各种有关集合操作的静态多态方法) 5、Java同步 Java同步机制可通过类比建筑物来阐明。 6、别名 别名意味着有多个变量指向同一可被更新的内存块,这些别名分别是不同的对象类型。 7、堆和栈 图解表明了方法和对象在运行时内存中的位置。 8、Java虚拟机运行时数据区域 图解展示了整个虚拟机运行时数据区域的情况。

优秀的个人博客,低调大师

深入理解 java volatile

在开始讲volatile之前,我们需要对以下的内容有所了解. java 内存模型(JMM) 在java中,java堆内存是存在数据共享的,这些共享数据的通信就是通过java内存模型(JMM)来控制的. JMM决定一个线程对共享数据的写入何时对另一个线程可见. JMM是一个抽象的结构,它定义了线程和主内存的关系: 线程之间的共享变量存储在主内存(Main Memory)中 每一个线程都有一个私有的本地内存(Local Memory) 本地内存中储存了该线程可以读写变量的副本. JMM 只有存放在java堆和方法区中的数据,才会被线程共享,对于其他区是属于线程私有的数据,不受JMM的影响. 从JMM中可以看出,如果线程之间的数据,是不能直接进行数据传递的,一定要经过主内存进行传递. A线程更新数据 -> 刷新主内存数据 -> B线程读取主线程数据 为什么需要JMM? 为什么需要内存模型,直接读写内存不可以吗? 主要是因为下面两个原因. CPU缓存一致性 CPU与内存读写和运算速度不在一个量级,CPU效率会比内存高的多. 为了解决CPU和内存效率差异问题,引入了 高速缓存(Cache)和写缓冲区(Write Buffer)等,来作为cpu和内存的传输媒介,使用缓冲中读写可能造成数据不一致的问题. 处理器优化和指令重排 处理器优化:处理器为了优化 执行效率, 可能会将输入的代码进行乱序执行处理.指令重排:JIT编译过程也可能会对指令进行乱序处理 . 为了解决上次两个问题,需要引入JMM,而不是直接操作内存变量。 为了解决 <1>,引入主内存和线程的本地内存概念. 为了解决 <2>,通过 禁止处理器优化, 和 内存屏障来解决. 原子性,可见性,有序性 原子性 : 表示不可被中断的一个或一系列操作.一旦开始,就一直运行到结束,中间不会有任何线程切换(context switch)。 可见性 : 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值. 有序性 : 由于指令的执行,会经过编译器和处理的重排序,有序性是指从指令上的执行结果上看,指令的执行顺序是有序的.根据as-if-serial语义,单线程中,程序的结果不能被改变.在多线程并发中, 提供 happens-before规则来支持有序性. volatile Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。 它被称为轻量级的 synchronized, 它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。 volatile的特性 可见性 : 对一个volatile的变量的读,总是能看到任意线程对这个变量最后的写入. 单个读或者写具有原子性 : 对于单个volatile变量的读或者写具有原子性,复合操作不具有.(如i++) 互斥性 : 同一时刻只允许一个线程对变量进行操作.(互斥锁的特点) volatile的内存语义 volatile的写和锁的释放具有相同的语义,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存. volatile的读和锁的获取有相同的语义,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。 volatile内存语义的实现 为了实现volatile语义,JMM分为编译器重排序和处理器重排序进行了特殊的处理. 针对编译器重排序的处理,有如下规则 volatile重排序规则表 针对处理器的重排序,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序 在每个volatile写操作的前面插入一个StoreStore屏障 // 禁止上面的写重排序 在每个volatile写操作的后面插入一个StoreLoad屏障 // 禁止上面的写或下面的读/写重排序 在每个volatile读操作的后面插入一个LoadLoad屏障 // 禁止下面的读重排序 在每个volatile读操作的后面插入一个LoadStore屏障 // 禁止下面的下重排序 volatile的使用条件 对变量的写操作不依赖于当前值 或 能够确保只有单一线程能够修改变量的值 如 i++操作,变量的写操作依赖当前值,所以不能保证线程安全. 该变量没有包含在具有其他变量的不变式中 如 i<value,即使i变量声明为volatile,也不能保证线程安全,value可能在运行判断的时候发生变化. 正确使用volatile 下面提出几种使用 volatile的场景. 状态标志 作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束. 状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换 volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // todo... } } 一次性安全发布(one-time safe publication) 在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源)。 //基于volatile的解决方案 public class SafeDoubleCheckSingleton { //通过volatile声明,实现线程安全的延迟初始化 private volatile static SafeDoubleCheckSingleton singleton; private SafeDoubleCheckSingleton(){ } public static SafeDoubleCheckSingleton getInstance(){ if (singleton == null){ synchronized (SafeDoubleCheckSingleton.class){ if (singleton == null){ //原理利用volatile在于 禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序 singleton = new SafeDoubleCheckSingleton(); } } } return singleton; } } 由于对象的创建,可以拆分成以下指令: 对象创建顺序 在多线程环境中,如果没有对变量 声明为volatile,将可能出现以下情况,其他线程可能得到的是null而不是完成初始化的对象. 对象创建乱序 独立观察(independent observation) 将 volatile变量用于多个独立观察结果的发布,是"状态标志"的拓展,该值随时会发生变化,同时会被反复使用,前者一般就是用一次 ;只是简单的赋值操作,不会做复合操作. class CustomLinkedList{ public volatile Node lastNode; ..... public void add() { Node node = new Node(); ..... lastNode = node;//将新节点作为最后一个节点 } } 开销较低的读-写锁策略 当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销 利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性 public class Counter { private volatile int value; //利用volatile保证读取操作的可见性, 读取时无需加锁 public int getValue() { return value; } // 使用 synchronized 加锁 public synchronized int increment() { return value++; } } 参考 memory barriers(内存屏障) 内存屏障的作用 : 阻止屏障两侧的指令重排序 强制刷新主内存数据,以及让缓存中相应的数据失效。 java的内存屏障有的四种,LoadLoad,StoreStore,LoadStore,StoreLoad 内存屏障类型表 as-if-serial as-if-serial的语义是, 不管怎么重排序,单线程中程序的执行结果不能被改变. 编译器,runtime,处理器都需要遵守该语义. happens-before 程序顺序原则:一个线程内保证语义的串行性.对于单线程来讲,必须保证重排后的结果与重排前一致。 volatile规则:volatile变量的写,先发生于后续对这个变量的读.这保证了volatile变量的可见性. 监视锁规则:对于一个锁的解锁,先发生于随后对这个锁的加锁. 否则随后的加锁将会失败. 传递性:A先于B,B等于C,那么A必然先于C. 线程启动规则:Thread对象的start()方法先发生于此线程的其他任意动作。 线程终止规则:线程的所有操作都先发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 线程中断规则:对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断时事件的操作。 对象终结规则:一个对象的初始化完成(构造函数执行结束)先发生于它的finalize()方法的开始 引用 java并发编程的艺术 并发番@Java内存模型&Volatile一文通(1.7版) 正确使用 Volatile 变量

优秀的个人博客,低调大师

理解Activity的启动模式

Activity的启动模式有哪几种,分别用于什么场景? standard:标准模式 系统的默认模式。一种典型的多实例实现。每次启动一个Activity都会重新创建一个新的实例。被启动的Activity会被放入启动者的栈中,如果启动者是非Activity类型的Context(如ApplicationContext),并没有所谓的任务栈,就会报错,此时需为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,创建一个新栈。没有特殊需求默认是这种模式。 singleTop:栈顶复用模式 如果一个Activity的实例已经在栈顶存在,启动这个Activity时,不会创建新的Activity,而会回调onNewIntent(),如果不是在栈顶存在,则会新建一个实例。 适合接收通知启动的内容显示页面。当收到多条新闻推送时,用于展示新闻的Activity设置成此模式,根据传来的intent数据显示不同的新闻信息,不会启动多个Activity。 singleTask:栈内复用模式 一个单实例模式。只要Activity的实例在一个栈中存在,再次启动Activity都不会重新创建实例只会回调onNewIntent(),并从栈中弹出实例上面所有的实例。 适合作为程序入口点,例如浏览器的主界面。不管从多少个不同应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。 singleInstance:单实例模式 具有singleTask的所有特性,设置此模式的Activity只能单独存在一个任务栈中。 清晰地描述下onNewIntent和onConfigurationChanged这两个生命周期方法的场景? onNewIntent 在singleTop、singleTask、singleInstance模式下,启动相同的Activity,期望只有一个实例存在,再次启动就会调用onNewIntent()。在onNewIntent中可以setIntent(intent)刷新intent数据。 onConfigurationChanged 当Android设备正在运行App时,如果设备的语言、屏幕方向、键盘的参数改变了,默认会销毁当前Activity并重新创建一次来加载新的配置信息。为了防止Actitvity被系统销毁,可以在AndroidManifest.xml文件中给<Activity>标签设置android:configChanges,可选值如下: 属性值 含义 mcc SIM卡唯一标识IMSI(国际移动用户标识码)中的国家代码,由三位数字组成(中国为460)。 这里表示mcc发生了改变 mnc SIM卡唯一标识IMSI(国际移动用户标识码)中的运营商代码,由两位数字组成,中国移动TD系统为00,中国联通为01,电信为03。这里表示mnc发生了改变 locale 设备的本地位置发生了改变,一般指的是切换了系统语言 touchscreen 触摸屏发生了改变 keyboard 键盘类型发生了改变,比如用户使用了外接键盘 keyboardHidden 键盘的可访问性发生了改变,比如用户调出了键盘 navigation 系统导航方式发生了改变 screenLayout 屏幕布局发生了改变,很可能是用户激活了另外一个显示设备 fontScale 系统字体缩放比例发生了改变,比如用户选择了个新的字号 uiMode 用户界面模式发生了改变,比如开启夜间模式-API8新添加 orientation 屏幕方向发生改变,比如旋转了手机屏幕 screenSize 当屏幕尺寸信息发生改变(当编译选项中的minSdkVersion和targeSdkVersion均低于13时不会导致Activity重启)—API13新添加 smallestScreenSize 设备的物理屏幕尺寸发生改变,这个和屏幕方向没关系,比如切换到外部显示设备—API13新添加 layoutDirection 当布局方向发生改变的时候,正常情况下无法修改布局的layoutDirection的属性—API17新添加 申明的配置发生改变时,将不会重启Activity,而是回调onConfigurationChanged。如果设置orientation,当屏幕方向发生旋转时,会阻止系统销毁Activity重建,而是保持运行,回调Activity的onConfigurationChanged(),我们需要在这个方法中自己处理旋转后的操作。onConfigurationChanged中会传入一个Configuration对象,通过读取对象中最新的配置信息,来自行适配新的UI界面。 从Android 3.2 (API13) 开始,当设备旋转时,screenSize也会改变,因此需要设置android:configChange="orientation|screenSize"。

优秀的个人博客,低调大师

理解synchronized关键字

Java多线程中的同步机制会对资源进行加锁,保证在同一时间只有一个线程可以操作对应资源,避免多程同时访问相同资源发生冲突。synchronized是Java中的关键字,它是一种同步锁,可以实现同步机制。 synchronized主要修饰的对象有以下三种: 修饰普通方法 一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。 修饰静态方法 由于静态方法是类方法,所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。 修饰代码块 其中普通代码块如synchronized(obj),这里的obj可以为类中的一个属性,也可以是当前的对象,它的同步效果和修饰普通方法一样;synchronized(obj.class)静态代码块,它的同步效果和修饰静态方法类似。 synchronized方法控制范围较大,它会同步对象中所有synchronized方法的代码。 synchronized代码块控制范围较小,它只会同步代码块中的代码,而位于代码块之外的代码是可以被多个线程访问的。 简单来说,就是synchronized代码块更加灵活精确。 问题 有如下一个类 A class A { public synchronized void a() { } public synchronized void b() { } } 然后创建两个对象 A a1 = new A(); A a2 = new A(); 然后在两个线程中并发访问如下代码: Thread1Thread2 a1.a();a2.a(); 请问二者能否构成线程同步? 如果A的定义是下面这种呢? class A { public static synchronized void a() { } public static synchronized void b() { } } 答案 问题1 :不能同步 问题2:能同步

优秀的个人博客,低调大师

理解Java线程池ThreadPoolExecutor

在Java的线程池的使用会有比较多的地方,有比较多的应用场景,介绍一下Java线程池ThreadPoolExecutor。 线程是一个操作系统概念。操作系统负责这个线程的创建、挂起、运行、阻塞和终结操作。而操作系统创建线程、切换线程状态、终结线程都要进行CPU调度----这是一个耗费时间和系统资源的事情。 大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。 1.ThreadPoolExecutor类 二话不多说,来看一下ThreadPoolExecutor类的具体实现源码。 继承抽象类AbstractExecutorService,它实现了ExecutorService接口 public class ThreadPoolExecutor extends AbstractExecutorService{····} public abstract class AbstractExecutorService implements ExecutorService {····} public interface ExecutorService extends Executor {····} public interface Executor {····} ThreadPoolExecutor类中提供了四个构造方法: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } 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.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } 构造函数中需要传入的参数包括corePoolSize、maximumPoolSize、keepAliveTime、timeUnit、workQueue、threadFactory和handler。 corePoolSize:线程池主要用于执行任务的是核心线程数量。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 非核心线程:设置的大于corePoolSize参数小于maximumPoolSize参数的部分,就是线程池可以临时创建的“非核心线程”的最大数量。这种情况下如果某个线程没有运行任何任务,在等待keepAliveTime时间后,这个线程将会被销毁,直到线程池的线程数量重新达到corePoolSize。 maximumPoolSize:当前线程池允许创建的最大线程数量。那么如果设置的corePoolSize参数和设置的maximumPoolSize参数一致时,线程池在任何情况下都不会回收空闲线程。keepAliveTime和timeUnit也就失去了意义。 keepAliveTime:线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。 timeUnit:参数keepAliveTime的时间单位。 workQueue:一个阻塞队列,用来存储等待执行的任务。一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue。 threadFactory:线程工厂,主要用来创建线程。 handler:表示当拒绝处理任务时的策略。有以下四种取值: ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 2.线程池实现原理 (1)任务的执行 先来看一下ThreadPoolExecutor类中其他的一些比较重要成员变量: private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务 private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小,runState等)的改变都要使用这个锁 private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集 private volatile long keepAliveTime; //线程存活时间 private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间 private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列) private volatile int maximumPoolSize; //线程池最大能容忍的线程数 private volatile int poolSize; //线程池中当前的线程数 private volatile RejectedExecutionHandler handler; //任务拒绝策略 private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程 private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数 private long completedTaskCount; //用来记录已经执行完毕的任务个数 在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可: public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* * Proceed in 3 steps: * * 1. If fewer than corePoolSize threads are running, try to * start a new thread with the given command as its first * task. The call to addWorker atomically checks runState and * workerCount, and so prevents false alarms that would add * threads when it shouldn't, by returning false. * * 2. If a task can be successfully queued, then we still need * to double-check whether we should have added a thread * (because existing ones died since last checking) or that * the pool shut down since entry into this method. So we * recheck state and if necessary roll back the enqueuing if * stopped, or start a new thread if there are none. * * 3. If we cannot queue task, then we try to add a new * thread. If it fails, we know we are shut down or saturated * and so reject the task. */ int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); } ····························································································· <未完待续>

优秀的个人博客,低调大师

理解Docker(2):Docker 镜像

本系列文章将介绍Docker的有关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 (4)Docker 容器的隔离性 - 使用 cgroups 限制容器使用的资源 (5)Docker 网络 对于每个软件,除了它自身的代码以外,它的运行还需要有一个运行环境和依赖。不管这个软件是象往常一样运行在物理机或者虚机之中,还是运行在现在的容器之中,这些都是不变的。在传统环境中,软件在运行之前也需要经过 代码开发->运行环境准备 -> 安装软件 -> 运行软件 等环节,在容器环境中,中间的两个环节被镜像制作过程替代了。也就是说,镜像的制作也包括运行环境准备和安装软件等两个主要环节,以及一些其他环节。因此,Docker 容器镜像其实并没有什么新的理论,只是这过程有了新的方式而已。 镜像(image)是动态的容器的静态表示(specification),包括容器所要运行的应用代码以及运行时的配置。Docker 镜像包括一个或者多个只读层( read-only layers ),因此,镜像一旦被创建就再也不能被修改了。一个运行着的Docker 容器是一个镜像的实例( instantiation )。从同一个镜像中运行的容器包含有相同的应用代码和运行时依赖。但是不像镜像是静态的,每个运行着的容器都有一个可写层( writable layer ,也成为容器层 container layer),它位于底下的若干只读层之上。运行时的所有变化,包括对数据和文件的写和更新,都会保存在这个层中。因此,从同一个镜像运行的多个容器包含了不同的容器层。 Docker有两种方式来创建一个容器镜像: 创建一个容器,运行若干命令,再使用 docker commit 来生成一个新的镜像。不建议使用这种方案。 创建一个 Dockerfile 然后再使用 docker build 来创建一个镜像。大多人会使用 Dockerfile 来创建镜像。 1. docker build 生成镜像 1.1 生成过程实例 在使用 Dockerfile 创建容器之前,需要先准备一个 Dockerfile 文件,然后运行 docker build 命令来创建镜像。我们通过下面的例子来看看Docker 创建容器的过程。 FROM ubuntu:14.04 MAINTAINER sammy "sammy@sammy.com" RUN apt-get update RUN apt-get -y install ntp EXPOSE 5555 CMD ["/usr/sbin/ntpd"] 这是一个非常简单的Dockerfile,它的目的是基于 Ubuntu 14.04 基础镜像安装 ntp 从而生成一个新的镜像。看看其过程: root@devstack:/home/sammy/ntponubuntu# docker build -t sammy_ntp2 . Sending build context to Docker daemon 2.048 kB Step 1 : FROM ubuntu:14.04 ---> 4a725d3b3b1c Step 2 : MAINTAINER sammy "sammy@sammy.com" ---> Using cache ---> c4299e3f774c Step 3 : RUN apt-get update ---> Using cache ---> 694a19d54103 Step 4 : RUN apt-get -y install ntp ---> Running in 9bd153c65a76 Reading package lists... ... Fetched 561 kB in 10s (51.1 kB/s) Selecting previously unselected package libedit2:amd64. (Reading database ... 11558 files and directories currently installed.) ... Processing triggers for libc-bin (2.19-0ubuntu6.9) ... Processing triggers for ureadahead (0.100.0-16) ... ---> 9cc05cf6f48d Removing intermediate container 9bd153c65a76 Step 5 : EXPOSE 5555 ---> Running in eb4633151d98 ---> f5c96137bec9 Removing intermediate container eb4633151d98 Step 6 : CMD /usr/sbin/ntpd ---> Running in e81b1eae3678 ---> af678df648bc Removing intermediate container e81b1eae3678 Successfully built af678df648bc Dockerfile 中的每个步骤都会对应每一个 docker build 输出中的 step。 Step 1:FROM ubuntu:14.04 获取基础镜像 ubuntu:14.04. Docker 首先会在本地查找,如果找到了,则直接利用;否则从 Docker registry 中下载。在第一次使用这个基础镜像的时候,Docker 会从 Docker Hub 中下载这个镜像,并保存在本地: Step 1 : FROM ubuntu:14.04 14.04: Pulling from library/ubuntu 862a3e9af0ae: Pull complete 6498e51874bf: Pull complete 159ebdd1959b: Pull complete 0fdbedd3771a: Pull complete 7a1f7116d1e3: Pull complete Digest: sha256:5b5d48912298181c3c80086e7d3982029b288678fccabf2265899199c24d7f89 Status: Downloaded newer image for ubuntu:14.04 ---> 4a725d3b3b1c 以后再使用的时候就直接使用这个镜像而不再需要下载了。 Step 2:MAINTAINER sammy "sammy@sammy.com" 本例中依然是从 Cache 中环境新的镜像。在第一次的时候,Docker 会创建一个临时的容器1be8f33c1846,然后运行 MAINTAINER 命令,再使用 docker commit 生成新的镜像 Step 2 : MAINTAINER sammy "sammy@sammy.com" ---> Running in 1be8f33c1846 ---> c4299e3f774c 通过这个临时容器的过程(create -> commit -> destroy),生成了新的镜像c4299e3f774c: 2016-09-16T21:58:09.010886393+08:00 container create 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 2016-09-16T21:58:09.060071206+08:00 container commit 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (comment=, image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 2016-09-16T21:58:09.071988068+08:00 container destroy 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras) 这个镜像是基于 ubuntu 14.04 基础镜像生成的,layers 没有变化,只是元数据 CMD 发生了改变: "Cmd": [ "/bin/sh", "-c", "#(nop) ", "MAINTAINER sammy \"sammy@sammy.com\"" ] 因此可以认为只是镜像的元数据发生了改变。生成的新的镜像作为中间镜像会被保存在 cache 中。 Step 3:RUN apt-get update 本例中Docker 仍然从缓存中获取了镜像。在第一次的时候,Docker 仍然是通过创建临时容器在执行 docker commit 的方式来创建新的镜像: Step 3 : RUN apt-get update ---> Running in 8b3b97af3bd7 Ign http://archive.ubuntu.com trusty InRelease Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB] ... Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB] Fetched 22.2 MB in 16min 21s (22.6 kB/s) Reading package lists... ---> 694a19d54103 Removing intermediate container 8b3b97af3bd7 通过以上步骤,生成了新的中间镜像694a19d54103,它也会被保存在缓存中。你可以使用 docker inspect694a19d54103 命令查看该中间镜像,但是无法在docker images 列表中找到它,这是因为 docker images 默认隐藏了中间状态的镜像,因此你需要使用 docker images -a 来获取它: root@devstack:/home/sammy# docker images -a | grep 694a19d54103 <none> <none> 694a19d54103 11 hours ago 210.1 MB 该镜像和原始镜像相比,多了一个 layer,它保存的是 apt-get update 命令所带来的变化: "RootFS": { "Type": "layers", "Layers": [ "sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4", "sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc", "sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4", "sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6", "sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a", "sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93" #这一层是新加的 ] } Step 4:RUN apt-get -y install ntp 和上面 Step 3 过程一样,这个步骤也会通过创建临时容器,执行该命令,再使用 docker commit 命令生成一个中间镜像9cc05cf6f48d。和上面步骤生成的镜像相比,它又多了一层: root@devstack:/home/sammy# docker images -a | grep 9cc05cf6f48d <none> <none> 9cc05cf6f48d 10 hours ago 212.8 MB root@devstack:/home/sammy# docker inspect --format={{'.RootFS.Layers'}} 9cc05cf6f48d [sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4 sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4 sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6 sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93 sha256:a93086f33a2b7ee18eec2454b468141f95a403f5081284b6f177f83cdb3d54ba] Step 5:EXPOSE 5555 这一步和上面的 Step 2 一样,Docker 生成了一个临时容器,执行 EXPOSE 55 命令,再通过 docker commit 创建了中间镜像f5c96137bec9。该镜像的 layers 没有变化,但是元数据发生了一些变化,包括: "ExposedPorts": { "5555/tcp": {} } "Cmd": [ "/bin/sh", "-c", "#(nop) ", "EXPOSE 5555/tcp" ] Step 6:CMD ["/usr/sbin/ntpd"] 这一步和上面的步骤相同,最终它创建了镜像af678df648bc,该镜像只是修改了 CMD 元数据: "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"/usr/sbin/ntpd\"]" ] 该镜像也是Docker 根据本 Dockerfile 生成的最终镜像。它也出现在了 docker images 结果中: root@devstack:/home/sammy# docker images | grep af678df648bc sammy_ntp2 latest af678df648bc 11 hours ago 212.8 MB 我们可以使用 docker history 命令查看该镜像中每一层的信息: root@devstack:/home/sammy/ntponubuntu# docker history af678df648bc IMAGE CREATED CREATED BY SIZE COMMENT af678df648bc 16 hours ago /bin/sh -c #(nop) CMD ["/usr/sbin/ntpd"] 0 B f5c96137bec9 16 hours ago /bin/sh -c #(nop) EXPOSE 5555/tcp 0 B 9cc05cf6f48d 16 hours ago /bin/sh -c apt-get -y install ntp 2.679 MB 694a19d54103 16 hours ago /bin/sh -c apt-get update 22.17 MB c4299e3f774c 17 hours ago /bin/sh -c #(nop) MAINTAINER sammy "sammy@sa 0 B 4a725d3b3b1c 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 3 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'doc 7 B <missing> 3 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB <missing> 3 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B <missing> 3 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /u 194.6 kB <missing> 3 weeks ago /bin/sh -c #(nop) ADD file:ada91758a31d8de3c7 187.8 MB 以上过程说明: 容器镜像包括元数据和文件系统,其中文件系统是指对基础镜像的文件系统的修改,元数据不影响文件系统,只是会影响容器的配置 每个步骤都会生成一个新的镜像,新的镜像与上一次的镜像相比,要么元数据有了变化,要么文件系统有了变化而多加了一层 Docker 在需要执行指令时通过创建临时镜像,运行指定的命令,再通过 docker commit 来生成新的镜像 Docker 会将中间镜像都保存在缓存中,这样将来如果能直接使用的话就不需要再从头创建了。关于镜像缓存,请搜索相关文档。 1.2 Docker 镜像分层,COW 和 镜像大小(size) 1.2.1 镜像分层和容器层 从上面例子可以看出,一个 Docker 镜像是基于基础镜像的多层叠加,最终构成和容器的 rootfs (根文件系统)。当 Docker 创建一个容器时,它会在基础镜像的容器层之上添加一层新的薄薄的可写容器层。接下来,所有对容器的变化,比如写新的文件,修改已有文件和删除文件,都只会作用在这个容器层之中。因此,通过不拷贝完整的 rootfs,Docker 减少了容器所占用的空间,以及减少了容器启动所需时间。 1.2.2 COW 和镜像大小 COW,copy-on-write 技术,一方面带来了容器启动的快捷,另一方也造成了容器镜像大小的增加。每一次 RUN 命令都会在镜像上增加一层,每一层都会占用磁盘空间。举个例子,在 Ubuntu 14.04 基础镜像中运行 RUN apt-get upgrade 会在保留基础层的同时再创建一个新层来放所有新的文件,而不是修改老的文件,因此,新的镜像大小会超过直接在老的文件系统上做更新时的文件大小。因此,为了减少镜像大小起见,所有文件相关的操作,比如删除,释放和移动等,都需要尽可能地放在一个 RUN 指令中进行。 比如说,通过将上面的示例 Dockerfile 修改为: FROM ubuntu:14.04 MAINTAINER sammy "sammy@sammy.com" RUN apt-get update && apt-get -y install ntp EXPOSE 5555 CMD ["/usr/sbin/ntpd"] 结果产生的镜像,不仅层数少了一层(7 -> 6),而且大小减少了 0.001M :),因为这个例子比较特殊,文件都是添加,而没有更新,因此size 的下降非常小。 1.2.3 使用容器需要避免的一些做法 这篇文章10 things to avoid in docker containers列举了一些在使用容器时需要避免的做法,包括: 不要在容器中保存数据(Don’t store data in containers) 将应用打包到镜像再部署而不是更新到已有容器(Don’t ship your application in two pieces) 不要产生过大的镜像 (Don’t create large images) 不要使用单层镜像 (Don’t use a single layerimage) 不要从运行着的容器上产生镜像 (Don’t create imagesfrom running containers) 不要只是使用 “latest”标签 (Don’t use only the “latest” tag) 不要在容器内运行超过一个的进程 (Don’t run more than one process in a single container) 不要在容器内保存 credentials,而是要从外面通过环境变量传入 (Don’t store credentialsin the image. Use environment variables) 不要使用 root 用户跑容器进程(Don’t run processes as a root user) 不要依赖于IP地址,而是要从外面通过环境变量传入 (Don’t relyon IPaddresses) 2. Dockerfile 语法 上面的步骤说明了 Docker 可以通过读取 Dockerfile 的内容来生成容器镜像。Dockerfile 的每一行都是INSTRUCTION arguments 格式,即 “指令 参数”。关于 Dockerfile 的预防,请参考https://docs.docker.com/engine/reference/builder/。下面只是就一些主要的指令做一些说明。 2.1 几个主要指令 2.1.1 ADD 和 COPY Add:将 host 上的文件拷贝到或者将网络上的文件下载到容器中的指定目录 # Usage: ADD [source directory or URL] [destination directory] ADD /my_app_folder /my_app_folder 例子: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> ADD temp dockfile ENTRYPOINT top ADD 指令会将本地 temp 目录中的文件拷贝到容器的 dockfile 目录下面,从而在镜像中增加一个 layer。在未指定绝对路径的时候,会放到 WORKDIR 目录下面。 root@cc2a5605f905:/# ls dockfile/ dockerfile-add dockerfile-cmd dockerfile-env dockerfile-ports dockerfile-user dockerfile-user-h root@cc2a5605f905:/# pwd / 那两者有什么区别呢? ADD 多了2个功能, 下载URL和对支持的压缩格式的包进行解压. 其他都一样。比如ADD http://foo.com/bar.go /tmp/main.go 会将文件从因特网上方下载下来,ADD /foo.tar.gz /tmp/ 会将压缩文件解压再COPY过去 如果你不希望压缩文件拷贝到container后会被解压的话, 那么使用COPY。 如果需要自动下载URL并拷贝到container的话, 请使用ADD 2.1.2 CMD CMD:在容器被创建后执行的命令,和 RUN 不同,它是在构造容器时候所执行的命令 # Usage 1: CMD application "argument", "argument", .. CMD "echo" "Hello docker!" CMD 有三种格式: CMD ["executable","param1","param2"](like an exec, preferred form) CMD ["param1","param2"](作为 ENTRYPOINT 的参数) CMD command param1 param2(作为 shell 运行) 一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。 2.1.3ENTRYPOINT ENTRYPOINT :设置默认应用,会保证每次容器被创建后该应用都会被执行。CMD 和 ENTRYPOINT 的关系会在下面详细解释。 2.1.4ENV:设置环境变量,可以使用多次 # Usage: ENV key value ENV SERVER_WORKS 4 设置了后,后续的RUN命令都可以使用,并且会作为容器的环境变量。举个例子,下面是 dockfile: FROM ubuntu:14.04 ENV abc=1 ENV def=2 ENTRYPOINT top 生成镜像:docker build -t envimg4 -f dockerfile-env . 其元数据包括了这两个环境变量: "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "abc=1", "def=2" ], 启动容器:docker run -it --name envc41 envimg4。也能看到: "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "abc=1", "def=2" ] 进入容器:能看到定义的 abc 和 def 变量 root@devstack:/home/sammy/ntponubuntu# docker exec -it envc41 bash root@ba460e0e9dc4:/# echo $abc 1 root@ba460e0e9dc4:/# echo $def 2 2.1.5EXPOSE :向容器外暴露一个端口 # Usage: EXPOSE [port] EXPOSE 8080 2.1.6 FROM:指定进行的基础镜像,必须是第一条指令 # Usage: FROM [image name] FROM ubuntu 2.1.7MAINTAINER:可以在任意地方使用,设置镜像的作者 # Usage: MAINTAINER [name] MAINTAINER authors_name 2.1.8RUN:运行命令,结果会生成镜像中的一个新层 # Usage: RUN [command] RUN aptitude install -y ntp 2.1.9USER:设置该镜像的容器的主进程所使用的用户,以及后续 RUN, CMD 和 ENTRYPOINT 指令运行所使用的用户 语法: # Usage: USER [UID] USER 751 Dockerfile 中的默认用户是基础镜像中所使用的用户。比如,你的镜像是从一个使用非 root 用户 sammy 的镜像继承而来的,那么你的 Dockerfile 中 RUN 指定运行的命令的用户就会使用 sammy 用户。 举例: (1)创建 dockerfile 文件 root@devstack:/home/sammy/dockerfile# cat dockerfile-user FROM ubuntu:14.04 USER 1000 ENTRYPOINT top (2)创建镜像:docker build -t dockerfile-user-1000 -f dockerfile-user . (3)启动容器:docker run -it --name c-user-1000-3 dockerfile-user-1000 top 能看出来当前用户ID 为 1000: PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 1000 20 0 4440 648 548 S 0.0 0.0 0:00.00 sh 5 1000 20 0 19840 1296 984 R 0.0 0.1 0:00.00 top (4)基于该镜像再创造一个镜像,然后再启动一个容器,可以发现容器中进程所使用的用户ID 同样为 1000. 2.1.10VOLUME:允许容器访问host上某个目录 # Usage: VOLUME ["/dir_1", "/dir_2" ..] VOLUME ["/my_files"] 2.1.11WORKDIR:设置 CMD 所指定命令的执行目录 # Usage: WORKDIR /path WORKDIR ~/ 2.1.12 HEALTHCHECK: 容器健康检查 这是 Docker 1.12 版本中新引入的指令,其语法为 HEALTHCHECK [OPTIONS] CMD command。来看一个例子: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> RUN apt-get update RUN apt-get -y install curl EXPOSE 8888 CMD while true; do echo 'hello world' | nc -l -p 8888; done HEALTHCHECK --interval=10s --timeout=2s CMD curl -f http://localhost:8888/ || exit 1 在启动容器后,其health 状态首先是 starting,然后在过了10秒做了第一次健康检查成功后,变为 healthy 状态。 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 7 seconds ago Up 6 seconds (health: starting) 8888/tcp c-health2 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 9 seconds ago Up 8 seconds (health: starting) 8888/tcp c-health2 root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2 4c459eef1894 img-health2 "/bin/sh -c 'while tr" 11 seconds ago Up 11 seconds (healthy) 8888/tcp c-health2 需要注意的是 CMD 是在容器之内运行的,因此,你需要确保其命令或者脚本存在于容器之内并且可以被运行。 2.2 几个比较绕的地方 2.2.1 EXPOSE 和 docker run -p -P 之间的关系 容器的端口必须被发出(publish)出来后才能被外界使用。Dockerfile 中的 EXPOSE 只是“标记”某个端口会被暴露出来,只有在使用了 docker run -p 或者 -P 后,端口才会被“发出”出来,此时端口才能被使用。 举例: (1)Dockerfile FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD while true; do echo 'hello world' | nc -l -p 8888; done (2)创建镜像:docker build -t no-exposed-ports -f dockerfile-ports . (3)启动容器1:docker run -d --name no-exposed-ports1 no-exposed-ports。此容器没有 exposed 和 published 任何端口。 (4)启动容器2:docker run -d --name no-exposed-ports2 -p 8888:8888 no-exposed-ports 此时容器的 8888 端口被发布为主机上的 8888 端口: "Ports": { "8888/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "8888" } ] } 该端口会正确返回: root@devstack:/home/sammy/dockerfile# telnet 0.0.0.0 8888 Trying 0.0.0.0... Connected to 0.0.0.0. Escape character is '^]'. hello world Connection closed by foreign host. (5)使用 -P 参数:docker run -d --name no-exposed-ports3 -P no-exposed-ports 此时没有任何端口被 published,说明 Docker 在使用了 “-P” 情形下只是自动将 exposed 的端口 published。 (6)使用 -p 加上一个不存在的端口:docker run -d --name no-exposed-ports4 -p 8889:8889 no-exposed-ports 此时,8889 端口会被暴露,但是没法使用。说明 -p 会将没有 exposed 的端口自动 exposed 出来。 (7)修改 dockerfile 为: FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> EXPOSE 8888 CMD while true; do echo 'hello world' | nc -l -p 8888; done 创建镜像exposed-ports, 再运行docker run -d --name exposed-ports1 -P exposed-ports 创建一个容器,此时 8888 端口自动被 published 为主机上的 32776 端口: "Ports": { "8888/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "32776" } ] } 可见: EXPOSE或者--expose只是为其他命令提供所需信息的元数据,或者只是告诉容器操作人员有哪些已知选择。它只是作为记录机制,也就是告诉用户哪些端口会提供服务。它保存在容器的元数据中。 使用 -p 发布特定端口。如果该端口已经被 exposed,则发布它;如果它还没有被 exposed,则它会被 exposed 和 published。Docker 不会检查容器端口的正确性。 使用 -P 时 Docker 会自动将所有已经被 exposed 的端口发出出来。 2.2.2 CMD 和 ENTRYPOINT 这两个指令都指定了运行容器时所运行的命令。以下是它们共存的一些规则: Dockerfile 至少需要指定一个 CMD 或者 ENTRYPOINT 指令 CMD 可以用来指定 ENTRYPOINT 指令的参数 没有 ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”] 没有 CMD 错误,不允许 /bin/sh -c exec_entry p1_entry exec_entry p1_entry CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd exec_entry p1_entry exec_cmd p1_cmd CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd exec_entry p1_entry p1_cmd p2_cmd CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd 备注 只有 CMD 时,执行 CMD 定义的指令 CMD 和 ENTRYPOINT 都存在时,CMD 的指令作为 ENTRYPOINT 的参数 举例: (1)同时有 CMD 和 ENTRYPOINT FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD top ENTRYPOINT ps 此时会运行的指令为/bin/sh -c ps /bin/sh -c top 但是实际上只是运行了 ps: root@devstack:/home/sammy/dockerfile# /bin/sh -c ps /bin/sh -c top PID TTY TIME CMD 10789 pts/3 00:00:00 su 10790 pts/3 00:00:00 bash 18479 pts/3 00:00:00 sh 18480 pts/3 00:00:00 ps root@devstack:/home/sammy/dockerfile# /bin/sh -c ps PID TTY TIME CMD 10789 pts/3 00:00:00 su 10790 pts/3 00:00:00 bash 18481 pts/3 00:00:00 sh 18482 pts/3 00:00:00 ps (2)CMD 作为 ENTRYPOINT 的参数 FROM ubuntu:14.04 MAINTAINER Sammy Liu <sammy.liu@unknow.com> CMD ["-n", "10"] ENTRYPOINT top 启动容器后运行的命令为/bin/sh -c top -n 10. 3. 在 Docker hub 上创建自己的镜像 当我们从docker镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。 从已经创建的容器中更新镜像,并且提交这个镜像 使用Dockerfile指令来创建一个新的镜像 通过以下步骤,采用第一种方法,在 docker hub 上创建自己的镜像: (1)创建 docker hub 帐号。https://hub.docker.com/ (2)基于一个镜像完成某些操作。比如基于 nginx 镜像,安装 ping ifconfig 等网络工具。首先运行docker run -it nginx /bin/bash基于 nginx:latest 创建一个容器,然后在容器中执行 apt-get 命令安装软件,然后运行 exit 退出容器。 (3)将容器中的内容保存为一个镜像 docker commit -m="install net tools" -a="sammyliu8" 3f8a4339aadd sammyliu8/nginx:v1 这里的3f8a4339aadd 为刚才容器的ID。此时,能在本地看到该镜像: (4)运行 docker login 登录 docker hub (5)运行docker push sammyliu8/nginx 将镜像上传到 docker hub。此时在 Docker hub 界面上能看到该镜像了。 (6)在其他节点上,可以运行 docker pull sammyliu8/nginx 拉该镜像了。 (7)不过,这样做出来的新nginx有个问题,那就是nginx 服务不会自动起来。这是因为,官方的 nginx 的CMD 为 nginx -g"daemon off;",但是新的镜像的CMD 为 /bin/bash。但是,运行前面命令启动的容器又无法安装软件。因此,只能先按照上面的步骤启动一个容器,制作镜像,然后基于该镜像再不带命令地再启一个容器,在另一个窗口中,使用docker commit 将其保存为新的镜像,并上传到docker hub中。问题解决。 参考链接 http://developers.redhat.com/blog/2016/03/09/more-about-docker-images-size/ http://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/ 本文转自SammyLiu博客园博客,原文链接:http://www.cnblogs.com/sammyliu/p/5877964.html ,如需转载请自行联系原作者

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册