关于Java多线程编程锁优化的深入学习

正文

并发环境下进行编程时,需要使用锁机制来同步多线程间的操作,保证共享资源的互斥访问。加锁会带来性能上的损坏,似乎是众所周知的事情。然而,加锁本身不会带来多少的性能消耗,性能主要是在线程的获取锁的过程。如果只有一个线程竞争锁,此时并不存在多线程竞争的情况,那么JVM会进行优化,那么这时加锁带来的性能消耗基本可以忽略。因此,规范加锁的操作,优化锁的使用方法,避免不必要的线程竞争,不仅可以提高程序性能,也能避免不规范加锁可能造成线程死锁问题,提高程序健壮性。下面阐述几种锁优化的思路。

一、尽量不要锁住方法

在普通成员函数上加锁时,线程获得的是该方法所在对象的对象锁。此时整个对象都会被锁住。这也意味着,如果这个对象提供的多个同步方法是针对不同业务的,那么由于整个对象被锁住,一个业务业务在处理时,其他不相关的业务线程也必须wait。下面的例子展示了这种情况:

LockMethod类包含两个同步方法,分别在两种业务处理中被调用:

public class LockMethod {
 public synchronized void busiA() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness A:"+i);
  }
 }
 public synchronized void busiB() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness B:"+i);
  }
 }
}

BUSSA是线程类,用来处理A业务,调用的是LockMethod的busiA()方法:

public class BUSSB extends Thread {
 LockMethod lockMethod;
 void deal(LockMethod lockMethod){
  this.lockMethod = lockMethod;
 }

 @Override
 public void run() {
  super.run();
  lockMethod.busiB();
 }
}

TestLockMethod类,使用线程BUSSA与BUSSB进行业务处理:

public class TestLockMethod extends Thread {
 public static void main(String[] args) {
  LockMethod lockMethod = new LockMethod();
  BUSSA bussa = new BUSSA();
  BUSSB bussb = new BUSSB();
  bussa.deal(lockMethod);
  bussb.deal(lockMethod);
  bussa.start();
  bussb.start();
 }
}

运行程序,可以看到在线程bussa 执行的过程中,bussb是不能够进入函数 busiB()的,因为此时lockMethod 的对象锁被线程bussa获取了。

二、缩小同步代码块,只锁数据

有时候为了编程方便,有些人会synchnoized很大的一块代码,如果这个代码块中的某些操作与共享资源并不相关,那么应当把它们放到同步块外部,避免长时间的持有锁,造成其他线程一直处于等待状态。尤其是一些循环操作、同步I/O操作。不止是在代码的行数范围上缩小同步块,在执行逻辑上,也应该缩小同步块,例如多加一些条件判断,符合条件的再进行同步,而不是同步之后再进行条件判断,尽量减少不必要的进入同步块的逻辑。

三、锁中尽量不要再包含锁

这种情况经常发生,线程在得到了A锁之后,在同步方法块中调用了另外对象的同步方法,获得了第二个锁,这样可能导致一个调用堆栈中有多把锁的请求,多线程情况下可能会出现很复杂、难以分析的异常情况,导致死锁的发生。下面的代码显示了这种情况:

synchronized(A){
 synchronized(B){
  }
}

或是在同步块中调用了同步方法:

synchronized(A){
 B b = objArrayList.get(0);
 b.method(); //这是一个同步方法
}

解决的办法是跳出来加锁,不要包含加锁:

{
  B b = null;
 synchronized(A){
 b = objArrayList.get(0);
 }
 b.method();
}

四、将锁私有化,在内部管理锁

把锁作为一个私有的对象,外部不能拿到这个对象,更安全一些。对象可能被其他线程直接进行加锁操作,此时线程便持有了该对象的对象锁,例如下面这种情况:

class A {
 public void method1() {
 }
}
class B {
 public void method1() {
  A a = new A();
  synchronized (a) { //直接进行加锁      a.method1();
  }
 }
}

这种使用方式下,对象a的对象锁被外部所持有,让这把锁在外部多个地方被使用是比较危险的,对代码的逻辑流程阅读也造成困扰。一种更好的方式是在类的内部自己管理锁,外部需要同步方案时,也是通过接口方式来提供同步操作:

class A {
 private Object lock = new Object();
 public void method1() {
  synchronized (lock){

  }
 }
}
class B {
 public void method1() {
  A a = new A();
  a.method1();
 }
}

五、进行适当的锁分解

考虑下面这段程序:

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);
  }
  }
 }
 }
 public void leave(Player player, Table table) {/*省略*/}
 public void createTable() {/*省略*/}
 public void destroyTable(Table table) {/*省略*/}
}

