Android(Java) | 如何使程序实现线程安全(拓展分析:ThreadLocal、重排序、volatile/final)

简介: Android(Java) | 如何使程序实现线程安全(拓展分析:ThreadLocal、重排序、volatile/final)

要点

  • 是否对线程安全有初步的了解(初级)
  • 是否对线程安全的产生原因有思考(中级)

优化线程安全要注意什么?

  • 是否知道final、volatile关键字的作用(中级)
  • 是否清楚1.5之前Java DCL 为什么有缺陷(中级)
  • 是否清楚地知道如何编写线程安全的程序(高级)
  • 是否对ThreadLocal的使用注意事项有认识(高级)

是否清楚地知道如何编写线程安全的程序

  • 什么是线程安全?
    • 不安全:资源不同步,脏读脏写;

如多个线程的工作内存读写主存时的不同步
“进程安全”问题不存在,
因为进程之间内存相互独立,各自独享内存的,
一个进程被杀掉的话,其所有内存都还给物理内存了;
可能共享CPU时间片;
线程是存在于进程当中的,
同一个进程中的线程之间是可以共享内存的;

- **线程安全产生的原因:`可变`资源(内存)线程间`共享`(关键词“可变”和“共享”)**

线程间不共享的资源不用考虑线程安全了;

  • PS:每一个线程都有自己的一个内存副本<Java内存模型>
  • 如何实现线程安全?
    • 不共享资源

共享才会产生线程安全问题,
所以尽量不共享;

- `共享不可变资源`(volatile、final)
    - 禁止重排序

- 有条件地`共享可变资源`
    - (更改刷新的)可见性

一个线程对共享资源的修改,其他线程能够马上看到!
实现:某个线程对共享资源进行了更新时,要马上刷新到主存!

    - 操作原子性
    - 禁止重排序

不共享资源

  • 可重入函数

传入一个参数进函数,经过一系列的运算,
再把运算结果返回出去,
中间不会涉及到任何对外部内存的访问、修改,
没有副作用,
像这样没有副作用的函数,
先天就具备线程安全的优势:

ThreadLocal实现不共享资源

  • **虽然说每个线程都会去访问一个ThreadLocal对象

但实际上最终访问的 都是自己线程内部的一个副本

比如下图中的token,
对应的场景如,
一个服务器提供了很多个服务,
每个服务的话,
每个用户进来请求,服务器都会为这个用户 开一个线程 来提供服务,
这个时候,
因为每个用户 就都是属于不同的线程的,
而ThreadLocal便是类似于服务器的设计,
这里每个线程都去访问这个token 的时候,
都会有一个自己的 String的 一个副本,
这样线程间便不会互相干扰;

如此便是实现了不共享资源
也就没有线程安全的问题了;
自己线程之内,不管怎么设置,都不会影响到其他线程;
【UUID,唯一识别码(Universally Unique Identifier),可以由Java工具类生成,用来唯一标注一个元素,如标注线程】
下面是一个用例:**

  • **ThreadLocal原理

看一下ThreadLocal源码set方法!!!!!!!!
可以看到,
ThreadLocal的底层,其实是绑定到线程上的一个ThreadLocalMap
添加值的时候置入键值对map.set(this,value)
使用的key,实际上就是this,即ThreadLocal类对象引用
value企图传入的值
既然是数据结构是绑定到线程上的,
也就是说,
假设,两个访问ThreadLocal的引用 它们所处的线程 是不一样的话,
那么,它们访问ThreadLocal的set、get时 处理的值,肯定也是不一样的!**

- ThreadLocal中这个ThreadLocalMap是,储存在、绑定在线程上的:![](https://upload-images.jianshu.io/upload_images/9125154-a111f56c0db65907.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

总结!!!

  • 两个点总结ThreadLocal特性:

    • **唯一 一个ThreadLocal对象,作为全局变量定义在主线程

为访问它(set())的N子线程
开启(createMap()N相互独立ThreadLocalMap
因此,每一个子线程访问主线程中的这个独一无二的ThreadLocal对象的时候,
总会访问到子线程自身对应的底层数据存储结构 ThreadLocalMap

故**

  • **不同线程,访问一个ThreadLocal对象的时候,

访问的是(绑定不同线程的)不同底层数据结构ThreadLocalMap
读写的是不同的数据


**
**实现了,
同属主线程的一系列子线程间的,
资源不共享,解决的了线程安全问题;

【服务器是一个服务端里边,
操作很多个线程,每个线程服务每个不同的用户;

ThreadLocal是一个ThreadLocal实例里边,
操作很多个ThreadLocalMap
每个ThreadLocalMap服务不同的子线程


另外我们可以发现Android的消息机制中,
正是把Looper交给ThreadLocal保管了,
所以同个线程的所有Handler中关联的Looper其实是同一个Looper的副本,
Handler通过Looper找到对应的MessageQueue,
把自己负责的Message加进去:**

实战案例如下:

package test;

public class ThreadLocalTest {
    
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

    public static class MyRunnable implements Runnable {
          
        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }
    }
  
  
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "A");
        Thread t2 = new Thread(new MyRunnable(), "B");
        Thread t3 = new Thread(new MyRunnable(), "C");

        t1.start();
        t2.start();
        t3.start();
    }


}

运行结果:


  • ThreadLocalMap 跟 WeakHashMap 很像

    • 本身对于对象的持有都是弱引用的;

区别是
ThreadLocalMap不用去监听ReferenceQueue,
(监听ReferenceQueue还是有一定的开销的)
因,ThreadLocalMap适用于对象较少的场景,
另外,
线程退出时会自动移除;

