首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

docker学习系列17 镜像和容器的导入导出

先说总结: docker save保存的是镜像(image),docker export保存的是容器(container); docker load用来载入镜像包,docker import用来载入容器,但两者都会恢复为镜像; docker load不能对载入的镜像重命名,而docker import可以为镜像指定新名称。 比如我本机上有一个 finleyma/express的镜像,容器ID为4a655b443069 使用如下命令分别导出镜像和容器docker save -o express-save.tar finleyma/expressdocker export -o express-export.tar 4a655b443069 发现如下特点: 镜像压缩包比容器要大。 目录结构不太一样 image.png export.tar 是很典型的Linux目录结构,还找到当初build时被ADD进的源码文件 image.png save.tar 其实就是分层的文件系统。Docker镜像就是由这样一层曾的文件叠加起来。 打开压缩包内的 repositories, 内容为 {"finleyma/express":{"latest":"dda6ce6f2c43f673353e2ce232b31d11ff15b444e338a0ef8f34b6ef74093d6c"}} 既这个镜像的名称,tag是latest,id为dda6ce6f2c43f673353e2ce232b31d11ff15b444e338a0ef8f34b6ef74093d6c 而且tar内有相同ID的目录。 image.png json文件的内容如下:里面记录着这一层容器文件的元信息,通过parent,还能知道依赖的上一层的文件系统是什么。 { "id": "dda6ce6f2c43f673353e2ce232b31d11ff15b444e338a0ef8f34b6ef74093d6c", "parent": "b75acde96878455ce36208008bb1143d4ea17723257c991f8bfb33ad9e27251d", "created": "2018-09-19T15:41:54.6130547Z", "container": "3cd78865317bce73179abc7d21fcbe860a96d14fc980c01566fa2c9412b17d7d", "container_config": { "Hostname": "3cd78865317b", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "ExposedPorts": { "8081/tcp": {} }, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "NODE_VERSION=8.9.4", "YARN_VERSION=1.3.2"], "Cmd": ["/bin/sh", "-c", "#(nop) ", "CMD [\"npm\" \"start\"]"], "ArgsEscaped": true, "Image": "sha256:91f850e6adbd56df68088dffe63c56e6f48fc24f763ff9d22c739742be71212a", "Volumes": null, "WorkingDir": "/usr/src/app", "Entrypoint": null, "OnBuild": [], "Labels": {} }, "docker_version": "18.06.1-ce", "config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "ExposedPorts": { "8081/tcp": {} }, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "NODE_VERSION=8.9.4", "YARN_VERSION=1.3.2"], "Cmd": ["npm", "start"], "ArgsEscaped": true, "Image": "sha256:91f850e6adbd56df68088dffe63c56e6f48fc24f763ff9d22c739742be71212a", "Volumes": null, "WorkingDir": "/usr/src/app", "Entrypoint": null, "OnBuild": [], "Labels": null }, "architecture": "amd64", "os": "linux" } 打开lay.tar, 对于的原来就是当初dockerfile中的ADD . /app/ image.png 那 node_modules 跑哪了,你很快就能猜测到,肯定在上一层文件中。事实确实是这样的。ADD . /app/ 之前对于的命令是 RUN npm install image.png 所以写dockerfile时,一行命令对于一层文件系统,要充分利用这样机制,层的数量尽可能少,只安装必要的依赖包。 参考:https://blog.csdn.net/liukuan73/article/details/78089138https://yeasy.gitbooks.io/docker_practice/content/appendix/best_practices.htmlhttps://docs.docker.com/develop/develop-images/dockerfile_best-practices/

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

12.Swift学习之Any、AnyObject与类型转化

Any、AnyObject Any是一个空协议集合的别名,它表示没有实现任何协议,因此它可以是任何类型,包括类实例与结构体实例。可以表示任何类型,包括函数类型。 AnyObject是一个成员为空的协议,任何对象都实现了这个协议。可以表示任何类类型的实例。 类型转化符号 is : 使用类型检查操作符 ( is )来检查一个实例是否属于一个特定的子类。如果实例是该子类类型,类型检查操作符返回 true ,否则返回 false 。 as : 类型转换操作符( as? 或 as! )进行向下类型转换至其子类类型。 如果不确定向下转换类型是否能够成功,使用条件形式的类型转换操作符 ( as? ) 如果确定向下转换类型会成功时,使用强制形式的类型转换操作符( as! ) 例子 // 1.定义数组 let array : [Any] = [12, "zhangsan"] // 2.取出数组中的第一个和最后一个元素 let objcFirst = array.first! let objcLast = array.last! // 3.判断第一个元素是否是一个Int类型 if objcFirst is Int { print("是Int类型") } else { print("非Int类型") } // 4.转成真正的类型来使用 // 4.1.as? 将Any转成可选类型,通过判断可选类型是否有值,来决定是否转化成功了 let name = objcLast as? String print(name) // 结果:Optional("zhangsan") // 4.2.as! 将Any转成具体的类型,如果不是该类型,那么程序会崩溃 let name2 = objcLast as! String print(name2) // 结果:zhangsan

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

深入学习Lock锁(1)——队列同步器

