网站黏度,平面设计素材图,网站需求分析报告范文,永久网站Java并发基础知识补全
启动
启动线程的方式只有#xff1a;
1、X extends Thread;#xff0c;然后X.start
2、X implements Runnable#xff1b;然后交给Thread运行
线程的状态
Java中线程的状态分为6种#xff1a;
1. 初始(NEW)#xff1a;新创建了一个线程对象
1、X extends Thread;然后X.start
2、X implements Runnable然后交给Thread运行
线程的状态
Java中线程的状态分为6种
1. 初始(NEW)新创建了一个线程对象但还没有调用start()方法。
2. 运行(RUNNABLE)Java线程中将就绪ready和运行中running两种状态笼统的称为“运行”。
线程对象创建后其他线程(比如main线程调用了该对象的start()方法。该状态的线程位于可运行线程池中等待被线程调度选中获取CPU的使用权此时处于就绪状态ready。就绪状态的线程在获得CPU时间片后变为运行中状态running。
3. 阻塞(BLOCKED)表示线程阻塞于锁。
4. 等待(WAITING)进入该状态的线程需要等待其他线程做出一些特定动作通知或中断。1
5. 超时等待(TIMED_WAITING)该状态不同于WAITING它可以在指定的时间后自行返回。
6. 终止(TERMINATED)表示该线程已经执行完毕。
状态之间的变迁如下图所示 死锁
概念
是指两个或两个以上的进程在执行过程中由于竞争资源或者由于彼此通信而造成的一种阻塞的现象若无外力作用它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
举个例子A和B去按摩洗脚都想在洗脚的时候同时顺便做个头部按摩13技师擅长足底按摩14擅长头部按摩。
这个时候A先抢到14B先抢到13两个人都想同时洗脚和头部按摩于是就互不相让扬言我死也不让你这样的话A抢到14想要13B抢到13想要14在这个想同时洗脚和头部按摩的事情上A和B就产生了死锁。怎么解决这个问题呢
第一种假如这个时候来了个15刚好也是擅长头部按摩的A又没有两个脑袋自然就归了B于是B就美滋滋的洗脚和做头部按摩剩下A在旁边气鼓鼓的这个时候死锁这种情况就被打破了不存在了。
第二种C出场了用武力强迫A和B必须先做洗脚再头部按摩这种情况下A和B谁先抢到13谁就可以进行下去另外一个没抢到的就等着这种情况下也不会产生死锁。
所以总结一下
死锁是必然发生在多操作者M2个情况下争夺多个资源N2个且NM才会发生这种情况。很明显单线程自然不会有死锁只有B一个去不要2个打十个都没问题单资源呢只有13A和B也只会产生激烈竞争打得不可开交谁抢到就是谁的但不会产生死锁。同时死锁还有几个要求1、争夺资源的顺序不对如果争夺资源的顺序是一样的也不会产生死锁
2、争夺者拿到资源不放手。
学术化的定义
死锁的发生必须具备以下四个必要条件。
1互斥条件指进程对所分配到的资源进行排它性使用即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源则请求者只能等待直至占有资源的进程用毕释放。
2请求和保持条件指进程已经保持至少一个资源但又提出了新的资源请求而该资源已被其它进程占有此时请求进程阻塞但又对自己已获得的其它资源保持不放。
3不剥夺条件指进程已获得的资源在未使用完之前不能被剥夺只能在使用完时由自己释放。
4环路等待条件指在发生死锁时必然存在一个进程——资源的环形链即进程集合{P0P1P2···Pn}中的P0正在等待一个P1占用的资源P1正在等待P2占用的资源……Pn正在等待已被P0占用的资源。
理解了死锁的原因尤其是产生死锁的四个必要条件就可以最大可能地避免、预防和解除死锁。
只要打破四个必要条件之一就能有效预防死锁的发生。
打破互斥条件改造独占性资源为虚拟资源大部分资源已无法改造。
打破不可抢占条件当一进程占有一独占性资源后又申请一独占性资源而无法满足则退出原占有的资源。
打破占有且申请条件采用资源预先分配策略即进程运行前申请全部资源满足则运行不然就等待这样就不会占有且申请。
打破循环等待条件实现资源有序分配策略对所有设备实现分类编号所有进程只能采用按序号递增的形式申请资源。
避免死锁常见的算法有有序资源分配法、银行家算法。 危害
1、线程不工作了但是整个程序还是活着的2、没有任何的异常信息可以供我们检查。3、一旦程序发生了发生了死锁是没有任何的办法恢复的只能重启程序对正式已发布程序来说这是个很严重的问题。
解决
关键是保证拿锁的顺序一致
两种解决方式 内部通过顺序比较确定拿锁的顺序
2、采用尝试拿锁的机制。
其他线程安全问题
活锁
两个线程在尝试拿锁的机制中发生多个线程之间互相谦让不断发生同一个线程总是拿到同一把锁在尝试拿另一把锁时因为拿不到而将本来已经持有的锁释放的过程。
解决办法每个线程休眠随机数错开拿锁的时间。
线程饥饿
低优先级的线程总是拿不到执行时间
ThreadLocal辨析
与Synchonized的比较
ThreadLocal和Synchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本使得每个线程在某一时间訪问到的并非同一个对象这样就隔离了多个线程对数据的数据共享。
ThreadLocal的使用
ThreadLocal类接口很简单只有4个方法我们先来了解一下
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除目的是为了减少内存的占用该方法是JDK 5.0新增的方法。需要指出的是当线程结束后对应该线程的局部变量将自动被垃圾回收所以显式调用该方法清除线程的局部变量并不是必须的操作但它可以加快内存回收的速度。
• protected Object initialValue()
返回该线程局部变量的初始值该方法是一个protected的方法显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法在线程第1次调用get()或set(Object)时才执行并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public final static ThreadLocalString RESOURCE new ThreadLocalString();RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量对它进行写入、读取操作都是线程安全的。
实现解析 上面先取到当前线程然后调用getMap方法获取对应的ThreadLocalMapThreadLocalMap是ThreadLocal的静态内部类然后Thread类中有一个这样类型成员所以getMap是直接返回Thread的成员。
看下ThreadLocal的内部类ThreadLocalMap源码 可以看到有个Entry内部静态类它继承了WeakReference总之它记录了两个信息一个是ThreadLocal?类型一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值set方法就是更新或赋值相应的ThreadLocal对应的值。 回顾我们的get方法其实就是拿到每个线程独有的ThreadLocalMap
然后再用ThreadLocal的当前实例拿到Map中的相应的Entry然后就可以拿到相应的值返回出去。当然如果Map为空还会先进行map的创建初始化等工作。 CAS基本原理
什么是原子操作如何实现原子操作
假定有两个操作A和B(A和B可能都很复杂)如果从执行A的线程来看当另一个线程执行B时要么将B全部执行完要么完全不执行B那么A和B对彼此来说是原子的。
实现原子操作可以使用锁锁机制满足基本的需求是没有问题的了但是有的时候我们的需求并非这么简单我们需要更有效更加灵活的机制synchronized关键字是基于阻塞的锁机制也就是说当一个线程拥有锁的时候访问同一资源的其它线程需要等待直到该线程释放锁
这里会有些问题首先如果被阻塞的线程优先级很高很重要怎么办其次如果获得锁的线程一直不释放锁怎么办这种情况是非常糟糕的。还有一种情况如果有大量的线程来竞争资源那CPU将会花费大量的时间和资源来处理这些竞争同时还有可能出现一些例如死锁之类的情况最后其实锁机制是一种比较粗糙粒度比较大的机制相对于像计数器这样的需求有点儿过于笨重。
实现原子操作还可以使用当前的处理器基本都支持CAS()的指令只不过每个厂家所实现的算法并不一样每一个CAS操作过程都包含三个运算符一个内存地址V一个期望的值A和一个新值B操作的时候如果这个地址上存放的值等于这个期望的值A则将地址上的值赋为新值B否则不做任何操作。
CAS的基本思路就是如果这个地址上的值和期望的值相等则给其赋予新值否则不做任何事儿但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作直到成功为止。 CAS实现原子操作的三大问题
ABA问题。
因为CAS需要在操作值的时候检查值有没有发生变化如果没有发生变化则更新但是如果一个值原来是A变成了B又变成了A那么使用CAS进行检查时会发现它的值没有发生变化但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号每次变量更新的时候把版本号加1那么A→B→A就会变成1A→2B→3A。举个通俗点的例子你倒了一杯水放桌子上干了点别的事然后同事把你水喝了又给你重新倒了一杯水你回来看水还在拿起来就喝如果你不管水中间被人喝过只关心水还在这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子不但关心水在不在还要在你离开的时候水被人动过没有因为你是程序员所以就想起了放了张纸在旁边写上初始值0别人喝水前麻烦先做个累加才能喝水。
循环时间长开销大。
自旋CAS如果长时间不成功会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时我们可以使用循环CAS的方式来保证原子操作但是对多个共享变量操作时循环CAS就无法保证操作的原子性这个时候就可以用锁。
还有一个取巧的办法就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i2ja合并一下ij2a然后用CAS来操作ij。从Java 1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性就可以把多个变量放在一个对象里来进行CAS操作。
Jdk中相关原子操作类的使用
AtomicInteger
•int addAndGetint delta以原子方式将输入的数值与实例中的值AtomicInteger里的value相加并返回结果。
•boolean compareAndSetint expectint update如果输入的数值等于预期值则以原子方式将该值设置为输入的值。
•int getAndIncrement()以原子方式将当前值加1注意这里返回的是自增前的值。
•int getAndSetint newValue以原子方式设置为newValue的值并返回旧值。
AtomicIntegerArray
主要是提供原子的方式更新数组里的整型其常用方法如下。
•int addAndGetint iint delta以原子方式将输入值与数组中索引i的元素相加。
•boolean compareAndSetint iint expectint update如果当前值等于预期值则以原子方式将数组位置i的元素设置成update值。
需要注意的是数组value通过构造方法传递进去然后AtomicIntegerArray会将当前数组复制一份所以当AtomicIntegerArray对内部的数组元素进行修改时不会影响传入的数组。
更新引用类型
原子更新基本类型的AtomicInteger只能更新一个变量如果要原子更新多个变量就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。
AtomicReference
原子更新引用类型。
AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多 AtomicStampedReference是使用pair的int stamp作为计数器使用AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子AtomicStampedReference可能关心的是动过几次AtomicMarkableReference关心的是有没有被人动过方法都比较简单。
AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReferenceV initialRefbooleaninitialMark。 阻塞队列和线程池原理
阻塞队列
队列 队列是一种特殊的线性表特殊之处在于它只允许在表的前端front进行删除操作而在表的后端rear进行插入操作和栈一样队列是一种操作受限制的线性表。进行插入操作的端称为队尾进行删除操作的端称为队头。
在队列中插入一个队列元素称为入队从队列中删除一个队列元素称为出队。因为队列只允许在一端插入在另一端删除所以只有最早进入队列的元素才能最先从队列中删除故队列又称为先进先出FIFO—first in first out线性表。
什么是阻塞队列
1支持阻塞的插入方法意思是当队列满时队列会阻塞插入元素的线程直到队列不满。
2支持阻塞的移除方法意思是在队列为空时获取元素的线程会等待队列变为非空。
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
在线程世界里生产者就是生产数据的线程消费者就是消费数据的线程。在多线程开发中如果生产者处理速度很快而消费者处理速度很慢那么生产者就必须等待消费者处理完才能继续生产数据。同样的道理如果消费者的处理能力大于生产者那么消费者就必须等待生产者。
为了解决这种生产消费能力不均衡的问题便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信而是通过阻塞队列来进行通信所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。
阻塞队列常用于生产者和消费者的场景生产者是向队列里添加元素的线程消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。 ·抛出异常当队列满时如果再往队列里插入元素会抛出IllegalStateExceptionQueuefull异常。当队列空时从队列里获取元素会抛出NoSuchElementException异常。
·返回特殊值当往队列插入元素时会返回元素是否插入成功成功返回true。如果是移除方法则是从队列里取出一个元素如果没有则返回null。
·一直阻塞当阻塞队列满时如果生产者线程往队列里put元素队列会一直阻塞生产者线程直到队列可用或者响应中断退出。当队列空时如果消费者线程从队列里take元素队列会阻塞住消费者线程直到队列不为空。
·超时退出当阻塞队列满时如果生产者线程往队列里插入元素队列会阻塞生产者线程一段时间如果超过了指定的时间生产者线程就会退出。
常用阻塞队列
·ArrayBlockingQueue一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue一个支持优先级排序的无界阻塞队列。
·DelayQueue一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue一个不存储元素的阻塞队列。
·LinkedTransferQueue一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque一个由链表结构组成的双向阻塞队列。
以上的阻塞队列都实现了BlockingQueue接口也都是线程安全的。
有界无界
有限队列就是长度有限满了以后生产者会阻塞无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞当然空间限制来源于系统资源的限制如果处理不及时导致队列越来越大越来越大超出一定的限制致使内存超限操作系统或者JVM帮你解决烦恼直接把你 OOM kill 省事了。
无界也会阻塞为何因为阻塞不仅仅体现在生产者放入元素时会阻塞消费者拿取元素时如果没有元素同样也会阻塞。
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列。此队列按照先进先出FIFO的原则对元素进行排序。默认情况下不保证线程公平的访问队列所谓公平访问队列是指阻塞的线程可以按照阻塞的先后顺序访问队列即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的当队列可用时阻塞的线程都可以争夺访问队列的资格有可能先阻塞的线程最后才访问队列。初始化时有参数可以设置
LinkedBlockingQueue
是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
Array实现和Linked实现的区别
1. 队列中锁的实现不同
ArrayBlockingQueue实现的队列中的锁是没有分离的即生产和消费用的是同一个锁
LinkedBlockingQueue实现的队列中的锁是分离的即生产用的是putLock消费是takeLock
2. 在生产或消费时操作不同
ArrayBlockingQueue实现的队列中在生产和消费的时候是直接将枚举对象插入或移除的
LinkedBlockingQueue实现的队列中在生产和消费的时候需要把枚举对象转换为NodeE进行插入或移除会影响性能
3. 队列大小初始化方式不同
ArrayBlockingQueue实现的队列中必须指定队列的大小
LinkedBlockingQueue实现的队列中可以不指定队列的大小但是默认是Integer.MAX_VALUE
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则或者初始化PriorityBlockingQueue时指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
DelayQueue
是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
DelayQueue非常有用可以将DelayQueue运用在以下应用场景。
缓存系统的设计可以用DelayQueue保存缓存元素的有效期使用一个线程循环查询DelayQueue一旦能从DelayQueue中获取元素时表示缓存有效期到了。
SynchronousQueue
是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作否则不能继续添加元素。SynchronousQueue可以看成是一个传球手负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素非常适合传递性场景。
LinkedTransferQueue
多了tryTransfer和transfer方法
1transfer方法
如果当前有消费者正在等待接收元素消费者使用take()方法或带时间限制的poll()方法时transfer方法可以把生产者传入的元素立刻transfer传输给消费者。如果没有消费者在等待接收元素transfer方法会将元素存放在队列的tail节点并等到该元素被消费者消费了才返回。
2tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收方法立即返回而transfer方法是必须等到消费者消费了才返回。
LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口在多线程同时入队时也就减少了一半的竞争。
多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法以First单词结尾的方法表示插入、获取peek或移除双端队列的第一个元素。以Last单词结尾的方法表示插入、获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast移除方法remove等效于removeFirst。但是take方法却等同于takeFirst不知道是不是JDK的bug使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。
线程池
为什么要用线程池
Java中的线程池是运用场景最多的并发框架几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中合理地使用线程池能够带来3个好处。
第一降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二提高响应速度。当任务到达时任务可以不需要等到线程创建就能立即执行。假设一个服务器完成一项任务所需时间为T1 创建线程时间T2 在线程中执行任务的时间T3 销毁线程时间。 如果T1 T3 远大于 T2则可以采用线程池以提高服务器性能。线程池技术正是关注如何缩短或调整T1,T3时间的技术从而提高服务器程序性能的。它把T1T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段这样在服务器程序处理客户请求时不会有T1T3的开销了。
第三提高线程的可管理性。线程是稀缺资源如果无限制地创建不仅会消耗系统资源还会降低系统的稳定性使用线程池可以进行统一分配、调优和监控。
ThreadPoolExecutor 的类关系
Executor是一个接口它是Executor框架的基础它将任务的提交与任务的执行分离开来。
ExecutorService接口继承了Executor在其上做了一些shutdown()、submit()的扩展可以说是真正的线程池接口
AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法
ThreadPoolExecutor是线程池的核心实现类用来执行被提交的任务。
ScheduledExecutorService接口继承了ExecutorService接口提供了带周期执行功能ExecutorService
ScheduledThreadPoolExecutor是一个实现类可以在给定的延迟后运行命令或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活功能更强大。
线程池的创建各个参数含义
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueueRunnable workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize
线程池中的核心线程数当提交一个任务时线程池创建一个新线程执行任务直到当前线程数等于corePoolSize
如果当前线程数为corePoolSize继续提交的任务被保存到阻塞队列中等待被执行
如果执行了线程池的prestartAllCoreThreads()方法线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了且继续提交任务则创建新的线程执行任务前提是当前线程数小于maximumPoolSize
keepAliveTime
线程空闲时的存活时间即当线程没有任务执行时继续存活的时间。默认情况下该参数只在线程数大于corePoolSize时才有用
TimeUnit
keepAliveTime的时间单位
workQueue
workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候线程会进入阻塞队列进行阻塞等待。通过workQueue线程池实现了阻塞功能。
一般来说我们应该尽量使用有界队列因为使用无界队列作为工作队列会对线程池带来如下影响。
1当线程池中的线程数达到corePoolSize后新任务将在无界队列中等待因此线程池中的线程数不会超过corePoolSize。
2由于1使用无界队列时maximumPoolSize将是一个无效参数。
3由于1和2使用无界队列时keepAliveTime将是一个无效参数。
4更重要的使用无界queue可能会耗尽系统资源有界队列则有助于防止资源耗尽同时即使使用有界队列也要尽量控制队列的大小在一个合适的范围。
threadFactory
创建线程的工厂通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名当然还可以更加自由的对线程做更多的设置比如设置所有的线程为守护线程。
Executors静态工厂里默认的threadFactory线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler
线程池的饱和策略当阻塞队列满了且没有空闲的工作线程如果继续提交任务必须采取一种策略处理该任务线程池提供了4种策略
1AbortPolicy直接抛出异常默认策略
2CallerRunsPolicy用调用者所在的线程来执行任务
3DiscardOldestPolicy丢弃阻塞队列中靠最前的任务并执行当前任务
4DiscardPolicy直接丢弃任务
当然也可以根据应用场景实现RejectedExecutionHandler接口自定义饱和策略如记录日志或持久化存储不能处理的任务。
线程池的工作机制
1如果当前运行的线程少于corePoolSize则创建新线程来执行任务注意执行这一步骤需要获取全局锁。
2如果运行的线程等于或多于corePoolSize则将任务加入BlockingQueue。
3如果无法将任务加入BlockingQueue队列已满则创建新的线程来处理任务。
4如果创建新线程将使当前运行的线程超出maximumPoolSize任务将被拒绝并调用RejectedExecutionHandler.rejectedExecution()方法。
提交任务
execute()方法用于提交不需要返回值的任务所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象通过这个future对象可以判断任务是否执行成功并且可以通过future的get()方法来获取返回值get()方法会阻塞当前线程直到任务完成而使用getlong timeoutTimeUnit unit方法则会阻塞当前线程一段时间后立即返回这时候有可能任务没有执行完。
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程然后逐个调用线程的interrupt方法来中断线程所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别shutdownNow首先将线程池的状态设置成STOP然后尝试停止所有的正在执行或暂停任务的线程并返回等待执行任务的列表而shutdown只是将线程池的状态设置成SHUTDOWN状态然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个isShutdown方法就会返回true。当所有的任务都已关闭后才表示线程池关闭成功这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池应该由提交到线程池的任务特性决定通常调用shutdown方法来关闭线程池如果任务不一定要执行完则可以调用shutdownNow方法。
合理地配置线程池
要想合理地配置线程池就必须首先分析任务特性
要想合理地配置线程池就必须首先分析任务特性可以从以下几个角度来分析。
•任务的性质CPU密集型任务、IO密集型任务和混合型任务。
•任务的优先级高、中和低。
•任务的执行时间长、中和短。
•任务的依赖性是否依赖其他系统资源如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。
CPU密集型任务应配置尽可能小的线程如配置Ncpu1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务则应配置尽可能多的线程如2*Ncpu。
混合型的任务如果可以拆分将其拆分成一个CPU密集型任务和一个IO密集型任务只要这两个任务执行的时间相差不是太大那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
执行时间不同的任务可以交给不同规模的线程池来处理或者可以使用优先级队列让执行时间短的任务先执行。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力可以根据需要设大一点儿比如几千。
如果当时我们设置成无界队列那么线程池的队列就会越来越多有可能会撑满内存导致整个系统不可用而不只是后台任务出现问题。
AbstractQueuedSynchronizer
学习AQS的必要性
队列同步器AbstractQueuedSynchronizer以下简称同步器或AQS是用来构建锁或者其他同步组件的基础框架它使用了一个int成员变量表示同步状态通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师Doug Lea期望它能够成为实现大部分同步需求的基础。
AQS使用方式和其中的设计模式
AQS的主要使用方式是继承子类通过继承AQS并实现它的抽象方法来管理同步状态在AQS里由一个int型的state来代表这个状态在抽象方法的实现过程中免不了要对同步状态进行更改这时就需要使用同步器提供的3个方法getState()、setState(int newState)和compareAndSetState(int expect,int update)来进行操作因为它们能够保证状态的改变是安全的。 在实现上子类推荐被定义为自定义同步组件的静态内部类AQS自身没有实现任何同步接口它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用同步器既可以支持独占式地获取同步状态也可以支持共享式地获取同步状态这样就可以方便实现不同类型的同步组件ReentrantLock、ReentrantReadWriteLock和CountDownLatch等。
同步器是实现锁也可以是任意同步组件的关键在锁的实现中聚合同步器。可以这样理解二者之间的关系
锁是面向使用者的它定义了使用者与锁交互的接口比如可以允许两个线程并行访问隐藏了实现细节
同步器面向的是锁的实现者它简化了锁的实现方式屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
实现者需要继承同步器并重写指定的方法随后将同步器组合在自定义同步组件的实现中并调用同步器提供的模板方法而这些模板方法将会调用使用者重写的方法。
模板方法模式
同步器的设计基于模板方法模式。模板方法模式的意图是定义一个操作中的算法的骨架而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。我们最常见的就是Spring框架里的各种Template。
实际例子
我们开了个蛋糕店蛋糕店不能只卖一种蛋糕呀于是我们决定先卖奶油蛋糕芝士蛋糕和慕斯蛋糕。三种蛋糕在制作方式上一样都包括造型烘焙和涂抹蛋糕上的东西。所以可以定义一个抽象蛋糕模型 然后就可以批量生产三种蛋糕 这样一来不但可以批量生产三种蛋糕而且如果日后有扩展只需要继承抽象蛋糕方法就可以了十分方便我们天天生意做得越来越赚钱。突然有一天我们发现市面有一种最简单的小蛋糕销量很好这种蛋糕就是简单烘烤成型就可以卖并不需要涂抹什么食材由于制作简单销售量大这个品种也很赚钱于是我们也想要生产这种蛋糕。但是我们发现了一个问题抽象蛋糕是定义了抽象的涂抹方法的也就是说扩展的这种蛋糕是必须要实现涂抹方法这就很鸡儿蛋疼了。怎么办我们可以将原来的模板修改为带钩子的模板。 小蛋糕的时候通过flag来控制是否涂抹其余已有的蛋糕制作不需要任何修改可以照常进行。 AQS中的方法
模板方法
实现自定义同步组件时将会调用同步器提供的模板方法 这些模板方法同步器提供的模板方法基本上分为3类独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。
可重写的方法 访问或修改同步状态的方法
重写同步器指定的方法时需要使用同步器提供的如下3个方法来访问或修改同步状态。
•getState()获取当前同步状态。
•setState(int newState)设置当前同步状态。
•compareAndSetState(int expect,int update)使用CAS设置当前状态该方法能够保证状态设置的原子性。
CLH队列锁
CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁申请线程仅仅在本地变量上自旋它不断轮询前驱的状态假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时
1. 创建一个的QNode将其中的locked设置为true表示需要获取锁myPred表示对其前驱结点的引用 2. 线程A对tail域调用getAndSet方法使自己成为队列的尾部同时获取一个指向其前驱结点的引用myPred 线程B需要获得锁同样的流程再来一遍 3.线程就在前驱结点的locked字段上旋转直到前驱结点释放锁(前驱节点的锁值 locked false)
4.当一个线程需要释放锁时将当前结点的locked域设置为false同时回收前驱结点 如上图所示前驱结点释放锁线程A的myPred所指向的前驱结点的locked字段变为false线程A就可以获取到锁。
CLH队列锁的优点是空间复杂度低如果有n个线程L个锁每个线程每次只获取一个锁那么需要的存储空间是OLnn个线程有n个myNodeL个锁有L个tail。CLH队列锁常用在SMP体系结构下。
Java中的AQS是CLH队列锁的一种变体实现。
ReentrantLock的实现
锁的可重入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞该特性的实现需要解决以下两个问题。
1线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程如果是则再次成功获取。
2锁的最终释放。线程重复n次获取了锁随后在第n次释放该锁后其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增计数表示当前锁被重复获取的次数而锁被释放时计数自减当计数等于0时表示锁已经成功释放。
nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑通过判断当前线程是否为获取锁的线程来决定获取操作是否成功如果是获取锁的线程再次请求则将同步状态值进行增加并返回true表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
如果该锁被获取了n次那么前(n-1)次tryRelease(int releases)方法必须返回false而只有同步状态完全释放了才能返回true。可以看到该方法将同步状态是否为0作为最终释放的条件当同步状态为0时将占有线程设置为null并返回true表示释放成功。
公平和非公平锁
ReentrantLock的构造函数中默认的无参构造函数将会把Sync对象创建为NonfairSync对象这是一个“非公平锁”而另一个构造函数ReentrantLock(boolean fair)传入参数为true时将会把Sync对象创建为“公平锁”FairSync。
nonfairTryAcquire(int acquires)方法对于非公平锁只要CAS设置同步状态成功则表示当前线程获取了锁而公平锁则不同。tryAcquire方法该方法与nonfairTryAcquire(int acquires)比较唯一不同的位置为判断条件多了hasQueuedPredecessors()方法即加入了同步队列中当前节点是否有前驱节点的判断如果该方法返回true则表示有线程比当前线程更早地请求获取锁因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 深入理解并发编程和归纳总结
JMM基础-计算机原理
Java内存模型即Java Memory Model简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型所以JMM是隶属于JVM的。Java1.5版本对其进行了重构现在的Java仍沿用了Java1.5的版本。Jmm遇到的问题与现代计算机中遇到的问题是差不多的。
物理计算机中的并发问题物理机遇到的并发问题与虚拟机中的情况有不少相似之处物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
根据《Jeff Dean在Google全体工程大会的报告》我们可以看到 计算机在做一些我们平时的基本操作时需要的响应时间是不一样的。
以下案例仅做说明并不代表真实情况。
如果从内存中读取1M的int型数据由CPU进行累加耗时要多久
做个简单的计算1M的数据Java里int型为32位4个字节共有1024*1024/4 262144个整数 则CPU 计算耗时262144 *0.6 157 286 纳秒而我们知道从内存读取1M数据需要250000纳秒两者虽然有差距当然这个差距并不小十万纳秒的时间足够CPU执行将近二十万条指令了但是还在一个数量级上。但是没有任何缓存机制的情况下意味着每个数都需要从内存中读取这样加上CPU读取一次内存需要100纳秒262144个整数从内存读取到CPU加上计算时间一共需要262144*100250000 26 464 400 纳秒这就存在着数量级上的差异了。
而且现实情况中绝大多数的运算任务都不可能只靠处理器“计算”就能完成处理器至少要与内存交互如读取运算数据、存储运算结果等这个I/O操作是基本上是无法消除的无法仅靠寄存器来完成所有运算任务。早期计算机中cpu和内存的速度是差不多的但在现代计算机中cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存Cache来作为内存与处理器之间的缓冲将运算需要使用到的数据复制到缓存中让运算能快速进行当运算结束后再从缓存同步回内存之中这样处理器就无须等待缓慢的内存读写了。 在计算机系统中寄存器划是L0级缓存接着依次是L1L2L3接下来是内存本地磁盘远程存储。越往上的缓存存储空间越小速度越快成本也更高越往下的存储空间越大速度更慢成本也更低。从上至下每一层都可以看做是更下一层的缓存即L0寄存器是L1一级缓存的缓存L1是L2的缓存依次类推每一层的数据都是来至它的下一层所以每一层的数据是下一层的数据的子集。 在现代CPU上一般来说L0 L1L2L3都集成在CPU内部而L1还分为一级数据缓存Data CacheD-CacheL1d和一级指令缓存Instruction CacheI-CacheL1i分别用于存放数据和执行数据的指令解码。每个核心拥有独立的运算处理单元、控制器、寄存器、L1、L2缓存然后一个CPU的多个核心共享最后一层CPU缓存L3
Java内存模型JMM
从抽象的角度来看JMM定义了线程和主内存之间的抽象关系线程之间的共享变量存储在主内存Main Memory中每个线程都有一个私有的本地内存Local Memory本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 可见性
可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量那么对于共享变量V它们首先是在自己的工作内存之后再同步到主内存。可是并不会及时的刷到主存中而是会有一定时间差。很明显这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题我们可以使用volatile关键字或者是加锁。
原子性
原子性即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断要么就都不执行。
我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间例如 50 毫秒过了 50 毫秒操作系统就会重新选择一个进程来执行我们称为“任务切换”这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后,
那么线程切换为什么会带来bug呢因为操作系统做任务切换可以发生在任何一条CPU 指令执行完注意是 CPU 指令CPU 指令CPU 指令而不是高级语言里的一条语句。比如count在java里就是一句话但高级语言里一条语句往往需要多条 CPU 指令完成。其实count至少包含了三个CPU指令
volatile详解
volatile特性
可以把对volatile变量的单个读/写看成是使用同一个锁对这些单个读/写操作做了同步 可以看成 所以volatile变量自身具有下列特性
可见性。对一个volatile变量的读总是能看到任意线程对这个volatile变量最后的写入。
原子性对任意单个volatile变量的读/写具有原子性但类似于volatile这种复合操作不具有原子性。
volatile虽然能保证执行完及时把变量刷到主内存中但对于count这种非原子性、多指令的情况由于线程切换线程A刚把count0加载到工作内存线程B就可以开始工作了这样就会导致线程A和B执行完的结果都是1都写到主内存中主内存的值还是1不是2
volatile的实现原理
volatile关键字修饰的变量会存在一个“lock:”的前缀。
Lock前缀Lock不是一种内存屏障但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁可以理解为CPU指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到系统内存中且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。
synchronized的实现原理
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步虽然具体实现细节不一样但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
对同步块MonitorEnter指令插入在同步代码块的开始位置而monitorExit指令则插入在方法结束处和异常处JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说当代码执行到该指令时将会尝试获取该对象Monitor的所有权即尝试获得该对象的锁
1、如果monitor的进入数为0则该线程进入monitor然后将进入数设置为1该线程即为monitor的所有者。
2、如果线程已经占有该monitor只是重新进入则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor则该线程进入阻塞状态直到monitor的进入数为0再重新尝试获取monitor的所有权。
对同步方法从同步方法反编译的结果来看方法的同步并没有通过指令monitorenter和monitorexit来实现相对于普通方法其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的当方法被调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先获取monitor获取成功之后才能执行方法体方法执行完后再释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象。
synchronized使用的锁是存放在Java对象头里面Java对象的对象头由 mark word 和 klass pointer 两部分组成
1mark word存储了同步状态、标识、hashcode、GC状态等等。
2klass pointer存储对象的类型指针该指针指向它的类元数据
另外对于数组而言还会有一份记录数组长度的数据。 锁信息则是存在于对象的mark word中MarkWord里默认数据是存储对象的HashCode等信息 但是会随着对象的运行改变而发生变化不同的锁状态对应着不同的记录存储方式 了解各种锁
锁的状态
一共有四种状态无锁状态偏向锁状态轻量级锁状态和重量级锁状态它会随着竞争情况逐渐升级。锁可以升级但不能降级目的是为了提高获得锁和释放锁的效率。
偏向锁
引入背景大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得为了让线程获得锁的代价更低而引入了偏向锁减少不必要的CAS操作。
偏向锁顾名思义它会偏向于第一个访问锁的线程如果在运行过程中同步锁只有一个线程访问不存在多线程争用的情况则线程是不需要触发同步的减少加锁解锁的一些CAS操作比如等待队列的一些CAS操作这种情况下就会给线程加一个偏向锁。 如果在运行过程中遇到了其他线程抢占锁则持有偏向锁的线程会被挂起JVM会消除它身上的偏向锁将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语进一步提高了程序的运行性能。
偏向锁获取过程
步骤1、 访问Mark Word中偏向锁的标识是否设置成1锁标志位是否为01确认为可偏向状态。
步骤2、 如果为可偏向状态则测试线程ID是否指向当前线程如果是进入步骤5否则进入步骤3。
步骤3、 如果线程ID并未指向当前线程则通过CAS操作竞争锁。如果竞争成功则将Mark Word中线程ID设置为当前线程ID然后执行5如果竞争失败执行4。
步骤4、 如果CAS获取偏向锁失败则表示有竞争。当到达全局安全点safepoint时获得偏向锁的线程被挂起偏向锁升级为轻量级锁然后被阻塞在安全点的线程继续往下执行同步代码。撤销偏向锁的时候会导致stop the word
步骤5、 执行同步代码。 偏向锁的释放
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放偏向锁线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点在这个时间点上没有字节码正在执行它会首先暂停拥有偏向锁的线程判断锁对象是否处于被锁定状态撤销偏向锁后恢复到未锁定标志位为“01”或轻量级锁标志位为“00”的状态。
偏向锁的适用场景
始终只有一个线程在执行同步块在它没有执行完释放锁之前没有其它线程去执行同步块在锁无竞争的情况下使用一旦有了竞争就升级为轻量级锁升级为轻量级锁的时候需要撤销偏向锁撤销偏向锁的时候会导致stop the word操作
在有锁的竞争时偏向锁会多做很多额外操作尤其是撤销偏向所的时候会导致进入安全点安全点会导致stw导致性能下降这种情况下应当禁用。
jvm开启/关闭偏向锁
开启偏向锁-XX:UseBiasedLocking -XX:BiasedLockingStartupDelay0
关闭偏向锁-XX:-UseBiasedLocking
轻量级锁
轻量级锁是由偏向锁升级来的偏向锁运行在一个线程进入同步块的情况下当第二个线程加入锁争用的时候偏向锁就会升级为轻量级锁
轻量级锁的加锁过程
1、在代码进入同步块的时候如果同步对象锁状态为无锁状态且不允许进行偏向锁标志位为“01”状态是否为偏向锁为“0”虚拟机首先将在当前线程的栈帧中建立一个名为锁记录Lock Record的空间用于存储锁对象目前的Mark Word的拷贝官方称之为 Displaced Mark Word。
2、拷贝对象头中的Mark Word复制到锁记录中。
3、拷贝成功后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针并将Lock record里的owner指针指向object mark word。如果更新成功则执行步骤4否则执行步骤5。
4、如果这个更新动作成功了那么这个线程就拥有了该对象的锁并且对象Mark Word的锁标志位设置为“00”即表示此对象处于轻量级锁定状态
5、如果这个更新操作失败了虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧如果是就说明当前线程已经拥有了这个对象的锁那就可以直接进入同步块继续执行。否则说明多个线程竞争锁那么它就会自旋等待锁一定次数后仍未获得锁对象。重量级线程指针指向竞争线程竞争线程也会阻塞等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”Mark Word中存储的就是指向重量级锁互斥量的指针后面等待锁的线程也要进入阻塞状态。
自旋锁
原理
自旋锁原理非常简单如果持有锁的线程能在很短时间内释放锁资源那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态它们只需要等一等自旋等持有锁的线程释放锁后即可立即获取锁这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗CPU的说白了就是让CPU在做无用功线程不能一直占用CPU自旋做无用功所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁就会导致其它争用锁的线程在最大等待时间内还是获取不到锁这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞这对于锁的竞争不激烈且占用锁时间非常短的代码块来说性能能大幅度的提升因为自旋的消耗会小于线程阻塞挂起操作的消耗
但是如果锁的竞争激烈或者持有锁的线程需要长时间占用锁执行同步块这时候就不适合使用自旋锁了因为自旋锁在获取锁前一直都是占用cpu做无用功占着XX不XX线程自旋的消耗大于线程阻塞挂起操作的消耗其它需要cup的线程又不能获取到cpu造成cpu的浪费。
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢如果自旋执行时间太长会有大量的线程处于自旋状态占用CPU资源进而会影响整体系统的性能。因此自旋次数很重要
JVM对于自旋次数的选择jdk1.5默认为10次在1.6引入了适应性自旋锁适应性自旋锁意味着自旋的时间不在是固定的了而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定基本认为一个线程上下文切换的时间是最佳的一个时间。
JDK1.6中-XX:UseSpinning开启自旋锁 JDK1.7后去掉此参数由jvm控制 不同锁的比较 看看一线大厂面试题
sychronied修饰普通方法和静态方法的区别什么是可见性?
对象锁是用于对象实例方法或者一个对象实例上的类锁是用于类的静态方法或者一个类的class对象上的。我们知道类的对象实例可以有很多个但是每个类只有一个class对象所以不同对象实例的对象锁是互不干扰的但是每个类只有一个类锁。
但是有一点必须注意的是其实类锁只是一个概念上的东西并不是真实存在的类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量那么对于共享变量V它们首先是在自己的工作内存之后再同步到主内存。可是并不会及时的刷到主存中而是会有一定时间差。很明显这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题我们可以使用volatile关键字或者是加锁。
锁分哪几类 CAS无锁编程的原理。
使用当前的处理器基本都支持CAS()的指令只不过每个厂家所实现的算法并不一样每一个CAS操作过程都包含三个运算符一个内存地址V一个期望的值A和一个新值B操作的时候如果这个地址上存放的值等于这个期望的值A则将地址上的值赋为新值B否则不做任何操作。
CAS的基本思路就是如果这个地址上的值和期望的值相等则给其赋予新值否则不做任何事儿但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作直到成功为止。
还可以说说CAS的三大问题。
ReentrantLock的实现原理。
线程可以重复进入任何一个它已经拥有的锁所同步着的代码块synchronized、ReentrantLock都是可重入的锁。在实现上就是线程每次获取锁时判定如果获得锁的线程是它自己时简单将计数器累积即可每 释放一次锁进行计数器累减直到计算器归零表示线程已经彻底释放锁。
底层则是利用了JUC中的AQS来实现的。
AQS原理 小米 京东
是用来构建锁或者其他同步组件的基础框架比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式独占式共享式。
AQS的主要使用方式是继承子类通过继承AQS并实现它的抽象方法来管理同步状态同步器的设计基于模板方法模式所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法如tryAcquire、tryReleaseShared等等。
这样设计的目的是同步组件比如锁是面向使用者的它定义了使用者与同步组件交互的接口比如可以允许两个线程并行访问隐藏了实现细节同步器面向的是锁的实现者它简化了锁的实现方式屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
在内部AQS维护一个共享资源state通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成每个Node结点维护一个prev引用和next引用分别指向自己的前驱和后继结点构成一个双端双向链表。
Synchronized的原理以及与ReentrantLock的区别。360
synchronized (this)原理涉及两条指令monitorentermonitorexit再说同步方法从同步方法反编译的结果来看方法的同步并没有通过指令monitorenter和monitorexit来实现相对于普通方法其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的当方法被调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先获取monitor获取成功之后才能执行方法体方法执行完后再释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象。 Synchronized做了哪些优化 京东
引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析
等技术来减少锁操作的开销。
逃逸分析
如果证明一个对象不会逃逸方法外或者线程外则可针对此变量进行优化
同步消除synchronization Elimination如果一个对象不会逃逸出线程则对此变量的同步措施可消除。
锁消除和粗化
锁消除虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争则会去掉这些锁。
锁粗化将临近的代码块用同一个锁合并起来。
消除无意义的锁获取和释放可以提高程序运行性能。
Synchronized static与非static锁的区别和范围小米
对象锁是用于对象实例方法或者一个对象实例上的类锁是用于类的静态方法或者一个类的class对象上的。我们知道类的对象实例可以有很多个但是每个类只有一个class对象所以不同对象实例的对象锁是互不干扰的但是每个类只有一个类锁。
但是有一点必须注意的是其实类锁只是一个概念上的东西并不是真实存在的类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
volatile 能否保证线程安全在DCL上的作用是什么
不能保证在DCL的作用是volatile是会保证被修饰的变量的可见性和 有序性保证了单例模式下保证在创建对象的时候的执行顺序一定是
1.分配内存空间
2.实例化对象instance
3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null了
的步骤, 从而保证了instance要么为null 要么是已经完全初始化好的对象。
volatile和synchronize有什么区别B站 小米 京东
volatile是最轻量的同步机制。
volatile保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性因此多线程下的写复合操作会导致线程安全问题。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用它主要确保多个线程在同一个时刻只能有一个线程处于方法或者同步块中它保证了线程对变量访问的可见性和排他性又称为内置锁机制。
什么是守护线程你是如何退出一个线程的
Daemon守护线程是一种支持型线程因为它主要被用作程序中后台调度以及支持性工作。这意味着当一个Java虚拟机中不存在非Daemon线程的时候Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上比如垃圾回收线程就是Daemon线程。
线程的中止
要么是run执行完成了要么是抛出了一个未处理的异常导致线程提前结束。
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的也就是不建议使用的。因为会导致程序可能工作在不确定状态下。
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断不过Thread.interrupted()会同时将中断标识位改写为false。
sleep 、wait、yield 的区别wait 的线程如何唤醒它东方头条
yield()方法使当前线程让出CPU占有权但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
yield() 、sleep()被调用后都不会释放当前线程所持有的锁。
调用wait()方法后会释放当前线程持有的锁而且当前被唤醒后会重新去竞争锁锁竞争到后才会执行wait方法后面的代码。
Wait通常被用于线程间交互sleep通常被用于暂停执行yield()方法使当前线程让出CPU占有权。
wait 的线程使用notify/notifyAll()进行唤醒。
sleep是可中断的么小米
sleep本身就支持中断如果线程在sleep期间被中断则会抛出一个中断异常。
线程生命周期。
Java中线程的状态分为6种
1. 初始(NEW)新创建了一个线程对象但还没有调用start()方法。
2. 运行(RUNNABLE)Java线程中将就绪ready和运行中running两种状态笼统的称为“运行”。
线程对象创建后其他线程(比如main线程调用了该对象的start()方法。该状态的线程位于可运行线程池中等待被线程调度选中获取CPU的使用权此时处于就绪状态ready。就绪状态的线程在获得CPU时间片后变为运行中状态running。
3. 阻塞(BLOCKED)表示线程阻塞于锁。
4. 等待(WAITING)进入该状态的线程需要等待其他线程做出一些特定动作通知或中断。
5. 超时等待(TIMED_WAITING)该状态不同于WAITING它可以在指定的时间后自行返回。
6. 终止(TERMINATED)表示该线程已经执行完毕。 ThreadLocal是什么
ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本使得每个线程在某一时间訪问到的并非同一个对象这样就隔离了多个线程对数据的数据共享。
在内部实现上每个线程内部都有一个ThreadLocalMap用来保存每个线程所拥有的变量副本。
线程池基本原理。
在开发过程中合理地使用线程池能够带来3个好处。
第一降低资源消耗。第二提高响应速度。第三提高线程的可管理性。
1如果当前运行的线程少于corePoolSize则创建新线程来执行任务注意执行这一步骤需要获取全局锁。
2如果运行的线程等于或多于corePoolSize则将任务加入BlockingQueue。
3如果无法将任务加入BlockingQueue队列已满则创建新的线程来处理任务。
4如果创建新线程将使当前运行的线程超出maximumPoolSize任务将被拒绝并调用RejectedExecutionHandler.rejectedExecution()方法。 有三个线程T1T2T3怎么确保它们按顺序执行
可以用join方法实现。