并发编程介绍
当今软件开发领域越来越强调性能和伸缩性,因此,利用并发编程可以更好地利用计算资源,提高应用程序的性能和响应速度。以下是一些关于并发编程的相关信息供您参考。
什么是并发编程
并发编程是指在一个程序中同时执行多个独立的计算任务,并通过各种手段来协调不同任务之间的交互,从而提高程序的效率和可扩展性。
并发编程的好处是什么
并发编程可以使程序可以同时利用多个CPU核心执行不同的任务,从而提升程序的效率和性能。此外,使用并发编程可以简化需要相互协调的任务之间的通信和同步,减少系统的复杂度和出错的风险。
并发编程的挑战是什么
并发编程面临许多挑战,如资源竞争、死锁、饥饿和活锁等问题。同时,在并发系统中,程序的错误也可能导致数据不一致和安全漏洞等问题。因此,在设计和开发并发系统时需要仔细考虑各种问题。
并发编程模型有哪些
常见的并发编程模型包括多线程模型、多进程模型和协程模型。每种模型都有自己的特点,相应的编程语言和框架也提供了支持。
如何学习并发编程
学习并发编程需要掌握并发编程的基本理论,同时需要熟悉各种并发编程模型和工具。建议初学者先学习基础的多线程编程和锁机制,逐步学习更高级的并发编程模型和相关技术,如Actor模型、RxJava、Go语言等。另外,实践也是学习并发编程的关键,可以通过实际编写并发程序来获得经验和技能。
本系列专题文章大全
实战原理
计算的问题
如何更快地计算一个包含亿级 Integer 类型数值的 List 的总和?
简单的方法:更快的CPU来遍历
单线程处理大数据量的数据集合的分析是一项挑战性的任务,因为单线程处理大数据量的数据集合可能会导致程序的运行速度变慢,甚至崩溃。伴随着数据量越来越大,耗费的时间约会越来越长,如下图所示。
靠谱的方法:分而治之来处理
在对性能的追求很可能是并发bug唯一最大的来源!同样不是免费的午餐,需要学习和批量实践。
进一步的办法:Fork/Join技术
Fork/Join是一种并行编程技术,常用于解决分治问题。具体来说,它将一个大问题分成若干个小问题,递归地将小问题分解成更小的子问题,直到问题的规模足够小可以被直接解决。然后将解决子问题的结果合并起来,得到大问题的解。Fork/Join使用了线程池的概念,并且自动运用一些启发式策略优化任务调度,从而保证任务的高效执行。Fork/Join 技术在并行计算中发挥着重要作用,能够提高多线程执行效率和程序处理大规模数据的能力。
在这个模式中,原始任务被递归地分解成更小的子任务,直到这些任务足够小,可以被直接处理。每个子任务都是在一个单独的线程中运行的,然后将它们的结果合并在一起,直到最终产生原始任务的结果。这种方式可以很好地利用多核CPU,从而提高性能。
线程是并发编程的基础
在Java中,线程技术是支持并发编程的基础。并发编程是指多个线程同时执行任务,它能够充分利用计算机的多核处理器,提高程序的执行效率和响应速度。
线程
线程:是实现多任务并发执行的关键。在Java中,我们可以使用多线程技术来同时执行多个任务,提高程序的并发性和吞吐量。因此,Java中的线程技术是并发编程的基础。
- 多线程并发中:如果修改了共享变量的值,必须将其同步回主内存,其他线程才能访问到。
- 为了保证内存访问的顺序,可以使用Java提供的同步机制或
volatile
关键字。 Cache coherency
指管理多处理器系统的高速缓存区结构,确保数据在高速缓存区到内存的传输中不会丢失或重复。
技术选型
Java 中的 happens-before ordering 通过一些关键字和类提供了可靠的内存可见性和线程同步机制,这些关键字和类包括:synchronized、volatile、final、java.util.concurrent.lock|atomic。
内存中的可见部分
当多个线程访问共享资源时,可能会出现数据不一致的问题,这时就需要使用同步机制来解决。因此JMM推出了三个特性:可见性、有序性、原子性。
synchronized同步机制
synchronized 是 Java 中最基本的同步技术,它可以在多个线程之间提供原子性和可见性,保证一段代码在同一时刻只有一个线程可以执行。
保证原子性和可见性
使用 synchronized 时,需要指定一个锁对象,在任意时刻,最多只有一个线程可以拥有该锁对象,从而保证同一时刻只能有一个线程执行被锁定的代码块。
锁的类型和范围
synchronized 可以用来修饰普通方法、静态方法和代码块,以适应不同场景的同步需求。
线程的Java Monitors监视器
在Java中,每个对象都有一个内部的锁,也称为对象监视器(monitor)。synchronized 关键字就是用来获得和释放对象的监视器的。当一个线程获得了一个对象的锁,并进入了该对象的同步代码块,其他线程如果需要获取该对象的锁就会被阻塞,直到该线程释放锁为止。因此,synchronized 关键字是实现线程同步的重要手段之一。在 Java 中,每个对象都有一个监视器锁(也称为内置锁或反应锁),它可用于实现同步。
synchronized 就是通过锁来实现同步的,当一个线程获取到了锁,其他需要访问被锁住的代码块的线程会被阻塞,等待锁被释放。同时,每个对象只有一个锁,一个线程对该对象的锁拥有之后,其他访问该对象的线程将无法操作该对象的所有同步方法和同步代码块,这就能够保证了多个线程在访问共享资源时的同步。因此,对象监视器和 synchronized 锁是密不可分的。
该图将监视器显示为三个矩形。在中心,一个大矩形包含一个线程,即监视器的所有者。在左侧,一个小矩形包含条目集。在右边,另一个小矩形包含等待集。活动线程显示为深灰色圆圈。悬挂的线程用浅灰色的圆圈表示。
线程的可见性
volatile关键字
- 简化实现或者同步策略验证的时候来使用它;
- 确保引用对象的可见性;
- 标示重要的生命周期的事件,例如:开始或者关闭。
脆弱的volatile的使用条件:
- 写入变量不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值;
- 变量不需要和其他变量共同参与不变约束;
- 访问变量时不需要其他原因需要加锁。
private volatile boolean isInterrupted = false;
线程:独占锁
非方法修饰符和类修饰符可以用来修饰 Java 中的各种程序元素,需要注意的是,在方法覆盖时需要添加 synchronized 修饰符。
关于经典的顺序锁问题,则是在多线程环境下,如果将两个线程安全的方法组合在一起,不一定能够保证整体的线程安全性。
另外还有一个常见的问题就是,在 Java 中当一个对象调用 getClass() 方法时,该方法会返回该对象所属的类的 Class 对象。
Object a new object();
Object b new object();
public void order(){
synchronized(a){
//do something
synchronized(b){
//do something
}
}
}
public void order1(){
synchronized(b){
//do something
synchronized(a){
//do something
}
}
}
线程:分拆锁
在多线程编程中,为了保证数据的正确性和避免竞态条件,我们通常会使用锁来对临界区进行保护。然而,在某些情况下,线程之间的锁竞争可能会导致性能瓶颈,造成程序的性能下降。
public class ServerStatus{
public final Set<String>users new HashSet<String>();
public final Set<String>queries new HashSet<String>();
public synchronized void adduser(String u){users.add(u);
public synchronized void addQuery(String q){queries.add(q);
public synchronized void removeUser(String u)(users.remove (u);}
public synchronized void removeQuery(String q)(queries.remove(q);
}
在这种情况下,我们可以尝试将锁拆成多个细粒度的部分,分别对每个细粒度部分进行锁定,并发执行,尽量减小锁竞争范围,以提高程序的并发性和性能。这种技术就被称为“线程分拆锁”。
public class SpinOff Server Status public final Set<String>users=new HashSet<String>() ;public void addUser(String u){
synchronized(users) {
users.add(u) ;
}
public void remove User(String u){
synchronized(users) {
users.remove(u) ;
}
}
例如,如果在一个大的数据结构上进行操作,我们可以将数据结构划分为多个小的数据块,对每个数据块进行单独的锁控制和并发访问。这样可以更细粒度地控制锁的范围,减小锁的竞争,提高并发性能。
需要注意的是,在使用线程分拆锁的技术时,要确保各个锁的操作是互不干扰、没有依赖关系的,否则可能会导致死锁或者其他并发问题。此外,线程分拆锁一般需要对数据结构进行重新设计和调整,需要仔细评估和测试,以确保其可行性和有效性。
线程:分离锁
private final Object[]locks;
private static final in tN_LOCKS=4;
private final String[] share;
private into pNum;
private in tN_A NUM;
public Stripping Lock(int on,int a num) {
opNum=on;
N_ANUM=anum;
share=new String[NANUM];
locks=new Object[NLOCKS];
for(int i=0;i<NLOCKS; i++)
locks[i] =new Object();
}
public synchronized void put1(int indx,String k)
share[indx]=k;//acquire the object lock
}
public void put 2(int indx, String k){
synchronized(locks[indx号N_LOCKS] ) {
share[indx] =k; //acquire the corresponding lock 1
}
}
分离锁存在的缺点之一是,对容器加锁可能会导致独占访问变得更加困难和昂贵。此外,使用分离锁可能会导致内存使用过多,例如在Action层使用ConcurrentHashMap曾因此问题导致内存使用过大,修改array后竟然单台服务器节省2G。