从Java多线程基础到Java内存模型;从synchronized关键字到Java并发工具包JUC。html
咱们不生产知识,咱们只作知识的搬运工!算法
线程与进程的不一样点:编程
起源不一样。先有进程后有线程。因为处理器的速度远远大于外设,为了提高程序的执行效率,才诞生了线程。segmentfault
概念不一样。进程是具备独立功能的程序运行起来的一个活动,是操做系统分配资源和调度的一个独立单位;线程是CPU的基本调度单位。数组
内存共享方式不一样。不一样进程之间的内存数据通常是不共享的(除非采用进程间通讯IPC);同一个进程中的不一样线程每每会共享:缓存
拥有的资源不一样。线程独有的内容包括:安全
进程和线程的数量不一样。数据结构
线程和进程建立的开销不一样。多线程
Java中没有协程的概念,协程每每指程序中的多个线程能够映射到操做系统级别的几个线程,Java中的线程数目与操做系统中的线程数目是一一对应的。架构
建立线程只有一种方式就是构造Thread类。实现线程的执行单元有两种方式:
从3个角度能够获得实现Runnable接口来完成多线程编程优于继承Thread类的完成多线程编程:
同步与异步:
同步是指被调用者不会主动告诉被调用者结果,须要调用者不断的去查看调用结果
异步是指被调用者会主动告诉被调用者结果,不须要调用者不断的去查看调用结果
复制代码
线程的正确启动与中止:
线程的正确启动方法是start()而不是run()。start()方法的本质是请求JVM来运行当前的线程,至于当前线程什么时候真正运行是由线程调度器决定的。start()方法的内部实现主要是包括三个步骤:一是检查要启动的新线程的状态,二是将该线程加入线程组,三是调用线程的native方法start0()。
线程的正确中止方法是:使用interrupt()来通知,而不是强制结束指定线程。
public class JavaDemo implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
System.out.println("go");
interrupt();
}
}
public void interrupt() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("出现异常,记录日志而且中止");
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new JavaDemoSi());
thread.start();
thread.sleep(1000);
thread.interrupt();
}
}
复制代码
线程的六种生命周期:
常见方法:
wait()方法:在同步代码块synchronized(object){}
中的线程A已经获取到锁时,其余线程不能获取当前锁从而会阻塞进入BLOCKED状态;当线程A执行object.wait()
时,线程A持有的锁会释放,此时其余线程获取到object锁;其余线程代码中执行了object.notify()
方法时,线程A会从新获取到object锁,能够进行线程的调用。
注意notify()、notifyAll()方法必需要在wait()方法以后调用,若顺序改变则程序会进入永久等待。
park()方法:在线程中调用LockSupport.park()进行线程的挂起,在其余线程中调用LockSupport(已挂起的线程对象)进行线程的唤醒。park()和unpark()是基于许可证的概念存在的,只要调用了unpark()在一次park()中就能够实现线程的一次唤醒(这里的一次是指线程只要调用了park()就要调用unpark(),不能实现调用屡次unpark()后面的park()屡次调用就能够直接实现线程的唤醒),park()和unpark()没有调用顺序的限制。
注意park()、unpark()方法不是基于监视器锁实现的,与wait()方法不一样,park()只会挂起当前线程并不会对锁进行释放。在线程中使用synchronized关键字的内部调用了park()容易致使死锁。
几个常见特性: 原子性、内存可见性和重排序。
原子性:
原子(Atomic)操做指相应的操做是单一不可分割的操做。 在多线程中,非原子操做可能会受到其余线程的干扰,使用关键字synchronized
能够实现操做的原子性。synchronized
的本质是经过该关键字所包括的临界区的排他性保证在任何一个时刻只有一个线程可以执行临界区中的代码,从而使的临界区中的代码实现了原子操做。
内存可见性:
CPU在执行代码时,为了减小变量访问的时间消耗会将代码中访问的变量值缓存到CPU的缓存区中,代码在访问某个变量时,相应的值会从缓存中读取而不是在主内存中读取;一样的,代码对被缓存过的变量的值的修改可能仅仅是写入缓存区而不是写回到内存中。这样就致使一个线程对相同变量的修改没法同步到其余线程从而致使了内存的不可见性。
可使用synchronized
或volatile
来解决内存的不可见性问题。二者又有点不一样。synchronized
仍然是 经过将代码在临界区中对变量进行改变,而后使得对稍后执行该临界区中代码的线程是可见的。volatile
不一样之处在于,一个线程对一个采用volatile关键字修饰的变量的值的更改对于其余使用该变量的线程老是可见的,它是经过将变量的更改直接同步到主内存中,同时其余线程缓存中的对应变量失效,从而实现了变量的每次读取都是从主内存中读取。
指令重排序:
在CPU多级缓存场景下,当CPU写缓存时发现缓存区正在被其余CPU占用,为了提升CPU处理性能,可能将后面的读缓存命令优先执行。运行时指令重排要遵循as-if-serial语义,即无论怎么重排序,单线程程序的执行结果不能改变而且编译器和处理器不会对存在的数据依赖关系的操做作重排序。
指令的重排序致使代码的执行顺序改变,这常常会致使一系列的问题,好比在对象的建立过程当中,指令的重排序使得咱们获得了一个已经分配好的内存而对象的初始化并未完成,从而致使空指针的异常。volatile
关键字能够禁止指令的重排序从而解决这类问题。
总之,synchronized
能够保证在多线程中操做的原子性和内存可见性,可是会引发上下文切换;而volatile
关键字仅能保证内存可见性,可是能够禁止指令的重排序,同时不会引发上下文切换。
首先介绍Java内存模型的特性
下面介绍内存模型图
基于JMM,Java提供了多种除了锁以外的同步机制来保证线程安全性。Java提供的TreadLocal以及前面概念中提到的volatile就是两种策略。
下面先介绍volatile关键字,ThreadLocal在下文并发工具类中介绍
volatile最主要的就是实现了共享变量的内存可见性,其实现的原理是:volatile变量的值每次都会从高速缓存或者主内存中读取,对于volatile变量,每个线程再也不会有一个副本变量,全部线程对volatile变量的操做都是对同一个变量的操做。
volatile变量的开销包括读变量和写变量两个方面。volatile变量的读、写操做都不会致使上下文的切换,所以volatile的开销比锁小。可是volatile变量的值不会暂存在寄存器中,所以读取volatile变量的成本要比读取普通变量的成本更高。
volatile常被称为"轻量级锁"。
互斥同步是指多个线程对共享资源是独占的,当一个线程得到共享资源时,其余全部的线程都将处于等待获取状态,不一样线程之间是敌对的。
根据不一样的分类标准存在多种锁类型,对于一种肯定的锁能够同时属于下面的多种类型:
多个线程可否共享一把锁:能够实现共享的称为共享锁;不能够实现共享的称为排他锁。共享锁又称为读锁,每个线程均可以获取到读锁,以后能够查看数据可是没法修改和删除数据。
synchronized属于排他锁**。 **
ReentrantReadWriteLock`同时具有共享锁和排他锁,其中读锁是共享锁,写锁是排他锁。
线程要不要锁住同步资源:锁住同步资源的称为悲观锁(又称为互斥同步锁);不锁住同步资源的称为乐观锁(又称为非互斥同步锁)。
优缺点:
悲观锁的性能相对较低:当发生长时间锁等不到释放或者直接出现死锁时,等待锁的线程永远得不到执行;同时悲观锁存在阻塞和唤醒这两种状态都是会消耗资源的;此外使用了悲观锁,线程的优先级属性设置将会失效。
相对于悲观锁而言,乐观锁性能较高,可是若是获取锁的线程数量过多,那么乐观锁会产生大量的无用自旋等消耗,性能也会所以而降低
悲观锁适用于并发写入多或者临界区持锁时间比较长的情形
乐观锁适用于并发写入少、并发读取多的情形
synchronized
和Lock
都属于悲观锁。 原子类和并发容器工具都采用了乐观锁的思想
乐观锁基于CAS算法实现。
CAS算法:
CAS(Compare and Swap),即比较并交换。
CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。
固然CAS除了有上面提到的乐观锁的缺点外,CAS还容易出现ABA问题。便可能存在其余线程修改过预期值执行过其余操做以后又写会预期值,这样反而不会被察觉。解决ABA问题的一个好方式就是增长版本号version字段,经过每次更新操做都修改version字段以及每次更新以前都检查version字段来保证线程执行的安全性
同一个线程是否能够重复获取同一把锁:能够重复获取的称为可重入锁;不能够重复获取的称为不可重入锁
可重入锁能够有效的避免死锁,当一个线程获取到锁时,能够继续获取该锁,而不会出现当前线程等待当前线程释放锁这一状况的发生。
synchronized
和ReentrantLock
都属于可重入锁。
多个线程竞争时根据是否排队:经过排队来获取的称为公平锁;先尝试插队,插队失败再排队的称为非公平锁
ReentrantLock既能够实现公平锁又能够实现非公平锁,经过指定ReentrantLock构造方法中fair的参数值来实现公平与非公平的效果
是否能够响应中断:可响应中断的称为可中断锁;不可响应中断的称为非可中断锁
等锁的过程不一样:等锁的过程当中若是不停的尝试而非阻塞称为自旋锁;等锁的过程当中若是阻塞等待称为非自旋锁
方法锁,即默认锁对象为this当前实例对象。同一个实例对象下的实例方法共享同一把锁,不一样的实例对象的实例方法锁不一样。
class SynchronizedDemo1 {
public synchronized void index1() {
//do something...
}
public synchronized void index2() {
//do something...
}
}
class SynchronizedDemo2 {
public synchronized void index1() {
//do something...
}
public synchronized void index2() {
//do something...
}
}
复制代码
以上代码中,SynchronizedDemo1实例对象demo1的方法index1和index2共享同一把锁,SynchronizedDemo2实例对象demo1的方法index1和index2共享同一把锁,多个线程访问同一个对象下的synchronized修饰的方法时是互斥同步的,访问不一样对象的synchronized修饰的方法互不干扰
同步代码块锁,即本身指定锁对象。
class SynchronizedDemo1 {
public synchronized void index() {
synchronized(this){
//do something...
}
}
}
复制代码
以上代码中,只有得到了当前对象锁的线程才能执行同步代码块中的代码,同步代码块的出现是为了减少方法锁的粒度,提升性能
synchronized修饰静态的方法。多个线程访问同一类的不一样实例对象的静态方法时,因为静态方法是类级别的而不是对象级别的,因此即使是不一样对象,方法之间的访问也是互斥同步的
指定的锁为Class对象。
class SynchronizedDemo1 {
public synchronized void index() {
synchronized(SynchronizedDemo1.class){
//do something...
}
}
}
复制代码
以上代码中,只有得到了当前类的Class对象锁的线程才能执行同步代码块中的代码,同步代码块的出现是为了减少方法锁的粒度,提升性能
在jdk1.5以后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,Lock接口提供了与synchronized关键字相似的同步功能,但须要在使用时手动获取锁和释放锁,也正由于如此,基于Lock接口实现的锁具有更好的可操做性。
Lock接口中的方法:
lock()
: 此方法用于获取锁,若是锁已被其余线程获取,那么线程进入等待状态,与synchronized不一样的是:当获取到锁而且在执行任务中发生了异常,synchronized会自动释放锁而lock()方法获取到的锁不会自动释放。使用lock()必须在try...finally...中手动释放。tryLock()
:因为lock()不能被中断,因此一旦陷入死锁,lock()就会陷入永久等待中;tryLock()方法是一种更为优雅的使用方式,tryLock()用来尝试获取锁,若是当前锁没有被其余线程占用,那么获取锁成功并马上返回true,不然马上返回false表示获取锁失败。ReetrantLock 是基于Lock接口最通用的实现,在上文中在介绍锁分类时也已经屡次提到过ReentrantLock,所以也了解过其许多特性,因为ReentrantLock很是值得深刻探究,在此也不在一文中过多阐述,在此给出一个连接进行参看:
[深刻ReentrantLock]blog.csdn.net/fuyuwei2015…
读写锁是一种改进型的排它锁。读写锁容许多个线程能够同时读取(只读)共享变量。读写锁是分为读锁和写锁两种角色的,读线程在访问共享变量的时候必须持有相应读写锁的读锁,并且读锁是共享的、多个线程能够共同持有的;写锁是排他的,以一个线程在持有写锁的时候,其余线程没法得到相应锁的写锁或读锁。总之,读写锁经过读写锁的分离从而提升了并发性。 ReadWriteLock接口是对读写锁的抽象,其默认的实现类是ReentrantReadWriteLock。ReadWriteLock定义了两个方法readLock()和writeLock(),分别用于返回相应读写锁实例的读锁和写锁。这两个方法的返回值类型都是Lock。
关于ReentrantReadWriteLock实现,这里给出一个连接参看: [ReentrantReadWriteLock详解]www.cnblogs.com/xiaoxi/p/91…
读写锁主要用于读线程持有锁的时间比较长的情景下。
非互斥同步指的是不一样的线程不对共享资源进行独占,不一样的线程均可以访问共享资源,只不过当多个线程同时对一个共享变量进行修改或删除时,只有一个线程的操做能成功其余的都会失败。
Java中的原子类分为6种,分别有:
直接使用Java中的原子类进行操做便可在并发状况下保证变量的线程安全,原子类相较于锁粒度更小,性能更高。原子类也是基于CAS算法来实现的,其都包括compareAndSet()方法即为先比较当前值是否等于预期的值而后进行数据的修改从而保证了变量的原子性。
须要注意的是累加器LongAdder是Java8开始引入的,相较于AtomicLong,因为LongAdder在每一个线程操做的过程当中并不会实时的进行数据同步(因为上文所提到的JMM,AtomicLong会实时的进行多个线程之间的数据通讯),因此效率更高。而LongAccumulator扩展了LongAdder使得原子变量不只只能进行累加操做也能够进行其余指定公式的计算
Java中并发容器由来已久,固然并发容器的种类也很是多。可是其中一部分诸如Vector、Hashtable、Collections.synchronizedList()、Collections.synchronizedMap()等底层是基于synchronized来实现的并发同步,效率会比较低,因此即便这些容器能够保证线程安全也再也不使用。与之相替代的就是下面的几种并发容器类,因为并发容器在实现上也有许多可学习之处,因此这里再也不在一文中介绍而是会初步引入,并放上我认为比较不错的几个博客连接,这样能够更好的深刻理解。
多个线程往HashMap中同时进行put(),若是有几个线程计算出的键的散列值相同,那么就会出现key丢失的状况,一样的,若是此时HashMap容量不够,多个线层同时扩容,也会只保留一个扩容后的Map,从而致使数据丢失。而ConcurrentHashMap则在底层数据结构的实现上与HashMap又有所区别,避免了HashMap会产生的问题。
关于ConcurrentHashMap的数据结构能够参看: [ConcurrentHashMap的数据结构]blog.csdn.net/weixin_4446…
为了保证List的线程安全,又要避免因使用Vector、Collections.synchronized等而产生的锁粒度过大而形成效率下降的问题,CopyOnWriteArrayList、CopyOnWriteArraySet应运而生,CopyOnWriteArrayList和CopyOnWriteArraySet在实现原理上大致一致,这里只给出CopyOnWriteArrayList的介绍.
关于CopyOnWriteArrayList的数据结构能够参看: [CopyOnWriteArrayList的数据结构]www.cnblogs.com/chengxiao/p…
BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。经过这些高效而且线程安全的队列类,为咱们快速搭建高质量的多线程程序带来极大的便利。在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,这些阻塞队列的实如今Java并发编程中常常要用到,其中最经常使用的就是ArrayBlockingQueue和LinkedBlockingQueue
关于BlockingQueue能够参看: [BlockingQueue相关]segmentfault.com/a/119000001…
关于ArrayBlockingQueue能够参看: [ArrayBlockingQueue相关]blog.csdn.net/u014799292/…
关于LinkedBlockingQueue能够参看: [LinkedBlockingQueue相关]blog.csdn.net/tonywu1992/…
ConcurrentLinkedQueue是一个基于连接节点的非阻塞无界线程安全队列。
关于ConcurrentLinkedQueue的数据结构能够参看: [ConcurrentLinkedQueue的数据结构]blog.csdn.net/qq_38293564…
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程能够根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
ThreadLocal采用的是上述策略中的第一种设计思想——采用线程的特有对象.采用线程的特有对象,咱们能够保障每个线程都具备各自的实例,同一个对象不会被多个线程共享,ThreadLocal是维护线程封闭性的一种更加规范的方法,这个类能使线程中的某个值与保存值的对象关联起来,从而保证了线程特有对象的固有线程安全性。
ThreadLocal类至关于线程访问其线程特有对象的代理,即各个线程经过这个对象能够建立并访问各自的线程特有对象,泛型T指定了相应线程持有对象的类型。一个线程可使用不一样的ThreadLocal实例来建立并访问其不一样的线程持有对象。多个线程使用同一个ThreadLocal实例所访问到的对象时类型T的不一样实例。代理的关系图以下:
ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,所以get老是能返回由当前执行线程在调用set时设置的最新值。其主要使用的方法以下:
public T get(): 获取与当前线程中ThreadLocal实例关联的线程特有对象。
public void set(T value):从新关联当前线程中ThreadLocal实例所对应的线程特有对象。
protected T initValue():若是没有调用set(),在初始化threadlocal对象的时候,该方法的返回值就是当前线程中与ThreadLocal实例关联的线程特有对象。
public void remove():删除当前线程中ThreadLocal和线程特有对象的关系。
复制代码
那么ThreadLocal底层是如何实现Thread持有本身的线程特有对象的?查看set()方法的源代码:
每个Thread都会维护一个ThreadLocalMap对象,ThreadLocalMap是一个相似Map的数据结构,可是它没有实现任何Map的相关接口。ThreadLocalMap是一个Entry数组,每个Entry对象都是一个"key-value"结构,并且Entry对象的key永远都是ThreadLocal对象。当咱们调用ThreadLocal的set方法时,实际上就是以当前ThreadLocal对象自己做为key,放入到了ThreadLocalMap中。
可能发生内存泄漏:
经过查看Entry结构可知,Entry属于WeakReference类型,所以Entry不会阻止被引用的ThreadLocal实例被垃圾回收。当一个ThreadLocal实例没有对其可达的强引用时,这个实例就能够被垃圾回收,即其所在的Entry的key会被置为null,可是若是建立ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,从而发生内存泄露。
解决内存泄漏的最有效方法就是,在使用完ThreadLocal以后,要注意调用threadlocal的remove()方法释放内存。
传统的Runnable来实现任务有两大缺陷,一个是Runnable中的run()没有返回值,另外一个是Runnable中的run()没法抛出异常。为了解决上述问题,Callable应运而生,而Future是为了更好的操做Callable实现业务逻辑而诞生的。
咱们能够用Future.get来获取Callable接口返回的执行结果,还能够经过Future.isDone()来判断任务是否已经执行完了以及取消这个任务,限时获取任务的结果等等。
线程池提供了复用线程的能力,若是不使用线程池,那么每一个任务都会新开一个线程,上文基石中也已经提到Java代码中的线程数量对应于操做系统的线程数量,这样对于线程的建立和销毁都会带来很大的开销,此外系统可建立的线程数量是有限的,使用线程池能够有效避免OOM等异常。
线程池的建立通常借助ThreadPoolExecutor
这个类,其中有5个参数比较关键,如下说明:
corePoolSize、maxPoolSize、workQueue
:线程池中默认存在的线程数量是corePoolSize,当任务多于corePoolSize时,新来的任务会首先存储在任务存储队列workQueue
中,当任务数量超出了任务存储队列的最大长度,线程池才会扩大其中的线程数量直到maxPoolSize
,当任务数量超出maxPoolSize
,线程池执行定义的拒绝策略handler
。
workQueue的三种经常使用类型:
1.SyncbronousQueue:最简单的直接交换队列,这队列长度为0不能存储新的任务,适用与任务不太多的场景,此外因为队列不能存储任务线程池很容易建立新的线程,因此maxPoolSize要设置的大一点,可是若是设置的maxPoolSize过大,线程建立的过多而不能获得调度从而产生堆积,就会引起OOM。Executors.newCachedThreadPool()、Executors.newScheduledThreadPool()
即为这种类型,其中Executors.newCachedThreadPool()的maxPoolSize这里设置的为Integer.MAX_VALUE,corePoolSize默认为0,keepAliveTime为60s
2.LinkedBlockingQueue:无解队列,这个相较于第一种队列属于另外一个极端,能够存储任意数量的任务。此类队列能够存储较多数量的任务而且此时maxPoolSize会失效,可是此时也要注意任务过多时会产生堆积出现OOM。Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()
即为这种类型
3.ArrayBlockingQueue:有界队列,能够设置队列长度,此时maxPoolSize有效
keepAliveTime
:若是线程池当前的线程数量多余corePoolSize
,那么当多余线程的空闲时间超过keepAliveTime
时,它们将被回收。
ThreadFactory
:线程池中新建立的线程是由ThreadFactory
建立的,默认使用Executors.defaultThreadFactory()
。
线程池应该手动建立,其中:
当任务属于CPU密集型时,线程池中的线程数量应该设置为CPU核心数的1-2倍;当任务属于资源密集型时,线程池中的线程数量通常设置为cpu核心数的不少倍,计算方法通常为num=CPU核心数*(1+平均等待时间/平均工做时间)
线程池中止:
shutdown()
:调用此方法后,线程池并不会马上中止而是拒绝接受新的任务并等待线程池中已在执行的线程任务和队列中的任务执行完毕 shutdownNow()
:调用此方法后,线程池经过调用terminated()方法来终止正在执行的线程同时将队列中未被调度的任务以集合的形式返回。
到此为止,本文要梳理的Java并发相关也告一段落,之因此如此说是由于Java并发相关确实是值得深刻探究的一个领域,本文的定位是基于Java来梳理并发相关的那些事儿,尽量经过一篇文章来概括出Java并发中应该掌握的知识点。 本文仍然有不少不足之处,好比文中没有介绍Java的并发工具类诸如CountdownLatch、Semaphore等,而关于ReentrantLock这种重要的锁的实现原理AQS本文也没有介绍,但愿在以后的文章中能对本文略过的点进行深刻的概括总结。