- 关于Hash冲突的解决方法也是不一样的,

单链表法传统HashMap解决办法
开放地址法则适合对象比较少的情况,
即线性探测、平方探测、双散列法等等;


  • ThreadLocal的使用建议:

    • **声明为全局静态final成员

ThreadLocal在一个主线程中有一个实例就够了,
没必要每次创建子线程都整一个出来,
并且我们set value的时候,
我们是以ThreadLocal的this为key的,
ThreadLocal这个对象的引用最好是独一的、不可更改的!

不设置final的话,还有另外的问题,
还要考虑什么时候去初始化它,还要考虑可见性,
这就还要考虑加锁了;**

- **`避免存储大量对象`

因,
底层数据结构、Hash冲突的解决方案和Hash计算算法,
已经做了限制;**

- **`用完后及时移除对象`

ThreadLocal自身没有监听机制,
如果你设置的ThreadLocal的存在周期非常的长,
那对应的线程就会一直存在,
其引用不会被回收,有内存泄漏风险**

共享不可变资源(加final/volatile,禁止重排序)

首先普及一下重排序,等下涉及到

  • 什么是重排序?重排序是指令的重排序。

为了提高性能,编译器和处理器常常会对指令做重排序,
重排序就会导致多线程执行的时候有数据不一致问题,
导致程序结果不是理想结果。

  • 重排序分为三类:

    • 编译器重排序:不改变单线程程序语义前提下,重新安排执行顺序
    • 指令级并行重排序:

指令并行技术可以将多条指令重叠执行,
如果不存在数据依赖性,
处理器会改变语句对应的机器指令执行顺序

  • 内存系统重排序


案例:
  • 定义一个类:

两个成员,x为final,y不为final;

class FinalFieldExample{
    final int x;
    int y;

    public FinalFieldExample(){
        x = 3;
        y = 4;
    }
}

**假设Thread1 为 writer线程,初始化了一个FinalFieldExample实例f,
Thread2 为 reader线程,读取实例f 的x、y值,赋值给 i、j;
那么表面上我们是期待结果是 i = 3, j = 4的:**

  • **实际上的情况可能会不如我们期待的那样子,

由于虚拟机的实现或者CPU架构的特征,
指令是可能发生重排序的,
重排序会把非final的变量赋值指令 排序到构造方法之外,**

这样的结果自然是,
x因为是final的所以自然会在构造方法之内进行赋值,
但y是非final的,
有可能构造方法执行完了,
y的赋值指令还没有走完,

这个时候因为构造方法走完,
reader读的时候发现f 是不等于null的,
就会把未完成赋值的y 的值给读出来,
那结果j的值就是0了:

所以,各单位请注意!
final啊,它还有一个禁止重排序的作用,
即,禁止被final修饰的代码对应的指令被重排序

补充:volatile

**volatile除了能保证线程间的可见性
也能禁止重排序!!**

  • **从1.5开始,其语义被增强了,明确了禁止重排序的作用;

1.4以前,即便使用双重校验锁的单例模式,也是有问题的;**
单例模式案例(两种加volatile的情况,正常):
**如果不加volatile,就可能会出现类似重排序的问题了:
有可能重排序之后,
构造方法的调用的指令被排到了后面,
这时候程序 还没等构造方法 执行完毕
就把分配好内存的实例赋值给了引用

这时候这个引用因为没有经过构造方法,
所以还没有被初始化,
此时Thread1解锁,
Thread2直接把这个没有初始化完的引用拿去使用了,
就可能出现问题了!**

所以千万注意,使用单例模式的时候
一定要为单例加上volatile关键字!

有条件地共享可变资源

保证可见性的方法
  • 使用final关键字
  • 使用volatile关键字
  • 加锁,锁释放时会强制将缓存刷新到主内存

不过加锁要注意,
加锁只是 对另外跟你这个线程 同样使用一个锁 的那些线程,
才能保证可见性,
如果某个线程没有加锁,它就不一定能够看到了;

加了锁的,
锁释放时会强制将缓存刷新到主内存,
**为什么刚说,其他线程加锁 才能看到 本线程 访问的主内存的对应值,
因为资源只有加锁,
才会去主内存刷新,
才会跟其他 同样对本资源 加了锁的线程 保持同步!
不对共享资源加锁的线程 可能拿着 自己运行内存的数据副本 就去读、写、运算、更新操作了;
如此便可能造成文首所说的,脏读脏写等线程不安全的情况!**

保证原子性
  • **加锁,保证操作的互斥性,

实现执行控制,
加锁的代码会实现原子性;**

  • **使用CAS指令(Unsafe.compareAndSwapInt

不过Unsafe不是公开的,
需要用到反射才能用得到它;**

  • 使用原子数值类型(如AtomicInteger
  • 使用原子属性更新器(AtomicReferenceFieldUpdater

**经典案例,a++,
++操作符不是原子性的,
任何编程语言在进行a++操作的时候,
都会先把值从a中读出来,给到一个临时变量如tmp中,
tmp加一,
之后再把tmp写回到a中,
全程经过了三步操作,不是一个不可拆分的运算单元,
即,非原子性!**

**如下图,两个线程同时进行a++,
因为a++非原子性操作,
由此可能造成脏读脏写:**





相关文章
|
8天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
3天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
4天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
19 4
|
4天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
24 3
|
5天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
17 1
|
7天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
20 2
|
9天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
9 2
|
29天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
14天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3
|
14天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
12 2