多线程互问

多线程

实现线程的方式知道哪些 ?

  • 实现Runnable接口
  • 实现Callable接口
  • 继承Thread类
  • 线程池

线程有哪些分类?

守护线程: 虚拟机创建(不一定),典型垃圾回收线程

用户线程: 程序创建

虚拟机会等待非守护线程结束运行后退出,但不会等待守护线程。


什么是线程饥饿?

饥饿 : 是由于线程无法获取需要的资源,由高优先级线程占用资源,线程处于等待状态不被唤醒。

产生饥饿的原因: 高优先级线程占用资源; 无法获取锁。


什么是死锁,活锁?

死锁: 多个线程都无法获得资源继续执行。
可以通过避免一个线程获取多个锁;一个锁占用一个资源;使用定时锁;数据库加解锁在一个连接中。
死锁产生的必要条件:

  • 环路等待
  • 不可剥夺
  • 请求保持
  • 互斥条件

活锁: 线程之间互相谦让资源,都无法获取所有资源继续执行。


yield方法会释放锁吗?Sleep和yield方法分别会让线程变成什么状态?

不释放锁
sleep当前线程由运行态进入阻塞状态,yield使当前线程由运行态到就绪态;

注意:sleep和yield,suppend,线程暂停但不释放锁。易引起死锁


知道有哪几种方式停止线程吗? 知道interrupt(),interrupted(),isInterrupted() 的区别吗?

stop方法;

使用interrupt () 和 isInterrupted() 配合使用。

  • interrupt() : 设置中断标志
  • interrupted(): 响应中断标志并复位中断标志;
  • isInterrupted(): 响应中断标志

知道volatile吗?

只能修饰变量,保证可见性,禁止指令重排序,但是不保证原子性。
可见性通过强制将数据写回主存,然后使其他CPU的缓存失效。
禁止指令重排是通过内存屏障来实现的。


volatile和sychronized的区别是什么?

  • volatille只能修饰变量; synchronized可以在类,变量,方法和代码块上修饰。
  • volatile只能保证可见性,synchronized能保证原子性&& 可见性
  • volatile禁止指令重排序,synchronized不会。
  • volatile 不会造成阻塞, synchronized会。

知道缓存一致性吗?

系统发现共享变量被修改,会通知其他CPU,从主存重新读取。
volatile就是通过这个保证可见性的。


对象头是什么,有什么用?

对象头由以下三部分组成:

  • Mark Word
  • 指向类的指针
  • 数组长度(只有数组对象才有)

Mark Word

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

  • Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
  • Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的

在这里插入图片描述
其中无锁和偏向锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
JDK1.6以后的版本在处理同步锁存在的锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

指向类的指针

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit
Java对象的类数据保存在方法区

数组长度

只有数组对象保存了这部分数据
该数据在32位和64位JVM中长度都是32bit

JVM一般这样使用Mark Word(锁升级过程)

  1. 当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
  2. 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
  3. 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
  4. 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
  5. 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
  7. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

知道锁升级的过程吗?

在这里插入图片描述

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到竞争的线程,适用自旋锁会消耗CPU追求响应速度,同步块执行速度非常块
重量级锁线程竞争不适用自旋锁,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢

CAS是什么,有什么问题,如何解决?

通过对指定内存地址的实际值与期望值进行比较,如果相同,则替换成新值,否则不替换。

  1. ABA问题,先替换成一个值,修改后在替换回来; 使用版本号;
  2. 循环开销时间大;

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

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


有三个线程 T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的 join ()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3 调用 T2,T2 调用 T1),这样 T1 就会先完成而 T3 最后完成


FutureTask了解吗?

在Java并发程序中FutureTask表示一个可以取消的异步运算。
它有启动和取消运算,查询运算是否完成和取回运算结果等方法。
只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。

Future实现了CallableFuture,而CallableFuture实现了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Execut or 来执行。


怎么检测一个线程是否拥有锁?

在java.lang.Thread 中有一个方法叫holdsLock() , 它返回true,如果当且仅当当前线程拥有某个具体对象的锁。


