Synchronized是jvm实现的一种互斥同步访问方式,底层是基于对象的监视器monitor实现的。 被synchronize修饰的代码在反编译后发现,在代码的开始和结束是通过monitorenter和monitorexit实现的。 当虚拟机执行到monitorenter时,线程会尝试获取对象的monitor锁,基于monitor锁,又产生了一个锁计数器的概念。 当执行到monitorenter时,若对象未被锁定,或当前线程已经持有该对象锁,则锁计数器+1。 当执行monitorexit时,该对象的锁计数器-1,当锁计数器=0时,该线程释放该对象锁权限,其他阻塞线程可以获取该对象锁权限。
可重入性: 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行 另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入 (reentrant或re-entrant)的。 简言之,一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞; 而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。 synchronized如何实现可重入性: synchronized关键字经过编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。每个锁对象内部维护一个计 数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。 根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的 锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计 数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0 时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁 被另外一个线程释放为止。
自旋锁: 在java6之前,monitor的实现都要依赖底层操作系统的互斥锁。 由于java线程和底层操作系统的线程是映射关系,所以线程的挂起和唤醒都要 与操作系统交互,从用户态转到内核态执行,开销太大。 一种优化方式是使用自旋锁:由于大部分的共享数据锁定时间很短,为了短暂时 间去挂起和唤醒线程非常不划算,自旋锁在jdk1.4引入,1.6默认开启,自旋等 待虽然避免了线程切换的开销,但自旋的线程要占用处理器时间的,所以若锁被 占用的时间很短,自旋等待的效果就会非常好,反之锁被占用的时间很长,那么 自旋的线程只会白白消耗 CPU 资源。 因此自旋等待的时间必须要有一定的限度,超过限定的次数仍然没有成功获得 锁,就应当挂起(阻塞)线程了。自旋次数的默认值是 10 次。
自适应自旋锁:
在 JDK 1.6 中引入了自适应自旋锁。
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
锁消除:
在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。从而取消对这部分代码的同步。
锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
锁粗化:
当 JIT 编译器发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。
在编写代码的时候,总是推荐将同步块的作用范围(锁粒度)限制得尽量小(只在共享数据的实际作用域中才进行同步),这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程可以尽快的拿到锁。
锁粒度:不要锁住一些无关的代码。
锁粗化:可以一次执行完的不要多次加锁执行
synchronized 使用的是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原由。
锁消除: 在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。从而取消对这部分代码的同步。 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。 锁粗化: 当 JIT 编译器发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。 在编写代码的时候,总是推荐将同步块的作用范围(锁粒度)限制得尽量小(只在共享数据的实际作用域中才进行同步),这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程可以尽快的拿到锁。 锁粒度:不要锁住一些无关的代码。 锁粗化目的:可以一次执行完的不要多次加锁执行
Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
乐观锁原理:先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。
这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。
乐观锁的核心算法是CAS(CompareandSwap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,
首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。
java.util.concurrent中的AtomicInteger底层也是根据CAS算法实现的。
那么CAS底层是根据什么实现的呢?
CAS->调用Native方法通过C++ -> 通过汇编语言local cmpex命令实现。简而言之,CAS最跟本还是通过cpu的互斥锁实现
不一定。乐观锁是相对于悲观锁而言的,是预测共享数据不会发生改变,适合于 读多写少的场景,如果是写多的场景可能会在验证预测值和内存实际值时由于数 据被频繁更改而造成该线程不停处于“获取内存值->改变值->判断值->获取内存值”的循环之中。
可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1, 所以要等到锁的计数器下降为0时才能释放锁。 锁的实现: Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。 性能的区别: 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下, 官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。 功能区别: 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。 锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized ReenTrantLock独有的能力: 1.ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 2.ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。 3.ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。 ReenTrantLock实现的原理: 简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。 想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。 什么情况下使用ReenTrantLock: 如果你需要实现ReenTrantLock的三个独有功能时。
同上
ReentrantLock在内部使用了内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。 Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作带来效率问题。
ReentrantLock内部自定义了同步器Sync(Sync既实现了 AQS,又实现了 AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下 当前维 护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。
ReentrantLock源码
// Sync继承于AQS abstract static class Sync extends AbstractQueuedSynchronizer { ... } // ReentrantLock默认是非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 可以通过向构造方法中传true来实现公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
线程抢锁过程(公平锁):
protected final boolean tryAcquire(int acquires) { // 当前想要获取锁的线程 final Thread current = Thread.currentThread(); // 当前锁的状态 int c = getState(); // state == 0 此时此刻没有线程持有锁 if (c == 0) { // 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到, // 看看有没有别人在队列中等了半天了 if (!hasQueuedPredecessors() && // 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了, // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_= // 因为刚刚还没人的,我判断过了 compareAndSetState(0, acquires)) { // 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁 setExclusiveOwnerThread(current); return true; } } // 会进入这个else if分支,说明是重入了,需要操作:state=state+1 // 这里不存在并发问题 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁 return false; }
通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面: 1、提供了 CountDownLatch、CyclicBarrier、Semaphore等,比 Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。 2、提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通 过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线 程安全的容器。 3、提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue等,各种并发队列实现。 4、强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。
虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限 性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写 操作,而是以并发读取为主,为了进一步优化并发操作的粒度, Java提 供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图 锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作 结束,这样就可以自动保证不会读取到有争议的数据。 ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构, 当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势。 读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其 表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进 入,就成功避免了开销;如果进入,则尝试获取读锁。
JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。 CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。 CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部 同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使 用。CyclicBarrier的典型应用场景是用来等待并发线程结束。 CyclicBarrier 的主要方法是await(),await()每被调用一次,计数便会减少1,并阻塞住 当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。 在这之后,如果再次调用await。,计数就又会变成N-1,新一轮重新开 始,这便是Cyclic的含义所在。CyclicBarrier.await。带有返回值,用来表 示当前线程是第几个到达这个Barrier的线程。 Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来 达到限制通用资源访问的目的,其原理是通过acquire。获取一个许可,如 果没有就等待,而release。释放一个许可。 如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比 如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类 似功能, 但其实不存在真正意义的持有者,除非我们进行扩展包装。
CountDownLatch实例代码:
package com.atguigu.thread; import java.util.concurrent.CountDownLatch; /** * * @Description: * *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。 * * CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。 * 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞), * 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。 * * 解释:6个同学陆续离开教室后值班同学才可以关门。 * * main主线程必须要等前面6个线程完成全部工作后,自己才能开干 */ public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致 { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室"); countDownLatch.countDown(); }, String.valueOf(i)).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长"); } }View Code
CyslicBarrier实例代码:
package com.atguigu.thread; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; /** * * * CyclicBarrier * 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是, * 让一组线程到达一个屏障(也可以叫同步点)时被阻塞, * 直到最后一个线程到达屏障时,屏障才会开门,所有 * 被屏障拦截的线程才会继续干活。 * 线程进入屏障通过CyclicBarrier的await()方法。 * * 集齐7颗龙珠就可以召唤神龙 */ public class CyclicBarrierDemo { private static final int NUMBER = 7; public static void main(String[] args) { //CyclicBarrier(int parties, Runnable barrierAction) CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ; for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 "); cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } }, String.valueOf(i)).start(); } } }View Code
Semphore实例代码:
package com.atguigu.thread; import java.util.Random; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * * @Description: TODO(这里用一句话描述这个类的作用) * * 在信号量上我们定义两种操作: * acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1), * 要么一直等下去,直到有线程释放信号量,或超时。 * release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。 * * 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 */ public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3);//模拟3个停车位 for (int i = 1; i <=6; i++) //模拟6部汽车 { new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"\t 抢到了车位"); TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println(Thread.currentThread().getName()+"\t------- 离开"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } }, String.valueOf(i)).start(); } } }View Code
它们的行为有一定相似度,区别主要在于: 1、CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这种限制,可以重用。 2、CountDownLatch的基本操作组合是countDown/await,调用await的线 程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可。 CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。 CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后, 自己再去做某个事(通过CyclicBarrier的第二个构造方法public CyclicBarrier(int parties, Runnable barrierAction), 在新线程里做事可以达到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有 的都达到某个状态,然后这N个线程再继续执行各自后续(通过CountDownLatch在某些场合也能完成类似的效果)。
1、一个任务提交到线程池,首先判断核心线程数是否满,如果核心线程数未满,则创建一个线程执行任务,否则进入下一步骤 2、判断阻塞队列是否满,如果未满则将任务放入阻塞队列,否则执行下一步骤 3、判断线程池中线程数是否达到最大线程数,如果未达到则创建新线程执行任务,否则启动拒绝策略。
int corePoolSize:核心线程数 int maximumPoolSize:最大线程数 long keepAliveTime:线程存活时间 TimeUnit unit:线程存活时间单位 BlockingQueue<Runnable> workQueue:阻塞队列 ThreadFactory threadFactory:线程工厂 RejectedExecutionHandler handler:任务拒绝策略
不是。线程池默认初始化后不启动Worker,等待有请求时才启动。
1、SingleThreadExecutor 线程池 这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有 任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代 它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 2、FixedThreadPool 线程池 FixedThreadPool是固定大小的线程池,只有核心线程。每次提交一个任 务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦 达到最大值就会保持不变,如果某个线程因为执行异常而结束, 那么线程 池会补充一个新线程。 FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器。 3、CachedThreadPool线程池 4、ScheduledThreadPool 线程池 ScheduledThreadPool :核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程池。如果闲置非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收。
1、execute: ExecutorService.execute 方法接收一个线程实例,它用来执行一个任务:ExecutorService.execut(Runnable runable) 2、submit: ExecutorService.submit。方法返回的是 Future 对象。可以用 isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果, 可以调用get。来获取结果。 也可以不用isDone。进行检查就直接调用 get(),在这种情况下,get()将阻塞,直至结果准备就绪。
Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变 量存储到内存和从内存中取出这样的底层细节。 Java中的数据存储在主内存中,各个线程如果需要读写数据,需要将主内存中的数据拷贝到各自线程的工作区,然后将工作区的数据写会主内存,主内存通知其他使用该数据的线程重新拉取最新数据。
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。当一个变量被定义成 volatile 之后,具备两种特性: 1、保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。 2、禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。 Java 的内存模型定义了 8 种内存间操作: lock 和 unlock 把一个变量标识为一条线程独占的状态。 把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定。 read 和 write 把一个变量值从主内存传输到线程的工作内存,以便 load。 把 store 操作从工作内存得到的变量的值,放入主内存的变量中。 load 和 store 把 read 操作从主内存得到的变量值放入工作内存的变量副本中。 把工作内存的变量值传送到主内存,以便 write。 use 和 assgin 把工作内存变量值传递给执行引擎。 将执行引擎值传递给工作内存变量值。 volatile 的实现基于这8种内存间操作,保证了一个线程对某个 volatile 变量的修改,一定会被另一个线程看见,即保证了可见性。
并不是,volatile只能保证共享数据的可见性,并不能保证对数据操作的原子性。(虽然所有线程都能获取到某线程操作后的最新结果,但是不能保证众多线程操作是否覆盖)
1、Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。 2、Volatile修饰实例变量和类变量,而Synchronized修饰方法以及代码块 3、volatile用于禁止指令重排序 4、volatile可以看做轻量版synchronize,如果只是对共享变量进行赋值操作,使用volatile就可以。
ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制 在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
假设没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存,如果不remove掉,很可能会出现内存泄漏的问题。