在Java中,线程的安全实际上指的是内存的安全,这是由操作系统决定的。

目前主流的操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的、分配给别的进程的内存空间,这一安全特性是由操作系统保障的。但是线程却与进程不同,因为在每个进程的内存空间中都会有一块特殊的公共区域,通常被称为堆(内存),这块内存区域是进程内所有的线程都可以访问得到的,这个特性是线程之间通信的一种方式,但是却会引发多个线程同时访问一块内存区域可能产生的一系列问题,这些问题被统称为线程的安全问题。

如何在Java中保证线程安全,也就是保证内存的安全,是一个重要的知识点。

使用局部变量保证线程安全(内存隔离法)

在程序中,操作系统会为每个线程分配专属的内存空间,通常被称为栈内存。栈内存是当前线程所私有的,其它线程无权访问,这是由操作系统保障的。那么,如果想要一些数据只能被某个线程访问的话,就可以把这些数据放入线程专属的栈内存中,其中最常见的就是局部变量,局部变量在线程执行方法时被分配到线程的栈内存中。

double avgScore(double[] scores) {
double sum = 0;
for (double score : scores) {
sum += score;
}
int count = scores.length;
double avg = sum / count;
return avg;
}

上面定义了一个算平均成绩的方法,其中的sum、count和avg都是局部变量,当有一个线程A来执行这个方法的时候,这些变量就会在A的栈内存中分配。如果在这时候有另外一个B线程来执行这个方法,这些变量也会在B的栈内存中分配,但是B的栈内存中的这些变量和A的栈内存中的这些变量是相互独立的,并不会相互影响。

简单来说,就是这些局部变量会在每个独立线程私有的栈内存中分配一份,而由于线程的栈内存只能被当前线程自己访问,所以栈内存分配的这些变量不能被别的线程访问,也就不会有线程安全的问题了。而局部变量之所以是安全的,是因为它的使用范围仅仅局限于方法中,生命周期随着方法的执行从开始到结束。然而实际开发中却不可能仅仅将一个变量局限于一个方法中,总是要有一个变量被多个方法使用的情况,这时就又会产生线程安全的问题了。

使用ThreadLocal类保证线程安全(标记隔离法)

如果想要一个变量能被多个方法使用,通常是将变量定义为类的成员变量。而按照主流编程语言的规定,类的成员变量不能再被分配在线程的栈内存中,而应该分配在公共的堆内存中。这样,变量从单个线程的私有变成了多个线程的公有,要保证线程安全就需要想一些特殊的办法,其中的一个方法就是使用ThreadLocal类。使用ThreadLocal类修饰变量之后,每个线程如果需要访问这个变量,都会拷贝一份出来,然后当前线程就只会访问这份拷贝。这样,因为每个线程都只能访问自己拷贝的变量,即这些拷贝出来的变量是线程私有的,也就保证了线程的安全了。

class SugarFactory {
ThreadLocal<String> sugar = new ThreadLocal<>(); String getSugar() {
return sugar.get();
}
}

上面是一个SugarFactory类,类中有一个ThreadLocal类型的成员变量sugar,每个线程在运行时,如果需要用到sugar变量,就会从堆中拷贝一份sugar变量出来,存放到线程对象(Thread类的实例对象)的成员变量中去。线程类(Thread)是有一个类似于Map类型的成员变量专门用于存储ThreadLocal类型的数据的。

从逻辑的从属关系来理解,这些ThreadLocal类型的数据是属于Thread类的成员变量级别的。但如果是从逻辑的内存位置上来看,实际上这些ThreaLocal类型的数据还是分配在公共区域的堆内存中。这种做法就类似于给该内存区域打上某种标记的做法,在堆内存中标记了这块内存是这个线程私有的。

完整地讲,就是当一个线程需要访问该ThredLocal类型变量的时候,就从堆内存中复制一份出来,并给这份复制打上一个标记,标记这份复制是该线程私有的。

使用常量保证线程安全(只读标记法)

我们知道,在Java中的常量是只能被读取而不能被修改的,常量通常使用final修饰符进行修饰,这时候对于多线程来说是安全的。

class MyPromise {
final String promise = "i love you forever.";
}

常量不会引起不经意被修改的问题,无论多少次读取,结果都是一样的。

使用悲观锁保证线程安全(加锁标记法)

