Java基础

1、Java基础

1.1 面向对象OOP

1.1.1 面向对象开发特点:继承、多态、封装

多态:

  1. 重载:发生在同一类中,要求方法名相同,但是参数列表不同

  2. 重写:发生在继承关系种,遵循两同两小一大原则:两同(方法名相同,参数列表相同),两小(如果返回类型是基本类型则要完全一致,如果是对象类型则需要小于父类的返回类型;方法抛出的异常一定要小于父类抛出的异常),一大(访问权限要大于父类)

1.2 面向对象六大基本原则

  1. 单一职责原则:一个类只做一件事情

  2. 依赖倒置原则:面向接口编程,不能直接依赖类

  3. 接口隔离原则:接口要小而专,不能大而全

  4. 开闭原则:一个类对修改时关闭的,对拓展是开放的

  5. 里氏替换原则:一个父类能出现的地方,他的父类也能够出现

  6. 最小知道原则:一个实体应该尽量少的与其他实体之间发生相互作用,是的系统功能模块相对独立

1.2 接口 与 抽象类

相同点:都不能实例化对象

不同点

  1. 接口里面只能包含抽象方法,而抽象类里面既能包含抽象方法,也能包含普通方法

  2. 一个类可以实现多个接口,而一个只能继承一个父类

  3. 接口强调的是功能,而抽象类强调的是继承关系

  4. 接口里面的变量都是public static final修饰的变量,而抽象类里可以有普通成员变量

  5. 接口不能拥有构造函数,而抽象类可以有构造函数

  6. 从jdk1.8以后,接口可以有default默认实现方法

1.3 session 和 cookie 区别

  1. 存储位置:session存储在服务器端,cookie存储在浏览器端,所以相对来说,session比较安全,而cookie存在cookie欺骗的风险

  2. 性能:由于session存储在服务器端,相对安全的前提下也比较消耗服务器资源

  3. 存储容量:单个cookie保存的数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie,而对session没有太多的要求

1.4 equals 和 == 区别

  1. 比较对象:==既能用于对象之间的比较,也能用于基本类型之间的比较,而equals只能用于对象的比较。

  2. 能否重载:==不能重载,equals可以重载

  3. 比较内容:equals默认比较的是内存地址,而String类的equals比较的是内容。==比较的是对象的内存地址

1.5 访问修饰符

 publicprotecteddefaultprivate
所有可见1000
子类可见1100
包内可见1110
自己可见111 

 

2、集合

2.1继承关系

2.2 Collection

2.2.1 Set

2.2.1.1 HashSet

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;
​
    private transient HashMap<E,Object> map;
​
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
}

底层使用HashMap数据结构存储,每次传入的值都是同一个Object对象

2.2.1.2 TreeSet

需要对传入的对象实现 Comparable接口,实现compare方法。或者传入比较器

2.2.2 List

2.2.2.1 ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
​
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
​
    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
}

底层数据结构:数组

思想:动态数组

初始化容量:10

扩容机制:扩充容量到原来的1.5倍数

存取原理

存:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
​
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
​
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
​
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
​
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
​
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
​
  1. 看一下要不要进行扩容,1.5倍

  2. 存进去

2.2.2.2 LinkedList

双向链表,不能随机访问

2.3 Map

2.3.1 HashMap

  • JDK1.8以前

底层数据结构:数据+链表

默认初始化容量:16

默认加载因子:0.75

扩容规则:当我使用的容量大于 16*0.75 = 12的时候,也就是我插入第13个元素的时候,会触发扩容。扩容分为两个步骤:数组变成当前的两倍,再对里面的元素进行一个rehash的一个操作。而且使用的是一个头插法

怎么处理hash冲突:拉链地址法,因为数组的容量总是2^n,那么HashMap计算Hash值得时候,不是像我们那样使用取余得方法,而是使用位运算,相与得到数组得下标,再进行操作

不安全体现:但是HashMap是不安全的,不安全体现在我们得resize()的过程中,当有两个线程同时进行resize操作的时候,容易产生相互指向的死锁,消耗CPU内存。我们可以使用ConcurrentHashMap、HashTable、SynchronizedMap来解决这个问题

存取过程

  • JDK1.8以后

底层数据结构:数据+链表+红黑树

默认初始化容量:16

默认加载因子:0.75

在一个链表大于8的时候,会转化成为红黑树,一红黑树结点<6的时候,会转换成为链表

 

存取过程

(1)put

  1. 根据key计算出hashcode

  2. 判断是否需要进行初始化。即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

  3. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  4. 如果都不满足,则利用 synchronized 锁写入数据。

  5. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

