volatitle在多线程状况下,能够保证数据数据的可见性。禁止指令重排优化,从而避免多线程环境下程序出现乱序执行致使执行结果不一致的问题,它不支持原子性(使用AutomicInteger来保证原子性)。java
在单例模式DCL中使用过。程序员
(1)Thread类继承存在单继承的局限性,而接口不会。
(2)Runable能够体现数据共享的概念(JMM内存模型图),代码能够被多个线程共享,代码和数据独立。
(3)线程池只能放入实现Runnable或callable类的线程,不能直接放入继承Thread的类
(3)Runnable实现线程能够对线程进行复用,由于runnable是轻量级对象,而Thread不行,它是重量级对象。缓存
(1)static保证惟一性,就是在主内存中是惟一的变量。各个线程建立时须要从主内存同一个位置拷贝到本身工做内存中去,也就是说只能保证线程建立时,变量的值是相同来源的,运行时仍是使用各自工做内存中的值,依然会有不一样步的问题。
(2)volatile是保证可见性,就是指在工做线程和主内存的数据的一致性,若是改变了工做线程中volatile修饰的变量,那么主内存也要发生更新。因此,volatile和static一块儿使用不矛盾。由于static修饰只能保证在主内存的惟一性,若是涉及到其余工做线程,改变参数可能就会致使static修饰的变量的内容没法同步,因此static和volatile能够一块儿使用,由于他们管的地方是不同的,互不影响。安全
CAS即比较而且交换,CPU去更新一个值,若是初始值与原来值不相同操做就失败,但若是初始值与原来值相同就操做就成功。服务器
CAS应用:CAS有3个操做数,V:要更新的变量;E:预期值;N:新值。若是V值等于E值,则将V值设为N值;若是V值不等于E值,说明其余线程作了更新,那么当前线程什么也不作。(放弃操做或从新读取数据)多线程
CAS是经过Unsafe实现,Unsafe是CAS的核心类,因为Java没法直接访问底层系统,须要经过本地native来访问,Unsafe至关于
一个后门,基于该类能够直接操做特定的内存数据。Unsafe类存在于sun.misc包中,其内部方法操做
能够像C语言的指针同样直接操做内存。由于Java中CAS的操做的执行依赖于Unsafe类的方法。并发
底层代码以下框架
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //Unsafe类中的getAndAddInt方法 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
CAS的缺点
1.循环时间长,开销很大。
2.只能保证一个共享变量的原子操做异步
假若有A,B两条线程,线程A,B都拷贝主内存的数据到本身的内存空间。这时候B线程将本身内存中的数据更改了并更新到主内存中,而后又将本身内存中的数据改回了原来的初始值。如今A线程在作修改的时候,将本身内存中的数据与主内存中的数据作对比发现是同样的并作更新。对与A线程而言它并不知道B线程已经作了一些相应的更改,这时候就产生了ABA问题。经过原子更新引用来解决ABA问题。函数
每修改一次值都对其添加版本号,对值和版本号都作CAS操做。在值和版本号都一致的状况下才能作修改。AtomicStampedReference是一个带有时间戳的对象引用,能很好的解决CAS机制中的ABA问题。
其实除了AtomicStampedReference类,还有一个原子类也能够解决就是AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。所以也只是在特定的场景下使用。
(1)Collection 是一个集合接口。它提供了对集合对象进行基本操做的通用接口方法。Collection接口在Java 类库中有不少具体的实现。Collection接口的意义是为各类具体的集合提供了最大化的统一操做方式。
(2)Collections 是一个包装类。它包含有各类有关集合操做的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
致使缘由:线程不安全问题,基本都是因为多线程并发争抢修改数据致使的。
故障现象:会抛出java.util.ConcurrentModificationException异常
a. 用Vector来解决,
b. 用Collections.synchronizedList(new ArrayList<>())
c. new CopyOnWriteArrayList<>()
a. 用Collections.synchronizedSet(new HashSet<>())来解决
说下HashSet的底层实现
HshSet底层是HashMap,既然是HashMap为何咱们add的时候只添加一个值,而不以key-value的形式添加呢。由于底层的HashMap已经put了Value值,这个Value值实际上是一个Object对象。咱们在调用HashSet的add方法时等于只是添加了一个key值。这也是set集合不重复的缘由。
a. 用Collections.synchronizedMap(new HashMap<>())
b. new ConcurrentHashMap<>()
公平锁:指多个线程按照申请锁的顺序来获取锁,相似排队购票,先到先得。
非公平锁:指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发状况下,有可能形成优先级反转或者饥饿现象。
并发包中的ReentrantLock的建立能够指定构造函数的boolean类型来获得公平锁或非公平锁,默认是非公平锁。
关于二者的区别:
公平锁:就是很公平,在并发环境中,每一个线程获取锁时会先看此锁维护的等待队列,若是为空,或者当前线程是等待队列的第一个,就占有锁。不然就会加入到等待队列中,后面按照FIFO的规则从队列中取到本身。
非公平锁:比较粗鲁,上来就尝试直接占有锁,若是尝试失败,就采用相似公平锁的方式。相对来讲,非公平锁会有更好的性能,由于它的吞吐量比较大。固然,非公平锁让获取锁的时间变得更加不肯定,可能会致使在阻塞队列中的线程长期处于饥饿状态。
说明:
对于Java ReentrantLock而言,经过构造函数指定该锁是不是公平锁,默认是非公平锁。非公平锁的优势在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。
可重入锁又名递归锁,一个线程拿到一个方法的一把锁能够访问其内部代码,可是其内部还嵌套了另一个方法,这个方法也上了锁。由于线程拿到了外层方法的锁,这把锁和嵌套方法的锁是同一把锁,因此能够直接访问嵌套方法内部。像ReentrantLock,Synchronized就是典型的非公平的可重入锁。可重入锁最大的做用就是能够避免死锁。
可重入锁代码实例
package com.example.thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentryLock{ /** * 对于 ReentrantLock 可重入锁 */
private Lock lock=new ReentrantLock(); private void getInfo(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"==获取信息"); getDetails(); } finally{ lock.unlock(); } } private void getDetails(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"==获取详情"); } catch (Exception e) { e.printStackTrace(); }finally{ lock.unlock(); } } /** * 对于synchronized可重入锁 */
private synchronized void getSMS(){ System.out.println(Thread.currentThread().getName()+"==开始发送短信!"); getEmail(); } private synchronized void getEmail(){ System.out.println(Thread.currentThread().getName()+"==开始发送邮件!"); } public static void main(String[] args) { ReentryLock reentryLock=new ReentryLock(); new Thread(()->{ reentryLock.getSMS(); },"t1").start(); new Thread(()->{ reentryLock.getInfo(); },"t2").start(); } }
是指读取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗。
缺点是循环会消耗CPU
package com.example.thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class SpinLock { private AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t into lock"); while (!atomicReference.compareAndSet(null, current)) { } } public void unlock() { Thread current = Thread.currentThread(); atomicReference.compareAndSet(current, null); System.out.println(Thread.currentThread().getName()+"\t leave lock"); } public static void main(String[] args) throws InterruptedException { SpinLock spinLock = new SpinLock(); new Thread(()->{ spinLock.lock(); try { TimeUnit.SECONDS.sleep(5); } catch (Exception e) { e.printStackTrace(); }finally{ spinLock.unlock(); } },"thread1").start(); TimeUnit.SECONDS.sleep(1); new Thread(()->{ spinLock.lock(); spinLock.unlock(); },"thread2").start(); } }
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程锁持有
CountDownLatch:当一个或多个线程经过调用await方法进入阻塞状态,等待另一些线程完成各自工做以后,再继续执行。使用一个计数器进行实现,计数器初始值为线程的数量。当每个线程调用countDown方法完成本身任务后,计数器的值就会减一。当计数器的值为0时,表示全部的线程都已经完成一些任务,而后唤醒哪些由于调用了await而阻塞的线程去执行接下来的任务。
CyclicBarrier:它的功能是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障,这时全部被屏障拦截的线程才会继续执行。它经过调用await方法让线程进入屏障。
Semaphore:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另外一个用于并发线程的控制(就是控制同时获得共享资源的线程数量)。
(1)CountDownLatch是作减法,CyclicBarrier是作加法,Semaphor的临界资源能够反复使用。
(2)CountDownLatch不能重置计数,CycliBarrier提供的reset()方法能够重置计数,不过只能等到第一个计数结束。Semaphor能够重复使用。
(3)CountDownLatch和CycliBarrier不能控制并发线程的数量,Semaphor能够实现控制并发线程的数量。
阻塞队列首先是一个队列。
(1)当阻塞队列是空的时候,从队列中获取元素的操做将被阻塞。
(2)当队列是满的时候,往队列里添加元素的操做会被阻塞。
优势:咱们不须要关心何时须要阻塞线程,何时须要唤醒线程,由于这一切BlockingQueue都给你一手包办了。
在concurrent包发布之前。在多线程环境下,咱们每一个程序员都必须去本身控制这些细节,尤为还要兼顾效率和线程
安全,而这会给咱们的程序带来不小的复杂度。
用途:a.生产者消费者模式 b.线程池 c.消息中间件
用if会形成虚假唤醒 , 详情能够百度。
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协做,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协做更加安全和高效。所以一般来讲比较推荐使用Condition,阻塞队列其实是使用了Condition来模拟线程间协做。
(1)原始构成:synchronized是JVM层面的,底层经过monitorenter和monitorexit来实现的。Lock是JDK API层面的。(synchronized一个enter会有两个exit,一个是正常退出,一个是异常退出(保证确定能够退出))
(2)使用方法:synchronized不须要手动释放锁,而Lock须要手动释放,若没有主动释放锁就可能致使出现死锁现象。
(3)是否可中断:synchronized不可中断,除非抛出异常或者正常运行完成。Lock是可中断的。
a.设置超时方法tryLock(long timeout,TimeUnit unit);
b. lockInterruptibly()方法放代码块中,调用interrupt()
(4)是否为公平锁:synchronized只能是非公平锁,而ReentrantLock既能是公平锁,又能是非公平锁。构造方法传入false/true,默认是非公平锁false。
(5)绑定多个条件Condition:synchronized不能,只能随机唤醒。而Lock能够经过Condition来绑定多个条件,精确唤醒。
线程池作的工做主要是控制运行的线程的数量,处理过程当中将任务放入队列,而后在线程建立后启动这些任务,若是线程数量超过了最大数量,则超出数量的线程排队等候,等待其余线程执行完毕,再从队列中取出任务来执行。
特色:线程复用;控制最大并发数;管理线程。
优点:
1.下降资源消耗。经过重复利用已经重建的线程下降线程建立和销毁形成的消耗。
2.提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。
3.提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。
Java中线程池是经过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
Executors.newFixedThreadPool(int) 建立一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
Executors.newSingleThreadPool() 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newScheduledThreadPool(int n) 建立一个定长线程池,支持定时及周期性任务执行。
Executors.newCachedThreadPool() 可缓存线程池,先查看池中有没有之前创建的线程,若是有,就直接使用。若是没有,就建一个新的线程加入池中,缓存型池子一般用于执行一些生存期很短的异步型任务。
7大参数
int corePoolSize:线程池中核心线程数的最大值
int maximumPoolSize:线程池中能拥有最多线程数
long keepAliveTime:表示空闲线程的存活时间。当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。
TimeUnit unit:表示keepAliveTime的单位
BlockingQueue<Runnable> workQueue:用于缓存任务的阻塞队列,被提交但还没有被执行的任务
ThreadFactory threadFactory:用于生成线程池中工做线程的线程工厂,用于建立线程通常用默认的便可。
RejectedExecutionHandler handler: 拒绝策略,表示当队列满了而且工做线程大于等于线程池的最大线程数时如何来拒绝。
底层工做原理
1.当经过线程池建立好线程以后,当任务请求过来时会被核心线程去处理。
2.随着请求的增多,若是核心线程已经被占满了没有时间去处理过来的请求,这时候会将这些请求放到阻塞队列中等待。
3.若是请求还在进一步增多,阻塞队列的空间都已经被占满了。这时候会开启新的线程直到线程数达到线程池中能拥有最多线程数,去处理请求。
4.若是请求仍是进一步增多,阻塞队列也满了。而且工做线程等于线程池的最大线程数,这时候会启用拒绝策略。
等待队列已经满了再也塞不下新任务了,同时线程池中的最大线程数也达到了,没法继续为新任务服务,这时候咱们就须要拒绝策略机制合理的处理这个问题。
四种拒绝策略:
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。
必须处理好抛出的异常,不然会打断当前的执行流程,影响后续的任务执行。
CallerRunsPolicy - "调用者运行"一种调用机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而下降新任务的流量。
可是,因为调用者本身运行任务,若是任务提交速度过快,可能致使程序阻塞,性能效率上必然的损失较大
DiscardPolicy - 直接丢弃,不予任何处理也不抛出异常。
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入队列中尝试再次提交新任务。
JDK 提供的线程池一个都不用。
1.newFixedThreadPool()、newSingleThreadExecutor() 底层代码 中 LinkedBlockingQueue 没有设置容量大小,默认容许的请求队列长度是 Integer.MAX_VALUE, 能够认为是无界的。线程池中 多余的线程会被缓存到 LinkedBlockingQueue中,最终内存撑爆。
2.newCachedThreadPool()、newScheduledThreadPool() 的 底层代码中的最大线程数(maximumPoolSize) 是Integer.MAX_VALUE,能够认为是无限大,若是线程池中,执行中的线程没有及时结束,而且不断地有线程加入并执行,最终会将内存撑爆。
建议经过ThreadPoolExecutor去建立线程池。
我会根据个人业务是CPU密集型仍是IO密集型来作决定。
CPU密集型:意思是该任务须要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能获得加速(经过多线程)。而在单核CPU上,不管你开几个模拟的多线程该任务都不可能获得加速,由于CPU总的运算能力就那些。CPU密集型任务配置尽量少的线程数量。
通常公式: CPU核数+1个线程的线程池
IO密集型:即任务须要大量的IO,即大量的阻塞。IO密集型时,大部分线程都阻塞,故须要都配置线程数:
参考公式: CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。
好比8核CPU:8/1-0.9=80个线程数
注意:必须熟悉本身的硬件,看服务器是4核仍是8核
死锁就是两个或两个以上的线程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,
若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁。
产生死锁的缘由:
(1)竞争系统资源 (2)进程的推动顺序不当 (3)资源分配不当
解决死锁
(1)jps命令定位进程号 (2)jstack找到死锁查看