1. 队列同步器 队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操 作,因为它们能够保证状态的改变是安全的。 同步器的子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域,也就是说同步器是锁或某个同步组件开发者所需要实现的基础插件,而程序员作为锁和同步组件的使用者只需要调用开发者开发的锁或同步组件进行使用即可。 1.1 队列同步器的接口与示例 同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。 重写同步器指定的方法时,需要使用同步器提供的3个方法来访问或修改同步状态: (1)getState(),获取当前同步状态。 (2)setState(int newState),设置当前同步状态。 (3)compareAndSetState(int expect,int update),使用CAS设置当前状态,该方法能够保证状态设置的原子性。 同步器可重写的方法有: (1)protected boolean tryAcquire(int arg):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再使用CAS设置同步状态。 (2)protected boolean tryRelease(int arg):独占式释放同步状态,等待同步状态的线程将有机会获取同步状态。 (3)protected int tryAcquireShared(int arg):共享式获取同步状态,返回大于等与0的值则表示获取成功,否则获取失败 (4)protected boolean tryReleaseShared(int arg):共享式释放同步状态。 (5)protected boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占。 同步器提供的模板方法具体有三类,独占式释放与获取同步状态,共享式获取与释放同步状态,以及查询同步队列中等待线程的状态,我们主要使用这些模板方法来实现自定义的同步组件。所谓独占式也就是同一时刻,只能有一个线程获取或释放同步状态,共享式则相反。同步器提供的模板方法有: (1)public final void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,该方法需要调用重写的tryAcquire(int arg)方法 (2)public final void acquireInterruptibly(int arg) throws InterruptedException:独占式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,但同时该方法也会响应中断,如果线程在同步队列中被中断,则该方法会抛出InterruptedException并返回。 (3)public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException:在acquireInterruptibly(int arg)增加了超时限制,超时未获得同步状态返回false,否则返回true (4)public final void acquireShared(int arg):共享式获取同步状态,如果未获取到则进入同步队列等待,与独占式不同的是,共享式可以同时有多个线程获取到同步状态。 (5)public final void acquireSharedInterruptibly(int arg):共享式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,但同时该方法也会响应中断,如果线程在同步队列中被中断,则该方法会抛出InterruptedException并返回。 (6)public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException:在acquireSharedInterruptibly(int arg)基础上增加了超时限制,超时未获得同步状态返回false,否则返回true (7)public final boolean release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,唤醒同步队列中第一个节点中的线程。 (8)public final boolean releaseShared(int arg):共享式释放同步状态。 (9)public final Collection<Thread> getQueuedThreads():获取在同步队列上等待的线程集合。 依据一个独占锁的实现来了解同步器工作原理,独占锁,也就是同一时刻只能有一个线程获取到锁,而其他线程只能在同步队列中等待,只有持有锁的线程释放锁,后继的线程才能获取锁,相当于synchronized锁和ReentrantLock锁。共享锁的实现可以参考Semaphore线程工具类的内部实现。我们依据ReentrantLock锁的内部同步器实现来作为示例: class Mutex implements Lock { // 静态内部类,自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { // 是否处于占用状态,或者说是否当前线程将要获得的锁是否被其他线程持有, //是则将状态置为1 protected boolean isHeldExclusively() { return getState() == 1; } /* 如果线程尝试获取锁时,获取的锁未被其他线程持有(即同步器状态为0时), * 则获取锁并返回true, * 否则(获取的锁被其他线程持有,即同步器状态为1时)返回false */ public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } //自定义方法,用该方法来加锁 final void lock() { /* 如果线程尝试获取锁时,获取的锁未被其他线程持有(即同步器状态为0时), * 则获取锁并返回,否则(获取的锁被其他线程持有,即同步器状态为1时) * 将一直阻塞,直到获取锁 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // 释放锁,将同步器状态设置为0 protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null);//设置当前持有同步器的线程对象 setState(0); return true; } // 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } } // 仅需要将操作代理到Sync上即可 private final Sync sync = new Sync(); public void lock() { sync.lock(); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } } 到了这里,我们应该就能明白一个队列同步器的简单工作原理。以上面的代码为例,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获 取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时 并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法lock()即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现 一个可靠自定义同步组件的门槛。 如果对”同步队列器中的获取与释放同步状态”中的同步状态难以理解,你可以理解为是否当前同步器正在被某一个线程占用的状态,也就是是否有某个线程正持有通过该同步器对象实现的锁,用0和1表示。 1.2 队列同步器的实现分析 1.同步队列 同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取 同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其 加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再 次尝试获取同步状态。 同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点。 static final class Node { /* 表明一个节点正处于共享模式 */ static final Node SHARED = new Node(); /*表明一个节点正处于独占模式 */ static final Node EXCLUSIVE = null; /* 这个节点状态值表示这个节点的线程等待超时或被中断,需要从同步队列中删除该节点, * 节点进入该状态后不会变化,并且会释放同步状态,使得后续节点可以运行 * 也就是说相当于应用中一个线程在同步队列中或正在运行的过程中被异常中断或 * 等待超时都会释放锁,并返回,将锁交给其他线程 */ static final int CANCELLED = 1; /* 这个节点状态值表示这个节点的线程释放了同步状态或者被取消,则会通知后面的节点, * 也就是相当于使用中线程的wait()/signal()机制 */ static final int SIGNAL = -1; /* 相当于使用Lock锁的Condition条件对象,表示该线程是否是通过Condition条件对象 * 来实现阻塞的 */ static final int CONDITION = -2; /* * 表示下一次共享式同步状态获取将会无条件传播下去 */ static final int PROPAGATE = -3; /* * waitStatus取以上CANCELLED,SIGNAL,CONDITION,PROPAGATE值,或初始值0 */ volatile int waitStatus; /* * 当前节点的前一个节点 */ volatile Node prev; /* * 连接到当前节点的下一个节点 */ volatile Node next; /* * 连接到当前节点被持有的线程 */ volatile Thread thread; /* * 是等待队列的后继节点,连接到下一个在同一个Condition条件对象上等待的节点。 * 或者当前节点是共享式节点,则该变量是特殊值Node类中的常量SHARED,比如在Semaphore线程并发类中就是使用该 * 节点类型 */ Node nextWaiter; /* * Returns true if node is waiting in shared mode. */ final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } } 节点是构成同步队列的基础,同步器拥有首节点(head) 和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。 同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。 试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转 而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此 同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。 同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。 设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节 点设置成为原首节点的后继节点并断开原首节点的next引用即可。 2.独占式同步状态获取与释放 (1)通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是 由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同 步队列中移出。源码如下: public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法 保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。试想一下:如果使用一个普通的LinkedList来维护节点之间的关系,那么当一个线 程获取了同步状态,而其他多个线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发 地被添加到LinkedList时,LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。 在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循 环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线 程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变 得“串行化”了。 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } 节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状 态,而只有前驱节点是头节点才能够尝试获取同步状态,因为头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,而且需要维护同步队列的FIFO原则。 图解独占式获取同步状态流程: 在图中,前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。 (2)独占式释放同步状态:当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能 够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释 放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。 public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用 LockSupport来唤醒处于等待状态的线程。 (3)总结:在获取同步状态时,同步器维 护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列 (或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。 3.共享式同步状态获取与释放 (1)共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,共享式同步队列的使用与实现具体可以参考Semaphore。 public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } 通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状 态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示 该次获取同步状态成功并从自旋过程中退出。 (2)与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以 释放同步状态,该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线 程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg) 方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为 释放同步状态的操作会同时来自多个线程。 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } 4.独占式超时获取同步状态 通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知, nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上 次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒, 反之,表示已经超时。 private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } 该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功 则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示 已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待 nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker,long nanos)方法返回)。 注意:如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行 超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果 这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超 时非常短的场景下,同步器会进入无条件的快速自旋。 独占式超时获取同步状态方法流程图 5.总结 同步队列器实际上对于所有的同步组件,如ReentrantLock,Semaphore等都通过该组件进行实现。其获取同步状态,在独占式情况下可以视为获取锁(如ReentrantLock),在共享式情况下可以视为获取访问同步资源的权限或是条件(如Semaphore)。所以,同步器的使用不仅仅是用于Lock锁的实现,Java中提供的许多同步组件都通过同步器实现,我们甚至可以根据需要,重写队列同步器的方法来实现自己所需要的同步组件。

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

Python学习笔记 (2)变量、常量和数据类型

变量 顾名思义,变量就是一个会变的量,用一个变量名表示,指向内存中一片区域,而指向的区域存的是什么,这个变量就是什么数据类型,和C/C++挺不一样的。变量数据类型可以通过赋值变来变去(这就叫动态语言,区别于静态语言)。 创建和赋值 python中创建变量时只需给变量赋一个值,像这样a = 1,然后变量名就指向存着那个值(1)的一片区域(表述不太对,应该是引用而不是指向)。用id() 可以查看该变量的地址。 还可以像这样赋值a, b = 1, 2 (a=1,b=2),或是这样a=b=1。 “可以把任何数据都看成一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。” 举个例子,我们给a赋值为1,a=1,就相当于创建了变量a,于是解释器在内存地址XXX中创建一个Number对象1,然后把a和XXX中的对象1关联起来。调用id(a) ,能得到内存地址XXX。 然后我们给a加上1a=a+1 或是给a赋值为2a=2,就会再次创建一个在内存地址YYY中的Number对象2,然后a就被关联到了对象2上,至于对象1,也不知道是留着还是回收了,这里先留坑。 再调用id(a),就会得到内存地址YYY,然后b=2 , id(b) ,得到的还是内存地址YYY(官方解释器倒是这样的,不过其他解释器就不一定了),这时a和b引用同一片内存区域了,于是a is b会返回True(is用来判断两个变量是否引用同一片内存区域)。还有一种情况,a和b的值都为1,但引用的是不同的对象(所在内存区域不同),这时a is b 会返回False,而a == b 会返回True(==用来判断两个变量所引用对象的值是否相等) 还有一件重要的事,变量名,规则和C/C++差不多——数字,字母,下划线, 任意组合,数字不能开头,对大小写敏感,python的关键字不能用,变量名尽量有意义(Python中变量的命名规范)。 查看自己版本python中的关键字可以使用命令help("keywords") 删除 del可以删除变量,但不同于C/C++中的free和delete()(别刮开我会说我不会用C/C++在堆区创建变量吗),python中的del不会删除内存中的数据,而是把变量删了(更高端一点的说法是把变量对对象的引用删了)。 使用方法del a 或del a,b,c 举个例子,建立变量aa=1,内存地址XXX就创建了对象1,于是a指向了XXX,建立变量bb=1 ,于是b也指向了XXX,然后del(a) ,调用id(b) 可以看到,这时b仍然指向XXX,但调用id(a) 会报错,说变量a未定义。 此外,Python还有垃圾回收机制,留坑。 全局变量和局部变量 留坑到学函数时 常量 习惯用全部大写的变量名表示常量,其实电脑觉得它还是个变量,Python里没有C语言里类似const的东西,换句话说,把变量当成常量用就好。此外,Python还有6个内置常量。 Python的内置常量 True、False、None、NotImplemented、Ellipsis、__debug__(注意有些首字母大写) 一些关于内置常量的更具体的信息 数据类型 标准数据类型 Number(数字) String(字符串) List(列表) Tuple(元组) Set(集合) Dictionary(字典) 一些补充说明 以上6种数据类型分类 按照存储数据个数区分 标量/原子类型(值能存放一个值) 数字,字符串 容器类型(存放多个值) 列表,元组,字典 按照可变不可变区分 //可变类型指的是在同一块内存地址之上可以将值替换掉(使用id()函数查看变量的内存地址) 可变 列表,字典,集合 不可变 数字,字符串,元组 按照访问顺序区分 直接访问(不可拆分) 数字 顺序访问(有下标的) 字符串,元组,列表 key值访问(映射类型) 字典 内置的type()函数可以用来查询变量所指的对象类型。此外还可以用 isinstance()来判断。使用方法 Python数据类型转换 这里借用C/C++的概念,可能不兴这么说,但好理解。 隐式类型转换 整数和浮点数做运算时,解释器将整数转换为浮点数; 布尔型和整型做运算时,将布尔型转换为整型;布尔型和浮点型做运算时,将布尔型转换为浮点型; 整型和布尔型与复数做运算时,被转换为浮点型; 两个整型做“/”运算时,无论是否除得尽,结果都为浮点型。 显式类型转换 (来自这里) 有时候,我们需要对数据内置的类型进行转换,数据类型的转换,你只需要将数据类型作为函数名即可。 以下几个内置的函数可以执行数据类型之间的转换。这些函数返回一个新的对象,表示转换的值。 函数 描述 int(x [,base]) 将x转换为一个整数 float(x) 将x转换到一个浮点数 complex(real [,imag]) 创建一个复数 str(x) 将对象 x 转换为字符串 repr(x) 将对象 x 转换为表达式字符串 eval(str) 用来计算在字符串中的有效Python表达式,并返回一个对象 Ps:eval()的一个应用——python 如何定义n个变量 (变量声明自动化) tuple(s) 将序列 s 转换为一个元组 list(s) 将序列 s 转换为一个列表 set(s) 转换为可变集合 dict(d) 创建一个字典。d 必须是一个序列 (key,value)元组。 frozenset(s) 转换为不可变集合 chr(x) 将一个整数转换为一个字符 ord(x) 将一个字符转换为它的整数值 hex(x) 将一个整数转换为一个十六进制字符串 oct(x) 将一个整数转换为一个八进制字符串 round(x[,y]) 将一个浮点数x四舍五入为一个整数,若有第二个参数,则四舍五入为y位浮点数 自定义数据类型 留坑

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

要想精通C语言,必须先学习汇编吗?

编程语言里面很少有人直接说出精通两个字,特别是一些入行好多年的程序员,从语法来讲C语言相对来讲入门还是比较容易,在高级语言还没有完全展开的年代,C语言算入门比较简单的编程语言了,起码要比语法细节繁杂的C++好太多了,现在很多做应用开发的程序员觉得能写C语言的都是高手,这完全是一种这山看着那山高的心态了,做C语言还觉得高级语言编程模式复杂,现在越来越多的编程入门人员已经不选择C语言作为入门语言觉得太难了,对于面向过程语言开发的C语言为什么让很多人觉得很难? 1.C语言硬件搭界由于很多人对硬件不熟悉,让很多人觉得讳莫如深 2.C语言由于指针的存在很多人觉得难以理解,觉得很难。 但是大部分老程序员觉得C语言是一门相对入手比较容易的编程语言,但现在编程向着集成化的方向发展,相比较而言C语言显得难了许多。 越是工作年限长的老程序员越是不轻易说出精通两个字,C语言直接底层属于汇编,汇编不仅仅是C语言的基础,也是计算机运行的基石,如果真是一位精通C语言的高手,那么对于汇编不说很熟悉,起码会懂常见的语法,因为C语言调试过程中遇到的一些奇怪的现象,拿不准的情况深入到汇编层面就很容易解决问题。所以想更好的学好C语言,对于汇编语言还是需要多少了解一点。 但如果一定说只有学好汇编才能学好C语言,这种因果关系是不存在的,本来就是就属于两种不同的编程语言,在有些地方存在一些交集而已,比如对性能要求非常大的地方,直接在C语言里面调用汇编来实现,这种在很多地方都使用过。 从市场上对于C语言的需求量依然很大,但对于比例相比别的语言少了许多,不是说C语言不重要了,主要是现在应用方便编程的需求更大,在很多领域C语言还是首选,通讯领域,操作系统,嵌入式开发等等还会选择C语言,而且现在很多主流的编程语言的底层就是C语言来完成的,如果喊着C语言已经过时了或者不行的话,如果是这样代表真的不懂编程。 原文发布时间为:2018-08-30 本文作者:东辉在线 本文来自云栖社区合作伙伴“程序员互动联盟”,了解相关信息可以关注“程序员互动联盟”。

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

写在学习golang一个月后

遇到的问题 连接池。由于PHP没有连接池,当高并发时就会有大量的数据库连接直接冲击到MySQL上,最终导致数据库挂掉。虽然Swoole有连接池,但是Swoole只是PHP的一个扩展,之前使用Swoole过程中就踩过很多的坑。经过我们的讨论还是觉得使用Golang更加可控一些。 框架的选择 在PHP中一直用的是Yaf,所以在Go中自然而言就选择了Gin。因为我们一直以来的原则是:尽量接近底层代码。 封装过于完善的框架不利于对整个系统的掌控及理解。我不需要你告诉我这个目录是干嘛的,这个配置怎么写,这个函数怎么用等等。 Gin是一个轻路由框架,很符合我们的需求。为了更好地开发,我们也做了几个中间件。 中间件——input 每个接口都需要获取GET或POST的参数,但是gin自带的方法只能返回string,所以我们进行了简单的封装。封装过后我们就可以根

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

SQL Serever学习14——存储过程和触发器

存储过程 在数据库中很多查询都是大同小异,编写他们费时费力,将他们保存起来,以后执行就很方便了,把SQL语句“封装”起来。 存储过程的概念 存储过程是一组SQL语句集,经过编译存储,可以”一次编译,多次执行“。除了第一次调用需要编译,后面都可以直接执行,执行速度更快,而不是普通SQL语句一样,每一次执行都要编译。 提供一种安全机制,如果某用户满意特定视图的使用权限,但是有使用存储过程的权限,通过执行存储过程,依旧可以获取存储过程中的表。 存储过程的优点 改善系统性能,一次编译,多次执行,而普通SQL是每一次都编译执行 安全机制 重用性,可以反复调用 共享性 减少网络流量,存储过程是服务器上编译好的T-sql代码,对一个可能需要几百行的T-sql操作,在客户端只需要通过一条执行语句完成,而不是发送几百行代码 存储过程分类 分为3类: 系统存储过程 通常以sp_开头,有sp_helpdb查看数据库名称和大小 所在位置 还有sp_helptext用于小时规则,默认值,触发器 sp_renamedb重命名数据库 sp_rename 重命名表,列,用户定义的类型 sp_helplogins看用户登录的数据 用户存储过程 用户编写的存储过程(主要是的的就是这个部分) 扩展存储过程 为扩展sqlserver提供的方法,可以动态的加载和执行动态链接库的函数,以xp_开头 存储过程的创建语句 CREATE PROC[EDURE] 存储过程名 [@参数名 参数类型 =默认值 ] OUTPUT WITH RECOMPILE FOR REPLICATION AS SQL语句组 说明:OUTPUT是输出参数, RECOMPILE表示sqlserver不对存储过程计划进行高速缓存,每一次重新编译, FOR REPLICATION表示存储过程只能在复制过程中使用,而且不和WITH RECOMPILE一起使用。 存储过程执行语法格式 EXEC[UTE] [@状态值=] 存储过程名 [@参数名=] 参数值 存储过程重新编译 sqlserver强制重新编译存储过程3种方法: 系统存储过程 ,EXEC sp_recompile 存储过程名 可以创建的时候使用WITH RECOMPILE 指定WITH RECOMPILE选项,EXEC 存储过程 WITH RECOMPILE 存储过程修改 就是把创建语句的CREATE 换成ALTER ,仅此而已。 ALTER PROC[EDURE] 存储过程名 [@参数名 参数类型 =默认值 ] OUTPUT WITH RECOMPILE FOR REPLICATION AS SQL语句组 存储过程删除 可以一次删除多个存储过程 DROP PROCEDURE PROC_3 , PROC_4 创建一个简单的存储过程 CREATE PROC 简单的存储过程 AS BEGIN SELECT 商品编号 ,商品名称,销售价 FROM 商品表 WHERE 商品名称='笔记本' END GO 存储过程中输入输出参数在AS之前,申明,局部变量在AS之后,申明。 创建带参数的存储过程 CREATE PROC 带参数的存储过程 @SPM VARCHAR(20) ='笔记本' AS BEGIN DECLARE @JG SMALLMONEY SELECT @JG=销售价 FROM 商品表 WHERE 商品名称=@SPM RETURN @JG END GO 使用带参数,有返回值的存储过程 DECLARE @MAXJG SMALLMONEY EXEC @MAXJG=带参数的存储过程 SELECT @MAXJG AS 最高的实际销售价格 数据库的触发器 触发器的概念 触发器是特殊的存储过程,只不过它是在操作数据表(增删改)之前或者之后,自动执行的,不能铜鼓名称来调用。 优点 自动,操作表之后立即激活 实施更为复杂的数据约束 级联修改数据库中相关表,自动触发其他与之相关操作 跟踪变化,撤销和回滚 返回自定义错误信息(一般的约束是无法返回信息的) 可以调用更多的存储过程 分类 触发器分为2类: DML DDL DML触发器 进行了增删改之后自动激活,有分为AFTER触发(操作后触发,执行优先级在约束检查之后),和INSTEAD OF 触发(不会执行增删改操作,而是执行INSTEAD OF指定操作,执行优先级在约束检查之前)。 DDL触发器 在执行CREATE, ALTER , GRANT , DENY , REVOKE, UPDATE STATISTICS语句触发。 触发器专用临时表 有2个触发器专用临时表INSERTED 和DELETED 表存在INSERT触发器(DDL触发器),插入数据,系统自动创建一个与表一样结构的INSERTED临时表,新的记录同时,添加到触发器表和INSERTED中,INSERTED保存的副本,方便用户查找当前插入数据。 表存在DELETE触发器,删除数据,系统自动创建一个DELETED临时表,用来保存被删除记录。 修改表,就是删除一条记录,然后插入一条记录,删除记录添加到DELETED表,新增记录添加到INSERTED表。 触发器的创建和触发 DML触发器 CREATE TRIGGER 触发器 ON 表名 FOR | AFTER | INSTEAD OF INSERT ,UPDATE , DELETE AS SQL语句 DDL触发器 CREATE TRIGGER 触发器 ON ALL SERVER | DATABASE FOR | AFTER 事件种类 AS SQL语句 触发器的修改 和创建类似,就是把CREATE 改为ALTER 触发器的删除 DROP TRIGGER 触发器, 触发器2 触发器的启用与禁用 禁用触发器 ALTER TABLE 表名 DISABLE TRIGGER 触发器名 DISABLE TRIGGER 触发器名 禁用数据库级别触发器 DISABLE TRIGGER 触发器名 ON DATABASE 启用触发器 ALTER TABLE 表名 ENABLE TRIGGER 触发器名 ENABLE TRIGGER 触发器 ON 表名 简单的触发器 USE 销售管理 GO CREATE TRIGGER tr_insert_mj ON 买家表 FOR INSERT --指定触发类型 AS BEGIN PRINT '有新买家插入到买家表' END GO 验证,插入一条数据 INSERT INTO 买家表 VALUES('M05','MARS',12232323,'J01') GO 看消息,触发器运行了 创建一个DDL触发器,当删除数据库中的表时,就撤销该操作,并提示:禁止删除数据库表! USE 销售管理 GO CREATE TRIGGER tr_drop_dll ON DATABASE FOR DROP_TABLE AS BEGIN ROLLBACK TRANSACTION PRINT '禁止删除数据表!' END GO 验证一下,删除表 DROP TABLE 买家表 GO 看信息提示 禁用触发器 ALTER TABLE 买家表 DISABLE TRIGGER tr_insert_mj 或者 DISABLE TRIGGER tr_insert_mj ON 买家表 启用触发器 ALTER TABLE 买家表 ENABLE TRIGGER tr_insert_mj 或者 ENABLE TRIGGER tr_insert_mj ON 买家表

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

深入学习Java虚拟机——类加载机制

当Java源码编译为字节码文件Class类文件即一串2进制字节流时虚拟机是如何将字节码文件加载到虚拟机中成为一个Class对象的 1. 类加载 1. 类从被加载到虚拟机内存中开始到卸出内存为止它的整个生命内存周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个阶段统称为连接。 2. 在类的加载过程的7个阶段中加载、验证、准备、初始化、卸载这5个阶段的顺序是固定的类加载过程中这5个阶段必须按照这个顺序开始顺序开始指这几个阶段都是互相交叉的混合式进行也就是说会在一个阶段执行过程中调用另一个阶段而不是按顺序完成每个阶段而解析阶段则不一定解析在某种情况下可以在初始化阶段之后执行这是为了支持Java的运行时绑定动态绑定或晚期绑定。 3. 对于类加载过程中加载阶段的执行时机并没有明确指定但初始化阶段加载、验证、准备自然会在这之前开始的时机有以下几项 1遇到new、getstatic、putstatic、invokestatic这4条字节码指令时如果类没有进行过初始化则必须进行初始化。这四条指令的源代码场景为使用new关键字实例化对象读取或设置一个静态变量的值调用静态方法。 2使用java.lang.reflect包的方法对类进行反射调用时如果类没有进行过初始化则必须进行初始化。 3当初始化一个类的时候如果其父类还没有初始化则先进行父类的初始化。 4当虚拟机启动时用户需要指定一个要执行的主类包含main方法的类虚拟机会先初始这个类。 5使用jdk1.7的动态语言支持时如果有一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄并且这个方法句柄对应的类没有初始化则会触发该类的初始化。 注意类的初始化必须满足以上条件中任何一个的所有要求不能少也不能加比如static静态变量前再加上final修饰符之后访问或设置其值就不会引起类的初始化因为对于常量的引用会直接将该引用转化成常量值存储进入调用该常量的所在类的常量池中使用时也只会使用常量池中的值而不是原静态常量的reference也就与静态常量所在类无关了所以不会对静态常量所在的类初始化 如果调用静态字段只有直接定义该字段的类会被初始化如果在其子类中调用不会初始化子类只会初始化父类。 如果是new某一个类的数组类型的变量同样不会初始化该类在虚拟机指令中进行newarray指令执行的是 "[*.*.ClassName" 这样一个类的初始化也就是由虚拟机自动生成的用 “[ ” +类的全限定名 生成的直接继承于Object类的子类所以对于一维数组对象的引用我们可以使用length属性和clone方法。 2. 类加载的过程 2.1 加载 1. 加载阶段虚拟机需要完成 1通过一个类的全限定名来获取定义此类的二进制字节流。 2将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。 3在内存准确说是方法区中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。 对于非数组类的加载过程其字节码文件可以有多种不同的方式来获取而非数组类的字节码文件可以使用虚拟机系统提供的引导类加载器完成也可以使用自己定义的类加载器完成。 2. 对于数组类的加载与非数组类使用字节码文件创建有所不同它是由虚拟机直接创建的但数组类的元素类型去掉所有维度的类型仍然需要类加载器去创建一个数组类比如说数组类C的创建过程有以下原则 1如果数组的组件类型是引用类型那就递归采用上面所说的加载过程去加载这个组件类型数组C将在加载该组件类型的类加载器的类名称空间上被标识。 2如果数组的组件类型不是引用类型如int[]数组Java虚拟机将会把数组C标记为与类引导加载器关联。 3数组类的可见性与它的组件类型的可见性一致如果数组组件类型不是引用类型那数组的可见性默认为public。 3. 加载阶段与连接验证、准备、解析阶段是交叉进行的但加载阶段与连接阶段的先后开始顺序是固定的。 2.2 验证 1. 这一阶段的目的是为了确保Class文件的字节流中的信息符合当前虚拟机的要求并且不会危害虚拟机的安全。 2. 需要完成4个检验工作 1文件格式检验检验字节流是否符合Class文件格式的规范比如以魔数0xCAFEBABE开头等验证点。 2元数据验证对字节码描述的信息进行语义分析保证其描述的信息符合Java语言规范比如这个类是否有父类是否继承了不允许集成的类等验证点。 3字节码验证通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。这一过程是最复杂的。 4符号引用验证发生在虚拟机将符号引用转化为直接引用的时候这个动作将在类加载过程的第四阶段解析中发生该验证是为了对类自身以外的信息进行匹配性校验。 2.3 准备 1. 准备阶段是正式给类变量分配内存并设置变量初始值的阶段这些变量所使用的内存都在方法区中分配。这里的变量是指类变量也就是有static修饰的变量而不包括实例变量实例变量将通过new生成对象时一同在堆中分配空间。此外对于仅有static修饰的变量在通常情况下准备阶段中的设置变量初始值将统一设置为 0、false、或者 null 值如果在源码中有赋值语句如下 public static int i=111; 那么会在类的初始化阶段再进行赋值操作而对于有 static 和 final共同修饰的常量如下 public static final int i=111; 那么就会在准备阶段就将 i 直接赋值为111因为此时 i 在编译时会成为ConstantValue属性根据这个属性虚拟机会直接将将 i 赋值为111。 2.4 解析 1. 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info。 2. 符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能准确的定位到目标即可。有符号引用但内存不一定有符号引用所指向的目标。 3. 直接引用可以是指向目标的指针、相对偏移量或间接定位到目标的句柄。如果直接引用存在那么内存中必定由直接引用所指向的目标。 4. 类或接口的解析过程 1.如果当前代码所处的类为D要把一个从未解析的符号引用N作为一个类或接口C的直接引用步骤如下 1如果C不是一个数组类型那么虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C在加载C类过程中由于C中可能还会涉及其他类的加载所以会触发其他相关类的加载例如父类或接口一旦有任何一个加载过程出现异常解析过程就失败了。 2如果C是一个数组类型并且数组的元素类型为对象也就是N的描述符会是“[Ljava/lang/Ingeter”L 代表的是引用类型然后按照上面第1点的规则加载数组元素类型。 3如果以上步骤没有异常则类C以及其相关的类或接口已经在虚拟机内存中存在是一个有效的类或接口了但还需要最后一步验证类D是否对C具有访问权限如果不具备则抛出java.lang.IllegalAccessError异常。 5. 字段解析 1. 要解析一个从未解析的字段符合引用首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析也就是字段所属的类或接口的符号引用如果在这个过程中发生异常那么整个字段解析的过程都会结束如果该解析成功完成如果将这个字段被声明的类或接口用C表示那么对C进行后续解析的步骤如下 1如果C本身就包含了简单名称和字段描述符都与目标匹配的字段则返回这个字段的直接引用查找结束。 2否则如果C实现了接口或类但C不能是Object类就会按照继承关系从下往上搜索其父类或接口如果找到了简单名称和字段描述符都与目标匹配的字段则返回这个字段的直接引用查找结束。 3以上都查找不到则查找失败抛出java.lang.NoSuchFieldError异常。 注意实际上如果在父类或接口中出现了与子类中相同的名称那么将无法通过编译。 如果成功找到并返回了直接引用还需要对字段进行权限验证如果不具备对此字段的访问权限则抛出java.lang.IllegalAccessError异常。 6. 类方法解析 1. 首先要依据类方法表中的class_index项中索引代表的类或接口的符号引用而这个类或接口的符号引用就是当前方法所属的类或接口如果解析成功用C来表示这个类或接口后续步骤如下 1如果发现类方法表中class_index中索引指向的C是个接口那就直接抛出java.lang.IncompatibleClassChangeError异常。 2如果通过第一步则在类C中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。 3否则则在类C中的父类中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。 4否则则在类C的父接口列表以及父接口所继承的父接口中查找是否有简单名称与描述符都与目标匹配的方法如果有则说明C是抽象类查找结束抛出java.lang.AbstractMethodError 5否则方法查找失败抛出java.lang.NoSuchMethodError 如果成功找到并返回了直接引用还需要对方法进行权限验证如果不具备对此方法的访问权限则抛出java.lang.IllegalAccessError异常。 7. 接口方法解析 1. 首先要依据接口方法表中的class_index项中索引代表的类或接口的符号引用而这个类或接口的符号引用就是当前方法所属的类或接口如果解析成功用C来表示这个类或接口后续步骤如下 1如果发现类方法表中class_index中索引指向的C是个类那就直接抛出java.lang.IncompatibleClassChangeError异常。 2如果通过第一步则在接口C中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。 3否则则在类C的父接口列表以及父接口所继承的父接口中递归查找是否有简单名称与描述符都与目标匹配的方法直到Object类为止如果有则返回这个方法的直接引用查找结束。 4否则方法查找失败抛出java.lang.NoSuchMethodError 接口中所有方法默认都是public不存在权限问题。 2.5 初始化 1. 初始化是类加载过程的最后一步在准备阶段虚拟机已经对类变量进行了一次初始化过程在之前的阶段过程中除了加载阶段用户可以自定义类加载器之外其余阶段的工作完全由虚拟机主导和控制而在初始化阶段才是真正执行字节码的阶段。初始化阶段由程序员自己去决定初始化的类变量和其他数据。或者说初始化就是执行类构造器方法<client>()的过程。 2.类构造器方法<client>()运行中的细节 1<client>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{ }块中的语句合并而成编译器收集顺序有语句在源文件中出现的先后顺序决定静态语句块中可以访问并且赋值在静态语句块之前出现的类变量而对于定义在静态语句块之后类变量静态语句块中只能进行赋值而不能进行访问。 2<client>()方法与类的对象构造器<init>( )方法不同它不需要显示调用父类构造器虚拟机会在子类<client>()执行之前执行完毕父类的<client>()。因此虚拟机中第一个被执行的肯定是Object类。 3由于父类的<client>()先执行那么父类的静态语句块和类变量的赋值要优先于子类中的。 4<client>()对于类或接口不是必须的如果类或接口中不含对类变量或静态语句块那么编译器可以不生成该类的<client>()方法 5接口中不能使用静态语句块但仍然有类变量的初始化赋值操作因此接口也会生成<client>()方法。但接口与类不同的是执行接口的<client>()不需要执行父接口的<client>()只有当父接口中的类变量使用时才会执行父接口的<client>()另外接口的实现类在初始化时也不会执行接口的<client>() 6虚拟机会保证一个类的<client>()方法再多线程环境中被正确的加锁、同步如果多个线程去初始化一个类那么只有一个线程去执行该类的<client>()方法其他线程都会阻塞等待直到活动线程执行<client>()完毕。如果<client>()方法执行时间很久就有可能造成多个进程阻塞。 3.类加载器 通过一个类的全限定名来获取描述此类的二进制字节流这一动作会在虚拟机的外部实现而实现这个动作的代码块就是类加载器。 3.1 类与类加载器 1. 类加载器虽然是用于实现类的加载动作但其作用不限于类的加载阶段。对于任何一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性也就是说比较两个类Class对象是否相等必须是在这两个类都在同一个类加载器加载的前提下才有意义否则即使这两个类来自于同一个Class文件被同一个虚拟机加载是要类加载器不同那么这两个类就不相等。判断两个类是否相等可以通过类的Class对象的equals( )方法、isAssignableFrom()方法、isInstance()方法返回的结果或者使用类的实例对象通过instanceof关键字来运算产生的结果。 public class ClassLoaderTest { public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { ClassLoader myLoader=new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try{ String filename=name.substring(name.lastIndexOf(".")+1)+".class"; System.out.println(filename);//执行结果中的前4行输出都来自这里 InputStream is=getClass().getResourceAsStream(filename); if(is==null){ return super.loadClass(name); } byte[] b=new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); }catch (IOException e) { throw new ClassNotFoundException(name); } } }; Class<?> c=myLoader.loadClass("About_Jvm.ClassLoaderTest");//此行代码的执行结果为1、2行 Object obj=c.newInstance(); //为何这里会引起执行结果中的3、4行 /* * 个人猜测当前代码的类ClassLoaderTest定义为A在A类中使用myLoader这个自定义类加载器加载后会生成另一个 * ClassLoaderTest类称为B虽然他们都来自于同一个字节码文件但他们在虚拟机中并不是同一个Class对象。 * 所以我们将A和B应该看做两个类更方便分析。 * 对于类A它由jdk中的应用程序类加载器来加载所以加载A类不会有任何输出而对于B类 * 是在A类中采用自定义的类加载器匿名内部类Ca类的对象myLoader来加载的Ca类属于A类。 * * B类的加载阶段完成后B类中与A类有相同的代码也有一个类加载器匿名内部类CbCb属于B类中的 * 当对B类进行实例化时会进行对B类的后续步骤验证解析准备然后初始化再调用无参 * 构造方法生成对象但进行这几个阶段时会发现Cb类并未加载或者说依据Cb类在常量池中的符号 * “ClassLoaderTest$1”无法在方法区中找到对应B类中的ClassLoaderTest$1类的Class对象 * 所以此时使用B类的类加载器也就是A类中的myLoader来进行尝试加载首先加载 * ClassLoaderTest$1的父类ClassLoader然后在加载ClassLoaderTest$1类。 * 所以会出现执行结果中的3、4行 */ System.out.println(obj.getClass()); System.out.println(obj instanceof About_Jvm.ClassLoaderTest); } } //执行结果 ClassLoaderTest.class Object.class ClassLoader.class ClassLoaderTest$1.class class About_Jvm.ClassLoaderTest false 上述代码中的ClassLoaderTest 类由两个类加载器完成加载一个是应用程序类加载器另一个是自定义加载器所以当进行比较时结果为false。而对于其中类加载器会涉及到双亲委派模型。 3.2 双亲委派模型 1. 类加载器在虚拟机角度来说有两种一种是启动类加载器这个类加载器使用C++语言实现是虚拟机自身的一部分另一种就是其他所有的类加载器由Java实现是虚拟机外的模块并且全部继承自抽象类java.lang.ClassLoader。 2. 在开发者角度来看虚拟机可以划分为更细致的4中类加载器 1启动类加载器Bootstrap ClassLoader负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootclassbath参数所指定的路径并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。 2扩展类加载器Extension ClassLoader这个加载器由sun.misc.Launcher$ExtClassLoader实现它负责<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库可以使用扩展类加载器。 3应用程序类加载器Application ClassLoader这个类加载器由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值所以一般也称为系统加载器负责加载用户类路径上所指定的类库开发可以直接使用这个类加载器一般默认该类加载器。 4自定义类加载器User ClassLoader开发时自定义的加载器可以加载指定路径中的类。 3. 双亲委派模型双亲委派模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器。类加载器之间的父子关系一般不会以继承关系而是以组合关系来复用父加载器的代码。 4. 双亲委派模型的工作过程如果一个类加载器收到了类加载的请求他不会立即自己去尝试加载这个类而是会先将这个请求委派给父类加载器去完成每一个层次的类加载器都是如此因此最终所有的加载请求都会传给顶层的启动类加载器中当父加载器无法完成这个请求时子加载器就会自己去加载。 5. 优点双亲委派模型使Java类随着它的类加载器一起具备了一种优先级的层次关系。例如java.lang.Object类它存放在rt.jar中无论哪一个类加载器加载这个类最终都会委派给启动类加载器加载所以Object类在程序中永远都是同一个类。相反如果没有这种双亲委派机制如果用户自己也编写了一个Object类并放在程序的classpath中那么系统将会出现多个Object类应用程序就会变得混乱。所以如果编写一个与rt.jar类库中已有的Java类可以被编译但永远无法加载运行。 6. 实现自定义类加载器双亲委派的代码都在java.lang.ClassLoader的loadClass()方法中因此如果要编写自己的ClassLoader类就必须继承java.lang.ClassLoader抽象类重写loadClass()方法来实现自己的类加载器比如在 3.1 中的代码实例就是一个标准的实现自定义类加载器的例子。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

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

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文件系统,支持十年生命周期更新。

用户登录
用户注册