Java多线程感悟二

写在前面

这篇是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();
}
时间: 2024-10-14 06:32:47

Java多线程感悟二的相关文章

Java多线程(二)、线程的生命周期和状态控制(转)

Java多线程(二).线程的生命周期和状态控制 分类: javaSE综合知识点 2012-09-10 16:11 15937人阅读 评论(3) 收藏 举报 一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadSt

java多线程系列(二)

对象变量的并发访问 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理解能让知识更加简单易懂. 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 java多线程系列(三)之等待通知机制 java多线程系列(四)之ReentrantLock的使用 线程安全 线程安全就是多线程访问时,采用了加锁机制,当一个

Java多线程(二)

本文承接上一篇文章<Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法. 1.join() join -- 让一个线程等待另一个线程完成才继续执行.如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行. 1 public class ThreadTest { 2 3 public static void main(String[] args)

Java总结篇系列:Java多线程(二)

四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法. 1.join() join -- 让一个线程等待另一个线程完成才继续执行.如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行. 1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 5 MyRunnable myRunna

【搞懂Java多线程之二】多线程调度及守护进程

在前一篇文章中说到,所有处在就绪状态中的线程,操作系统会选择优先级最高的优先进行调度,那么是不是优先级高的线程就一定比优先级低的线程先执行呢?线程的优先级又是怎么划分的呢?这篇文章,楼楼就要来说说这个问题啦!欢迎关注我的个人博客主页www.anycodex.com 1.线程的优先级 在Java中,线程优先级的范围为0-10,整数值越大,说明优先级更高. 几个相关的宏定义: MAX_PRIORITY 10,最高优先级 MIN_PRIORITY 1,最低优先级 NORM_PRIORITY 5,默认优

Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)

一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性.因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问. 线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的:当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题. 二.线程同步(synchr

java多线程解读二(内存篇)

线程的内存结构图 一.主内存与工作内存 1.Java内存模型的主要目标是定义程序中各个变量的访问规则.此处的变量与Java编程时所说的变量不一样,指包括了实例字段.静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为它们是线程私有的,不会被共享. 2.Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的虚拟内存.线程的虚拟内存中保存了该线程使用到的变量到主内存副本拷贝.线程对变量的所有操作(读取.赋值)都必须在自己的虚拟内存中进行,而不能直接读写主内存中的变量.不同

java多线程(二)锁对象

转载请注明出处:http://blog.csdn.net/xingjiarong/article/details/47679007 在上一篇博客中,我们讨论了Race Condition现象以及它产生的原因,现在我们知道它是不好的一种现象了,那么我们有什么方法避免它呢.最直接有效的方式就是放弃多线程,直接改为使用单线程但操作数据,但是这是不优雅的,因为我们知道有时候,多线程有它自己的优势.在这里我们讨论两种其他的方法--锁对象和条件对象. 锁对象 java SE5.0之后为实现多线程的互斥引入了

Java多线程基础(二)

信号量Semaphore,类似于锁的功能,用于多线程中对一组资源的控制. acquire方法用于尝试获取一个资源,未获取前将一直等待.release用于释放一个资源,release的前提是已经获得了一个资源. package multiThread; import java.util.concurrent.Semaphore; public class SemaphoreTest { public static void main(String [ ] args) { int N = 8; //