JAVA基础(三)-面试篇

本文详细探讨了Java并发编程的关键概念,如synchronized与volatile的区别,Thread对象的wait、yield和join方法,ThreadLocal的作用及原理,死锁的条件和AQS源码分析。涵盖了线程安全的集合、编程单元与存储单元,以及并发控制的高级技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系列文章目录

JAVA基础系列只是列出博主在刷牛客JAVA转项练习的总结

JAVA基础篇一:继承、多态、导包问题 、排序、变量初始值等

JAVA基础篇二:父子类问题、Map空值问题、异常、真假问题等

JAVA基础篇三:并发源码、Math类等

并发参考原有博文:https://www.cnblogs.com/waterystone/p/4920797.html



# 21、 集合安全问题

线程安全(Thread-safe)的集合对象:

  • Vector 线程安全:
  • HashTable 线程安全:
  • StringBuffer 线程安全:

非线程安全的集合对象:

  • ArrayList :
  • LinkedList:
  • HashMap:
  • HashSet:
  • TreeMap:
  • TreeSet:
  • StringBulider:

22、编程单元、存储单元

java的基本编程单元是类,基本存储单元是变量。

23、语言函数

基本的Java语言函数存储在以下哪个java包中?

java.lang

24、并发

24.1 synchronized关键字和volatile关键字

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

24.2 Thread中的方法

1.sleep()方法

在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。

sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

2.wait()方法

在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。

当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。

唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。

waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

3.yield方法

暂停当前正在执行的线程对象。

yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

yield()只能使同优先级或更高优先级的线程有执行的机会。

4.join方法

等待该线程终止。

等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

wait和join方法会释放锁资源其余的不会

24.3 ThreadLocal

1、ThreadLocal的类声明:

public class ThreadLocal

可以看出ThreadLocal并没有继承自Thread,也没有实现Runnable接口。

2、ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量。

所以ThreadLocal重要作用并不在于多线程间的数据共享,而是数据的独立

由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,不会被其他线程访问,

变量被彻底封闭在每个访问的线程中。所以E对。

3、ThreadLocal中定义了一个哈希表用于为每个线程都提供一个变量的副本:

 static class ThreadLocalMap {

   static class Entry extends WeakReference<ThreadLocal> {

     /** The value associated with this ThreadLocal. */

     Object value;

      Entry(ThreadLocal k, Object v) {

        super(k);

        value = v;

     }

   }

    /**

     \* The table, resized as necessary.

     \* table.length MUST always be a power of two.

     */

    private Entry[] table;

}

24.4 线程间的通知和唤醒

wait()、notify()和notifyAll()是 Object类 中的方法

从这三个方法的文字描述可以知道以下几点信息:

1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。

2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)

3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;

4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;

有朋友可能会有疑问:为何这三个不是Thread类声明中的方法,而是Object类中声明的方法

(当然由于Thread类继承了Object类,所以Thread也可以调用者三个方法)?其实这个问

题很简单,由于每个对象都拥有monitor(即锁),所以让当前线程等待某个对象的锁,当然

应该通过这个对象来操作了。而不是用当前线程来操作,因为当前线程可能会等待多个线程

的锁,如果通过线程来操作,就非常复杂了。

上面已经提到,如果调用某个对象的wait()方法,当前线程必须拥有这个对象的monitor(即

锁),因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者

synchronized方法)。

调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,

等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从

而让其他线程有机会继续执行,但它并不释放对象锁);

notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象

的monitor的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。

同样地,调用某个对象的notify()方法,当前线程也必须拥有这个对象的monitor,因此调用

notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。

nofityAll()方法能够唤醒所有正在等待该对象的monitor的线程,这一点与notify()方法是不同的。

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。

  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用Conditon中的await()对应Object的wait(); Condition中的signal()对应Object的notify(); Condition中的signalAll()对应Object的notifyAll()

24.5单任务和并发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrFLrKIl-1616599707653)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20210304151912799.png)]

把第一种模型理解为单任务的。如果我们遇到了一个需要等待的IO操作,可能会让此进程阻塞,其他的进程得不到执行。如果其他进程等待时间很长的话,可能会导致其他进程饿死。

把第二种模型理解为并发的。举个例子,当我们设计一款类似于wps这样的文字处理软件的时候,我们可以开一个线程来与用户进行交互,开第二个线程来对读取进内存中的数据进行计算,当我们计算完了之后,可能需要把这些数据保存进磁盘中,这时候,我们可以开第三个线程来负责把数据写入磁盘中,我们知道,对于磁盘的读写操作是毫秒级别的(而对于内存的读写是纳秒级别的。不要觉得毫秒级别的时间很短,对于计算机来说已经很长了。对于大量数据排序都不一定需要毫秒),所以非常的慢,所以,如果我们把这个耗时比较长的操作专门交给一个线程来处理的话,就可以充分利用CPU。

24.6 代码死锁

下列哪种操作可能带来死锁?

A:lock(m1) lock(m2) unlock(m1) unlock(m2)

B:lock(m1) lock(m2) unlock(m2) lock(m2) unlock(m1) unlock(m2)

C:lock(m1) lock(m2) unlock(m1) lock(m1) unlock(m2) unlock(m1)

D:lock(m1) lock(m2) unlock(m1) unlock(m2) lock(m1) unlock(m1)

假设有两个线程,线程1执行到lock(m1),lock(m2),unlock(m1),此时线程1持有锁m2,想要获取锁m1;线程2执行到lock(m1),此时线程2持有锁m1,想要获取锁m2。两个线程都拿着对方想要得到的锁,造成死锁。

