JAVA多线程之深入研究 Java Synchronize 和 Lock 的区别与用法

在分布式开发中,锁是线程控制的重要途径。Java为此也提供了2种锁机制,synchronized和lock。做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方。
 
我们先从最简单的入手,逐步分析这2种的区别。
 
一、synchronized和lock的用法区别
 
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
 
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
 
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。
 
二、synchronized和lock性能区别
 
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
 
说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
 
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
 
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
 
我也只是了解到这一步,具体到CPU的算法如果感兴趣的读者还可以在查阅下,如果有更好的解释也可以给我留言,我也学习下。
 
三、synchronized和lock用途区别
 
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
 
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
 
下面细细道来……
 
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。(如果你没有了解java的中断机制,请参考下相关资料,再回头看这篇文章,80%的人根本没有真正理解什么是java的中断,呵呵)
 
这里来做个试验,首先搞一个Buffer类,它有读操作和写操作,为了不读到脏数据,写和读都需要加锁,我们先用synchronized原语来加锁,如下:

public class Buffer {
    private Object lock;
    public Buffer() {
        lock = this;
    }
    public void write() {
        synchronized (lock) {
            long startTime = System.currentTimeMillis();
            System.out.println("开始往这个buff写入数据…");
            for (;;)// 模拟要处理很长时间
            {
                if (System.currentTimeMillis()
                        - startTime > Integer.MAX_VALUE)
                    break;
            }
            System.out.println("终于写完了");
        }
    }   

    public void read() {
   synchronized (lock) {
            System.out.println("从这个buff读数据");
        }
    }
}

接着,我们来定义2个线程,一个线程去写,一个线程去读。

public class Writer extends Thread {
    private Buffer buff;
    public Writer(Buffer buff) {
        this.buff = buff;
    }   

    @Override
    public void run() {
   buff.write();
    }
}   

public class Reader extends Thread {
    private Buffer buff;
    public Reader(Buffer buff) {
        this.buff = buff;
    }   

    @Override
    public void run() {
        buff.read();//这里估计会一直阻塞
        System.out.println("读结束");   

    }   

}

好了,写一个Main来试验下,我们有意先去“写”,然后让“读”等待,“写”的时间是无穷的,就看“读”能不能放弃了。

public class Test {
    public static void main(String[] args) {
        Buffer buff = new Buffer();
        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);
        writer.start();
        reader.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    //等5秒钟去中断读
                    if (System.currentTimeMillis()
                            - start > 5000) {
                        System.out.println("不等了,尝试中断");
                        reader.interrupt();
                        break;
                    }
                }
            }
        }).start();   

    }
}

我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。

当然,要对reader和writer做响应的修改

public class Reader extends Thread {
    private BufferInterruptibly buff;
    public Reader(BufferInterruptibly buff) {
        this.buff = buff;
    }   

    @Override
    public void run() {
        try {
            buff.read();//可以收到中断的异常,从而有效退出
        } catch (InterruptedException e) {
            System.out.println("我不读了");
        }
        System.out.println("读结束");
    }
}   

/*** Writer倒不用怎么改动*/
public class Writer extends Thread {
    private BufferInterruptibly buff;
    public Writer(BufferInterruptibly buff) {
        this.buff = buff;
    }   

    @Override
    public void run() {
        buff.write();
    }
}   

public class Test {
    public static void main(String[] args) {
        BufferInterruptibly buff = new BufferInterruptibly();
        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);
        writer.start();
        reader.start();   

        new Thread(new Runnable() {   

            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    if (System.currentTimeMillis()
                            - start > 5000) {
                        System.out.println("不等了,尝试中断");
                        reader.interrupt();
                        break;
                    }
                }
            }
        }).start();
    }
}

这次“读”线程接收到了lock.lockInterruptibly()中断,并且有效处理了这个“异常”。

至于第二种情况,ReentrantLock可以与Condition的配合使用,Condition为ReentrantLock锁的等待和释放提供控制逻辑。
 
例如,使用ReentrantLock加锁之后,可以通过它自身的Condition.await()方法释放该锁,线程在此等待Condition.signal()方法,然后继续执行下去。await方法需要放在while循环中,因此,在不同线程之间实现并发控制,还需要一个volatile的变量,boolean是原子性的变量。因此,一般的并发控制的操作逻辑如下所示:

volatile boolean isProcess = false;
ReentrantLock lock  = new ReentrantLock();
Condtion processReady = lock.newCondtion();
thread: run() {
    lock.lock();
    isProcess = true;
   try {
    while(!isProcessReady) {  //isProcessReady 是另外一个线程的控制变量
      processReady.await();//释放了lock,在此等待signal
     }catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        } finally {
          lock.unlock();
          isProcess = false;
        }
      }
    }
}

这里只是代码使用的一段简化,下面我们看Hadoop的一段摘取的源码:

