Java:Java 线程不安全分析,同步锁和Lock机制,哪个解决方案更好( 二 )



  1. 小红、小强、小明都同时抢到了1号气球 , 由于线程调度 , 小强获取了cpu时间片 , 得以执行 , 而小明和小红则进入睡眠;小强打印出结果后 , 对num减一 , 此时num为0;

  2. 小明醒来 , 获得的num为0 , 然后小明将num打印出来 , 再对num减一 , 此时num为-1;

  3. 小红醒来 , 获得的num为-1 , 随后小红将num打印出来 , 再对num减一 , 此时怒木为-2;

  4. 由于多个线程是并发操作 , 所以对num做判断时可能上一个线程还未对num减一 , 故都能通过(num > 0)的判断;

解决方案:
在案例中的抢气球其实是两步操作:先抢到气球 , 再对气球总数减一;既然是两步操作 , 在并发中就完全有可能会被分开执行 , 且执行顺序无法得到控制;想要解决上述的线程不安全的问题 , 就必须要将这两步操作作为一个原子操作 , 保证其同步运行;也就是当一个线程A进入操作的时候 , 其他线程只能在操作外等待 , 只有当线程A执行完毕 , 其他线程才能有机会进入操作 。
原子操作:不能被分割的操作 , 必须保证其从一而终完全执行 , 要么都执行 , 要么都不执行 。
为解决多线程并发访问同一个资源的安全性问题 , Java 提供如下了几种不同的同步机制:
  1. 同步代码块;

  2. 同步方法;

  3. Lock 锁机制;

同步代码块同步代码块:为了保证线程能够正常执行原子操作 , Java 引入了线程同步机制 , 其语法如下:
同步代码块语法
上述中同步锁 , 又称同步监听对象、同步监听器、互斥锁 , 同步锁是一个抽象概念 , 可以理解为在对象上标记了一把锁;
Java 中可以使用任何对象作为同步监听对象 , 但在项目开发中 , 我们会把当前并发访问的共享资源对象作为同步监听对象 , 在任何时候 , 最多只能运行一个线程拥有同步锁 。
卫生间的使用就是一个很好的例子 , 一个卫生间在一段时间内只能被一个人使用 , 当一个人进入卫生间后 , 卫生间会被上锁 , 其他只能等待;只有当使用卫生间的人使用完毕 , 开锁后才能被下一个人使用 。
然后就可以使用同步代码块来改写抢气球案例 , 示例代码如下:
使用同步代码块来改写抢气球案例
通过查看运行结果 , 线程同步的问题已经得到解决 。

同步方法同步方法:使用synchronized修饰的方法称为同步方法 , 能够保证当一个线程进入该方法的时候 , 其他线程在方法外等待 。 比如:
同步方法语法
PS:方法修饰符不分先后顺序 。

使用同步方法来改写抢气球案例 , 代码如下:
使用同步方法来改写抢气球案例
注意:不能使用synchronized修改线程类中的run方法 , 因为使用之后 , 就会出现一个线程执行完了所有功能 , 多个线程出现串行;原本是多行道 , 使用synchronized修改线程类中的run方法 , 多行道变成了单行道 。

synchronized 的好与坏好:synchronized保证了并发访问时的同步操作 , 避免了线程的安全性问题 。
坏:使用synchronized的方法、代码块的性能会比不用要低一些 。
StringBuilder和StringBufferStringBuilder和StringBuffer 区别就在于StringBuffer中的方法都使用了synchronized修饰 , StringBuilder中的方法没有使用synchronized修饰;这也是StringBuilder性能比StringBuffer高的主要原因 。
Vector和ArrayList两者都有同样的方法 , 有同样的实现算法 , 唯一不同就是Vector中的方法使用了synchronized修饰 , 所以Vector的性能要比ArrayList低 。


推荐阅读