知识点-线程

Start、join、wait、yield:

线程通过start()方法可以由新建状态变成就绪状态,进而可以通过run()方法由就绪状态变成运行中状态,这个时候通过yield()方法实现就绪状态和运行中状态两种之间的切换,一般我们把就绪状态和运行状态统称为runable运行状态;

当线程显式调用sleep()、wait()、join()方法且传入睡眠时间时,会使线程由运行状态转成超时等待状态,待时间结束之后或显式调用notify()方法,继续进入运行状态;

当线程显式调用sleep()、wait()、join()方法时,会使线程由运行状态转成等待状态,显式调用notify()方法,继续进入运行状态;

当线程碰到由synchronized修饰的同步代码块时,线程变成阻塞状态,获取到锁后,进入运行状态;

线程结束之后,进入死亡状态,生命周期结束。

 

notify()和notifyAll()的区别:
锁池:每个对象都有锁池和等待池,假设A、B两个线程同时竞争一个对象的锁,A成功了,B就进入锁池;

等待池:A线程获取成功之后,可能由于一些意外情况不能立即执行,调用了wait()方法,A线程就会进入等待池。
notify()会根据一定算法从等待池中挑选一个线程放入锁池,notifyAll()会将等待池中的所有线程放入锁池,另外只有锁池中的线程才有成功获取到锁的机会。

 

wait,sleep分别是谁的方法,区别:

wait()方法是Object类的方法,sleep()方法是Thread类的方法,区别就是wait方法调用的时候可以设置时间,也可以不设置时间,sleep方法必须设置时间;
sleep()方法可以使当前线程释放cpu但不释放同步锁,wait()方法会使当前线程cpu和同步锁都释放。

 

countLatch的await方法是否安全,怎么改造:

如果我们在代码里边使用了await方法,就会使当前线程阻塞,且会占用一个活动线程数,如果子线程里边没有及时countdown,当前线程会一直阻塞,线程池的活动线程数就有一直被占满的风险,从而使线程池不可用。解决办法:不使用awiat方法,另外如果必须使用的话,可以让使用await的线程和其要等待的线程使用两个线程池。

 

线程池参数,整个流程描述

①corePoolSize:线程池的基本大小,就是处于活动和空闲状态的线程数;

②RunnableTaskQueue:用以保存等待任务的阻塞队列,例如ArrayBlockingQueue(基于数组)、LinkedBlockingQueue(基于链表)、SynchronousQueue(不存储元素、每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,也就是相当于队列长度为1);

③maximumPoolSize:线程池的最大大小,就是队列满了且线程数已达到基本大小值,就会继续创建线程,直到线程的最大大小。但是如果使用了无界队列,这个参数就没什么用了。

④ThreadFactory:用于设置创建线程的工厂

⑤RejectedExecutionHandler:饱和策略,就是线程池和队列都满了之后,对于后进来的线程如何处理的问题。

 CallerRunsPolicy:只用调用者所在线程来运行任务。

 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

 DiscardPolicy:不处理,丢弃掉。

 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

⑥keepAliveTime:线程空闲保持时间,就是一个空闲线程的存活时间

⑦TimeUnit:线程空闲保持时间的单位

流程描述:

①先判断线程池中的核心线程们是否空闲,如果空闲,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的核心线程数还小于 corePoolSize,那就再创建一个核心线程。

②如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新来的任务放到等待队列中去。如果等待队列又满了,那么查看一下当前线程数是否到达maximumPoolSize,如果还未到达,就继续创建线程。

③如果已经到达了,就交给RejectedExecutionHandler(拒绝策略)来决定怎么处理这个任务。

 

aqs,cas背后的底层原理:

  AQS是java实现的同步队列,是除了synchronized之外的一种锁实现机制。实现了Lock接口的锁都是基于AQS去实现的。它内部有一个先进先出的双向队列和一个由valitale修饰的变量state,当线程进来且获取同步资源锁失败的时候,就会进入队列中,排队等待。同步资源的锁是否被占用是通过state来表示的,线程获取到锁之后,会通过CAS去修改位于主存中的值,state由valitle修饰,保证了内存可见性。
