# 线程安全

# 什么是线程安全?

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

# 线程安全也是有几个级别的?

  • 不可变
    不可变对象不需要任何同步手段就可以直接在多线程环境下使用

  • 绝对线程安全
    不管运行时环境如何,调用者都不需要额外的同步措施 要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,
    不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet。

  • 相对线程安全
    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断。

  • 线程非安全

# 实现线程安全的方法?

  1. 互斥同步

  2. 非阻塞同步

  3. 无同步方案

(1) 可重入代码

也叫纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身)而在控制权返回后。
原来的程序不会出现任何错误,所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

(2)线程本地存储

共享数据的代码是否能保证在同一个线程中执行? 如果能保障,我们就可以把共享数据的可见范围限制在同一个线程之内。

# 不可变对象对多线程有什么帮助?

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

# 什么是字符串常量池?

  • 设计思想 JVM为字符串开辟一个字符串常量池,类似于缓存区,创建字符串常量时,首先坚持字符串常量池是否存在该字符串, 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。

  • 实现的基础 该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享,运行时实例创建的全局字符串常量池中有一个表。
    总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,
    所以,在常量池中的这些字符串不会被垃圾收集器回收。

字符串常量池则存在于方法区。

# 什么是逃逸分析?

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用。
例如作为调用参数传递到其他方法中,成为方法逃逸,
甚至还可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。

  • 栈上分配 如果确定一个对象不会逃逸出方法外,那让这个对象在栈上分配内存将会是一个不错的注意,对象所占用的内存空间就可以随栈帧出栈而销毁。 如果能使用栈上分配,那大量的对象就随着方法的结束而销毁了,垃圾收集系统的压力将会小很多。

  • 同步消除 如果确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。

  • 标量替换 标量就是指一个数据无法在分解成更小的数据表示了,int、long等及refrence类型等都不能在进一步分解,它们称为标量。 如果一个数据可以继续分解,就称为聚合量,Java中的对象就是最典型的聚合量。

如果一个对象不会被外部访问,并且这个对象可以被拆散的化,
那程序正整执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

# 锁优化的思路?

  • 自旋锁

如果物理机器上有一个以上的处理器,能让两个或以上的线程同时并行执行。
我们就可以让后面请求锁的那个线程稍等一下,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。
为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

  • 自适应自旋转

是由前一次在同一个锁对象上,自旋等待刚刚成功获得过锁。
并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自过程,以避免浪费处理器资源。

  • 锁消除

虚拟机即时编辑器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
如果在一段代码中。推上的所有数据都不会逃逸出去从而被其他线程访问到,
那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

  • 锁粗化

如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

  • 轻量级锁

  • 偏向锁

目的是消除无竞争情况下的同步原语,进一步提高程序的运行性能 如果轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把这个同步都消除掉,CAS操作都不做了。
如果在接下俩的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要在进行同步。

# 什么是自旋?

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,
因为线程阻塞涉及到用户态和内核态切换的问题。

既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。
如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

# 简述Java中具有哪几种粒度的锁?

Java中可以对类、对象、方法或是代码块上锁。

# ReadWriteLock是什么?

是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现。
实现了读写的分离, 读锁是共享的,写锁是独占的。
读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

# 什么是乐观锁和悲观锁?

  • 乐观锁将 比较-替换 这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
  • 悲观锁 对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,
    因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

# 什么是CAS?

Compare and Swap,即比较-替换,
当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值。

# Java中进行线程同步的方法?

  • volatile
  • synchronized
  • ReentrantLock 尝试获取锁的线程可以被中断并可以设置超时参数

# volatile关键字的作用?

使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
使用volatile则会对禁止语义重排序。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性。

# CyclicBarrier 和 CountDownLatch 的区别?

都在java.util.concurrent下,都可以用来表示代码运行到某个点上

CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行。
CountDownLatch是某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

# synchronized 和 ReentrantLock 的区别?

synchronized是关键字,ReentrantLock是类。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量。
体现在:
ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁。
ReentrantLock可以获取各种锁的信息。
ReentrantLock可以灵活地实现多路通知。

ReentrantLock底层调用的是Unsafe的park方法加锁。

# FutureTask是什么?

FutureTask表示一个异步运算的任务。
FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

# 什么是AQS?

AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心。
AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,
开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

# Semaphore有什么作用?

限制某段代码块的并发数。
Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问。
可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

# 同步方法和同步块,哪个是更好的选择?

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
一般情况下 同步的范围越小越好。 但是在Java虚拟机中还是存在着一种叫做 锁粗化 的优化方法,这种方法就是把同步范围变大。

比方说 StringBuffer
它是一个线程安全的类,自然最常用的append()方法是一个同步方法,
我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,
因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换。

因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,
将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。