浅谈线程安全与sychronized

使用多线程处理相关业务,在一定程度上能够获得更高的执行效率,提高程序性能,但是,如果我们在写多线程程序时,不加强注意,容易出现数据不一致性,也就是我们常说的 "线程安全" 问题。线程安全是我们在设计多线程程序时必须保证的一点,如果我们写出的程序数据正确性不能得到保证,即使程序性能得到提高,也是毫无意义的。

非线程安全在什么时候会出现呢?

当多个线程对同一对象实例进行并发访问时,可能得到错误的结果,这就是线程不安全的。相反,无论多少线程对同一对象实例进行并发访问,都能得到一致的结果,这就是线程安全的。

下面我们给出一个经典的线程不安全的例子:

我们在当前程序中定义来了一个instance对象,对它执行++操作,按照正常逻辑,它返回的结果应该是200,然而结果总是差强人意。这里,我们可以看到返回的结果是144,大家可以多执行几次,看看有什么不同,由于instance++不是原子性操作,因此,我们的程序存在安全性问题。那么怎样才能使结果变为200呢?我们做一点小的修改:

大家可以发现,结果是我们预期值200了。这里我们在instance++操作上加了一段synchronized语句块,好神奇,它为什么就能解决线程安全性问题呢。

sychronized关键字,实现线程间的同步,它的作用就是给同步代码加锁,每一次只能有一个线程进入同步块,从而保证线程安全性问题。在上一段代码中,当线程A在对instance变量进行读写的时候,线程B只能等待线程A执行完成,才能对instance变量进行++操作。

sychronized大致有三种用法,这里详细讲解一下:

1.指定对象加锁。当线程访问同步块时,首先获取加锁对象,如果对象锁被其它线程持有,则进行等待。

2.作用于实例方法上。给当前实例加锁,访问同步代码时,首先获得当前实例的锁。

3.作用于静态方法上。静态方法加锁,给当前class加锁,访问同步代码时,需要获得当前类的锁。

大家现在应该立马就能想到,上述代码,我们使用的就是第一种方式,给指定对象加锁。我们加锁的对象就是当前类的实例对象,当我们对同一实例对象进行访问时,一次只有一个线程获得对象锁。关于另外两种加锁方式,我们修改一下上述代码来理解它们:

实例对象加锁:

结果符合预期,我们把之前的同步块改成了实例方法,该方法,锁住的是SychronizedDemo的实例对象,和之前同步块中this的效果一样。当线程需要访问同步方法时,必须先获得当前类的实例对象,否则,只能进行等待。

静态方法加锁:

结果符合预期,大家可以看到,main()方法中,我们new 了两个实例对象,但是结果竟然是正确的。因为我们使用的是静态方法加锁,这样锁住的就是当前类对象,无论我们定义多少个实例对象,结果都是一样的。

sychronized的另外一个特性:锁重入。在使用sychronized关键字时,当一个线程获得对象锁后,再次请求此对象锁时是可以再次获得该对象锁的。简单来说就是,在一个sychronized方法或同步块中调用当前类的其它sychronized方法,是能够得到锁的。大家可以想一下,当前线程在还未释放锁时,再一次请求锁,如果不可锁重入的话,就会形成死锁,这是大家最不想看到的。关于锁重入,大家可以写三个sychronized修饰的方法,然后相互调用,看看是否可以正常执行。限于篇幅,这里我就不贴出代码了。

sychronized关键字,大家是不是觉得它在解决线程安全时,特别的简单,无需太多的操作。大家再思考一下,如果我们现在有两个方法,第一个方法执行写操作,第二个方法执行读操作,当我们使用sychronized关键字修饰时,那么我们多个线程同时执行读方法,也会相互等待,这是我们不愿意看到的。我们只希望读写互斥,写写互斥,读读不互斥,那么我们就需要了解另外一个重入锁Lock了,下一节,我们关于Lock的用法进行详细讲解。

猜你喜欢

转载自blog.csdn.net/p_programmer/article/details/81160468