所谓的CAS就是java允许通过Unsafe类的方法直接修改主内存中的变量值,里边提供了一个compareAndSet方法,涉及到两个参数,期望值,修改成的值,方法在执行的时候,会首先查一遍主内存中值是不是等于期望值,是的话直接修改成功,否则就修改不成功。另外别的地方调用的时候,一般通过自旋等待的方式去修改,实现线程同步的效果。这里提到的主内存指的就是所有线程共享的资源,每个工作线程执行的时候,会复制一份到自己的工作内存,修改过后,再同步进到主内存。

 

ThreadLocal原理,注意事项,参数传递

ThreadLocal主要用作线程隔离,填充的数据只属于当前线程,跟别的线程相互隔离。可以防止本线程的数据被其他线程修改。另外就是填充的对象实际存储在ThreadLocalMap对象里边,它本质上是一个map键值对,键就是具体的threadLocal,值就是对应的value。

参数传递就是假设我们一个线程的执行横跨多个方法,同时都要使用一个变量,我们就可以通过在threadLocal里边设置值来解决过度传参的问题。

注意事项就是我们在get之后一定要显式调用remove方法,不然就会有内存泄漏的风险。因为ThreadLocalMap中包含一个Threadlocal弱引用,在系统GC的时候,一定会被回收掉,相当于map里边存的key就变成了null,但是具体的值value还在,在我们使用线程池的时候,线程肯定会一直存活,就造成了内存泄漏。另外如果我们不显式调用remove方法,在线程池里边使用也会造成脏数据的情况,就是threadlocalmap里边一直保留着没被清掉的值。

 

还有Java的锁,内置锁,显示锁,各种容器及锁优化 锁消除,锁粗化,锁偏向,轻量级锁

①乐观锁vs悲观锁
乐观锁就是一个线程自己在修改数据的时候认为其他线程一定不会修改,所以只有在更新的时候判断下是否有其他线程修改了数据,若修改了,就重新读取数据。
悲观锁就是一个线程自己在修改数据的时候认为其他线程一定会修改,所以在读取的时候就加锁,更新完成之后再释放锁。
乐观锁主要是根据CAS来实现的,线程修改数据的时候会首先比较主存中的值跟期待的值是否一样,一样就可以修改成功,否则就不成功;
另外CAS是CPU指令集的操作,只有一步原子操作,速度非常快。
②自旋锁vs自适应自旋锁
自旋锁就是一个线程正在持有同步资源的锁,这个时候另外一个线程进来了,要访问同步资源,让其循环等待,直到上个线程释放了锁。另外一个就是阻塞唤醒线程的操作
要涉及到操作系统切换CPU的工作状态,这种操作比较耗时,所以上面提到的自旋操作要比阻塞线程更合适。
自旋操作是线程一直在那循环等待,不释放CPU时间片,一直循环也比较耗费资源,如果可以设置一个循环次数,到了之后,就自动返回就比较好,自适应自旋锁就是会保留上次
自旋操作循环等待成功获取到锁经过的次数,之后的循环等待就拿这个作为依据,次数到了就不再循环等待。
③无锁-偏向锁-轻量级锁-重量级锁,参考synchronized锁升级过程
④公平锁vs非公平锁
公平锁会给同步资源的锁维护一个队列,然后一个线程进来要访问同步资源,会先进入队列排队,依次获取锁。
非公平锁就是一个线程进来,会首先尝试获取锁,获取到锁直接访问同步资源,否则就进入队列排队,这种有可能会导致位于队列中的线程一直处于等待中。
⑤可重入锁vs不可重入锁
可重入锁就是一个线程访问同步资源,会首先获取到对应的锁,然后执行同步代码的过程中,又遇到要获取之前的锁的操作,且可以获取成功,就是说同步资源的锁在一个线程中获取到了多次,同时释放的话也是释放了多次。

