线程安全和非线程安全: 一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不做特别处理也能运行正常,我们就称其实线程安全的。反之,一个类在单线程环境下运行正常,而在多线程环境下无法正常运行,这个类就是非线程安全的。
线程安全问题体现在:
并发:多个任务在同一个 CPU 核上,按时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
并行:单位时间内,多个处理器同时处理多个任务,是真正意义上的“同时进行”。
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
多线程:宏观上看,一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
坏处:
并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
这里我以java为例讲进程与线程的区别:
windows上面用任务管理器看,linux下可以用 top 这个工具看。
死锁:指两个或两个以上的进程(线程)由于竞争资源而造成的一种阻塞的现象,若无外力作用,将永远在互相等待,互相僵持下去。
比如说,两个线程互相持有对方资源,同时他们都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的
破坏请求与保持条件
一次性申请所有的资源。
破坏不可剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
1) 继承 Thread 类
2 )实现 Runnable 接口
3)实现 Callable 接口
4. 创建实现Callable接口的类myCallable
5. 以myCallable为参数创建FutureTask对象
6. 将FutureTask作为参数创建Thread对象
7. 调用线程对象的start()方法
同步:
发送一个请求,等待返回,然后再发送下一个请求。
异步:
发送一个请求,不等待返回,随时可以再发送下一个请求。
Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
什么是 FutureTask
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
乐观锁:
每次访问数据的时候都认为其他线程不会修改数据,所以直接访问数据,更新的时候再判断在此期间其他线程是否修改数据。CAS和版本号机制是乐观锁的实现。
作用:
乐观锁适合多读场景,悲观锁适合多写情况。
版本号机制:数据有个version字段,表示被修改的次数。
CAS:无琐算法,非阻塞同步,需要读写的内存值V和旧的期望值A相同时,更新为B.一般都是自旋CAS,不断的重试。
乐观锁缺点:
1、ABA问题(加入版本号机制)
2、自旋CAS如果一直不成功,开销大。
3、只对单变量有效,当涉及多个共享变量时,无效。
乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。 如果内存值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。CAS是通过硬件命令保证了原子性。
悲观锁:
-每次访问数据的时候都会认为其他线程会修改数据,所以先获取锁,再访问数据。synchronized和ReentrantLock都是悲观锁思想的实现。
Synchronized关键字三种实现方式:
修饰实例方法,对当前实例对象加锁,进入同步代码前要获取对象实例的锁。
修饰静态方法,对当前类对象加锁,
修饰代码块,指定加锁对象,给对象加锁。
具体实例,双重校验锁实现对单例模式;
Synchronized同步的实现,是基于进入退出监视器Monitor对象实现的,无论是同步代码块还是同步方法,都是如此;同步代码块,是根据monitorenter 和 monitorexit 指令实现的,同步方法,是通过设置方法的 ACC_SYNCHRONIZED 访问标志;监视器Monitor对象存在于每个对象的对象头中。
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。
比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,
当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
1)synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API,synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
2)ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
3)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
4)ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
5)相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
互斥锁:同一时间只能被一个线程持有。
可重入锁:可以被单个线程多次获取。
公平锁:线程依次排队获取锁。
非公平锁:不管是不是队头都能获取。
公平锁和非公平锁,它们尝试获取锁的方式不同:
公平锁在尝试获取锁时,即使“锁”没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;
是的话,才获取锁。
而非公平锁在尝试获取锁时,如果“锁”没有被任何线程持有,则不管它在CLH队列何处,都直接获取锁。
公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,
如果有自己要挂起,加到队列后面,然后唤醒队列最前面的线程。
这种情况下相比较非公平锁多了一次挂起和唤醒。
线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
JDK1.6引入了大量的锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术减少开销。
锁主要存在4种状态:无琐状态,偏向锁状态,轻量级锁状态,重量级锁状态 。
锁可升级不可降级,提供获取锁和释放锁的效率。
自旋锁:进程进入阻塞的开销很大,为防止进入阻塞状态,在线程请求共享数据锁的时候循环自旋一段时间,如果在这段时间内获取到锁,就避免进入阻塞状态了。
1.6引入自适应自旋锁,自旋次数不再固定:由锁拥有者状态和上次获取锁的自旋次数决定。
锁消除:对于被检测出不可能存在竞争的共享数据的锁进行消除。(逃逸分析)
锁粗化:虚拟机探测到一系列连续操作都对同一个对象加锁解锁,就将加锁的范围粗化到整个操作系列的外部。
偏向锁:当锁对象第一次被线程获取的时候,进入偏向状态,标记为101,
同时CAS将线程ID进入到对象头的Mark Word中,如果成功,这个线程以后每次获取锁就不再需要进行同步操作,
甚至CAS不都需要。当另一个线程尝试获取这个锁,偏向状态结束,恢复到未锁定状态或者轻量级状态。
轻量级锁:对象头的内存布局Mark Word,有个tag bits,记录了锁的四种状态:无琐状态,偏向锁状态,轻量级锁状态,重量级锁状态.轻量级锁相对重量级锁而言,使用CAS去避免重量级锁使用互斥量的开销。线程尝试获取锁时,如果锁处于无琐状态,先采用CAS去尝试获取锁,如果成功,锁状态更新为轻量级锁状态。如果有两条以上的线程争用一个锁,状态重为重量级锁。
区别
AQS原理:如果被请求的共享资源空闲,则将当前请求线程设为有效的工作线程,并且将共享资源设置为锁定状态。如果请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的。即将暂时获取不到的线程放入队列中。
CLH,是虚拟的双向队列,即不存在队列实例,仅存在节点与节点之间的pre和next关系。
AQS将每条请求共享资源的线程封装成一个CLH锁队列的一个节点来实现锁的分配。
AQS属性(Node head, Node tail, int state(这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁), Thread 持有独占锁的线程);
等待队列中每个线程被封装为一个Node实例
(thread + waitStatus(-1: 当前node的后继节点对应的线程需要被唤醒,) + pre + next);
State:表示当前锁的状态,等于0时,表示没有被线程占用。当大于0时,表示被线程占用。
Node节点的属性 watiStatus:默认为0,
当大于0时,表示放弃等待,ReentrantLock是可以指定timeouot的。
等于-1,表示当前node的后继节点对应的线程需要被唤醒。
当等于-2时,标志着线程在Condition条件上等待的线程唤醒。
等于-3时,用于共享锁,标志着下一个acquireShared方法线程应该被允许。
公平锁,只有处于队头的线程才被允许去获取锁。非公平性锁模式下线程上下文切换的次数少,因此其性能开销更小。公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
Executors是个静态工厂类。
在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
Executors 各个方法的弊端:
newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。
ThreadPoolExecutor,真正线程池实现类。
ThreadPoolExecutor线程池的7大参数:
corePoolSize:核心池的大小:创建线程池之后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:非核心线程的最大空闲时间。
TimeUnit:空闲时间的单位。
BlockingQueue workQueue :等待执行的任务阻塞队列,队列分为有界队列和无界队列。有界队列:队列的长度有上限,当核心线程满载的时候,新任务进来进入队列,当达到上限,有没有核心线程去即时取走处理,这个时候,就会创建临时线程。(警惕临时线程无限增加的风险)
无界队列:队列没有上限的,当没有核心线程空闲的时候,新来的任务可以无止境的向队列中添加,而永远也不会创建临时线程。(警惕任务队列无限堆积的风险)
ThreadFactory threadFactory:线程工厂,用来创建线程
RejectedExecution handler:队列已满,而且任务量大于最大线程的异常处理策略
基本类型:AtomicInteger,AtomicLong,AtomicBoolean;
数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray;
引用类型,对象属性修改类型。
在32位操作系统中,64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性;
原子类基本通过自旋CAS来实现,期望的值和现在的值是否一致,如果一致就更新。
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
主要利用CAS+volatile + native方法来保证操作的原子性,从而避免同步方法的高开销。CAS原理是那期望的值和现在的值进行比较,如果相同则更新成新的值。