文档章节

Java并发-线程安全性

贲大侠
 贲大侠
发布于 2017/08/16 20:00
字数 2393
阅读 32
收藏 0

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.总结

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

 

© 著作权归作者所有

贲大侠
粉丝 1
博文 21
码字总数 19667
作品 0
海淀
程序员
私信 提问
浅谈Java中的ThreadLocal的多线程应用问题

什么是ThreadLocal?首先要说明的一点是ThreadLocal并不是一个Thread,而是Thread的局部变量。在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了...

小欣妹妹
2017/10/23
81
0
彻底理解ThreadLocal

知其然 synchronized这类线程同步的机制可以解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容。为了防止在多线程访问的过程中,可能会出现的并发错误。不得不...

相见欢
2013/03/02
33.4K
44
深入研究java.lang.ThreadLocal类

一、概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局...

FoxHu
2012/05/08
107
0
多线程之volatile与synchronized(二)

JMM中主要是围绕并发过程中如何处理原子性,可见性和有序性三个特性来建立的。最终可以保证线程安全性,volatile和synchronized两个关键字又是我们最常碰到与最容易提到的关键字,这次放在一...

艾贺
2018/04/05
0
0
[Java 并发编程] 集合框架之 同步容器类 & 并发容器类

吾生也有涯,而知也无涯。———《庄子》 通过上一篇文章,我们已经知道设计一个线程安全类的原则和步骤,以及在设计过程中我们应当注意的细节。实际上,Java 的集合库包含了线程安全集合和非...

seaicelin
2018/05/25
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
40分钟前
4
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
今天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
今天
4
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
今天
6
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部