不可重入锁就是在一个线程中要获取之前的锁会直接失败。ReentrantLock和NonReentrantLock就是对应的java实现,内部是通过继承的AQS类中 的valitale变量state实现的,当一个线程获取多次时,通过state加一记录。

另外可重入锁的引入主要是为了解决死锁问题,一个同步代码块里边需要多次获取同一个锁,若不是可重入锁,就会造成死锁问题,ReentrantLock和synchronized都是可重入锁。
⑥独享锁vs共享锁
一个线程对同一对象施加了独享锁后,其他线程都不能对该对象施加任何类型的锁,且获取该类型锁的线程可以读数据和修改数据。若是共享锁,则其他线程可以该对象施加共享锁,都可以读取该对象,但不能修改该对象。在java的ReentrantReadWriteLock中,ReadWrite就是共享锁,WriteLock就是独享锁。

 

多线程的好处

现在的计算机都是多核,一个cpu多个核心,使用多线程能充分利用cpu核心资源,处理效率更高。

 

线程安全的定义、如何保证线程安全

①线程安全就是在多线程的情况下,程序始终能够得到预期的结果。

②产生线程不安全的原理主要有三个,原子性(就是一个在cpu执行的操作被中断),可见性(就是一个线程对数据修改后,不能立即被其他线程知道),有序性(就是java编译器为了执行效率会对代码编译好的指令重排序,从而使代码的执行顺序跟实际写的不一致)。

③保证线程安全:首先针对第一个原子性的问题,可以使用atomic修饰的类,例如AtomicInteger, AtomicLong, AtomicBoolean等,底层是通过cas+valitale实现的,也可以使用锁保证同一时刻只有一个线程访问。

针对第二个可见性的问题,可以通过valitale修饰变量的方式实现,也可以使用锁。第三个指令重排的问题可以使用synchronized保证同一时刻只有一个线程访问,也可以通过valitale关键字修饰。

④另外valitale底层其实是通过内存屏障实现的,内存屏障主要有两个作用:一个是可以保证对没有依赖关系的指令进行重排序,从而保证了有序性。另外一个就是把高速缓存中的数据同步至主内存,从而保证了可见性。另外valitale保证不了原子性问题,也就是加个valitale关键字修饰解决不了线程安全的问题。

 

多线程中的i++线程安全吗,为什么,多个线程访问i++,如何保证线程安全

i++对于cpu而言,其实是三个操作,读、+1、写,在读的过程中可能会被其他线程打断,从而保证不了线程安全。多个线程访问i++,可以通过原子类实现,也可以通过锁实现。

 

创建线程有几种方式,你喜欢哪一种,为什么,java中有几种方式启动一个线程

继承Thread类、实现Runnable接口、应用程序可以使用Executor框架来创建线程池、实现Callable接口 四种方式。

我更喜欢实现Runnable接口,因为这样不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承,而Java只能单继承,所以只能实现接口。

启动一个线程:thread.start()方法,runnable.run()方法,callable.call()方法,或者线程池的Executor.submit()方法和Executor.execute()方法。

 

Callable和Runnable接口区别

callable可以有一个返回值,且里边的call()方法会抛出异常,runnable接口没有返回值,run()方法也没有抛出异常

 

线程池的excute()和submit()方法的区别

excute()方法没有返回值,适用于异步处理逻辑且主线程不需要捕获处理结果的。submit()方法有Future返回值,在主线程中可通过future.get()方法获取线程处理结果,但是会阻塞主线程。除此之外,关于异常信息的获取,submit是主线程future.get()方法获取结果的时候,抛出来的,excute()方法体内会直接抛出来。

 

原子类的底层原理?Atomic在高并发场景下有什么问题,缺点

