Java并发编程艺术

一、并发编程面临的挑战并发编程的目的是为了让程序运行更快,但并不是启动多线程就能让程序最大限度地并发执行。还会面临上下文切换、死锁、软硬件的资源限制等问题。上下文切换CPU通过给每个线程分配CPU时间片来实现多线程并发执行。时间片一般几十毫秒,因此CPU通过不停地切换线程执行,让我们感官上觉得多个线程是同时执行的。通过时间片分配算法来循环执行任务,当前任务执行一个时间片后切到下一时间片,但在切换前会保存上一任务的状态,以便下次切回来时可以再加载该任务状态。任务从保存到再加载的过程就
摘要由CSDN通过智能技术生成

一、并发编程面临的挑战

并发编程的目的是为了让程序运行更快,但并不是启动多线程就能让程序最大限度地并发执行。还会面临上下文切换、死锁、软硬件的资源限制等问题。

上下文切换

CPU通过给每个线程分配CPU时间片来实现多线程并发执行。时间片一般几十毫秒,因此CPU通过不停地切换线程执行,让我们感官上觉得多个线程是同时执行的。

通过时间片分配算法来循环执行任务,当前任务执行一个时间片后切到下一时间片,但在切换前会保存上一任务的状态,以便下次切回来时可以再加载该任务状态。

任务从保存到再加载的过程就是一次上下文切换。

线程有创建和上下文切换的开销。上下文切换开销可通过无锁并发编程、CAS算法、使用最少线程和使用协程来减少。

无锁并发编程:减少多线程锁的竞争,如数据ID按hash算法取模分段,不同线程处理不同段的数据(JUC下的ConcurrentHashMap);

CAS算法:Atomic包即使用的CAS算法来更新数据;

使用最少线程:避免创建不必要的线程,如通过查看dump线程信息发现大量等待的线程时减少工作线程数;

协程:在单线程里实现多任务调度,并在单线程里维持多个任务间的切换。

死锁避免

a、避免一个线程同时获取多个锁;

b、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源;

c、尝试使用定时锁,使用Lock.tryLock(timeout)来替代使用内部锁机制

d、对于数据库锁,加锁和解锁都必须在一个数据库连接里,否则会出现解锁失败的情况。

资源限制

即并发执行时程序执行速度受限于软硬件。如带宽的上传/下载速度、硬盘读写速度和CPU的处理速度、数据库连接数、socket连接数等。

资源限制可能导致本来串行的任务在并发执行时不起作用,仍在串行地执行,而增加了上下文切换和资源调度时间,反而变得更慢了。

硬件上可考虑集群多机运行,如不同机器处理不同数据。软件可考虑资源池进行资源复用,如数据库连接池的连接复用。

二、JAVA并发机制的底层实现原理

Java代码编译后生成Java字节码,经过类加载器加载到JVM中,JVM执行字节码,最终转化为汇编指令在CPU上执行,所以java并发机制依赖于JVM的实现和CPU指令。

volatile

volatile是轻量级的synchronized,其“可见性”保证共享变量修改的立即可见,不会引起线程上下文切换和调度。一个字段被声明为volatile,则java 线程内存模型确保所有线程都能看到这个变量的值是一致的。

内存屏障:处理器指令,实现对内存操作的顺序限制。

原子操作:不可中断的一个或一系列操作。

volatile修饰的共享变量在转换为汇编语言后会出现一个lock前缀指令,使得线程的工作内存中的数据会被写会主内存中,同时使得其他工作内存的该变量的数据失效,只能从主内存中读取。

synchronized

普通同步方法,锁是当前实例对象;静态同步方法,锁是类对象;同步方法块,锁是synchronized括号内设置的对象。锁的是对象。

同步代码块采用monitorenter、monitorexit指令显式的实现。

同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。

monitorenter

每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:

  • 如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。

  • 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。

  • 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。

monitorexit

只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。

对象头(32bit的JVM中)

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。  synchronized用的锁是存在于Java对象头里的。如果对象是数组,则用3字宽(word,32bitJVM中1字宽为4字节),如果是非数组类型,则2字宽。

Mark Word: 锁状态、25bit hashCode、4bit 分代年龄、1bit 是否是偏向锁、2bit 锁标志位

Class MetaData Address:对象类型数据的指针。

Array length:数组长度(是数组的话)。

运行期间Markword的数据会随着锁标志位的变化而变化。

锁状态(JDK1.6引入)

级别由低到高:无锁状态、偏向锁、轻量级锁和重量级锁。状态随着竞争逐渐升级,但不能降级。这种锁只能升级不能降级的策略,其目的是为了提高获得锁和释放锁的效率

偏向锁

无锁竞争的情况下为了减少锁竞争的资源开销,引入偏向锁。当线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出时不需要进行CAS操作来加锁和解锁,只需测试下对象头Markword里是否存储着指向当前线程的偏向锁。成功则表示获得了锁。失败则再测试下Markword偏向锁标识是否置为1,没有则使用CAS竞争锁,否则尝试CAS将对象头的偏向锁指向当前线程等到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。撤销偏向锁时,在没有正在执行的字节码的情况下,会先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,若是线程不处于活动状态,则将对象头置为无锁状态;如果仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Markword要么重新偏向于其他线程,要么恢复到无锁的状态或标记对象不适合作为偏向锁,最后唤醒暂停的线程。

