1.什么是线程安全性?

在线程安全性的定义中,最核心的就是正确性。当多线程访问调用某个类时,线程之间不会出现错误的交互,不管运行时线程如何交替执行,并且在主调代码不需要任何同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

2.原子性

无状态的对象一定是线程安全的。那么什么是有状态什么是无状态?简单的来说:有状态的对象就是有实例变量的对象,可以保存数据的,这样的对象是非线程安全的。而无状态的对象就是没有实例变量的对象,不能保存数据,类不可变,所以线程安全。下面举一个有状态非线程安全的简单例子:

public class A {

private int a = 0;

public void increase(){

this.a++;

}

public void reduce(){

this.a--;

}

public int getValue(){

return this.a;

}

}

在对象A中有一个变量a,这个类很简单,有三个方法,递增、递减、获取a。很明显这个类是线程非安全的,尽管在单线程中它可以正确的运行,但是假设当多线程访问A类,并执行自增操作,它的操作序列是“读取-修改-写入”,也就是说我们假设,线程1号和线程2号同时访问A,并且同时执行increase()的情况下,那么就会演变成 线程1号获取到的a变量为0,线程2号获取到的a变量还是0的后果,然后线程1号和2号进行修改,在写入,最后得到的结果是 1。这就很尴尬了,在非线程安全并发的情况下,变量a的递增丢失了1。导致a更新丢失是因为increase()的a++是非原子的,它不会作为一个不可分割的操作来执行。

而在并发编程中出现以上代码的情况叫作:“竞态条件”,最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步操作,延迟初始化是竞态条件的常见情形:

public class B {

private A a = null;

public A getInstance() {

if (a == null) {

a = new A();

}

return a;

}

}

在B中包含竞态条件,当线程1号判断a==null,线程2号也判断到了a==null,这时候两个线程分别初始化A对象,然后就尴尬了。

如果想要避免竞态条件,就要在线程修改变量时,避免其他线程使用这个变量,确保其他线程只能在修改操作完成的情况下才能读取这个变量,在A的例子中,导致线程不安全的操作有两处,increase()和reduce(),我们可以对递增和递减使用原子操作来保证线程安全。java.util.concurrent.atomic的包提供很多支持原子操作的类。(所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何切换线程的动作)

我们可以把A的变量a修改为原子变量,适用原子操作来保证线程安全:

public class A {

private AtomicInteger a = new AtomicInteger(0);

public void increase(){

this.a.incrementAndGet();// 以原子方式将当前值加 1(返回新值)

//this.a.getAndIncrement();//以原子方式将当前值加 1(返回旧值)

}

public void reduce(){

this.a.decrementAndGet();//以原子方式将当前值减 1(返回新值)

//this.a.getAndDecrement();//以原子方式将当前值减 1(返回旧值)

}

public int getValue(){

return this.a.get();

}

}

3.可见性

把变量修改为原子变量,使用原子操作可以保证线程安全,但还有一些别的情况,把A在改造一下:

public class A {

private int a = 0;

private int cacheA = 0;

public void increaseVariable() {

System.out.println("a:----------" + ++a);

System.out.println("cachea:----------" + ++cacheA);

}

public void contrast() {

if (a==cacheA) {

System.out.println("一致");

} else {

System.out.println("不一致");

}

}

public void test() {

this.increaseVariable();

this.contrast();

}

public static void main(String[] args) throws InterruptedException {

A a = new A();

for (int i = 0; i < 10; i++) {

new Thread(new Runnable() {

@Override

public void run() {

a.test();

}

}).start();

}

}

}

输出结果:

a:----------1

a:----------6

cachea:----------1

不一致

a:----------5

cachea:----------3

不一致

a:----------8

a:----------4

a:----------3

a:----------2

cachea:----------6

不一致

cachea:----------5

不一致

cachea:----------4

不一致

a:----------10

a:----------9

a:----------7

cachea:----------2

cachea:----------10

一致

cachea:----------9

一致

cachea:----------8

一致

cachea:----------7

一致

一致

我们假设有10个线程同时调用了test(),在多线程的情况下,increaseVariable()的可见性已经被破坏了,可以看到代码中在针对a和cachea都是递增的操作,更新完a后在更新cachea,按照正常的逻辑,在contrast()对两者进行判断的时候应该是相等的,但是得到的结果让人十分尴尬。

