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