原子类底层主要通过valitale获取线程间可见的值,另外通过cas循环重试机制保证线程安全。

Atomic在高并发场景下,多个线程若一直循环的话,效率不高且自旋也会浪费资源。

JDK1.8中引入了LongAdder和DoubleAdder,因为AtomicLong是对同一个值进行循环重试,LongAdder就把这个值分开放到个数组里边,然后进来的线程就通过cas循环重试处理这个数组里边的所有元素,从而提高效率。另外值得注意的是LongAdder中记录值大小的除了一个数组cell[],还有个valitale修饰的base变量,比较类似于hashmap里边记录map长度的方法,值得大小就是cell[]里边元素的和再加上base的值。

 

CAS和ABA原理

CAS是CPU级别的锁,跟操作系统没关系,不涉及到内核态和用户态的装换,所以速度更快。

CAS 操作包含三个操作数,内存位置(V)、预期原值(A)和 新值(B) ,如果实际值跟预期原值一样就更新成功,否则失败。

ABA问题,就是对于一个主内存中的值,从A改成了B,又从B改为了A,通过CAS还是会修改成功,没法知道这个A已经是被更新过的问题。

 

JDK哪里用到了cas?CAS check的字段是哪里,set到哪里去,cas有什么问题?jdk中是如何改进的,如果很多个线程通过cas操作数据,如何提高效率

原子类里边用到了cas,例如AtomicLong、AtomicInteger等。涉及到三个字段,比较的值,主内存中的实际存储值,跟比较之后修改的值。Cas会有ABA问题,就是对于一个值,从A改成了B,又从B成为了A,通过CAS还是会修改成功,JDK为了解决这个问题,引入了版本号,就是第一个A版本为1,以此类加,例如jdk中的AtomicStampedReference。另外通过cas修改数据,循环等待,系统开销大,造成了资源浪费,jdk为了解决这个问题,引入了LongAdder。

 

如何线程安全的实现一个计数器

利用原子类AtomicInteger的incrementAndGet()方法。

 

请说出你说知道的线程同步的方法

利用synchronized锁对象或类、利用Lock接口的实现类、利用CAS + valitale。

 

线程数和内核数的关系

一个CPU可以由多个核心,然后inter有超线程技术,就是1个核心可以对应2个线程,就是我们常说的四核八线程。另外每个核心都有固定的主频,即使现在有强大的超频技术,主频的范围也提高不到哪里去。

 

线程中有哪些是私有的,哪些是共享的

线程私有:程序计数器(记录线程运行到了哪个位置,抢到了CPU时间片之后,从这继续执行);虚拟机栈和本地方法栈(这两个都是保存方法运行时的一些局部变量表、操作数栈、常量池引用等,方法执行的过程就是在栈帧中出栈和入栈的过程。虚拟栈对应的是java方法,本地方法栈对应的是native方法)。

线程共享:堆区(对象)、元空间区(1.8 开始 JVM 移除了永久代,使用本地内存来存储元数据,类的基础信息)、 运行时常量池(需要注意的是1.7之前字符串常量池在运行时常量池里边,1.7及之后,字符串常量池放到了堆里边。而运行时常量池仍然在方法区(元空间区)中),直接内存(NIO,直接内存就是 JVM 内存之外有一块内存区域,我们通过堆上的一个对象可以操作它).

 

同步方法和同步代码块的区别是什么

同步方法锁住的是整个方法,如果是静态方法,锁住的就是整个类;另外同步代码块可以选择同步的代码范围,粒度更小,效率更高。

 

为什么object方法的notify和wait方法必须在synchronized里使用

在生产者消费者场景里边,假设没有synchronized,且产品被消费完,需要生产时。假如生产者使产品加一之后,提前调用了notify()方法,这个时候消费者才走到了wait()方法这里,会导致消费者一直卡在这里,不会被唤醒,使程序一直阻塞在这里。

原文地址:https://www.cnblogs.com/20158424-hxlz/p/14085888.html