多线程的同步

Posted yumingxing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程的同步相关的知识,希望对你有一定的参考价值。

  在Java中,有四种方式来实现同步互斥访问:synchronized 、 Lock 、wait() / notify() / notifyAll() 方法和 CAS(硬件CPU同步原语)。

一、synchronized 

1. 同步代码块

1 synchronized(object){
2 }

  表示线程在执行的时候会将object 对象上锁。(注意这个对象可以是任意类的对象,也可以使用this 关键字表示本对象或者是class 对象)。
  可能一个方法中只有几行代码会涉及到线程同步问题,所以synchronized块比synchronized 方法更加细粒度地控制了多个线程的访问,只有synchronized 块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized 块之前的和之后的)。

2. 作用于非静态方法

1 public synchronized void increase(){
2      i++;
3 }

  当一个线程访问某个对象的synchronized 方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized 方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的 synchronized 方法。

3. 作用于静态方法

1 static int i=0;
2 public synchronized void increase(){
3      i++;
4 }    

  当一个synchronized 关键字修饰的方法同时又被static 修饰,之前说过,非静态的同步方法会将对象上锁,但是静态方法不属于对象,而是属于类,它会将这个方法所在的类的Class 对象上锁。

二、Lock 的用法。

java.util.concurrent.locks包中常用的类和接口:

  Lock 接口及其实现类 ReentrantLock

  ReadWriteLock 接口 及其实现类 ReentrantReadWriteLock

1.Lock  & ReentrantLock

Lock lock = new ReentrantLock();
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

  ReentrantLock是唯一实现了Lock接口的类。使用Lock 必须在 try-catch-finally 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。lock 必须要手动释放锁,中断不会自动释放,synchronized 中断后会自动释放锁。

2.ReadWriteLock & ReentrantReadWriteLock

 1 public interface ReadWriteLock {
 2     /**
 3      * return the lock used for reading.
 4      */
 5     Lock readLock();
 6  
 7     /**
 8      * return the lock used for writing.
 9      */
10     Lock writeLock();
11 }

  读锁是共享锁,写锁是排他锁。如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

三、wait() / notify() / notifyAll() 方法的使用(Java 中怎样唤醒一个阻塞的线程?)

  在Java 发展史上曾经使用suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。

  解决方案可以使用以对象为目标的阻塞,即利用Object 类的wait()和notify()方法实现线程阻塞。
  首先,wait、notify 方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其  次,wait、notify 方法必须在synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify 方法的对象是同一个,如此一来在调用wait 之前当前线程就已经成功获取某对象的锁,执行wait 阻塞后当前线程就将之前获取的对象锁释放。

四、CAS(硬件CPU同步原语)

1. 如果不用锁机制如何实现共享数据访问?

  无锁化编程的常用方法:硬件CPU 同步原语CAS(Compare and Swap)。

  CAS是一种非阻塞的同步方式。CAS 实现了区别于sychronized 同步锁的一种乐观锁,当多个线程尝试使用CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 有3 个操作数,内存值V,旧的预期值A,要修改后的新值B。当且仅当预期值A 和内存值V 相同时,将内存值V 修改为B,否则什么都不做。

  一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用CAS刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果一致,就可以更新成功。

1 public final int incrementAndGet() {
2     for (;;) {
3         int current = get();
4         int next = current + 1;
5         if (compareAndSet(current, next))
6         return next;
7     }
8 }            

  在这里采用了CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行CAS 操作,如果成功就返回结果,否则重试直到成功为止。

  首先假设有一个变量i,i 的初始值为0。每个线程都对i 进行+1 操作。CAS是这样保证同步的:假设有两个线程,线程1 读取内存中的值为0,current = 0,next = 1,然后挂起,然后线程2 对i 进行操作,将i 的值变成了1。线程2 执行完,回到线程1,进入if 里的compareAndSet 方法,该方法进行的操作的逻辑是,(1)如果操作数的值在内存中没有被修改,返回true,然后compareAndSet 方法返回next 的值(2)如果操作数的值在内存中被修改了,则返回false,重新进入下一次循环,重新得到current 的值为1,next 的值为2,然后再比较,由于这次没有被修改,所以直接返回2。

 

2. CAS 的优缺点:

优点:CAS 由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。

缺点:

1、循环时间长开销大。自旋CAS 如果长时间不成功,会给CPU 带来非常大的。执行开销。因此CAS 不适合竞争十分频繁的场景。

2、只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环CAS 就无法保证操作的原子性,这个时候就可以用锁。



以上是关于多线程的同步的主要内容,如果未能解决你的问题,请参考以下文章

多线程编程

线程同步-使用ReaderWriterLockSlim类

详解C++多线程

进程线程同步异步

java基础入门-多线程同步浅析-以银行转账为样例

Java多线程——Lock&Condition