(2)get

  1. 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。

  2. 如果是红黑树那就按照树的方式获取值。

  3. 就不满足那就按照链表的方式遍历获取值。

 

主要参数:

  1. 存放数据的数组

  2. 结构体Node

  3. 默认初始化长度

  4. 最大长度 2^30

  5. 默认加载因子

  6. modcount 修改次数

  7. 链表转红黑树的值、红黑树转链表的值

 

2.3.2 TreeMap

红黑树实现

  1. 每个节点要么是黑的,要么是红的

  2. 根节点是黑的

  3. 叶节点是黑的

  4. 如果一个节点是红的,他的两个儿子节点都是黑的

  5. 对于任一节点而言,其到叶节点树尾端NIL指针的每一条路径都包含相同数目的黑节点

 

  • 插入

新插入的结点总是设为红色的,如果父节点是黑色的,就不需要修复,只有父节点是红色节点的时候需要做修复

情况1 :当前节点的父节点是红色,且祖父节点的另一个子节点(叔叔节点)也是红色,则把当前结点的父节点和父节点的兄弟结点都变成黑色

对策 :把父节点和叔叔节点变黑,爷爷节点涂红,然后把当前节点指针给到爷爷,让爷爷节点那层继续循环,接受红黑树特性检测。

 

情况2: 当前节点的父节点是红的,叔叔节点是黑的,当前节点是父节点的右子树。

对策:当前节点的父节点作为新的当前节点,以新当前指点为支点左旋

 

情况3:当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左儿子

对策: 父节点变黑,祖父变红,以祖父节点为支点右旋

 

  • 删除

在删除一个节点后,如果删除的节点时红色节点,那么红黑树的性质并不会被影响,此时不需要修正,如果删除的是黑色节点,原红黑树的性质就会被改变,此时我们需要做修正。

 

2.3.3 HashTable

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {}

2.3.4 HashMap 与 HashTable

  1. null值 :HashTable不允许K或V为null,HashMap允许K和V都为null

  2. 实现方式:HashTable继承了Dictionary,HashMap继承AbstractMap

  3. 初始化容量:HashTable 初始化容量为11,HashMap初始化容量为16

  4. 扩容机制:HashTable 当前容量 * 2+1,HashMap当前容量 * 2

  5. 迭代器:HashTable 安全失败,HashMap快速失败

2.4 String StringBuilder StringBuffer区别

底层原理:

  1. String类为final类,表示不可继承不可修改,底层使用final char[] value存储内容。而且String的各种方法返回的对象都是new String()返回一个新的对象。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        }
  2. StringBuilder是继承自AbstractStringBuilder,而AbstractStringBuilder底层是用char[] value来存储元素的,修改的是数组的本身

  3. StringBuffer就是StringBuilder的基础之上加上了synchronized关键字锁住,保证线程安全

    public final class StringBuilder
        extends AbstractStringBuilder
        implements java.io.Serializable, CharSequence
    {}
    ​
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        /**
         * The value is used for character storage.
         */
        char[] value;
    }

StringBuilder StringBuffer

  1. 默认初始化容量为:16

  2. 扩容规则:当我们使用StringBuffer对象的append(...)方法追加数据时,如果char类型数组的长度无法容纳我们追加的数据,StringBuffer就会进行扩容。扩容时会用到Arrays类中的copyOf(...)方法,每次扩容的容量大小是原来的容量的2倍加2。

3、并发

3.1 线程

3.1.1 线程状态

 

3.1.2 线程池

3.1.2.1 什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。

如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

3.1.2.2 创建线程池的好处

  1. 降低资源消耗,重复利用。创建线程是一个是非耗费资源的操作,如果我们每次都重新创建一个线程来执行一个任务的话,那么对CPU内存的损耗是十分大的。如果我事先就创建好了一个线程池,每次我要执行的时候,就去拿一个线程,那么速度和资源分配来讲,都是比较快的,是一种空间换时间的思想

  2. 提高管理性。可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

3.1.2.3 线程池的种类?

  1. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  3. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  4. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

3.1.2.4 有什么参数?

  1. corePoolSize:就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收

  2. maximumPoolSize:就是线程池中可以容纳的最大线程的数量

  3. keepAliveTime:就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间

  4. util:就是计算这个时间的一个单位

  5. workQueue:就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。

  6. threadFactory:就是创建线程的线程工厂。

  7. handler:是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。

3.1.2.5 线程池的拒绝策略

  1. AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。

  2. CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。

  3. DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

  4. DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。

3.1.2.6 execute和submit的区别?

execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。

submit方法适用于需要关注返回值的场景

