多线程的一些面经知识点

多线程知识

1. 什么是进程和线程

1.1 什么是进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序就是一个进程从创建,运行到消亡的过程。

1.1.1 进程间怎么通信
  1. 管道,匿名管道和命名管道
    匿名管道只能用于父子进程间的通信
    命名管道(FIFO)可以用于不相关进程间的通信

  2. 消息传递
    相当于A给B发消息,就要写信,然后通过邮差发给B。
    直接通信方式:邮差直接把信送到B的手上
    间接通信方式:邮差把信放到邮箱上,B再自己拿

  3. 信号量:
    不能传递复杂消息,只能用来同步 ,PV操作

  4. 共享内存区(这是最快的一种IPC):
    能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

  5. 消息队列
    相比于 FIFO,消息队列具有以下优点:

    • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
    • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
    • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
  6. 套接字
    与其它通信机制不同的是,它可用于不同机器间的进程通信。

1.2 什么是线程

线程和进程相似,但是线程是一个比进程更小的执行单位。一个进程在执行的过程可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区,每一个线程都有自己的程序计数器,虚拟机栈和本地方法栈。所以各个线程之间作切换工作的时候,负担要比进程小得多。
一个Java程序的运行就是main线程和多个其他线程同时运行。

1.3 线程和进程的生命周期

在这里插入图片描述
在这里插入图片描述

1.2.1 多线程的三种写法

1.继承Thread这个类,重写run方法。
2.实现Runnable接口,实现run方法。(Thread是Runnable的实现类)
(以上两种方法都 无法抛出异常 不拥有返回值)
3.实现Callable接口,实现call方法。(这个方法可对外抛出异常和拥有返回值)

2. 请简要描述线程与进程的关系,区别及优缺点

2.1 为什么程序计数器是私有的

程序计数器有两个作用

  • 字节码解释器通过改变程序计数器来读取指令,从而实现代码的流程控制。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置。当线程被切回来的时候就能够知道该线程上次运行到哪里了。
    需要注意的时,当执行本地方法时,程序计数器的值是空(undefined)。只有执行Java代码的时候程序计数器记录的才是下一条指令的地址。
    所以程序计数器私有是为了线程切换后能恢复正确的执行位置。

2.2 为什么虚拟机栈和本地方法栈是私有的

  • 虚拟机栈:每一个Java方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,常量池引用等信息。从方法调用到执行完成,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。
  • 本地方法栈:和虚拟机栈发挥的作用是类似的,区别是虚拟机栈为执行Java方法服务,本地方法栈为虚拟机使用到的本地方法服务。在HotSpot虚拟机中和本地方法栈和Java虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

2.3 并发和并行的区别

并发:同一时间段内,多个任务在执行(同一时刻不一定同时运行)
并行:在同一时刻,多个任务在执行。

2.4 为什么使用多线程

  • 线程是轻量级的进程,线程的切换和调度的成本远远小于进程
  • 从互联网的发展而言,多线程并发编程是开发高并发系统的基础。
  • 在单核时代,多线程是为了提高CPU和IO设备的综合利用率。
  • 在多核时代,CPU的一个核就能运行一个线程,多线程是为了提高CPU的利用率。

2.5 什么是上下文切换

正在运行的线程可能会被剥夺处理器的使用权,这叫做切出。一个线程被分配处理器开始运行,这叫做切入。在切入和切出的时候,操作系统需要保存和恢复相应线程的进度信息,即线程执行的任务到哪种程度了。这个信息就是上下文。上下文包括通用寄存器和程序计数器的内容。CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

2.6 线程和进程的区别

  • 进程是操作系统分配和管理资源的单位,线程是CPU调度和管理的单位,是CPU调度的最小单元
  • 进程拥有独立的地址空间,线程间共享地址空间
  • 进程创建的开销比较大,线程创建的开销小。

3.线程死锁

3.1 什么是线程死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,每一个线程都在等待资源被释放,但是这些资源已经被别的线程占有了,因此这些线程只能无限期地阻塞。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
在这里插入图片描述