24.7 死锁必要条件

  • 产生死锁的四个必要条件:
    (1) 互斥条件:一个资源每次只能被一个进程使用。
    (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

24.8 AQS源码分析

1、AQS结构

ReentrantLock是唯一实现了Lock接口的类

ReentrantLock中有一个静态抽象类Sync,Sync继承了AQS(AbstractQueuedSynchronizer)AQS也是抽象类

在AQS中有个Node类(static final)

重要的是信号量:waitStatus 在Node节点中定义,代表节点的五种状态:

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。cancelled
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。signal
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。conedition
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。propagate
  • 0:新结点入队时的默认状态。

负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

2、AQS中主要的方法

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意**,获取多少次就要释放多少次**,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

3、lock方法的底层acquire(int arg)

lock底层其实是调用acquire(int)方法去实现加锁

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:

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

函数流程如下:

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
3.1、tryAcquire(int)底层

此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。如下是tryAcquire()的源码:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

什么?直接throw异常?说好的功能呢?好吧,**还记得概述里讲的AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?**就是这里了!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。

这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。

3.2 、addWaiter(Node)底层
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node pred = tail;
    //尝试快速方式直接放到队尾。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
     //上一步失败则通过enq入队。
    enq(node);
    return node;
}
3.2.1、enq(Node)底层
private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
3.3 acquireQueued(Node, int)

通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。下一步:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过

    //又是一个“自旋”!
    for (;;) {
        final Node p = node.predecessor();//拿到前驱
        //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
        if (p == head && tryAcquire(arg)) {
            setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
            p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
            failed = false; // 成功获取资源
            return interrupted;//返回等待过程中是否被中断过
        }

        //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
            interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
    }
} finally {
    if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
        cancelAcquire(node);
}
}
3.3.1 shouldParkAfterFailedAcquire(Node, Node)

此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
3.3.2 parkAndCheckInterrupt()

此方法就是让线程去休息,真正进入等待状态

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); //调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:

1)被unpark();

2)被interrupt()。

需要注意的是,Thread.interrupted()会清除当前线程的中断标记位

小结

看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),总结下该函数的具体流程:

  1. 结点进入队尾后,检查状态,找到安全休息点;
  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程。
acquire()小结
  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

img

这也就是ReentrantLock.lock()的流程!

24.9 Thread详解

1、状态转换图

img

24.10 Lock接口详解

Lock中的方法:

public interface Lock {
  void lock();
  void lockInterruptibly() ``throws` `InterruptedException;
  boolean tryLock();
  boolean tryLock(``long` `time, TimeUnit unit) ``throws` `InterruptedException;
  void unlock();
  Condition newCondition();
}

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

Lock lock = ...;
lock.lock();
try{
  //处理任务
}catch(Exception ex){
  
}finally{
  lock.unlock();  //释放锁
}

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

24.11 ThreadLocal详解

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。 可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。 举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的。 有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

ThreadLocal提供的几个类

public` `T get() { }
public` `void` `set(T value) { }
public` `void` `remove() { }
protected` `T initialValue() { }
  • **get()**方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本
  • **remove()**用来移除当前线程中变量的副本
  • **initialValue()**是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。
1、Get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);//取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//获取到<key,value>键值对,注意这里获取键值对传进去的是  this,而不是当前线程t
        //如果获取成功,则返回value值。
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();// 如果map为空,则调用setInitialValue方法返回value。
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;//调用当期线程t,返回当前线程t中的一个成员变量threadLocals
}

threadLocals是什么呢?其实他就是ThreadLocal的一个内部类

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

看一下这个内部类

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。

2、setInitialValue()
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

继续看createMap()

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

24.12守护线程

java线程是一个运用很广泛的重点知识,我们很有必要了解java的daemon线程.

1.首先我们必须清楚的认识到java的线程分为两类: 用户线程和daemon线程

A.  用户线程: 用户线程可以简单的理解为用户定义的线程,当然包括main线程(以前我错误的认为main线程也是一个daemon线程,但是慢慢的发现原来main线程不是,因为如果我再main线程中创建一个用户线程,并且打出日志,我们会发现这样一个问题,main线程运行结束了,但是我们的线程任然在运行).

B.  daemon线程: daemon线程是为我们创建的用户线程提供服务的线程,比如说jvm的GC等等,这样的线程有一个非常明显的特征: 当用户线程运行结束的时候,daemon线程将会自动退出.(由此我们可以推出下面关于daemon线程的几条基本特点)

2. daemon 线程的特点:

A.  守护线程创建的过程中需要先调用setDaemon方法进行设置,然后再启动线程.否则会报出IllegalThreadStateException异常.(个人在想一个问题,为什么不能动态更改线程为daemon线程?有时间一个补上这个内容,现在给出一个猜测: 是因为jvm判断线程状态的时候,如果当前只存在一个线程Thread1,如果我们把这个线程动态更改为daemon线程,jvm会认为当前已经不存在用户线程而退出,稍后将会给出正确结论,抱歉!如果有哪位大牛看到,希望给出指点,谢谢!)

B.  由于daemon线程的终止条件是当前是否存在用户线程,所以我们不能指派daemon线程来进行一些业务操作,而只能服务用户线程.

C.  daemon线程创建的子线程任然是daemon线程.

25、Math类常用

  • floor: 求小于参数的最大整数。返回double类型-----n. 地板,地面

​ 例如:Math.floor(-4.2) = -5.0

  • ceil: 求大于参数的最小整数。返回double类型-----vt. 装天花板;

​ 例如:Math.ceil(5.6) = 6.0

  • round: 对小数进行四舍五入后的结果。返回int类型

​ 例如:Math.round(-4.6) = -5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值