3.1.2.7 线程池都有哪几种工作队列?

  1. ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序

  2. LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

  3. SynchronousQueue 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

  4. PriorityBlockingQueue 一个具有优先级的无限阻塞队列

3.1.3 创建线程的三种方式

  1. 继承Thread类

    public class Main {
    ​
        /**
         *
         * @param args
         */
       public static void main(String[] args) {
           MyThread m = new MyThread();
           m.start();
        }
    ​
        static class MyThread extends Thread {
    ​
            @Override
            public void run() {
                System.out.println("1");
            }
        }
    }
  2. 实现runnable接口

    public class Main {
    ​
        /**
         *
         * @param args
         */
       public static void main(String[] args) {
           new Thread(new Runnable() {
               @Override
               public void run() {
                   System.out.println("1");
               }
           }).start();
       }
    ​
    ​
    }
  3. 通过Callable和Future创建线程

3.1.4 线程同步方法

  1. wait():使一个线程处于等待状态,并且释放所持有的对象的lock。让出CPU进入等待队列

  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。占着茅坑不拉屎,不让出CPU

  3. notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

  4. notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

3.1.5 sleep和wait比较

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法

  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;

  3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

3.2 锁

3.2.1 synchronized

底层原理:

如果锁的是代码块,你那么进入的是monitorenter,退出锁是monitorexit,但是我们看21行还有一个monitorexit,是因为如果在抛出异常过后,也是进行monitorexit的,所以早期的synchronized就是通过这两种方式实现的锁,两个指令的执行是JVM通过调用操作系统的互斥量mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

synchronized用的就是monitor机制,常常也被称作为管程,而monitor机制底层其实用的就是Unsafe的CAS操作,就是谁先进入同步代码块,谁就更改标志位当前线程,monitor机制是通过操作系统的互斥量mutex来实现的

锁升级过程

 

  1. 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝

  2. 然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态

  3. 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

  4. 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作

  5. 一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行

3.2.2 lock

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {}
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    	private volatile int state;
    }

ReentrantLock是通过 AbstractQueuedSynchronizer实现的

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。如图

 

AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数

lock()

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。

“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。

若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state变量值
    int c = getState();
    if (c == 0) { //没有线程占用锁
        if (compareAndSetState(0, acquires)) {
            //占用锁成功,设置独占线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新state值为新的重入次数
        setState(nextc);
        return true;
    }
    //获取锁失败
    return false;
}

/**
 * 将新节点和当前线程关联并且入队列
 * @param mode 独占/共享
 * @return 新节点
 */
private Node addWaiter(Node mode) {
    //初始化节点,设置关联线程和模式(独占 or 共享)
    Node node = new Node(Thread.currentThread(), mode);
    // 获取尾节点引用
    Node pred = tail;
    // 尾节点不为空,说明队列已经初始化过
    if (pred != null) {
        node.prev = pred;
        // 设置新节点为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
    enq(node);
    return node;
}

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。

B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。

/**
 * 初始化队列并且入队新节点
 */
private Node enq(final Node node) {
    //开始自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 如果tail为空,则新建一个head节点,并且tail指向head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // tail不为空,将新节点入队
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。

B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。

/**
 * 已经入队的线程尝试获取锁
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try {
        boolean interrupted = false; //标记线程是否被中断过
        for (;;) {
            final Node p = node.predecessor(); //获取前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,将当前节点设置为head节点
                p.next = null; // 原head节点出队,在某个时间点被GC回收
                failed = false; //获取成功
                return interrupted; //返回是否被中断过
            }
            // 判断获取失败后是否可以挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程若被中断,设置interrupted为true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

unlock()

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases;
    // 如果不是当前线程占用锁,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 锁被重入次数为0,表示释放成功
        free = true;
        // 清空独占线程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}

流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败

ryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。 v

4、IO

流的特性

  1. 先进先出:最先写入输出流的数据最先被输入流读取到。

  2. 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)

  3. 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。

分类

  1. 按数据流的方向:输入流、输出流

  2. 按处理数据单位:字节流、字符流

  3. 按功能:节点流、处理流

 

字节流与字符流的区别

  1. 字节流单次读取8位字节,字符流单次读取16位

  2. 字节流能处理一切文件类型,字符流只能处理txt文本

  3. 字节流本身不带缓冲区,字符流本身带有缓冲区,缓冲区容量为1024

 

IO模型介绍

  1. 阻塞IO模型 最常用的IO模型,所有文件操作都是阻塞的

  2. 非阻塞IO模型

  3. IO复用模型

  4. 型号驱动IO模型

  5. 异步IO

 

BIO NIO AIO

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值