Java多线程感悟二

写在前面

这篇是Java多线程感悟的第二篇博客,主要讲述的JAVA层面对并发的一些支持。第一篇博客地址为:http://zhangfengzhe.blog.51cto.com/8855103/1607712  下一篇博客将介绍线程池和一些同步工具类。


目录

9.  并发内存模型及并发问题概述

10. volatile和synchronized原理分析

11. ThreadLocal原理及其在Struts/Spring中的应用

12. Atomic

13. Lock


并发内存模型及并发问题概述

首先看一个图:


wKioL1TUH5Cz58H9AAC-L63M7JY507.jpg


在多核CPU的情况下,每一个CPU都有自己的缓存cache,当多个CPU对同一份内存的数据进行操作时,显然就有可能导致缓存不一致的问题。


然后,我们再来看看多线程的工作模型:


wKiom1TUISaB7uG2AAENAumeHqk464.jpg


从上面的模型图,可以得到如下结论:


第一,同一个进程内部的线程通信(数据交换)是通过内存来实现的


第二,每个线程在进行操作时,都会先从主内存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应用场景如下:


wKioL1TUN_rwz1S2AADyZEVYONY383.jpg


由于读线程不需要加锁可以并发执行,这样通过volatile减少synchronized的代码区域开销。



ThreadLocal原理及其在Struts/Spring中的应用

要想彻底弄懂ThreadLocal,还得看看它的源码!


对于ThreadLocal,我们用的最多的方法就是:get()/set(value)/remove()这3个操作。那么先看看set(value)方法的源码:


wKiom1TUYhXCB1viAAB-6--R7pE656.jpg


说明:

在set的时候,取出当前线程,并通过当前线程获得一个ThreadLocalMap,如果存在那么将ThreadLocal作为KEY,用户提供的值为VALUE设置进去。


追踪下getMap(Thread)和createMap(Thread,T)方法:


wKioL1TVakbS7gUcAAAyeH9XK-I690.jpg


返回了一个线程的成员变量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更是无处不在!


wKiom1TVcCPDX4CIAAC-IbgxKgU436.jpg

在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()为例:


wKiom1TVfoWzVeNjAAB1Q_ck9wg022.jpg


注意get()返回的就是那个成员变量value,实际上利用compareAndSet进行对比和修改,如果current和当前value进行比对,如果一致,说明老值一样,并没有其他线程修改过,那么可以将老值设置为next,否则死循环,尝试修改!其实这就是所谓的CAS机制。



Lock

我们不仅仅可以通过synchronized关键字来实现锁的目的,还可以通过java.util.concurrent.locks.Lock来达到目的。


比如我们经常这样写:

lock.lock();
try{
//xxx业务操作
}finally{
//务必释放锁
lock.unlock();
}




你可能感兴趣的:(java,多线程)