线程安全问题

1.线程安全问题的产生

        当不同的线程,针对同一个变量进行修改,产生与预期不同的结果,就产生了线程安全问题。举个例子:

        如上述代码中,两个线程t1,t2,都同时针对变量count进行修改,每个线程将count自增10000次,按照正常的预期,那么两个线程自增完成后,count的值应该为20000;但是实际上运行结果不是这样的,上述代码的运行结果如下:

        产生这样的问题的原因是,自增操作对于CPU来说不具备原子性。对于CPU来说,CPU进行自增分为三个步骤:1.读内存,2.自增,3.写内存;t1,t2两个线程同时进行自增,可能会出现t1和t2的三个步骤出现穿插:如下图:

        虽然t1完成了3个步骤,t2也完成了3个步骤,但是count并没有从0变成2,而是只增加了1;

        只要步骤不是t1先完成123三个步骤,t2再完成123三个步骤,或t2完成123三个步骤,然后t1完成123三个步骤,那么count都只会比原来增加1,这就产生了线程安全的问题。

2.线程安全问题的分析

        如1中所述,产生了线程安全问题,为什么会产生线程安全问题?

        首先,因为操作系统执行线程是随机调度,抢占式执行;这就导致t1和t2执行是无序的,可能t1先执行,也可能t2先执行,也可能t1和t2同时执行,具备随机性,这是产生线程安全问题的根本原因;

        第二,多个线程同时修改同一个变量。如上述例子,t1和t2同时修改count的值,count的值没有像预期一样出现20000;如果一个时间段内,只有一个线程修改count的值,那么是不会出现这种问题的;

        第三,修改操作不具备原子性;如果“1.读内存,2.自增,3.写内存”这三个操作是原子的,那么就不会出现两个线程步骤穿插的问题,那么count的值就不会算错;

3.如何解决上述线程安全的问题

        要想解决线程安全问题,那么就需要从产生线程安全问题产生的原因入手。主流的操作系统,都是采用随机调度的方式,修改操作系统的调度方式,至少目前来看还不现实;我们可以尽量避免多个线程修改同一个变量的情况,但是如果实际项目中有这样的需求,那么第二个原因,很难去规避。从第三个原因入手,是当前最常用的方式。我们可以让多个操作具备原子性,这样就不会出现穿插执行的问题。

        为了让多个操作放在一起具备原子性,Java中引入了一个关键字synchronized。使用这个关键字修饰的代码块或者方法,就具备原子性,在执行该代码块或者方法时,就会将其视为一个不可分割的整体,只有将代码块或者方法执行完毕后,另外的线程才能再执行这个代码块或者方法。这种解决方式,相当于是将并行的执行方式改为了串行的执行方式,牺牲了一些效率,当时保证了代码结果的正确性。

我们再来修改上面例子的代码,如下:

运行结果:

        上面代码值得注意的是,在使用synchronized()的时候,括号里写了一个“locker”,这个locker是一个实例化的一个对象,用哪个类来实例化都可以,这个对象可以理解为“一把锁”。t1和t2两个线程加锁的代码块里面内容,想要不穿插执行,那必须要加同一把锁,也就是括号里要写同一个对象。当一个线程里的代码块执行的时候,另外一个线程里的必须要等待前一个线程里的代码块执行完毕,执行完毕之后解锁,第二个线程才能加锁,执行代码块里的内容。不同线程的代码块同一时段加同一把锁,就会形成竞争局面,谁先获得锁谁先执行,待其执行完毕之后,下一个获得锁的线程才能继续执行,否则就会进入阻塞(BLOCKED)状态。这种竞争锁的情况,称为“锁竞争/锁冲突”。

上面是代码块加锁的情况,下面演示一个方法加锁的例子:

运行结果:

        上述synchronized实例方法的方式,相当于实例对象就是锁,在上述例子中demo就是锁。

同样,synchronized也可以修饰静态方法,相当于给类对象加锁:

运行结果:

synchronized使用方式总结:

a.synchronized(){} 圆括号指定锁对象;

b.修饰实例方法,相当于针对this加锁;

c.修饰静态方法,相当于针对类对象加锁;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值