本文将介绍何为线程安全,并给出具体的实现代码。

线程不安全性

先来举例说明线程不安全是什么情况下发生的:例如一个变量可以被多个线程进行访问,那么在大量线程并发访问这个变量的情况下,线程执行的顺序会给最后的结果带来不可预估的错误。
先定义一个单例类SimpleWorkingHardSingleton:

public class SimpleWorkingHardSingleton {
    private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton();
    
    // 数量
    private int count;
    
    private SimpleWorkingHardSingleton() {
        count = 0;
    }
    
    public static SimpleWorkingHardSingleton getInstance() {
        return simpleSingleton;
    }

    public int getCount() {
        return count;
    }
    
    public void addCount(int increment) {
        this.count += increment;
        System.out.println(this.count);
    }

}
//单例若在多线程环境下运行,count是被多个线程同时操纵的变量
for (int i = 0; i < 5; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            SimpleWorkingHardSingleton simpleSingleton = SimpleWorkingHardSingleton.getInstance();
            simpleSingleton.addCount(1);
        }
    }).start();
}
//结果
3
2
2
4
5

匪夷所思的结果,想不懂为什么会有两个2出现,1哪儿去了,为什么输出不是 1 2 3 4 5,下面就来解释一下

为什么没有1?
可能是a线程算出count=1,然后输出count的时候,此时a线程挂起,b线程执行,b线程对count自增,此时a线程再输出的时候,count已经发生了变化,这就导致了1没有被输出
为什么两个2?
可能是a,b两个线程都完成了count的计算,然后a线程输出,输出结束后立即被挂起,然后紧接着b线程立即也进行了输出,那么此时a b线程一定是输出了相同的count,就导致了相同值的出现。
将循环次数加大到100或者200,就会发现最后输出的count并不会到100或者200
这是由于count++这个操作也不是原子的,也就是说count++并不是一次性完成,而是分为3步骤。第一步,获取count;第二步,给count指向的值加1;第三步,讲计算结果写回count。因此如果三个步骤再混合上多线程执行顺序的错乱因素,就会导致不可预测的问题了。比如a线程获取count为1,此时a线程立马被挂起,b线程获取count也为1,然后a,b线程各自去执行,最后写回count都是2,这就导致了count被少加一次。

线程安全性

其实我们看到线程安全性的定义的关键点在于正确性,即在多线程的环境下,无论运行时候环境采用如何的调度方式,系统或者类或者方法总能表现出预期的相符的行为,那么就是线程安全的。

竞态条件

上述发生非安全性问题,在于线程的执行次序发生了改变,操作变量非原子性,我们称之为竞态条件。

静态条件发生主要有两种情况
1、先检查后执行:先判断某个条件是否为真,然后根据这个判断去做动作。其实很有可能在多次调用间你的判断是无效的,会导致(文件覆盖,未预期的异常,数据覆盖),最常见的就是延迟初始化,建议不要这么做。

class BaseProxy
{
    static $ins;
    public static function getInstance()
    {/*{{{*/
        if (false == self::$ins instanceof self)
        {
            self::$ins = new self();
        }
        return self::$ins;
    }/*}}}*/

2、还有另一种静态条件:先读取-修改-写入,非原子性,例如cnt++操作,导致发生异常。

如何解决静态条件

1、对于先读取-修改-写入情况,可以利用java.util.concurrent.atomic类解决这种问题,保证数值和对象引用的原子转换

import java.util.concurrent.atomic.AtomicInteger;
	public class AtomicIntegerTest {
	    static AtomicInteger ai = new AtomicInteger(1);
	    public static void main(String[] args) {
	        System.out.println(ai.getAndIncrement());
	        System.out.println(ai.get());
	    }
	}

2、加锁
  Java提供了强制性的内置锁机制:synchronized块。一个synchronized块有两个部分:锁对象的引用,以及这个锁保护的代码块。执行线程进入synchronized块之前会自动获得锁,无论通过正常控制路径退出还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁。

内部锁是可重进入的,这意味着锁的请求是基于“每线程”,而不是基于“每调用”的,重进入的实现是通过为每个锁关联一个请求技术和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将锁记录锁的占有者且将请求计数值置为1,。如果同一线程再次请求这个锁,计数 将递增,每次占用线程退出同步块,计数值将递减。直到计数器达到0时,锁被释放。

package com.bjsxt.base.sync001;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程安全概念:当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
 * synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
 * 
 * @author alienware
 *
 */
public class MyThread extends Thread {

    private int count = 5;

    // synchronized加锁
    public synchronized void run() {
        count--;
        System.out.println(this.currentThread().getName() + " count = " + count);
    }

    public static void main(String[] args) {
        /**
         * 分析:当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的),
         * 一个线程想要执行synchronized修饰的方法里的代码: 1 尝试获得锁 2
         * 如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,
         * 而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)
         */
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread, "t1");
        Thread t2 = new Thread(myThread, "t2");
        Thread t3 = new Thread(myThread, "t3");
        Thread t4 = new Thread(myThread, "t4");
        Thread t5 = new Thread(myThread, "t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

总结

在多线程环境下,之所以会出现并发的线程安全性问题,是由于多个线程去操纵一个共享的变量或者一组变量,而且变量的操作过程不是原子的,那么线程的执行顺序就会干扰到变量。

为了保证线程安全性,解决方法:
1、多个线程访问一个不可变量
2、变量不可以被多线程共享
3、线程做同步处理(原子性处理)