private class MapOutputBuffer<K extends Object, V extends Object>
      implements MapOutputCollector<K, V>, IndexedSortable {
...
    boolean spillInProgress;
    final ReentrantLock spillLock = new ReentrantLock();
    final Condition spillDone = spillLock.newCondition();
    final Condition spillReady = spillLock.newCondition();
    volatile boolean spillThreadRunning = false;
    final SpillThread spillThread = new SpillThread();
...
    public MapOutputBuffer(TaskUmbilicalProtocol umbilical, JobConf job,
                           TaskReporter reporter
                           ) throws IOException, ClassNotFoundException {
    ...
      spillInProgress = false;
      spillThread.setDaemon(true);
      spillThread.setName("SpillThread");
      spillLock.lock();
      try {
        spillThread.start();
        while (!spillThreadRunning) {
          spillDone.await();
        }
      } catch (InterruptedException e) {
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
        spillLock.unlock();
      }
    }

    protected class SpillThread extends Thread {
      @Override
      public void run() {
        spillLock.lock();
        spillThreadRunning = true;
        try {
          while (true) {
            spillDone.signal();
            while (!spillInProgress) {
              spillReady.await();
            }
            try {
              spillLock.unlock();
              sortAndSpill();
            } catch (Throwable t) {
              sortSpillException = t;
            } finally {
              spillLock.lock();
              if (bufend < bufstart) {
                bufvoid = kvbuffer.length;
              }
              kvstart = kvend;
              bufstart = bufend;
              spillInProgress = false;
            }
          }
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        } finally {
          spillLock.unlock();
          spillThreadRunning = false;
        }
      }
    }

代码中spillDone 就是 spillLock的一个newCondition()。调用spillDone.await()时可以释放spillLock锁,线程进入阻塞状态,而等待其他线程的 spillDone.signal()操作时,就会唤醒线程,重新持有spillLock锁。
 
这里可以看出,利用lock可以使我们多线程交互变得方便,而使用synchronized则无法做到这点。
 
最后呢,ReentrantLock这个类还提供了2种竞争锁的机制:公平锁和非公平锁。这2种机制的意思从字面上也能了解个大概:即对于多线程来说,公平锁会依赖线程进来的顺序,后进来的线程后获得锁。而非公平锁的意思就是后进来的锁也可以和前边等待锁的线程同时竞争锁资源。对于效率来讲,当然是非公平锁效率更高,因为公平锁还要判断是不是线程队列的第一个才会让线程获得锁。

原文地址:http://blog.csdn.net/natian306/article/details/18504111

原文地址:https://www.cnblogs.com/PrestonL/p/9507975.html

时间: 2024-10-03 23:13:42

JAVA多线程之深入研究 Java Synchronize 和 Lock 的区别与用法的相关文章

深入研究 Java Synchronize 和 Lock 的区别与用法

在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方. 我们先从最简单的入手,逐步分析这2种的区别. 一.synchronized和lock的用法区别 synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象. lock:需要显示指定起始位置和终止位置.一般使用Reen

[转] 深入研究 Java Synchronize 和 Lock 的区别与用法

在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方. 我们先从最简单的入手,逐步分析这2种的区别. 一.synchronized和lock的用法区别 synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象. lock:需要显示指定起始位置和终止位置.一般使用Reen

Synchronize 和 Lock 的区别与用法

一.synchronized和lock的用法区别 (1)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象. (2)lock(显示锁):需要显示指定起始位置和终止位置.一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对 象才能保证锁的生效.且在加锁和解锁处需要通过lock()和unlock()显示指出.所以一般会在finally块中写unloc

java 多线程(0) Java线程

线程 线程是系统调度的基本单元,每当创建一个进程时,会有许多的线程,也叫轻量级进程,在一个进程中拥有多个线程,各自都有自己的计数器,堆和局部变量属性,并且能够分享内存变量. 为什么要使用多线程  1.更多的处理器核心 2.更快的响应时间 3.更好的编程模型 优先级 线程优先级决定了线程需要多或少分配一些处理器资源的线程属性,通过priority来控制优先级,范围从1-10,在线程构建SetPriority(int)方法来修改优先级,默认为5. 线程的运行状态 状态名称 说明 NEW 初始状态,线

JAVA多线程suspend()、resume()和wait()、notify()的区别(转载)

suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态.典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情 形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复.但suspend()方法很容易引起死锁问题, 已经不推荐使用了. wait() 和 notify() 方法:两个方法配

Java多线程学习笔记——从Java JVM对多线程数据同步的一些理解

   我们知道在多线程编程中,我们很大的一部分内容是为了解决线程间的资源同步问题和线程间共同协作解决问题.线程间的同步,通俗我们理解为僧多粥少,在粥有限情况下,我们怎么去防止大家有秩序的喝到粥,不至于哄抢都没得喝.线程讲协作,我们可以理解为我们在医院看病的时候,我们要先挂号,才能看病.现在医院有很多病人排队,怎么协调病人都有秩序的先挂号,后看病.本篇文章的重点不在此,也不是在此一下子能分析完,我们先从Java JVM的角度来理解多线程的一些方面. 我们知道多线程间的数据同步,我们是通过加锁的操作

【java多线程】(3)---synchronized、Lock

synchronized.Lock 一.概述 1.出现线程不安全的原因是什么? 如果我们创建的多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中一个线程操作共享数据时,还未操作完成,另外的线程就参与进来,导致对共享数据的操作出现问题. 2.线程不安全解决办法 要求一个线程操作共享数据时,只有当其完成操作完成共享数据,其它线程才有机会执行共享数据.java提供了两种方式来实现同步互斥访问:synchronized和Lock. 二.synchronized synchronized可以

(Java多线程系列七)Java内存模型和线程的三大特性

Java内存模型和线程的三大特性 多线程有三大特性:原子性.可见性.有序性 1.Java内存模型 Java内存模型(Java Memory Model ,JMM),决定一个线程对共享变量的写入时,能对另一个线程可见.从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本.本地内存是JMM的一个抽象概念,并不真实存在. 用一张图表示

JAVA多线程(八)模式-Read Write Lock

Read Write Lock 写入是互斥的,读取是共享的,写入与读取不能同时发生. 适用环境 类似缓存更新的场景. 样例 缓存,多读多写,LRU淘汰. 读写锁 读读共享,读写.写写互斥. package ReadWriteLock; public class ReadWriteLock { private int waitWriters=0; private int writingWriters=0; private int readers=0; //读写都在等待时,写优先,读线程多,写线程少