java-并发-解决锁竞争的问题

java锁的分类

锁作为并发共享数据,保证一致性的工具。在java.util.lock中有一些不错的策略
1、自旋锁

2、阻塞锁

3、可重入锁

4、读写锁

5、互斥锁

6、悲观锁

7、乐观锁

8、公平锁

9、非公平锁

10、偏向锁

11、对象锁

12、线程锁

13、锁粗化

14、轻量级锁

15、锁消除

16、锁膨胀

17、信号量
竞争锁与无竞争锁的区别。如果一个线程尝试进入另一个线程正在执行的同步块或者方法时,便会出现锁竞争。第二个线程就必须等待前一个线程执行完这个同步块并释放掉监视器(monitor)。如果只有一个线程在执行这段同步的代码,这个锁就是无竞争的。JVM中的同步已经针对这种无竞争的情况进行了优化,对于绝大多数应用而言,无竞争的锁几乎是没有任何额外的开销的。因此,出了性能问题不能光怪锁,主要是竞争锁。

减少锁的竞争或者竞争的时间

实现线程安全最快的方法就是直接将整个方法上锁

class GameServer {
public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();
public synchronized void join(Player player, Table table) {
if (player.getAccountBalance() > table.getLimit()) {
    List<Player> tablePlayers = tables.get(table.getId());
    if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
    }
  }
}
public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}
public synchronized void createTable() {/*body skipped for brevity*/}
public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
}
作者的想法是好的——就是当新的玩家加入的时候,必须得保证桌上的玩家的数量不能超过9

不过这个上锁的方案更适合加到牌桌上,而不是玩家进入的时候——即便是在一个流量一般的扑克网站上,这样的系统也肯定会由于线程等待锁释放而频繁地触发竞争事件。被锁住的代码块包含了帐户余额以及牌桌上限的检查,这里面很可能会包括一些很昂贵的操作,这样不仅会容易触发竞争并且使得竞争的时间变长。
解决问题的第一步就是要确保你保护的是数据,而不是代码,先将同步从方法声明移到方法体里。在上面这个简短的例子中,刚开始好像能修改的地方并不多。不过我们考虑的是整个GameServer类,而不只限于这个join()方法:

class GameServer {
public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {
synchronized (tables) {
if (player.getAccountBalance() > table.getLimit()) {
  List<Player> tablePlayers = tables.get(table.getId());
  if (tablePlayers.size() < 9) {
    tablePlayers.add(player);
  }
}
}
}
public void leave(Player player, Table table) {/* body skipped for brevity */}
public void createTable() {/* body skipped for brevity */}
public void destroyTable(Table table) {/* body skipped for brevity */}
}

这看似一个很小的改动,却会影响到整个类的行为。当玩家加入牌桌 时,前面那个同步的方法会锁在GameServer的this实例上,并与同时想离开牌桌(leave)的玩家产生竞争行为。而将锁从方法签名移到方法内部以后,则将上锁的时机往后推迟了,一定程度上减小了竞争的可能性

缩小锁的作用域

public class GameServer {
public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {
if (player.getAccountBalance() > table.getLimit()) {
synchronized (tables) {
  List<Player> tablePlayers = tables.get(table.getId());
  if (tablePlayers.size() < 9) {
    tablePlayers.add(player);
  }
}
}
}
//other methods skipped for brevity
}

现在检查玩家余额的这个耗时操作就在锁作用域外边了。注意到了吧,锁的引入其实只是为了保护玩家数量不超过桌子的容量而已,检查帐户余额这个事情并不在要保护的范围之内。

分拆锁

public class GameServer {
public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {
if (player.getAccountBalance() > table.getLimit()) {
List<Player> tablePlayers = tables.get(table.getId());
synchronized (tablePlayers) {
  if (tablePlayers.size() < 9) {
    tablePlayers.add(player);
  }
}
}
}
//other methods skipped for brevity
}

现在我们把对所有桌子同步的操作变成了只对同一张桌子进行同步,因此出现锁竞争的概率就大大减小了。如果说桌子中有100张桌子的话,那么现在出现竞争的概率就小了100倍。

使用并发的数据结构

public class GameServer {
public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();

public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}
public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}

public synchronized void createTable() {
Table table = new Table();
tables.put(table.getId(), table);
}

public synchronized void destroyTable(Table table) {
tables.remove(table.getId());
}
}

join()和leave()方法的同步操作变得更简单了,因为我们现在不用再对tables进行加锁了,这都多亏了ConcurrentHashMap。然而,我们还是要保证每个tablePlayers的一致性。因此这个地方ConcurrentHashMap帮不上什么忙。同时我们还得在createTable()与destroyTable()方法中创建新的桌子以及销毁桌子,这对ConcurrentHashMap而言本身就是并发的,因此你可以并行地增加或者减少桌子的数量。

网上还有一些方案

●降低锁的可见性。在上述例子中,锁是声明为public的,因此可以被别人所访问到,你所精心设计的监视器可能会被别人锁住,从而功亏一篑。

●看一下java.util.concurrent.locks包下面有哪些锁策略对你是有帮助的。

●使用原子操作。上面这个例子中的简单的计数器其实并不需要进行加锁。将计数的Integer换成AtomicInteger对这个场景来说就绰绰有余了。

    原文作者:java锁
    原文地址: https://blog.csdn.net/jianghuxiaojin/article/details/51988532
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