悲观锁通常的理解就是互斥锁。所谓的悲观,指的就是悲观地认为一定会发生线程安全问题,于是就给公共的数据加上一把锁,如果一个线程想要访问该数据,就需要先获取该数据上所加的锁,才能访问该数据,并且在该线程没有释放锁之前,其他的线程是不能够访问该数据的,这样就保证了只有持有锁的线程能够访问该数据,也就保证了线程安全。

class LoveYou {
double love = 100;
final Lock lock = new Lock(); void increaseLove(double love) {
lock.obtain();
this.love += love;
lock.release();
} void decreaseLove(double love) {
lock.obtain();
this.love -= love;
lock.release();
}
}

上面的代码中展示了一个这样的场景:我对你的爱初始值是100,如果你做了一些让我开心的事情,我对你的爱意就会增加;如果你做了一些让我难过的事情,我对你的爱意就会减少。因为你让我开心或难过总是反反复复的,如果把时间线拉得无限长,这就是一个并发的场景。增加爱意和减少爱意这两个方法被并发调用,它们共同操作总的爱意,而为了保证爱意的前后一致性,就需要在每次对数据进行操作之前先获取锁,操作完成之后再释放锁。

这种对数据进行加锁的做法,虽然能够很好地解决线程安全问题,但是锁的获取和释放是需要耗费资源的,如果在线程很少的情况下(并发很少),即线程安全问题发生概率较小的情况下,就很容易造成资源的浪费。

使用乐观锁(CAS)保证线程安全(状态比较法)

乐观锁是在并发量小的情况下对悲观锁的一种替代方案,具体是为了降低悲观锁可能产生的资源浪费。

所谓的乐观,指的就是乐观地认为数据在并发量小的情况下,被意外修改的可能性较小。

乐观锁通常的实现就是CAS(Compare and Swap,比较并交换)。假如有一个线程在操作数据,操作到一半想要休眠(挂起)了,然后它就会记录下当前数据的状态(当前数据值),然后就休眠(挂起)了。然后线程重新唤醒之后想要接着操作数据,这时候又担心数据可能被修改了,于是就把线程休眠前保存的数据状态和现在的数据状态做一个比较,如果是一样的话,说明在线程休眠的过程中数据没有被别的线程动过(也有可能数据已经被别的线程改过好多轮了,只是最后的数据和该线程休眠前的数据一致,这就是所谓的ABA问题),然后就可以接着完成线程还没完成的操作。如果数据前后不一致,则说明数据被修改,那么这时候线程前面的所有操作都要放弃,从头开始重新再处理一遍逻辑。

然后说一下ABA问题的解决方案,解决方案通常是给数据另外加一个作为标记的版本号字段,并规定每次修改数据都使版本号加1,就能有效判断数据到底有没有被修改过了。

最后再说一下乐观锁和悲观锁的使用场景。乐观锁通常适用在并发量较小的场景下,因为这种场景下数据被并发操作的概率很小,加互斥锁会浪费资源;而悲观锁通常适用在并发量很大的场景下,因为这种场景下数据被并发操作的概率很大,如果使用乐观锁的话,在每次数据被修改后线程都从头开始重新处理一遍逻辑,资源的消耗会远大于互斥锁的资源消耗,因此加互斥锁基本上是目前高并发场景的最优方案。

总结

线程的安全问题,从我的理解上,其实很大程度上是在于线程不是持续工作,而是会在工作的途中休眠所造成的,但是这个可能并没有办法解决,因为这是操作系统所决定的,也可以说是CPU的运行机制所决定的,我们只能从另外的入口去想办法解决问题。

"今天起了风,你站在风口,我的整个世界都是你的味道。"