3.2 产生死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求并保持条件:一个进程因为请求资源而阻塞时,不放弃已占有的资源
  • 不剥夺条件:进程已获得的资源未使用完时不能被其他线程剥夺,只有自己使用完毕后才能释放。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。
3.2.1 通过破坏四个条件来避免程序死锁

第一个条件无法破坏
破坏请求并保持条件:一次性申请所有的资源
破坏不剥夺条件:已经申请到资源的线程进一步申请其他资源失败的时候,就主动释放已经占有的资源
破坏循环等待:按某一顺序申请资源,释放资源则反序进行。

3.2.2 能用代码写线程死锁的代码吗
public class solution {
    private  Object lock_a = new Object();
    private  Object lock_b = new Object();
    public static void main(String[] args) {
        new solution().deadLock_method();
    }
    private void deadLock_method(){

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock_a){
                    System.out.println("Thread1刚刚获得lock_a");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1刚结束睡眠");
                    System.out.println("Thread1想要获得锁lock_b");
                    synchronized (lock_b){
                        System.out.println("Thread1刚刚获得锁lock_b");
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock_b){
                    System.out.println("Thread2刚刚获得lock_b");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread2刚睡了0.5s");

                    System.out.println("Thread2正在获得lock_a");
                    synchronized (lock_a){
                        System.out.println("Thread2刚刚获得lock_a");
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

4.说说sleep()方法和wait()方法的区别和共同点

  • 两者最重要的区别是:sleep方法没有释放锁,但是wait方法释放了锁
  • wait用于通常用于线程间交互,sleep用于暂停执行
  • wait方法如果是无参调用,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。

相同点
两者都能暂停线程的执行,并且控制暂停的时间。

5.为什么调用start()方法的时候会执行run方法,为什么不能直接调用run方法?

new一个线程,线程进入了new状态;调用start()方法,会启动一个线程并且使线程进入Ready状态,当分配时间片就能开始运行run方法的内容,在这里start是执行了线程相应的准备工作,这是真正的多线程工作。而直接调用run方法,会把run方法当成main线程下的一个普通方法运行,并不会在某个线程中执行它,所以这不是多线程工作。

总结:调用start方法可以启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

5. volatile和Atomic原子类

volatile变量不能保证原子性,所以引入了原子类。

5.1 volatile的原理

https://www.jianshu.com/p/6efe8d5bd567
每次使⽤volatile变量都到主存中进⾏读取

  1. 写volatile变量
    在写volatile变量的时候,会在这条写语句前面和后面加上内存屏障,前面的内存屏障禁止写语句与前面的语句进行重排序;后面的内存屏障用于把volatile变量的值从cpu缓存刷新到主存中。
  2. 读volatile变量
    在读volatile变量的时候,会在这条读语句前面和后面加上内存屏障,前面的内存屏障用于把volatile变量的值从主存中刷新到cpu缓存; 后面的内存屏障禁止读语句与后面的语句进行重排序。

5.2 volatile和synchronized的区别

1)volatile不需要加锁,比synchronized更轻量级,不会引起上下文切换。

  1. volatile只能修饰变量。synchronized可以修饰方法和代码块。

3)synchronized既能保证可见性,又能保证原子性和有序性,而volatile只能保证可见性,有序性,只能保证long/double类型变量的原子性。

5.3 AtomicInteger的原理

使用volatile变量和CAS操作来实现。volatile变量存储的整个AtomicInteger的值。
在AtomicInteger类里面有这些代码

private volatile int value;

public final int get() {
    return value;
}

//循环获取volatile变量的值,直到成功使用cas操作将volatile变量的值加1,然后返回volatile变量旧值。
public final int getAndIncrement() {
     return unsafe.getAndAddInt(this, valueOffset, 1);
 }

5.4 CAS操作的原理

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

原子变量类是基于CAS和volatile变量实现的,能够保证自增操作的原子性。
CAS也并非完美的,它会导致ABA问题,就是说,当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。

那么如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。

==,equals和hascode的区别

https://blog.csdn.net/weixin_45548912/article/details/106585619

5. synchronized的原理

https://blog.csdn.net/javazejian/article/details/72828483

5.1 先验知识

  • 每个对象都有对象头,对象头包含mark word,mark word里分为bitfield和tag bit。当tag bit为10的时候,bitfield存储的是指向monitor对象的指针。
  • monitor使用C++实现,里面有四个字段需要用到,分别是count,owner,WaitSet,EntrySet。count表示持有这个锁的线程的重入次数,EntrySet和WaitSet是两个队列,当多个线程访问同一段代码的时候,都会在EntrySet进行等待,当某个线程获得对象锁后,把owner设置为当前线程,把count加1.如果线程调用wait方法,就释放当前monitor,owner置null,count减1,同时该线程进入WaitSet。当线程执行完毕也释放当前的monitor。

5.2 同步语句块的原理

synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。 当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。当monitor的计数器为0则可以成功获取,获取后将锁计数器设为1,也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

5.3 synchronized方法的底层原理

synchronized方法底层有一个ACC_SYNCHRONIZED标识,这个标识指明该方法是一个同步方法。如果这个方法是同步方法的话,就执行相应的同步调用。

5.4 为什么早期版本的synchronized速度慢

在Java早期版本,synchronized属于重量级锁,效率低下,因为监视器锁是依赖操作系统完成的,如果要挂起或唤醒一个线程,就需要操作系统的帮忙;而操作系统切换线程需要从用户态切换为内核态,需要较长时间。在Java6之后,Java官方对synchronized进行了JVM层面的优化。所以,JDK1.6引入了大量的优化,自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术。

6. AQS

AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH队列锁实现的,即将暂时获取不到锁的线程封装成一个节点加⼊到队列中。

6. 可重入锁

ReentrantLock里面有两个静态类,公平锁和非公平锁,这两个静态类都继承自AQS。所以能使用AQS的函数。

6.1 怎么实现非公平锁和公平锁

6.1.1 非公平锁

https://www.cnblogs.com/wang-meng/p/12816829.html
可重入锁使用AQS来实现。AQS维护了一个volatile的int变量和一个FIFO的等待队列。这个int变量的变量名叫做state。
场景分析:
场景一:
现在队列是空的,state值为0. 有三个线程来申请同一个资源,它们3个都调用lock()方法,使用cas操作将state置1。假如线程1抢占成功,它把state成功置1,然后把将AQS的变量exclusiveOwnerThread设置为线程1,表示现在是线程1占据了锁。线程2通过CAS修改state的值不成功,加锁失败,调用acquire函数。acquire()函数首先尝试抢占共享资源,如果失败就加入等待队列中。

final void lock() {
    //使用CAS操作设置state,如果成功
      // 把当前线程设置为独占线程,表示获得了锁。
   	if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

其中tryAcquire()方法(非公平版)的实现如下

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    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)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

acquireQueued方法的逻辑是这样的

while(true){
	if 当前节点的前一个节点为head&&使用tryAcquire()加锁操作成功。
	  	 当前节点变成head节点。
	  	 把之前的head节点置空,让其等着被垃圾回收
	if 加锁失败或者当前节点前一个不是head
		当前节点代表的线程挂起。
}

在acqiure函数里面调用tryAcquire方法继续获得锁。如果还是失败,那么调用addWaiter()就加入到队列里面。
这时候队列里面没有头结点,队列里首先加入一个空的头结点,然后把线程2包装成一个Node节点挂在头结点之后,这时候AQS里面就有两个Node节点了。我们把线程2对应的节点叫做2号节点。2号节点加入队列之后,就会把2号节点的内容传入acquireQueued()方法。,
显然,线程2是抢不到锁,所以只能挂起了。然后线程3也是这样,生成3号节点,然后挂在2号节点后面。

场景二:
接着场景一,线程一释放锁,state被置为0,然后唤醒head节点后面的那一个节点,被唤醒的节点是2号节点。2号节点的线程在之前挂起的位置继续执行。判断2号节点的前驱是否为头结点并且使用tryAcquire()尝试获取锁,如果能获取锁,这时候就把2号节点设置为head节点,并且把之前的head节点置空。

当线程二释放锁的时候,唤醒被挂起的线程三,线程三执行tryAcquire()方法使用CAS操作来尝试修改state值,如果此时又来了一个线程四也来执行加锁操作,同样会执行tryAcquire()方法(在lock函数里面,CAS操作失败之后会进行一次加锁操作)。

6.1.2 公平锁

公平锁也是使用了acquire函数,只不过里面的tryAcquire函数是公平版的,当state为零的时候,不直接使用CAS去抢,而是首先判断队列是否为空,如果为空那就抢锁。如果队列不为空,那么就加入队列,实现了FIFO。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
static final class FairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
6.1.3 公平锁和非公平锁的表现
  1. 公平锁
    新来的线程就直接放到队列尾部。当持有锁的线程释放线程的时候,从队列的头拿出第一个节点,让这个节点代表的线程获得锁执行。
  2. 非公平锁
    当持有锁的线程释放线程的时候,唤醒头节点后面的第一个节点,记作节点2,如果现在来了一个或多个新的线程竞争锁,那么节点2和这些新的线程一起竞争锁,竞争失败的就放到队列的尾部,成功的就设置为队列的头节点。

6.2 两者优缺点

  1. 为什么非公平锁性能高于公平锁性能。
    在公平锁中,几乎每一个线程都要阻塞并放到队列的后面(例外就是队列为空并且没有线程持有锁),然后在持有锁的线程释放锁的时候,再唤醒队列的第一个线程,这样相当于每一个线程都要阻塞,再唤醒。
    非公平锁则是让线程可能避免阻塞,在持有锁的线程释放锁的时候,新来的线程可能直接获得锁,这样就不用阻塞了。

非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。

6.3 可重入怎么体现

可重入是使用了TryAcquire方法,如果state为0,那么就是用CAS操作获取锁,把state置1.如果state不为0,那么看看获取锁的线程是否是自己,如果是自己,那把state加一。同理,释放锁的时候,也要把state减一。

6.4 可重入锁和Synchronize的区别

1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。

Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
Lock使用起来比较灵活,,而synchronized可用于修饰方法、代码块等

7. 锁优化

这里的锁优化主要是指JVM对synchronized的优化

7.1 自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

7.1.1 自己实现一个简单的自旋锁

https://www.geeksforgeeks.org/atomicboolean-compareandset-method-in-java-with-examples/
AtomicBoolean的compareAndSet方法

public class SpinLockTest {

    private AtomicBoolean available = new AtomicBoolean(false);

    public void lock(){
        // 循环检测尝试获取锁
        while (!tryLock()){
            // doSomething...
        }
    }

    public boolean tryLock(){
        // 尝试获取锁,成功返回true,失败返回false
        return available.compareAndSet(false,true);  // compareAndSet(期望值,要改的新值)
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("释放锁失败");
        }
    }
}

7.2 锁消除

锁消除是指对于被检测出来的不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其他线程访问到,那么可以把它们当成私有数据对待,也就可以将它们的锁进行消除。

7.3 锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁地加锁操作加大性能的损耗。如果虚拟机检测到这样的一串零碎操作都对同一个对象加锁和解锁,就会把加锁的范围扩展到整个操作序列的外部。

7.4 轻量级锁

对象头的定义https://blog.csdn.net/sted_zxz/article/details/76854371
对象头,是对象的一部分,对象头中有一部分叫做mark word(https://blog.csdn.net/javazejian/article/details/72828483),mark word由两部分组成,第一部分是bitfields,第二部分是tag bits。其中bitfields存储的内容由tag bits的内容决定。而且tag bits还可以确定对象的状态。如下图。
在这里插入图片描述
hash是hash码的意思,age是GC分代年龄。thread id是偏向线程的id,epoch是偏向时间戳。
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
在这里插入图片描述
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。

当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,**然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。**如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
在这里插入图片描述
如果CAS操作失败,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

7.5 偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

8. ThreadLocal

https://www.cnblogs.com/micrari/p/6790229.html 讲得非常详细,代码都有注释,但是感觉面试用不了这么多知识,而且我也记不住。
https://juejin.im/post/6844903809018232846 这个讲得有点不详细

8.1 ThreadLocal的用途

多个线程访问非线程安全的对象,可能会出错,这可以通过加锁来保证线程安全。如果不选择加锁的话,可以让每一个线程都创建一个属于该线程的非安全线程对象,一个线程不能访问另一个线程的对象。这种一个对象只能被一个线程访问的对象被称为线程特有对象。

ThreadLocal就是用来方便地创建线程特有对象的。

一个ThreadLocal变量能被多个线程使用,使用ThreadLocal的线程都能通过这个ThreadLocal创建一个属于这个线程的变量,其他的线程不能够访问这个变量。

public class Test {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new MyThread("lucy");
        Thread thread2 = new MyThread("lily");
        thread1.start();
        thread2.start();
    }

    private static class MyThread extends Thread {

        MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            threadLocal.set("i am " + thread.getName());
            try {
                //睡眠两秒,确保线程lucy和线程lily都调用了threadLocal的set方法。
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + " say: " + threadLocal.get());
        }
    }
}