4.加锁

Java提供了一种内置的锁机制来支持原子性,同时也能很好的处理可见性的问题:同步块 Synchronized Block同步块分为两种,一种是针对整个对象为锁的引用,一个作为由这个锁保护的代码块。另一种就是用Synchronized来修饰的方法,其中的锁就是方法所在的对象。我们在改造一下A:

public class A {

private int a = 0;

private int cacheA = 0;

public void increaseVariable() {

System.out.println("a:----------" + ++a);

System.out.println("cachea:----------" + ++cacheA);

}

public void contrast() {

if (a==cacheA) {

System.out.println("一致");

} else {

System.out.println("不一致");

}

}

public void test() {

synchronized (this) {//加同步块来处理可见性问题,还能保证递增的原子性

this.increaseVariable();

this.contrast();

}

}

public static void main(String[] args) throws InterruptedException {

A a = new A();

for (int i = 0; i < 10; i++) {

new Thread(new Runnable() {

@Override

public void run() {

a.test();

}

}).start();

}

}

}

输出结果:

a:----------1

cachea:----------1

一致

a:----------2

cachea:----------2

一致

a:----------3

cachea:----------3

一致

a:----------4

cachea:----------4

一致

a:----------5

cachea:----------5

一致

a:----------6

cachea:----------6

一致

a:----------7

cachea:----------7

一致

a:----------8

cachea:----------8

一致

a:----------9

cachea:----------9

一致

a:----------10

cachea:----------10

一致

每个Java对象都可以用做一个实现同步的锁,这些锁被称之为  内置锁或监视器锁。县城在进入同步代码之前会自动获得锁,退出同步代码释放锁。获得锁的唯一途径就是进入被同步代码保护的代码块或方法。

Java的内置锁相当于一种互斥体,也就是说只有一个线程能持有锁。当线程1号尝试获取线程2号持有的锁时,线程1号必须等待或阻塞,知道线程2号释放锁,线程1号才能获取锁。如果线程2号一直不释放,那么就等吧。

在锁保护的同步代码会使用原子方式执行,多个线程在执行该代码时也不会互相干扰。并发环境中的原子性   与     事务应用程序中的原子性     有这相同的含义“一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。”

A的代码经过改良使用同步的方式以后,现在是线程安全的了,但是,这种方式十分极端,换个场景假设有一个购物网站1元促销活动,100个用户同时去购买某件商品,而代码加上了同步处理,然后就尴尬了,先获取到锁的线程持有了锁,而后面的99个线程只能等待,等到先获取锁的线程释放了锁,第二个线程才能去获取锁,然后剩下的98个线程继续等待,这就十分尴尬了,很明显,这样的服务响应是无法让人接受,也就引发了性能问题,那么在改造一下A:

public class A {

private AtomicInteger a = new AtomicInteger(0);

private AtomicInteger cacheA = new AtomicInteger(0);

public void increaseVariable() {

synchronized (this) {

System.out.println("a:----------" + this.a.incrementAndGet());

System.out.println("cachea:----------" + this.cacheA.incrementAndGet());

}

}

public void contrast() {

if (a.get() == cacheA.get()) {

System.out.println("一致");

} else {

System.out.println("不一致");

}

}

public void test() {

this.increaseVariable();

this.contrast();

}

public static void main(String[] args) throws InterruptedException {

A a = new A();

for (int i = 0; i < 10; i++) {

new Thread(new Runnable() {

@Override

public void run() {

a.test();

}

}).start();

}

}

}

其实我们的目的只是想要increaseVariable()方法保持原子性和可见性,只需要在方法里进行递增的操作加个同步块或者把方法改为同步方法即可。这样便缩小了所得范围,性能得到了提升。

4.1 重入

内置锁是可以重入的,也就是当某个线程试图获得它已经持有的锁,那么这个请求是可以成功的获取到锁的。重入的时候每个锁会关联一个计数值和一个所有者线程。计数值为0的时候,这个锁就是没有任何线程持有的。当线程获取锁的时候,jvm虚拟机会记录锁的持有者,计数值设置为1。当这个线程想要再次获取锁的时候,计数值会进行递增,线程退出同步代码的时候,计数值会相对进行递减,直到计数值为0时,锁释放。

5.总结

