Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码,会发现被Synchronized修饰过的程序块,在编译前后生成了monitorenter和monitorexit两个字节码指令。
虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1;当计数器为0时,锁就被释放了。
如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止了。
Java中Synchronized通过在对象头设置标记,达到了获取锁和释放锁的目的。
“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。
注意,当一个对象被锁住时,对象里面所有用Synchronized 修饰的 方法都将产生堵塞,而对象里非 Synchronized 修饰的方法可正常被 调用,不受锁影响。
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。
对 Synchronized 来说,可重入性是显而易见的,刚才提到,在执行 monitorenter 指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计 数器 +1,其实本质上就通过这种方式实现了可重入性。
在 Java 6 之前,Monitor 的实现完全依赖底层操作系统的互斥锁来 实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
由于 Java 层面的线程与操作系统的原生线程有映射关系,如果要将一 个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换 到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代 JDK 中做了大量的优化。
一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等 待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程 执行阻塞操作,避免了用户态到内核态的切换。
现代 JDK 中还提供了三种不同的 Monitor 实现,也就是三种不同的 锁:
这三种锁使得 JDK 得以优化 Synchronized 的运行,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、 降级。
当没有竞争出现时,默认会使用偏向锁:
JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因 为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定, 使用偏斜锁可以降低无竞争开销。
如果有另一线程视图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切换到轻量级实现。
轻量级锁依赖CASMark Word来视图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等 待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁, 这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的: 不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转 换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。 随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。 先进行操作,如果没有其他线程征用数据,那操作就成功了;
如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种 乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。 乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉 及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相 等时才将内存值修改为新值。
这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一 样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操 作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此 块内存。
CAS 具有原子性,它的原子性由 CPU 硬件指令实现保证,即使用 JNI 调用 Native 方法调用由 C++ 编写的硬件级别指令,JDK 中提 供了 Unsafe 类执行这些操作。
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
Synchronized 通过在对象头中设置标记实现了这一目的,是一种 JVM 原生的锁实现方式,而 ReentrantLock 以及所有的基于 Lock 接口的 实现类,都是通过用一个 volitile 修饰的 int 型变量,并保证每个线 程都能拥有对该 int 的可见性和原子修改,其本质是基于所谓的 AQS 框架。
AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器 的框架,各种 Lock 包中的锁(常用的有 ReentrantLock、 ReadWriteLock) , 以 及 其 他 如 Semaphore、 CountDownLatch, 甚 至是早期的 FutureTask 等,都是基于 AQS 来构建。
AQS 在内部定义了一个 volatile int state 变量,表示同步状态:当线 程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源 的锁,可以获得锁并将 state=1;如果 state=1,则说明有线程目前正在 使用共享变量,其他线程必须加入同步队列进行等待。
AQS 通过 Node 内部类构成的一个双向链表结构的同步队列,来完成线 程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
Node 类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫 waitStatus(有五种不同 取值,分别表示是否被阻塞,是否等待唤醒, 是否已经被取消等),每个 Node 结点关联其 prev 结点和 next 结 点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个 FIFO 的过 程。
Node 类有两个常量,SHARED 和 EXCLUSIVE,分别代表共享模式和独 占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore 就是基于 AQS 的共享模式实现的),独占模式是同一个时 间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待 ( 如 ReentranLock) 。
AQS 通过内部类 ConditionObject 构建等待队列(可有多个),当 Condition 调用 wait() 方法后,线程将会加入等待队列中,而当Condition 调用 signal() 方法后,线程将从等待队列转移动同步队列中
进行锁竞争。
AQS 和 Condition 各自维护了不同的队列,在使用 Lock 和 Condition 的时候,其实就是两个队列的互相移动。
ReentrantLock 是 Lock 的实现类,是一个互斥的同步锁。 从功能角度,ReentrantLock 比 Synchronized 的同步操作更精细 (因为可以像普通对象一样使用),甚至实现 Synchronized 没有的 高级功能,如:
ReentrantLock 内部自定义了同步器 Sync(Sync 既实现了 AQS, 又实现了 AOS,而 AOS 提供了一种互斥锁持有的方式),其实就是 加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获 取锁的时候,看下当前维护的那个线程 ID 和当前请求的线程 ID 是否 一样,一样就可重入了。
通常所说的并发包(JUC)也就是 java.util.concurrent 及其子包,集 中了 Java 并发的各种基础工具类,具体主要包括几个方面:
提 供 了 CountDownLatch、 CyclicBarrier、 Semaphore 等 , 比 Synchronized 更加高级,可以实现更加丰富多线程操作的同步结构。
提 供 了 ConcurrentHashMap、 有 序 的 ConcunrrentSkipListMap, 或
者通过类似快照机制实现线程安全的动态数组 CopyOnWriteArrayList 等,各种线程安全的容器。
提 供 了 ArrayBlockingQueue、 SynchorousQueue 或 针 对 特 定 场 景 的 PriorityBlockingQueue 等,各种并发队列实现。
强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运 行等。
虽然 ReentrantLock 和 Synchronized 简单实用,但是行为上有一 定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量 竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒 度,Java 提供了读写锁。
读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写 锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这 样就可以自动保证不会读取到有争议的数据。
读写锁看起来比 Synchronized 的粒度似乎细一些,但在实际应用 中,其表现也并不尽如人意,主要还是因为相对比较大的开销。 所以,JDK 在后期引入了 StampedLock,在提供类似读写锁的同时, 还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写 操作冲突,其逻辑是先试着修改,然后通过 validate 方法确认是否进 入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获 取读锁。
JUC 中的同步器三个主要的成员:CountDownLatch、CyclicBarrier 和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。 CountDownLatch 叫倒计数,允许一个或多个线程等待某些操作完 成。看几个场景:
CountDownLatch 目的是让一个线程等待其他 N 个线程达到某个条 件后,自己再去做某个事(通过 CyclicBarrier 的第二个构造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新线 程里做事可以达到同样的效果)。而 CyclicBarrier 的目的是让 N 多 线程互相等待直到所有的都达到某个状态,然后这 N 个线程再继续执 行各自后续(通过 CountDownLatch 在某些场合也能完成类似的效
果)。
Java 中的线程池的创建其实非常灵活,我们可以通过配置不同的参 数,创建出行为不同的线程池,这几个参数包括:
显然不是的。线程池默认初始化后不启动 Worker,等待有请求时才启 动。
每当我们调用 execute() 方法添加一个任务时,线程池会做如下判 断:
当一个线程完成任务时,它会从队列中取下一个任务来执行。 当一个 线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大 小。
1. SingleThreadExecutor:
这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所 有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来 替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. FixedTheadPool:
FixedThreadPool 是固定大小的线程池,只有核心线程。每次提交一个 任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小 一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那 么线程池会补充一个新线程。
FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于 服务器。
3. CachedThreadPool:
CachedThreadPool 是无界线程池,如果线程池的大小超过了处理任务 所需要的线程,那么就会回收部分空闲(60 秒不执行任务)线程,当 任务数增加时,此线程池又可以智能的添加新线程来处理任务。 线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程 大小。SynchronousQueue 是一个是缓冲区为 1 的阻塞队列。 缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面 向连接的 daemon 型 SERVER 中用得不多。但对于生存期短的异步 任务,它是 Executor 的首选。
4. ScheduledThreadPool:
ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程 池支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线 程池。如果闲置,非核心线程池会在 DEFAULT_KEEPALIVEMILLIS 时 间内回收。
1. execute():ExecutorService.execute:
接 收 一 个 Runable 实例,它用来执行一个任务
2. dubmit():ExecutorService.dubmit():
返 回 的 是Future对象。可以用 isDone() 来查询 Future 是否已经完成,当任务完成时, 它具有一个结果,可以调用 get() 来获取结果。也可以不用 isDone() 进行检查就直接调用 get(),在这种情况下,get() 将阻塞,直至结果 准备就绪。
Java 的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将 变量存储到内存和从内存中取出这样的底层细节。 此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包 括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不 存在竞争问题。
Java 中各个线程是怎么彼此看到对方的变量的呢?Java 中定义了主内 存与工作内存的概念: 所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被 该线程使用到的变量的主内存副本拷贝。 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能 直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存 的变量,线程间变量值的传递需要通过主内存。
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。当一个 变量被定义成 volatile 之后,具备两种特性:
Java 的内存模型定义了 8 种内存间操作:
lock 和 unlock
read 和 write
load 和 store
use 和 assgin
volatile 的实现基于这 8 种内存间操作,保证了一个线程对某个 volatile 变量的修改,一定会被另一个线程看见,即保证了可见性。
显然不是的。基于 volatile 变量的运算在并发下不一定是安全的。 volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的 工作内存中 volatile 变量,每次使用前都要刷新到主内存)。
但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并 发下一样是不安全的。
Synchronized 既能保证可见性,又能保证原子性,而 volatile 只能 保证可见性,无法保证原子性。
ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务 在共享资源上产生冲突。但是 ThreadLocal 与 Synchronized 有本质 的区别。
Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在 某一时该只能被一个线程访问,是一种 “以时间换空间” 的方式。
而 ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在 某一时间访问到的并不是同一个对象,根除了对变量的共享,是一种 “以空间换时间” 的方式。
ThreadLocal 这是 Java 提供的一种保存线程私有信息的机制,因为 其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同 业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。
ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限 制在同一个线程之内,其实现原理是,在 ThreadLocal 类中有一个 Map,用于存储每一个线程的变量的副本。
使用ThreadLocal要注意remove!
ThreadLocal 的实现是基于一个所谓的 ThreadLocalMap,在 ThreadLocalMap 中,它的 key 是一个弱引用。
通常弱引用都会和引用队列配合清理机制使用,但是 ThreadLocal 是 个例外,它并没有这么做。
这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结 束,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所 以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配 合,因为 worker 线程往往是不会退出的。