Java并发编程

一、Java并发编程的挑战

1.1 如何减少上下文切换?

  1. 无锁并发编程:
  2. CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  3. 使用最少线程:避免创建不必要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成很多线程都处于等待状态。
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

1.2 死锁
避免同一个线程,同时获取多个锁;
避免同一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源;
尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制;
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
1.3 资源限制的挑战
(1)什么是资源限制
资源限制指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接
数和socket连接数等。
(2)资源限制引发的问题
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
(3)如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。
对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
(4)在资源限制情况下进行并发编程
如何在资源限制的情况下,让程序执行得更快呢? 方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

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

Java代码在编译后会变成Java字节码,字节码被ClassLoader加载到JVM中,JVM执行字节码,最终需要转化成为汇编指令在CPU中执行。
Java中所使用的的并发机制依赖于JVM的实现和CPU的指令。

2.1 volatile的使用

可见性:
当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
有volatile变量修饰的共享变量进行写操作的时候会多出一个lock指令。
Lock前缀的指令在多核处理器下会引发了两件事情:
1) 将当前处理器缓存行的数据写入到内存;
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile的两条实现原则:
1)Lock前缀指令会引起处理器缓存回写到内存。
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

2.2. synchronized的实现原理与应用

Java中的每一个对象都可以作为锁。具体表现为3种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。
    从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
    monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter
    指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
    处理器如何实现原子操作:
    32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
    处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
    (1)使用总线锁保证原子性
    如果多个处理器同时对共享变量进行读、改、写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作。
    这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。
    所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
    (2)使用缓存锁定来保证原子性
    在Java中可以通过锁和循环CAS的方式来实现原子操作。

你可能感兴趣的:(Java并发编程)