在这个例子中,join方法只使用一个同步锁,来获取tables中的List<Player>对象,然后判断玩家数量是不是小于9,如果是,就调增加一个玩家。当有成千上万个List<Player>存在tables中时,对tables锁的竞争将非常激烈。在这里,我们可以考虑进行锁的分解:快速取出数据之后,对List<Player>对象进行加锁,让其他线程可快速竞争获得tables对象锁:

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 = null;            synchronized(tables) {                tablePlayers = tables.get(table.getId());            }            synchronized(tablePlayers) {                if (tablePlayers.size() < 9) {                    tablePlayers.add(player);                }            }        }    }    public void leave(Player player, Table table) {        /*省略*/    }    public void createTable() {        /*省略*/    }    public void destroyTable(Table table) {        /*省略*/    }}

您可能感兴趣的文章:

  • java实现死锁的示例代码
  • 利用Python+Java调用Shell脚本时的死锁陷阱详解
  • java避免死锁的常见方法代码解析
  • java语言描述Redis分布式锁的正确实现方式
  • Java并发问题之乐观锁与悲观锁
  • 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)
  • java线程死锁代码示例
  • Java多线程之显示锁和内置锁总结详解
  • Java线程同步Lock同步锁代码示例
  • 浅谈Java堆外内存之突破JVM枷锁
(0)

相关推荐

  • 利用Python+Java调用Shell脚本时的死锁陷阱详解

    前言 最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编写 Spark 任务时都是封装为一个 Jar 包,然后采用 Shell 脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用 Shell 脚本完成不利于开发测试,所以调研使用了 Python 和 Java 分别调用 Spark 脚本的方法. 使用版本为 Python 3.6.4 及 JDK 8 Python 主要使用 subprocess 库.Python 的 API 变动比较频繁,在 3.5 之后新增了

  • Java线程同步Lock同步锁代码示例

    java线程同步原理 java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronizedmethods)被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求. 当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor.如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法:如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放. 当线程退出同步方法调用时

  • 浅谈Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM给的内存不够用,怎么办? 此时,堆外内存登场!利用堆外内存,不仅可以随意操控内存,还能提高网络交互的速度. 背景1:JVM内存的分配 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里

  • java避免死锁的常见方法代码解析

    死锁 索是一个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解.但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用.让我们先来看一段代码,这段代码会引起死锁,使线程 thread_1 和线程 thread_2 互相等待对方释放锁. package thread; public class DeadLockDemo { private static String A = "A"; private static String B = &

  • java语言描述Redis分布式锁的正确实现方式

    分布式锁一般有三种实现方式:1.数据库乐观锁:2.基于Redis的分布式锁:3.基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在持有锁的期间

  • java线程死锁代码示例

    死锁是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一. 事实上,计算机世界有很多事情需要多线程方式去解决,因为这样才能最大程度上利用资源,才能体现出计算的高效.但是,实际上来说,计算机系统中有很多一次只能由一个进程使用的资源的情况,例如打印机,同时只能有一个进程控制它.在多通道程序设计环境中,若干进程往往要共享这类资源,而且一个进程所需要的资源还很有可能不止一个.因此,就会

  • 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    synchronized 和 Reentrantlock 多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢?是仅仅增加了一种选择还是另有其因?本文为您一探究竟. // synchronized关键字用法示例 public synchronized void add(int t){// 同步方法 this.v += t; } public stati

  • java实现死锁的示例代码

    什么是死锁 我们先看看这样一个生活中的例子:在一条河上有一座桥,桥面较窄,只能容纳一辆汽车通过,无法让两辆汽车并行.如果有两辆汽车A和B分别由桥的两端驶上该桥,则对于A车来说,它走过桥面左面的一段路(即占有了桥的一部分资源),要想过桥还须等待B车让出右边的桥面,此时A车不能前进:对于B车来说,它走过桥面右边的一段路(即占有了桥的一部分资源),要想过桥还须等待A车让出左边的桥面,此时B车也不能前进.两边的车都不倒车,结果造成互相等待对方让出桥面,但是谁也不让路,就会无休止地等下去.这种现象就是死锁

  • Java多线程之显示锁和内置锁总结详解

    总结多线程之显示锁和内置锁 Java中具有通过Synchronized实现的内置锁,和ReentrantLock实现的显示锁,这两种锁各有各的好处,算是互有补充,这篇文章就是做一个总结. *Synchronized* 内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁. synchronized(list){ //获得锁 list.append(); list.count(); }//释放锁 通信 与Synchronized配套使用的通信方法通常

  • Java并发问题之乐观锁与悲观锁

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁.再比如Java里面的同步原语synchronized关键字的实现也是悲观锁. 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版

随机推荐