java并发编程学习整理-高效并发理论知识点总结 weir 2018-04-23 10:23:45.0 java,并发 2040 Java的高并发应该是在jdk1.4,1.5开始的,比核心还是IO的多路复用技术或者是IO的异步处理技术,我想现在已经铺天盖地的可以找到这些资料。 在往深处追究就是操作系统层面的封装才使得各种语言的高效,我之前也写过几篇文章啰嗦好多次了,语言层面的高性能离不开底层技术的发展和支持(封装)。 慢慢来吧,你越了解底层的东西你越在语言层面上得心应手。 高效并发 n Java内存模型 JVM规范定义了一种Java内存模型,用来屏蔽各种硬件和操作系统对内存访问的差异。形如 1:所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存 2:工作内存中保存该线程使用到的变量的主内存副本拷贝 3:线程对变量的所有操作都应该在工作内存中完成 4:不同线程不能相互访问工作内存,交互数据要通过主内存 n 内存间交互操作 Java内存模型规定了如下操作来实现内存间交互,JVM会保证它们是原子的。 1:lock:锁定,把变量标识为线程独占,作用于主内存变量 2:read:读取,把变量值从主内存读取到工作内存 3:load:载入,把read独取到的值放入工作内存的变量副本中 4:use:使用,把工作内存中一个变量的值传递给执行引擎 5:assign:赋值,把从执行引擎接收到的值赋给工作内存里面的变量 6:store:存储,把工作内存中一个变量的值传递到主内存中 7:write:写入,把store进来的数据存放如主内存的变量中 8:unlock:解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量 n 内存间交互操作的规则 1:不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但 没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指 令的。 2:不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变 化同步回主内存。 3:不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回 主内存中。 4:一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化 (load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执 行过了assign和load操作。 5:一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线 程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。 6:如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变 量前,需要重新执行load或assign操作初始化变量的值。 7:如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去 unlock一个被其他线程锁定的变量。 8:对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操 作)。 我想这个过程你知道的不多,现在网上的文章涉及到这个层面的不多,反正我看到的的几乎没有。 n 多线程中的可见性 所谓可见性,就是一个线程修改了变量,其他线程可以知道。 1:保证可见性的常见方法 volatile synchronized (unlock之前,写变量值回主存) final(一旦初始化完成,其他线程就可见) n volatile volatile基本上是JVM提供的最轻量级的同步机制。 1:用volatile修饰的变量,对所有的线程可见,即对volatile变量所做的写操作能立即反映 到其它线程中。 2:用volatile修饰的变量,在多线程环境下仍然是不安全的 3:适合使用valatile的场景: (1)运算结果不依赖变量的当前值 (2)或者能确保只有一个线程修改变量的值 (3)变量不需要与其他的状态变量共同参与不变约束 4:volatile修饰的变量,是禁止指令重排优化的 给出一个例子说说volatile在多线程下的不安全性,大家用起来还是要注意的: package weir.thread; public class T1 { private volatile int t; public synchronized void num() { t++; } public int getNum() { return t; } } package weir.thread; public class TRun implements Runnable { private T1 t; private String name; public TRun(T1 t1, String name1) { t = t1; name = name1; } @Override public void run() { for (int i = 0; i < 1000; i++) { t.num(); } } } package weir.thread; public class VoT1 { public static void main(String[] args) throws Exception { T1 t = new T1(); Thread thread1 = new Thread(new TRun(t, "w111")); Thread thread2 = new Thread(new TRun(t, "w222")); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(t.getNum()); } } 大家一运行就知道了,private volatile int t; public synchronized void num() { t++; } synchronized 把它去掉那就是错误的结果 n 指令重排 指的是JVM为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行 的后续指令,避开获取下一条指令所需数据造成的等待。 1:线程内串行语义,不考虑多线程间的语义 2:不是所有的指令都能重拍,比如: 写后读a = 1;b = a; 写一个变量之后,再读这个位置 写后写a = 1;a = 2; 写一个变量之后,再写这个变量 读后写a = b;b = 1; 读一个变量之后,再写这个变量 以上语句不可重排,但是a=1;b=2;是可以重排的 n 指令重排的基本原则 1:程序顺序原则:一个线程内保证语义的串行性 2:volatile规则:volatile变量的写,先发生于读 3:锁规则:解锁(unlock)必然发生在随后的加锁(lock)前 4:传递性:A先于B,B先于C 那么A必然先于C 5:线程的start方法先于它的每一个动作 6:线程的所有操作先于线程的终结(Thread.join()) 7:线程的中断(interrupt())先于被中断线程的代码 8:对象的构造函数执行结束先于finalize()方法 n 多线程中的有序性 在本线程内,操作都是有序的 在线程外观察,操作都是无序的,因为存在指令重排或主内存同步延时 n Java线程安全的处理方法 1:不可变是线程安全的 2:互斥同步(阻塞同步):synchronized、java.util.concurrent.ReentrantLock。目前这 两个方法性能已经差不多了,建议优先选用synchronized,ReentrantLock增加了如下特 性: (1)等待可中断:当持有锁的线程长时间不释放锁,正在等待的线程可以选择放弃等待 (2)公平锁:多个线程等待同一个锁时,须严格按照申请锁的时间顺序来获得锁 (3)锁绑定多个条件:一个ReentrantLock对象可以绑定多个condition对象,而 synchronized是针对一个条件的,如果要多个,就得有多个锁。 3:非阻塞同步:是一种基于冲突检查的乐观锁定策略,通常是先操作,如果没有冲突,操作 就成功了,有冲突再采取其它方式进行补偿处理。 4:无同步方案:其实就是在多线程中,方法并不涉及共享数据,自然也就无需同步了。 n 锁优化之自旋锁与自适应自旋 1:自旋:如果线程可以很快获得锁,那么可以不在OS层挂起线程,而是让线程做几个忙循 环,这就是自旋。 2:自适应自旋:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者 状态来决定。 3:JDK1.7以上已经是内置实现,默认开启 4:如果锁被占用时间很短,自旋成功,那么能节省线程挂起、以及切换时间,从而提升系统 性能 5:如果锁被占用时间很长,自旋失败,会白白耗费处理器资源,降低系统性能 n 锁优化之锁消除 在编译代码的时候,检测到根本不存在共享数据竞争,自然也就无需同步加锁了。 1:通过-XX:+EliminateLocks来开启 2:同时要使用-XX:+DoEscapeAnalysis开启逃逸分析,所谓逃逸分析: (1)如果一个方法中定义的一个对象,可能被外部方法引用,称为方法逃逸 (2)如果对象可能被其它外部线程访问,称为线程逃逸,比如赋值给类变量或者可以在其它 线程中访问的实例变量 n 锁优化之锁粗化 通常我们都要求同步块要小,但一系列连续的操作导致对一个对象反复的加锁和解锁,这会导致 不必要的性能损耗。这种情况建议把锁同步的范围加大到整个操作序列 n 锁优化之轻量级锁 轻量级是相对于传统锁机制而言,其本意是没有多线程竞争的情况下,减少传统锁机制使用OS实 现互斥所产生的性能损耗。 1:其实现原理很简单,就是类似乐观锁的方式 2:如果轻量级锁失败,表示存在竞争,升级为重量级锁,导致性能下降 n 锁优化之偏向锁 偏向锁是在无竞争情况下,直接把整个同步消除了,连乐观锁都不用,从而提高性能。 1:所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程 2:只要没有竞争,获得偏向锁的线程,在将来进入同步块,也不需要做同步 3:当有其它线程请求相同的锁时,偏向模式结束 4:如果程序中大多数锁总是被多个线程访问的时候,也就是竞争比较激烈,偏向锁反而会降低性能 5:使用-XX:-UseBiasedLocking来禁用偏向锁,默认是开启的 6:使用-XX:BiasedLockingStartupDelay来设置JVM启动后多长时间启动偏向锁 n JVM中获取锁的步骤 1:会先尝试偏向锁 2:然后尝试轻量级锁 3:再然后尝试自旋锁 4:最后尝试普通锁,使用OS互斥量在操作系统层挂起 n 同步代码的基本规则 1:尽量减少锁持有的时间 2:尽量减小锁的粒度 这些就先总结到这里,以后学习API会慢慢用到这些。