同步的方式可以很好的控制原子性和可见性,但是随着同步而来的性能问题也着实让人头疼,但是无论什么情况下线程安全都是十分重要的。如果在执行时间比较长且调用频繁的代码尽量不要使用锁。

java线程安全性_Java并发-线程安全性相关推荐

  1. java 线程百科_Java并发——线程介绍

    前言: 互联网时代已经发展到了现在.从以前只考虑小流量到现在不得不去考虑高并发的问题.扯到了高并发的问题就要扯到线程的问题.你是否问过自己,你真正了解线程吗?还是你只知道一些其他博客里写的使用方法.下 ...

  2. java线程属性_Java 并发 线程属性

    Java 并发 线程属性 @author ixenos 线程优先级 1.每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程 2.默认情况下,一个线程继承它的父线程的优先级 当在一个运行的线 ...

  3. java queue 线程安全_java并发编程之线程安全方法

    线程安全的实现方法,包含如下方式 一, 互斥同步 使用互斥锁的方式. 举个栗子 synchronized,最常用的同步实现方案, ReentrantLock,java并发包中工具,后续介绍. 互斥同步 ...

  4. java 对象 线程安全_JAVA并发编程学习:构造线程安全的对象

    设计线程安全的类 实例限制 当一个对象被另一个对象封装时,所有访问被被封装对象的代码路径就是全部可知的,这相比于让对象可被整个系统访问来说,更容易对代码路径进行分析.将数据封装在对象内部,把对数据的访 ...

  5. java 并发 线程安全_Java并发教程–线程安全设计

    java 并发 线程安全 在回顾了处理并发程序时的主要风险(例如原子性或可见性 )之后,我们将通过一些类设计来帮助我们防止上述错误. 其中一些设计导致了线程安全对象的构造,从而使我们能够在线程之间安全 ...

  6. java 线程工厂_Java并发编程:Java的四种线程池的使用,以及自定义线程工厂

    引言 通过前面的文章,我们学习了Executor框架中的核心类ThreadPoolExecutor ,对于线程池的核心调度机制有了一定的了解,并且成功使用ThreadPoolExecutor 创建了线 ...

  7. java统计系统线程数_Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  8. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

  9. java线程池最大线程数_Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

最新文章

  1. 阿里云 物联网产品架构
  2. java抽象类到底能不能够实例化?
  3. unity5.x C# 获取屏幕宽度 设置不受重力影响
  4. 雷神开机logo更改_国产外星人雷神再发新品 911MT逐影者RTX2060光追游戏本评测
  5. 常见的通配符_8、数据库常见操作
  6. mysql查询包含字符串的记录_MySQL查询字符串中包含字符的记录
  7. java启动脚本_java启动脚本
  8. HCIP-RS H12-221-题库包含答案 1-50题(不定期更新剩余题目)
  9. unity3d插件分享paint in 3d插件的简单使用
  10. 操作系统源码及GeekOS学习
  11. 启动计算机 登管理员用户,Windows xp系统使用管理员账户登入系统的技巧
  12. 蓝凌OA系统任意文件读取
  13. 埃尼阿克计算机怎么运行的
  14. 华为line服务器无响应,line注册链接不到服务器
  15. 337调查之「普遍排除令」简介及2018年度典型案例探讨
  16. 【LSTM新闻数据集分类代码】
  17. 【转】看完这篇,请不要再说不懂MOSFET
  18. 魔兽地图编辑器触发器笔记
  19. 翻译——奇偶校验矩阵和低密度奇偶校验码的构造方法
  20. 演讲文档和视频《元宇宙与区块链IT基础设施》下载

热门文章

  1. CSDN写作Markdown编辑器中的Python命令帮手
  2. 基于ESP8266 WiFi控制的步进升降机械平台
  3. 2021西南位育高考成绩查询,上海近40所高中2020高考录取情况汇总!
  4. rabbimq与PHP,PHP初次使用rabbitMQ
  5. 数字政通图像处理面试_SLAM面试问题大全
  6. 汇编语言ac和c何时为1,汇编语言课堂练习一(参考答案)
  7. jsp中设置自动换行_微信公众号文章中如何设置自动回复?
  8. 快速创建 shell脚本
  9. -i 可以编译添加多个_C语言的编译过程
  10. 数组反向遍历ios_LeetCode106.从中序与后序遍历序列构造二叉树(Construct Binary Tree from Inor...)...