Java多线程---多线程安全问题

   上一次我们说到在卖票问题中如果不将总票数设置为static静态变量,就会出现错票,

    即同样一张票会出售多次。

    在今天的问题中,我们继续通过卖票问题来进行研究。

    我们在每一个线程进行判断条件后让线程睡眠一段时间(让判断条件与数据操作之间相隔一段时间),看看会有什么效果?

    

运行结果:                                                                                   

通过运行结果我们可以发现有出售第0张票和第-1张票的情况 ,这是为什么呢?

是因为当有多个线程对一个共享数据进行操作时,可能会出现多线程的安全问题

这也是我们今天研究的主题。

当遇到这样的多线程安全问题时,我们可以通过synchronized关键字将它解决。

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

我们用synchronized关键字修饰一个代码块,将if判断语句“包”起来--->放入代码块中,再次运行

结果如下:

我们可以看出,没有再发生出售第0,-1张票的情况。

这是因为我们使用了synchronized关键字,使得一个线程在调用此同步锁的时候,将对象锁锁住(拥有对象锁)

注意,这里锁的是作为对象锁的对象,并不是代码块,切记切记......

线程拥有对象锁即只有该线程在执行完代码块中的内容后,将对象锁释放,其它线程才能拥有此对象锁,执行代码快中的内容,

若该线程没有将对象锁释放,则其它线程便只能等待,等到该线程执行完代码块中内容后将锁释放,然后其它线程拥有此对象锁,进而执行代码快中的内容。

若没有synchronized关键字,则假设此时总票数ticket为1,A线程此时获得CPU执行权,通过判断ticket=1 > 0,进入if语句,然后睡眠。(此时ticket没有减1,仍为1)

A线程进入睡眠后,B线程获得CPU执行权,通过判断ticket=1 > 0,进入if语句,然后睡眠。

A线程苏醒后,进行ticket--操作,此时ticke输出t为1,之后ticket=0,通过判断ticket=0 > 0失败,不执行if语句,进而退出while循环,执行完毕。

B线程苏醒后,此时ticket为不为1,为0,则ticket进行--操作后,输出为0,之后ticket=-1,通过判断ticket= -1 > 0失败,不执行if语句,进而退出while循环,执行完毕。

若程序主线程中还定义了第三个线程,则如线程B,输出为-1 ----->这就是出现出售第0,-1张票情况的原因。

若加了synchronized代码块,则,A线程在执行完代码块中的内容后(进行ticket-1操作后),

其它线程才能根据ticket进行判断,若不满足条件,就会结束循环,执行完毕。就不会发生上述出售第0,-1张票的情况

从而解决多个线程对一个共享数据进行操作时出现的安全问题......

接下来我们使用两个线程分别调用同步代码块和同步方法

 

运行结果仍会有出售第0张票的情况,,小伙伴们可以自己试一下。

这是因为两个线程的对象锁是不同的,A线程的对像锁是obj,而B线程的对像锁是this,

(这里需要注意的是,B线程调用了同步方法【同步方法的对像锁是this】

【静态同步方法的对象锁是当前类的字节码文件对象】,定义方法--->【类.class | 对象.getClass()】

即A线程虽然将它的对象锁(obj)锁住,但并不影响B线程拥有它自己的对像锁(this),

它们的对像锁是不同的,结果很明显--->这样并不能解决多线程安全问题

当我们将同步代码块的对像锁设置为this时,两个线程的对像锁相同,便能解决多线程安全问题

使用静态同步方法与同步方法类似,将同步代码块的对像锁设置为  类.class  或者  对象.getClass(),也能解决多线程安全问题。

最后再说两个相关知识点: 

同步方法与普通方法可以同时调用

synchronized获得的锁是可重入的

一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.

【具体理解为:在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时,是可以再次得到该对象的锁的。

                         也就是说在一个synchronized方法或块的内部调用本类的其他synchronized方法或块时,是永远可以得到锁的。】

这里需要注意的是,子类的同步方法中也可以调用父类的同步方法(通过super关键字)。

猜你喜欢

转载自www.cnblogs.com/dawner/p/9790942.html