这个例子非常简单,就是创建了lucy和lily两个线程。在线程内部,调用threadLocal的set方法存入一字符串,睡眠2秒后输出线程名称和threadLocal中的字符串。我们运行这单代码,看一下输出内容。

lucy say: i am lucy
lily say: i am lily

8.2 ThreadLocal的实现

下面是ThreadLocal的set方法。

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = t.threadLocals;
    if (map != null)
        //将数据放入ThreadLocalMap中,key是当前ThreadLocal对象,值是我们传入的value。
        map.set(this, value);
    else
        //初始化ThreadLocalMap,并以当前ThreadLocal对象为Key,value为值存入map中。
        t.threadLocals = new ThreadLocalMap(this, value);
}

具体原理如下:

  • 每一个线程都维护着一个Map,这个map叫做ThreadLocalMap,这个Map的Key就是ThreadLocal对象,Value就是这个线程对应于ThreadLocal对象的特有对象。
    下面就是Thread类的代码,可见Thread类是有ThreadLocal.ThreadLocalMap类的对象的,也就是上文说的“每一个线程都维护一个Map”。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap的实现

ThreadLocalMap是ThreadLocal类的静态类

static class ThreadLocalMap {
    private ThreadLocal.ThreadLocalMap.Entry[] table;

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

