写在前面
这篇是Java多线程感悟的第二篇博客,主要讲述的JAVA层面对并发的一些支持。第一篇博客地址为:http://zhangfengzhe.blog.51cto.com/8855103/1607712 下一篇博客将介绍线程池和一些同步工具类。
目录
9. 并发内存模型及并发问题概述
10. volatile和synchronized原理分析
11. ThreadLocal原理及其在Struts/Spring中的应用
12. Atomic
13. Lock
并发内存模型及并发问题概述
首先看一个图: 在多核CPU的情况下,每一个CPU都有自己的缓存cache,当多个CPU对同一份内存的数据进行操作时,显然就有可能导致缓存不一致的问题。 然后,我们再来看看多线程的工作模型: 从上面的模型图,可以得到如下结论: 第一,同一个进程内部的线程通信(数据交换)是通过内存来实现的 第二,每个线程在进行操作时,都会先从主内存COPY一份到自己的工作内存中,当完成计算后,会在某个时候将工作内存中的数据刷新到主内存中。显然如果我们不提供一种机制保证各个线程的load/save操作的次序,那么就会导致各种问题。 需要注意的是: Java线程之间的通信由Java内存模型(JMM:Java Memory Model)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。另外,Java为了获得最优性能,在不修改程序语义和单线程执行结果的前提下,允许编译器对指令进行重排,允许CPU决定指令的执行顺序,当然如果在多线程环境下就有可能因为指令重排产生问题。 总结: 在多线程环境下,当我们考虑并发问题时,需要注意如下几点: 原子性:保证一个线程的几个操作要么一起执行成功,要么一起不被执行,不允许其他线程打断。 可见性:当一个线程对共享变量进行了操作,什么时候对其他线程可见。 排序性:确保程序的执行顺序按照代码的先后顺序。 |
volatile和synchronized原理分析
volatile和synchronized是JAVA在语法层面提供对并发支持的2个关键字。 synchronized锁住的是什么? 只有明白锁住了什么,才能根据业务情况去构造一个对象,锁住它,来达到同步的目的,以及去优化synchronized的锁粒度! synchronized(obj){ ... } 需要注意的是锁住的是一个对象(一个普通对象或者一份class),并不仅仅是这一个synchronized的{}区域。也就是说synchronized(obj)和任何其他synchronized(obj)互斥。需要注意的是子类对象,父类对象,类class他们是3个不同对象,是3把不同的“锁”。 synchronized背后都干了些什么? 第一,同一时刻,只有一个线程能拿到“钥匙”进入临界区域 第二,进入临界区域时,该线程的工作缓存失效,强制从主内存中读取最新值 第三,退出临界区域时,该线程的工作缓存强制刷新到主内存 第四,当这个线程OVER,其他某个线程拿到“钥匙”后,重复上面3个步骤 实际上,通过上面的分析,synchronized保证了: 原子性:因为任意时刻只有一个线程才能执行这段代码 可见性:因为线程在进入、退出临界区域时,都会强制和主内存交互,这样当前线程可以看到上一个线程操作后的变化 有序性:由于临界区域其实是一个单线程的执行环境,自然就不存在这个问题 volatile 对于普通共享变量,工作内存和主内存之间什么时候交互由于是不确定,因此会导致可见性问题。volatile这个关键字是专门用来保证Java线程中的可见性的。实际上,我们可以这样认为多个线程之间对volatile的变量的读写操作,是直接在主内存中进行的,工作缓存中的是失效的。同时,JMM还保证了volatile变量前后操作的一定的“有序性”,但是不能保证原子性。因此volatile提供了synchronized的一部分功能,带来的开销小于一段代码的同步锁机制,但是在业务场景下,往往需要操作的原子性,所以volatile的应用场景有限。比如一个典型的volatile应用场景如下: 由于读线程不需要加锁可以并发执行,这样通过volatile减少synchronized的代码区域开销。 |
ThreadLocal原理及其在Struts/Spring中的应用
要想彻底弄懂ThreadLocal,还得看看它的源码! 对于ThreadLocal,我们用的最多的方法就是:get()/set(value)/remove()这3个操作。那么先看看set(value)方法的源码: 说明: 在set的时候,取出当前线程,并通过当前线程获得一个ThreadLocalMap,如果存在那么将ThreadLocal作为KEY,用户提供的值为VALUE设置进去。 追踪下getMap(Thread)和createMap(Thread,T)方法: 返回了一个线程的成员变量threadLocals,查看下Thread的源码发现: ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocalMap本是定义在ThreadLocal类中的内部类,但是却是Thread的一个成员变量! 其实到这里,我们就可以得出结论: 往一个ThreadLocal变量里面存东西,就相当于往当前线程的一个MAP成员变量里面存东西,KEY是ThreadLocal对象,VALUE就是你要放的东西。这样的话,在一个线程的任何地方都可以取出来,并且是绝对安全的,因为它是一个线程本身的属性,并非多个线程共享。 可以看下createMap(Thread,T)来验证上面的结论: void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } 正是由于ThreadLocal的特性,使得其在Struts/Spring中得到应用! 当一个请求到达web容器,一般而言,web容器会从线程池中取出一个空闲线程,那这个请求的数据比如request,是如何和这个线程建立关系的?struts2会将请求的数据做一下封装,然后放入到ThreadLocal中,所以一个线程中的请求数据是绝对安全的! 而在Spring中,ThreadLocal更是无处不在! 在DAO层,我们并没有在显式的给DAO方法传递Connection,它是怎么取到Connection的? 为什么在Spring的一个线程中我们取得的是同一个Connection? ...... |
Atomic
Atomic,英文的意思是“原子性的”,JDK在java.util.concurrent.atomic包中给我们提供了一组原子操作类,直接看一个例子,就能明白。 package test14; import java.util.concurrent.atomic.AtomicInteger; public class IntegerTest { public static void main(String[] args) throws InterruptedException { AddTask task = new AddTask(1); Thread[] threads = new Thread[10]; for(int i = 0 ; i < 10 ; i++){ threads[i] = new Thread(task); threads[i].start(); } for(Thread t : threads){ t.join(); } System.out.println("最终结果为:"); task.display(); } } class AddTask implements Runnable{ private int i = 0; //private AtomicInteger atomic ; public AddTask(int i){ this.i = i; //this.atomic = new AtomicInteger(i); } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } i = i + 1; //atomic.incrementAndGet(); } void display(){ System.out.println("i = " + i); //System.out.println("atomicInteger = " + atomic); } } 当变量为普通int型时,由于i = i + 1这个操作并不是原子性,导致并发问题,往往结果<= 11,如果使用AtomicInteger时,就会始终得到11了。 在java.util.concurrent.atomic包下,提供了Integer/Long/Boolean类型的原子操作类,还提供了数组/引用类型的原子操作类。下面,以AtomicInteger为例,简单分析下原子操作类的实现原理。 注意在AtomicInteger类中的成员变量: private volatile int value; 注意用了volatile修饰,在并发时,其他线程可见! 我们分析一个方法就可以了,以incrementAndGet()为例: 注意get()返回的就是那个成员变量value,实际上利用compareAndSet进行对比和修改,如果current和当前value进行比对,如果一致,说明老值一样,并没有其他线程修改过,那么可以将老值设置为next,否则死循环,尝试修改!其实这就是所谓的CAS机制。 |
Lock
我们不仅仅可以通过synchronized关键字来实现锁的目的,还可以通过java.util.concurrent.locks.Lock来达到目的。 比如我们经常这样写: lock.lock(); try{ //xxx业务操作 }finally{ //务必释放锁 lock.unlock(); } |