【Java】Java并发编程
文章主要目的帮助开发人员创建安全和高性能的并发类,提供各种实用设计规则,同时更加轻松应对并发编程相关面试。
简介
线程是实现并发的基础,能使复杂的异步代码变得更简单,极大简化了复杂系统的开发。充分发挥多处理器系统的强大功能。
操作系统为各个独立执行的进程分配各种资源,包括内存、文件句柄以及安全证书等。如果需要的话,在不同的进程之间可以通过一些粗粒度的通信机制来交换数据,包括:套接字、信号处理器、共享内存、信号量以及文件等。
需要多个程序同时执行的原因
1. 资源利用性
2. 公平性
3. 便利性
线程允许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如文件句柄、内存句柄,但每个线程有独立程序计数器、栈以及局部变量等。
线程又称轻量级进程,大多数操作系统中都是以线程为基本的调度单位。
线程的优势
1. 发挥多处理器的强大功能。多线程能在单处理器系统上获得更高的吞吐率
2. 建模的简单性。为模型中的每类人物分配一个专门的线程,形成串行执行的假象,并将其他问题分离开来。例如Servlet和RMI(Remote Method Invocation)
3. 异步事件的简化处理。非阻塞IO(select、poll等系统调用)
4. 响应更灵敏的用户界面。现代GUI框架,AWT和Swing,采用事件分发线程来替代主事件循环
风险
1. 安全性问题。多线程操作执行顺序时不可预测的,需要同步解决;共享变量访问操作协同
2. 活跃性问题。正确的事一直不发生,死锁、无限循环等
3. 性能问题。服务时间、响应灵敏、吞吐率过低、资源消耗过高、可伸缩性较低、性能开销(上下文切换、同步机制)
基础知识
线程安全性
编写线程安全的代码,核心在于对状态访问操作进行管理,特别是对共享的和可变状态的访问。
多线程环境下要使对象是线程安全的需要采用同步机制来协同对对象可变状态的访问。Java中主要同步机制是synchronized提供一种独占的加锁方式,还有volatile类型变量、显示锁以及原子变量。
三种方式修复可变状态变量未同步发生的错误:
1. 不在线程之间共享状态变量
2. 将状态变量修改为不可变变量
3. 在访问状态变量时使用同步
访问变量的代码越少,越容易确保对变量的所有访问都实现正确同步,同时也更容易找出变量在哪些条件下被访问。程序的封装性越好,越容易实现程序的线程安全,代码维护也越容易保持这种方式。
1. 什么是线程安全性
正确性:某个类的行为与其归规范完全一致。
线程安全性:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类始终都能表现出正确的行为。
在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。无状态对象一定是线程安全的。大多数Servlet都是无状态的,从而极大降低了在实现Servlet线程安全时的复杂性。只有当Servlet在处理请求时需要保存一些信息,线程安全才是一个问题。
2. 原子性
++count操作并非原子的,并不会作为一个不可分割的操作来执行。包含了三个独立的操作读取、修改、写入,并且结果状态以来之前的状态。
竞态条件:在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况
2.1 竞态条件
常见的竞态条件就是先检查后执行
2.2 延迟初始化中的竞态条件
常见的先检查后执行竞态条件:延迟初始化
@NotThreadSafe
public class LazyInitRace {private Object instance = null;public Object getInstance() {if (instance == null) {instance = new Object();}return instance;}
}
2.3 复合操作
包含一组必须以院子方式执行的操作以确保线程安全性。
//计数器可以用atomic包来确保原子性.
private final AtomicLong count = new AtomicLong(0);
count.incrementAndGet();
3. 加锁机制
当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
要保持一致性,就需要在单个原子操作中更新所有相关的状态变量。
3.1 内置锁
同步代码块一种内置的锁机制保证原子性,包括两块:锁的对象引用,由这个锁保护的代码块。静态的锁以Class对象作为锁。
Java的内置锁相当于一种互斥体,最多只有一个线程能持有这种锁。
多个客户端无法同时使用,服务的响应非常低,会有性能问题。
3.2 重入
同一线程多次获得同一个锁,意味着锁操作的粒度是线程而不是调用。计数器和所有者线程实现。
重入进一步提升了加锁行为的封装性,同时避免了特定情况死锁的发生。
4. 用锁来保护状态
锁能使其保护的代码以串行形式访问
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,称状态变量是由这个锁保护的。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。
为什么不把所有方法作为同步方法?不一定能实现原子性,还会有活跃性问题、性能问题
不存在则添加竞态条件需要在多个已同步方法上加锁
5. 活跃性与性能
缩小同步代码块的范围,很容易做到即确保并发性,又维护线程安全。
不要将原子操作拆分到多个同步代码块,尽量将不影响共享状态且执行时间较长的操作从代码块中分离出去。
atomic和同步代码块不要一起使用,两种不同的同步机制会带来混乱,也不会在性能或安全性上带来任何好处。
简单性与性能之间存在着相互制约因素,当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性。
当执行时间较长的计算或者无法快速完成的操作时(网络IO、控制台IO)一定不要持有锁。
对象的共享
synchronized实现原子性,以及内存可见性
1. 可见性
同步机制来确保多个线程之间堆内存写入操作的可见性
重排序:java内存模型允许编译器对操作顺序重排序,并将数值缓存在寄存器之中。允许CPU对操作顺序重排序,并将数值缓存在处理器特定的缓存中
解决办法:只要有数据在多个线程中共享,就使用正确的同步
1.1 失效数据
失效数据不会同时出现,失效数据可能会导致意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环
@NotThreadSafe
public class MutableInteger {private int value;public int getValue() {return value;}public void setValue(int value) {this.value = value;}
}@ThreadSafe
public class MutableInteger {@GuardedBy("this") private int value;public synchronized int getValue() {return value;}public synchronized void setValue(int value) {this.value = value;}
}
1.2 非原子的64位操作
最低安全性:失效数据为前一个线程的数据的安全性保证。适用于绝大多数变量,除了非volatile类型的64位数值变量(double、long)
java内存模型要求,变量的读写操作必须是原子操作,对于非volatile64位变量,jvm允许将64位分为两个32位操作。
1.3 加锁与可见性
访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说是可见的。
加锁的含义不仅局限于互斥行为,还包括内存可见性。
1.4 volatile变量
一种较弱的同步机制,用来确保将变量的更新操作通知到其他线程。
编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取时总会返回最新写入的值。
仅当volatile变量能简化代码的实现以及同步策略的验证时,才应该使用它们。
如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。
volatile的正确使用方式:
1. 确保它们自身的可见性
2. 确保它们所引用对象的状态的可见性
3. 标识一些重要的程序生命周期的发生(初始化或关闭)
volatile boolean asleep;while(!asleep){doSomething();
}
tips:对于服务应用程序,开发测试启动JVM时一定都要指定-server,比client模式进行更多的优化,例如将循环中为修改的变量提升到循环外面。
volatile通常用做某个操作完成、发生中断或者状态的标志。
加锁机制既可以保证可见性又可以保证原子性,volatile只能保证可见性。
当切仅当以下所有条件满足时才使用:
- 对变量的写入操作不依赖当前值,或者能确保只有单个线程更新变量
- 该变量不会与其他状态一起纳入不变性条件中
- 在访问变量时不要加锁
2. 发布与溢出
发布是使对象能够在当前作用域之外的代码中使用。
溢出是指当某个不应该发布的对象被发布。
//发布对象
public static Set<Object> knownSecrets;public void initialize() {knownSecrets = new HashSet<>();
}//溢出
private String[] states = new String[] {"ak","al"};public String[] getStates() {return states;
}
发布对象时,该对象的非私有域中引用的所有对象同样会被发布。
发布一个内部的类实例
//隐式地使this引用溢出
public class ThisEscape {public ThisEscape(EventSource source) {source.registerListener(new EventListener() {public void onEvent(Event e) {doSomething();}});}
}
//不要在构造过程中使this溢出
在构造过程中使this溢出的一个常见错误是,在构造函数中启动一个线程。可以创建线程但最好不要启动它,而是通过start或initialize启动。
在构造过程中调用一个可改写的实例方法时(既不是私有也不是终结方法),同样会导致this引用在构造过程中溢出。
3. 线程封闭
不共享数据,Swing把可视化组件和数据模型封闭到Swing的事件分发线程来实现线程安全性,JDBC中Connection对象封闭在线程中。
3.1 Ad-hoc线程封闭
是指维护线程封闭性的指责完全由程序实现来承担。非常脆弱,通常是因为要将某个特定的子系统实现为一个单线程子系统。
3.2 栈封闭
线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。如果在线程内部上下文中使用非线程安全的对象,那么该对象任然是线程安全的。
3.3 ThreadLocal类
提供了set和get方法,为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前线程在调用set时设置的最新值。
通常用于防止对可变的单实例变量或全局变量进行共享,例如全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。且JDBC非线程安全,通过ThreadLocal保存,每个线程都会有拥有属于自己的连接。
当某个频繁执行的操作需要一个临时变量,例如一个缓冲区,而同时又希望避免在每次执行都重新分配该临时对象,就可以使用这项技术。
类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此要小心使用。
4. 不变性
不可变对象一定是线程安全的。只有一种状态,并且该状态由构造函数来控制。
不可变性并不等于对象中所有域都声明为final类型,即使对象中所有域都是final类型的,这个对象仍是可变的,因为在final类型的域中可以保存对可变对象的引用。
不可变对象满足的条件:
- 对象创建以后其状态就不能改变
- 对象的所有域都是final类型
- 对象是正确创建的(在对象创建期间,this引用没有溢出)
4.1 final域
final类型的域是不可修改的,除非域所引用对象是可变的。
final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。
4.2 用volatile类型发布不可变对象
构造过程中初始化
5. 安全发布
不安全发布可能会导致其他线程看到尚未初始化的对象
5.1 不正确的发布
n != n 抛出AssertionError
5.2 不可变对象与初始化安全性
Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。这种保证还将延伸到被正确创建对象中所有final类型的域。如果final域所指对象是可变对象,那么在访问这些域所指对象仍然需要同步。
5.3 安全发布的常用模式
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。
通过以下方式安全地发布:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final域中
- 将对象的引用保存到一个由锁保护的域中
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
线程安全容器:Hashtable、synchronizedMap、ConcurrentMap、Vector、CopyOnWriteArrayList、BlockingQueue、ConcurrentLinkedQueue
5.4 事实不可变对象
使用和共享对象时的实用策略:
- 线程封闭
- 只读共享
- 线程安全共享
- 保护对象
Java并发编程
一. 并发编程
线程
- 单核CPU设定多线程是否有意义?
有,IO密集型CPU可以让出资源等待时间让其他线程执行。
- 工作线程是不是越大越好?
不是,操作系统资源花在线程切换就越多。
- 工作线程数(线程池线程数量)多少合适?
Ntheads = Ncpu * Ucpu * (1 + W / C)
Ncpu是处理器的核的数目,Runtime.getRuntime().availableProcessors()获得
Ucpu是期望的CPU利用率(0-1)
W/C是等待时间与计算时间的比率,Profiler测算,已部署用Arthas,分布式微服务服务链路问题
- 启动线程的5种方式:
继承Thread类重写run方法;实现Runable接口,变量形式传入Thread类;使用线程池;Callable接口用线程池启动,调用call方法,可通过范性指定返回值;用自己启动的线程,FutureTask传入Callable;
new MyThread().start();new Thread®.start();new Thread(lamda).start();ThreadPool;Future Callable and FutureTask
- 线程的6种状态:
NEW,线程刚创建还没有启动;RUNNABLE(READY,RUNNING),可运行状态,由线程调度器可以安排执行;WAITING,等待被唤醒;TIME WAITING,隔一段时间后自动唤醒;BLOCKED,被阻塞,正在等待锁;TERMINATED,线程结束
- interrupt打断(优雅的终止线程)
interrupt打断某个线程(设置标志位)
isInterrupted查询某线程是否被打断过
static interrupted查询当前线程是否被打断过,并重置打断标志
sleep、wait过程中设置标志位会抛InterruptedException异常,具体处理方法自己定义,同时重制标识位
synchronized锁竞争过程和lock()中不会被理会interrupt标识位
ReentrantLock锁可以被打断,用lockInterruptibly来允许处理interrupt标志位
- 结束线程
自然结束
stop,容易产生数据不一致的问题
suspend暂停,resume恢复,暂停不释放锁的话容易导致死锁
volatile变量作为标志位,不能精确控制量,wait、sleep无法控制
interrupt标志位,能处理wait、sleep,不能精确控制量
并发编程三大特性:可见性、原子性、有序性
- 可见性
volatile保持线程的可见性,System.out.println()里面加锁了,同时会保持可见性,触发本地缓存与主内存的刷新同步
volatile只能保证引用本省的可见性,不能保证内部字段的可见性
缓存行读取,按块读取,一个缓存行64字节
可以把数据加到64字节避免用缓存一致性协议来改善效率,缓存行对齐,disruptor框架用过
jdk1.8 @Contended注解保证标注数据与其他数据不为同一行,需要加jvm参数,-XX:-RestrictContended
MESI Cache一致性协议是缓存一致性协议的一种,Intel的协议
缓存行越大,局部空间效率高,读取满;
缓存行越小,局部空间效率低,读取快。
- 有序性
乱序存在(指令重排)的条件:不影响单线程的最终一致性,语句之间不存在依赖性
对象初始化过程:先创建对象初始化值,然后初始化,指向引用;2和3可能发生指令重排,未经初始化但已引用导致this溢出问题(不要在构造方法里启动线程即可规避问题)
- 原子性
操作不可被打断就是原子操作,上锁可保证原子性
synchronized能保证原子性、可见性(解锁后缓存和内存会进行刷新),不保证有序性
管程又叫monitor,是把锁,synchronized后跟的那把锁;如果临界区执行时间长,语句多,叫做锁的力度比较粗。
synchronized是悲观锁,CAS乐观锁、自旋锁、无锁(cpu在汇编级别支持CAS操作cmpxchg,多核加lock指令),cmpxchg不是原子操作所以加lock
两种锁的效率:
乐观锁自旋消耗cpu资源,悲观锁在队列中等待;
使用场景:临界区执行时间比较长,用重量级锁;时间短用自旋锁
实用:用synchronized锁即可,已优化
- synchronized
用户态内核态:JDK早期重量级锁,申请锁资源必须通过kernel,系统调用,切换状态会调用int 0x80中断
java对象内存布局:HotSpot中,8字节markword,4字节类指针,n字节成员变量(需要8字节对齐,8的整数倍)。JOL Java Object Layout工具可以看内存布局 ,ClassLayout.parseInstance(实例化对象名).toPrintable();
用户空间锁vs重量级锁:
偏向锁 自旋锁都是用户空间完成;
重量级锁是需要向内核申请。
偏向锁:再无锁的基础上更新当前线程的指针以及偏向锁标志(StringBuffer),hashCode存到线程栈里
轻量级锁:CAS线程竞争锁,成功后将改线程Luck Record更新到markword
锁重入:
synchronized是可重入锁,重入次数必须记录为了解锁对应
偏向锁、轻量级锁记录在线程栈,每重入一次多一个Luck Record
重量级锁记录在ObjectMonitor字段上
轻量级锁升级重量级锁:
有线程超过10次自旋,-XX:PreBlockSpin设置,自旋线程超过CPU核数的一半,1.6之后加入自适应自旋Adapative Self Spinning,JVM自己控制
重量级锁的实现:
申请资源的线程放入ObjectMonitor WaitSet-等待队列,节省消耗自旋等待的资源
偏向锁不一定比自旋锁效率高,在有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接用自旋锁。JVM启动过程中,会有很多线程竞争,默认启动时不打开偏向锁,过一段时间打开
-XX:BiasedLockingStartupDelay=0,默认四秒
若未指定markword里的对象则偏向锁已启动(匿名偏向101),否则偏向锁未启动(001)指定markword里普通对象
偏向锁轻度竞争变为轻量级锁,偏向锁重度竞争(耗时过长wait等)变为重量级锁
【Java】Java并发编程相关推荐
- Java 7并发编程实战手册
2019独角兽企业重金招聘Python工程师标准>>> Java 7并发编程实战手册 本书是 Java 7 并发编程的实战指南,介绍了Java 7 并发API 中大部分重要而有用的机 ...
- Java 7 并发编程指南
原文是发表在并发编程网上翻译后的 <Java 7 并发编程指南>,这里对其中的目录做个更加详细的描述,并且写出了重点说明,方便日后快速查阅.建议仔细查看每节的代码实现,非常具有参考价值.可 ...
- 阿里云 刷新缓存 java_【从入门到放弃-Java】并发编程-NIO-Buffer
前言 上篇[从入门到放弃-Java]并发编程-NIO-Channel中我们学习到channel是双向通道,数据通过channel在实体(文件.socket)和缓冲区(buffer)中可以双向传输. 本 ...
- Java Review - 并发编程_ 回环屏障CyclicBarrier原理源码剖析
文章目录 Pre 小Demo 类图结构 CyclicBarrier核心方法源码解读 int await() int await(long timeout, TimeUnit unit) int dow ...
- Java Review - 并发编程_ScheduledThreadPoolExecutor原理源码剖析
文章目录 概述 类结构 核心方法&源码解析 schedule(Runnable command, long delay,TimeUnit unit) scheduleWithFixedDela ...
- Java Review - 并发编程_ArrayBlockingQueue原理源码剖析
文章目录 概述 类图结构 构造函数 主要方法源码解析 offer操作 put操作 poll操作 take操作 peek操作 size 小结 概述 Java Review - 并发编程_LinkedBl ...
- Java Review - 并发编程_LinkedBlockingQueue原理源码剖析
文章目录 概述 类图结构 主要方法 offer操作 概述 Java Review - 并发编程_ConcurrentLinkedQueue原理&源码剖析 介绍了使用CAS算法实现的非阻塞队列C ...
- Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析
文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...
- Java Review - 并发编程_原子操作类LongAdder LongAccumulator剖析
文章目录 概述 小Demo 源码分析 重要的方法 long sum() reset sumThenReset longValue() add(long x) longAccumulate(long x ...
- 操作系统锁的实现方法有哪几种_「从入门到放弃-Java」并发编程-锁-synchronized...
简介 上篇[从入门到放弃-Java]并发编程-线程安全中,我们了解到,可以通过加锁机制来保护共享对象,来实现线程安全. synchronized是java提供的一种内置的锁机制.通过synchroni ...
最新文章
- 第2关:计算二叉树的深度和节点个数
- B树和B+树分别是什么?区别在哪里?MySQL使用的是哪一种树?
- oracle创建表分区表,oracle创建分区表
- mysql报错2_MySQL基于报错注入2
- 【学习笔记】2、Python - Jupyter Notebook界面基础
- Python-OpenCV基本操作cv2
- python判断日期是星期几_python 判断日期是星期几
- 惠普10代的服务器有哪些型号,英特尔官方科普:秒懂十代酷睿型号怎么认!
- for语句 2017-03-17
- ElasticSearch面试 - es 生产集群的部署架构是什么?
- 【论文】使用bilstm在中文分词上的SOTA模型
- 设计模式 -- Facade
- Tony.SerialPorts.RS232串口参数配置模块:扫描事件例程
- K.im团队与Kim Dotcom AMA直播回顾
- 新能源车动力总成技术探讨:混动和纯电之争、电驱动未来发展趋势
- svg --- 可缩放矢量图形
- ddns client
- 注册表残留内容的删除
- CentOS6-yun install wget失败
- 狂神说Spring讲解第19动态代理中错误java: 不兼容的类型: com.Orac.kuang.Host无法转换为com.kuang.demo3.Rent
热门文章
- 有没有命令让服务器cpu占用升高,怎样通过iisapp命令查找pid来解决IIS的cpu占用率过高问题...
- 推荐今日 火火火火 的开源项目
- fatal error C1010: unexpected end of file while looking for precompile
- java php 架构_php 架构和java架构的区别?
- STM32CubeIDE编译时出现的问题汇总
- 网页设计需要学习哪些技术
- 六个方法助你提高背书效率
- 报错 | Failed to load resource: the server responded with a status of 403 (Forbidden)
- java指令_常用java的命令有哪些
- Windows 10 版本 21H1不推送的手动更新方法