-XX:-UserBiasedLocking=false关闭偏向锁,程序默认进入轻量级锁状态。

轻量级锁

轻量级锁所适应的场景是线程交替执行同步块的情况。

加锁:线程执行同步块前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头重的Markword复制到锁记录中,即displaced Markword。然后线程尝试使用CAS将对象头重的Markword替换为指向锁记录的指针。如果成功,则当前线程获得锁,失败则表示其他线程竞争锁,当前线程便尝试自旋来获取锁。

解锁:使用原子的CAS操作将displaced Markword替换回对象头,如成功表示没有竞争发生。失败表示当前锁存在竞争,锁就会膨胀成重量级锁。

自旋会消耗CPU,为避免无用自旋,一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。该状态下,其他线程试图获取锁时都会被阻塞。持有锁的线程释放后会唤醒这些线程,开启新一轮的夺锁之争。

锁粗化(Lock Coarsening): 也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁,如方法内多个连续的StringBuffer.append()操作。

锁消除(Lock Elimination): 锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除,如方法内多个连续的StringBuffer.append()操作的作用域在方法内,sb的引用不会逃逸到方法外部。

适应性自旋(Adaptive Spinning): 自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。


 CAS

CAS,即比较并交换,是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。

JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。JUC包下的并发框架通过自旋CAS来实现原子操作。

优点:竞争不大的时候系统开销小。

缺点ABA问题:加版本号,JDK1.5引入AtomicStampedReference来解决ABA问题:当前引用是否等于预期引用,当前标志是否等于预期标志,全相等才以原子方式将该引用和该标志的值设置为给定的新值。循环时间长开销大。如长时间自旋CAS会给CPU带来非常大的执行开销。只能保证一个共享变量的原子操作。可将多个共享变量合成一个来进行处理,如JDK1.5引入的AtomicReference来保证引用对象之间的原子性。

三、Java内存模型

线程的通信是指线程之间以何种机制来交换信息。线程之间的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里,线程之间通过写-读内存中的共享变量来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。在消息传递的并发模型里,线程之间没有共享变量,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。

堆内存在线程间共享。Java线程间的通信由Java内存模型JMM控制,JMM决定一个线程对共享变量的写入何时对另一线程可见。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。

 共有以下操作:lock/unlock(主内存中)、read/load(从主内存到工作内存,前主后工)、use/assign(工作内存,传给执行引擎/执行引擎计算结果的赋值)、store/write(从工作内存到主内存,先工后主)。其中从read起都是原子操作,具有原子性;可通过volatile来保证可见性和有序性,防止指令重排

先行发生原则

程序顺序规则:按控制流顺序,线程中书写在前的操作先行发生于后面的操作;

管程锁定原则:一个锁的解锁,先行发生于随后对这个锁的加锁;

volatile变量原则:对一个volatile变量的写,先行发生于后面对这个变量的读;

传递性:A先行发生于B,B先行发生于C,则A先行发生于C。

JUC包实现:首先声明共享变量为volatile;然后使用CAS的原子条件更新来实现线程之间的同步。

final的内存语义

编译器和处理器要遵守两个重排序规则:

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

final域为引用类型:

  • 增加了如下规则:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

final语义在处理器中的实现:

  • 会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
  • 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障

双重检查锁定与延迟初始化

即类似于懒汉式单例模式及双重校验的例子。序列化可破坏volatile的单例模式。(饿汉式即类初始化就生成实例)

// 懒汉式,双重检验
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance为volatile,现在没问题了
            }
        }
        return instance;
    }
}
// 饿汉式
1.线程安全;:类加载时已创建实例,不存在线程安全的问题。
2.在类加载的同时已经创建好一个静态对象,调用时反应速度快
缺点
资源效率不高,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化

class Singleton{
    private Singleton(){}
    //final可有可无,有了更好
    private static final Singleton s=new Singleton();
    public static Singleton getInstance(){
        return s;
    }
}

四、Java并发编程基础

操作系统运行一个程序时,会为其创建一个进程。如运行一个Java程序就会为其创建一个Java进程。线程是操作系统调度的最小单元,也叫轻量级进程,如main线程。一个进程可以有多个线程,这些线程拥有各自的计数器、局部变量等属性,并可访问共享内存变量。处理器在这些线程上高速切换,让我们觉得这些线程是在同时执行。

线程优先级

线程分配到的时间片多少决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多活少分配一些处理器资源的线程属性。priority变量来控制优先级,由低到高为1-10,默认为5。优先级高的线程分配时间片的数量要多于优先级低的线程。针对频繁阻塞的设置高优先级,而偏重计算的设置较低优先级,确保处理器不会被占用。

线程状态

初始状态:刚被创建,但还未调用start方法; 

运行状态:准备就绪和运行两种,统称为运行中;

阻塞状态:线程阻塞于锁;block(synchronized未获取到锁,Lock是等待,用了LockSupport)

等待状态:线程进入等待,需其他线程唤醒或中断;wait会释放锁;

超时等待:可在指定时间自行返回;

终止状态:表明当前线程执行完毕。

线程状态图

线程的构建方式

// 1、继承Thread类,重写run方法,创建线程对象并启动
public class MyThread extends Thr
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值