java中synchronized和ReentrantLock有什么不同?

  1. 底层实现不一样,synchronized它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK1.5 之后提供的API层面的互斥锁,需要lock() 和 unlock() 方法配合try/finally语句块来完成。也就是说synchronized隐式获得释放锁,ReentrantLock显示的获得,释放锁。
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生, 而Lock在发生异常时,如果没有主动释放unLock() 去释放锁,则可能会造成死锁现象,因此使用Lock时需要注意在finally语句块中释放锁。
  3. synchrinized是同步阻塞,使用的是悲观并发策略。lock是同步非阻塞,采用的是乐观并发策略。
  4. ReentrantLock可以让等待的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。通过ReentrantLock可以知道有没有成功获取锁,而synchronized却无法办到。最重要的是ReentrantLock可以提供公平锁,而synchronized只能是非公平锁。
  5. ReentrantLock可以提高多个线程进行读操作的效率,实现读写锁。
  6. ReentrantLock通过Condition可以绑定多个条件。

Java线程池中,submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute() 方法的返回类型是void,它定义在Executor接口中,而submit() 方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其他线程池类像ThreadPoolExecutor和ScheduledTreadPoolExecutor都有这些方法。


JUC下都有哪些包?

在这里插入图片描述

  1. 线程安全类
  • ConcurrentHashMap,CopyWriteArrayList,CountDownLatch,CyclicBarrier,Samephone。
  1. 阻塞队列
  • ArrayBlockingQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,BlockingQueue
  1. 多线程,线程池相关的类
  • Executor,ExecutorService/ScheduleExecutorService
  • ScheduleThreadPoolExecutor/ThreadPoolExecutor
  • newFixedThreadPool/newWorkStearlingPool/newSingThreadExecutor/newCacheThreadPool
  1. Atomic
  • AtomicInteger(addAndGet,compareAndSet,get,incremantAndGet)
  • AtomicLong
  • AtomicBoolean
  • AtomicInteferArray
  • AtomicReference: 以原子的方式更新对象的应用
  1. Locks
  • Lock: 相比synchronized提供了更广泛的操作,更灵活
  • ReentrantLock
  • ReadWrite:读的共享锁和写的独占锁
  • ReentrantReadWriteLock

TreadLocl 了解吗? 知道它的内存泄漏问题吗?

ThreadLocal是什么?

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充变量属于当前线程。该变量对于其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了副本,那么每个线程可以访问自己的内部变量。

从字面意思来看非常容易理解,但是从实际使用角度来看,就没那么容易了,作为一个面试常问的点,使用场景也是相当丰富的。

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息
  4. 数据库连接,Session会话管理

ThreadLocal怎么用
既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:


import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
 * @author chenbin
 * @datetime 2020-10-20 18:27
 */

public class ThreadLocalTest01 {
    public static void main(String[] args) {
        //新建一个ThreadLocal
        ThreadLocal<String> local = new ThreadLocal<>();
        // 新建一个随机数类
        Random random = new Random();
        // 使用java8 的Stream新建5个线程
        IntStream.range(0,5).forEach(a -> new Thread(() ->{
            local.set(a+" "+random.nextInt(10));
            System.out.println("线程和local值分别是:"+local.get());
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }).start());
    }
}

从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。
这就是ThreadLocal的基本使用,是不是非常的简单。那么为什么会在数据库连接的时候使用的比较多?
在这里插入图片描述
上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次连接的和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。
以上主要是讲解了一个基本的案例,然后还分析了为什么在数据库连接的时候会使用ThreadLocal。下面我们从源码的角度来分析一下,ThreadLocal的工作原理。

ThrealLocal
在最开始的例子中,只给出了两个方法也就是get和set方法,其实还有几个需要我们注意。
在这里插入图片描述

  1. set方法
    在这里插入图片描述
    从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
    OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
    在这里插入图片描述
    我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

还有一个getMap

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。

  1. get方法
    在这里插入图片描述
    通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。

如何设置一个初始值呢?
在这里插入图片描述
原理很简单

  1. remove方法

在这里插入图片描述
从我们的map移除即可。
OK,其实内部源码很简单,现在我们总结一波
(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap。

ThreadLocal其他几个注意的点
只要是介绍Threadlocal的文章都会帮助大家认识的一个点,那就是内存泄漏问题。我们先来看下下面这张图。
在这里插入图片描述
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

  1. Thread中有一个map,就是ThreadlocalMap
  2. ThreadLocalMap的key是T和readLocal,值是我们自己设定的
  3. ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收。
  4. 重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样的,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值