java中的线程安全的更多相关文章

  1. Java中的线程

    http://hi.baidu.com/ochzqvztdbabcir/item/ab9758f9cfab6a5ac9f337d4 相濡以沫 Java语法总结 - 线程 一 提到线程好像是件很麻烦很复 ...

  2. [译]线程生命周期-理解Java中的线程状态

    线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...

  3. Java中的线程Thread总结

    首先来看一张图,下面这张图很清晰的说明了线程的状态与Thread中的各个方法之间的关系,很经典的! 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口. 要注意的是Threa ...

  4. JAVA中创建线程的三种方法及比较

    JAVA中创建线程的方式有三种,各有优缺点,具体如下: 一.继承Thread类来创建线程 1.创建一个任务类,继承Thread线程类,因为Thread类已经实现了Runnable接口,然后重写run( ...

  5. 浅谈利用同步机制解决Java中的线程安全问题

    我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等 ...

  6. 第9章 Java中的线程池 第10章 Exector框架

    与新建线程池相比线程池的优点 线程池的分类 ThreadPoolExector参数.执行过程.存储方式 阻塞队列 拒绝策略 10.1 Exector框架简介 10.1.1 Executor框架的两级调 ...

  7. Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。

    java中,线程的状态使用一个枚举类型来描述的.这个枚举一共有6个值: NEW(新建).RUNNABLE(运行).BLOCKED(锁池).TIMED_WAITING(定时等待).WAITING(等待) ...

  8. Java中创建线程的三种方式以及区别

    在java中如果要创建线程的话,一般有3种方法: 继承Thread类: 实现Runnable接口: 使用Callable和Future创建线程. 1. 继承Thread类 继承Thread类的话,必须 ...

  9. Java中的线程同步

    Java 中的线程同步问题: 1. 线程同步: 对于访问同一份资源的多个线程之间, 来进行协调的这个东西. 2. 同步方法: 当某个对象调用了同步方法时, 该对象上的其它同步方法必须等待该同步方法执行 ...

  10. Java中的线程状态转换和线程控制常用方法

    Java 中的线程状态转换: [注]:不是 start 之后就立刻开始执行, 只是就绪了(CPU 可能正在运行其他的线程). [注]:只有被 CPU 调度之后,线程才开始执行, 当 CPU 分配给你的 ...

随机推荐

  1. bugku--web--输入密码查看flag

    首先打开网页链接 随机五位数的密码爆破,先用python写一个脚本来生成随机五位数: x=range(0,10) f=open("3.txt",'w') for i in x: f ...

  2. bugku猫片

    这个猫片思路清奇,真的让我长知识了. 开局一只猫,挺可爱的.   拿到图片,老套路来一波,首先 winhex打开是正常png图片,binwalk ,stegslove都没有任何收获. 折腾了好久没有任 ...

  3. 如何成长为一名合格的web前端开发工程师呢?

    前端开发工程师不仅仅要掌握一些基础的美工设计等还要懂得网页设计类的HTML JavaScript和css,这三种能力缺一不可,虽不要求你特别的精通,但至少要熟练的掌握,能够运用自己所了解的这些技术和知 ...

  4. [Quarks PwDump]Hash dump神器

    好不好用就不用说了哈  记录下使用方式 也支持导出本地哈希.域控哈希等.配合hashcat神器 奇效. 它目前可以导出 : – Local accounts NT/LM hashes +history ...

  5. [BZOJ5280] [Usaco2018 Open]Milking Order

    Description Farmer John的N头奶牛(1≤N≤105),仍然编号为1…N,正好闲得发慌.因此,她们发展了一个与Farmer John每 天早上为她们挤牛奶的时候的排队顺序相关的复杂 ...

  6. 【Spring Cloud】全家桶介绍(一)

    一.微服务架构 1.微服务架构简介 1.1.分布式:不同的功能模块部署在不同的服务器上,减轻网站高并发带来的压力. 1.2.集群:多台服务器上部署相同应用构成一个集群,通过负载均衡共同向外提供服务. ...

  7. NodeJs编写Cli实现自动初始化新项目目录结构

    应用场景 前端日常开发中,会遇见各种各样的cli,这些工具极大地方便了我们的日常工作,让计算机自己去干繁琐的工作,而我们,就可以节省出大量的时间用于学习.交流.开发. 注释:文章附有源码链接! 使用工 ...

  8. 研究了3天,终于将 Shader 移植到 Cocos Creator 2.2.0 上了!

    预览 扫光特效-Fluxay2 马赛克像素特效-Mosaic 过渡效果-Transfer Shawn 花了3天时间,研究了Cocos Creator 2.2.0 的 Effect 语法,终于在1024 ...

  9. 『嗨威说』算法设计与分析 - 动态规划思想小结(HDU 4283 You Are the One)

    本文索引目录: 一.动态规划的基本思想 二.数字三角形.最大子段和(PTA)递归方程 三.一道区间动态规划题点拨升华动态规划思想 四.结对编程情况 一.动态规划的基本思想: 1.1 基本概念: 动态规 ...

  10. CodeForces - 1214D B2. Books Exchange (hard version)

    题目链接:http://codeforces.com/problemset/problem/1249/B2 思路:用并查集模拟链表,把关系串联起来,如果成环,则满足题意.之后再用并查集合并一个链,一个 ...