        Entry(ThreadLocal<?> var1, Object var2) {
            super(var1);
            this.value = var2;
        }
    }
}

ThreadLocalMap是一个Map,内部是一个Entry数组,每一个Entry内部有ThreadLocal变量和线程对应于ThreadLocal对象的特有对象,要注意的是,Entry对象是弱引用ThreadLocal对象的。

在put元素的时候,先用hash函数出应该存放的数组的下标index,然后放元素。
ThreadLocalMap使用了线性探测法来解决hash冲突。

ThreadLocal的内存分析

在这里插入图片描述
ThreadLocal在内存中的模样,这时候调用ThreadLocalMap中的Entry的Entry.get()方法会得到TreadLocal对象,这个是弱引用的代码使用规则(https://www.jianshu.com/p/964fbc30151a)
现在,我们假设ThreadLocal完成了自己的使命,与ThreadLocalRef断开了引用关系。此时内存图变成了这样。
在这里插入图片描述
系统GC发生时,由于Heap中的ThreadLocal只有来自key的弱引用,因此ThreadLocal内存会被回收到。
在这里插入图片描述
这时候,调用Entry.get()方法就会得到null。
所以,当这个key没有强引用的时候,会被回收,但是value不会被回收。这个value无法被访问,如果不处理的话,这个value就会导致溢出。
ThreadLocalMap已经做了优化,在调用ThreadLocalMap的get方法和put方法的时候,碰到key为空的Entry,会把这些Entry清理掉。从而尽量避免内存溢出。为保险起见,当我们使用完ThreadLocal方法之后,最好手动调用remove方法,清理key为空的Entry。

9. 如何优雅地停止一个线程

  1. 错误方法
    Java中Thread类有一个stop()方法可以终止线程,但是这个方法会让线程直接终止,执行的任务立刻停止,未执行的任务无法反馈。所以不建议使用。

  2. 正确方法

  • 当线程进入 runnable状态之后,通过设置一个标识位,线程不断检查该标识位,发现符合终止条件,自动退出 run ()方法,线程终止。
  • 使用interrupt()方法和interrupted()方法搭配使用。假设被中断的线程是target线程,别的线程调用target.interrupt()方法,把中断标志置1. 然后target线程在运行的时候,使用this.interrupted()方法判断中断标志是否已经置位,如果置位,那么就退出运行。
    参考资料,里面有代码:
    https://blog.csdn.net/mayifan_blog/article/details/85545363

10.线程池

10.1 线程池的流程

在这里插入图片描述

10.2 线程池的方法

ExecutorService接口方法:
提交方法:
execute(Runnable)、submit(Runnable)、submit(Callable)、invokeAny(…)、invokeAll(…);
execute(Runnable)和submit(Runnable)没有返回值,submit(Callable)返回一个Future对象(包装执行结果),通过这个Future对象可以判断任务是否执行成功。并且可以通过Future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成,get()方法还可以设定等待时间,到达等待时间后,不管任务有没有执行完,都立即返回。
invokeAny(…)接收一个Callable集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果,这个方法也无法保证返回的是哪个任务的执行结果,反正是其中一个;
invokeAll(…)接收一个Callable集合,返回一个Future的list,对应每个Callable任务执行后的Future对象;

关闭方法:
shutdown()、shutdownNow()
在调用shutdown()方法之后,线程池不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,所有在shutdown()执行之前提交的任务都会被执行;
调用shutdownNow()方法,将跳过所有执行中和未执行(已提交)的任务直接关闭。但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。

10.3 为什么要用线程池

  • 降低资源消耗。重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度。当任务到达时,任务不需要等待线程创建就能够立即执行。
  • 提高线程的可管理性。使用线程池可以方便线程池的分配,调优和监控。

实现Runnable接口和Callable接口的区别

  • Runnable接口不会返回结果,不会抛出检查异常。但是Callable接口是可以的。

10.3 线程池的使用

10.3.1 线程池的创建
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,
milliseconds,runnableTaskQueue,handler);

这些参数的意思分别是:核心线程数,最大线程数,线程存活时间,存活时间的时间单位,任务队列,

  1. 任务队列: 一个阻塞队列,用于保存等待执行的任务
  • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,使用FIFO原则对元素进行排序。是有界的。
  • LInkedBlockingQueue: 基于链表的阻塞队列,按照FIFO原则对元素进行排序。吞吐量高于ArrayBlockingQueue。是有界的,最大值为Integer.MAX_VALUE;
  • SynchronousQueue: 一个阻塞队列,其中每个插入操作必须等待另一个线程进行相应的删除操作,反之亦然。同步队列没有任何内部容量,这个队列用于CacheThreadPool。
  • PriorityBlockingQueue:具有优先级的无限阻塞队列。
  1. handler:饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,这时候必须采用一种策略处理提交的新任务。这个策略默认是AbortPolicy。
  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:只用调用者所在线程来运行任务。
  • DiscardOldestPolicy:丢弃队列最老的一个任务,执行当前任务。
  • DiscardPolicy:不处理,丢弃掉
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值