线程安全是并发编程很重要的概念,那么什么是线程安全呢?
线程安全其实并不是指线程是否是安全的,线程本身是没有思想的,它是由我们的业务逻辑来决定它的行为的。《Java并发编程实践》和《深入了解Java虚拟机》的作者认为线程安全的主体是对象,也就是说我们可以说hashtable是线程安全的,hashmap是线程不安全的。但是线程安全的定义并不是统一的,也有一些定义描述认为线程安全的主题是一段代码或者一个方法,也就是我们也可以说同步方法或同步代码块中的线程是安全的。其实这两种说法都没有问题,为什么这样说呢?我们来了解下线程安全的本质。
我们知道现在主流的操作系统都是多进程的,它可以支持多个任务同时进行,同个进程的多个线程共享同一片内存区域,而有些区域比如堆是共享的,且绝大多数对象都是在堆中创建的,那当多个线程去访问堆中的对象时,就会产生问题。
举例:线程a要去操作堆中一个对象完成累加操作,当进行到一半时,cpu被线程b调度,它要对这个对象进行删除操作,当线程a再去执行时,这个对象已被删除,那么对线程a来说,它的行为与运行结果不符,则是线程不安全的
我们可以看出线程安全的本质一定涉及到共享变量,如果不是设计到多个线程访问同一个对象或访问同一个对象的方法,就不会存在线程安全的问题了。
总结:多个线程访问共享变量产生的线程执行结果混乱的行为就是线程不安全的,那么线程安全就是反过来说,即多个线程访问共享变量都能按照自己的行为执行正确的逻辑得到正确的结果,这是《Java并发编程实践》对于线程安全的描述
当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,即是线程安全的
我们上面讨论的线程安全其实是相对的线程安全,为什么这么说呢?这里引入Vector的例子
Vector其实就是动态数组,和ArrayList用法上区别不大,只是前者是线程相对安全的,后者为不能实现线程同步。我们来看vector的几个内部方法
其余方法大家自己看即可,我们可以得出为什么说vector是线程安全的呢?因为官方在可能发生线程安全问题的地方都加了一把同步锁,那为什么又说vector是线程不安全的呢?虽然同步锁的加入可以让一个线程操作同一个方法,但不能避免多个线程操作多个方法,在这里remove方法和add方法可以同时进行。删除元素方法分两步,先找到需要删除元素下标再将其删除,同样增加方法需要先找到添加的下标再传入参数,这样就可能存在add方法需要找的的下标已经被删除的问题,代码如下
public static void main(String[] args)
{
Vector vector = new Vector();
new Thread(()->
{
for (int i = 0; i < 100 ; i++)
{
vector.add(i);
vector.remove(i);
}
}).start();
new Thread(()->
{
for (int i = 0; i < 100 ; i++)
{
vector.add(i);
vector.remove(i);
}
}).start();
}
运行后报错
为了解决这种问题需要在调用方加锁,这种方式称为绝对的线程安全。
怎么使对象的行为线程安全呢?
最简单的方式是让对象不可变,即创建对象时用final和private修饰且不提供set访问器。
第二种方式是给每一个线程都拷贝一份数据,解决资源冲突问题,我们在这里消除了同一资源的概念来保证线程安全问题。
第三种方式是加上互斥锁,保证同一个时间只有一个线程可以拿到锁,synchronized,ReentrantLock都可以实现互斥锁。
第四种方式称为乐观锁,就是通过不加锁的方式来实现线程安全,认为当前线程操作的数据不会被其他线程修改,当前线程处理数据时对数据进行标记,当回来继续执行时如果数据被修改就放弃这次操作,可以对当前操作进行回滚然后重试,重试的次数和逻辑需要用户根据实际情况自己判定。