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的源码发现:

1
     ThreadLocal.ThreadLocalMap threadLocals =  null ;


ThreadLocalMap本是定义在ThreadLocal类中的内部类,但是却是Thread的一个成员变量!


其实到这里,我们就可以得出结论:


往一个ThreadLocal变量里面存东西,就相当于往当前线程的一个MAP成员变量里面存东西,KEY是ThreadLocal对象,VALUE就是你要放的东西。这样的话,在一个线程的任何地方都可以取出来,并且是绝对安全的,因为它是一个线程本身的属性,并非多个线程共享。


可以看下createMap(Thread,T)来验证上面的结论:

1
2
3
     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包中给我们提供了一组原子操作类,直接看一个例子,就能明白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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类中的成员变量:

1
private  volatile  int  value;


注意用了volatile修饰,在并发时,其他线程可见!


我们分析一个方法就可以了,以incrementAndGet()为例:


wKiom1TVfoWzVeNjAAB1Q_ck9wg022.jpg


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



Lock

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


比如我们经常这样写:

1
2
3
4
5
6
7
lock.lock();
try {
//xxx业务操作
} finally {
//务必释放锁
lock.unlock();
}


本文转自zfz_linux_boy 51CTO博客,原文链接:http://blog.51cto.com/zhangfengzhe/1612581,如需转载请自行联系原作者


目录
打赏
0
0
0
0
69
分享
相关文章
|
9天前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
122 60
【Java并发】【线程池】带你从0-1入门线程池
|
5天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
51 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
87 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
53 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
2月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
121 17
|
3月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
Java 多线程 面试题
Java 多线程 相关基础面试题
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等