多线程的同步

  在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()方法实现线程阻塞。

  1. wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。

  2. wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

  3. 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。


  首先,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 就无法保证操作的原子性,这个时候就可以用锁。

扩展:

1、synchronized 与 lock 的区别

  1. Lock 是一个接口,synchronized 是一个关键字

  2. Lock 发生异常不会自动释放锁,可能导致死锁,需要在 try catch 的 finally 中通过 unlock() 主动释放锁, synchronized 发生异常会自动释放线程占有的锁,不会导致死锁。

  3. Lock 可以让等待锁的线程响应中断,而 synchronized 不能响应中断,会一直等待下去。

  4. Lock 可以实现公平锁, synchroinzed 不能保证公平性。

  5. Lock 通过读写锁 可以提高多个线程读的效率。

  6. Lock 可以通过tryLock()方法,知道有没有成功获取锁,synchronized 则不行。

2、volatile 和 synchronized 的区别

  1. volatile 作用于变量,synchronized 作用于代码块或方法。

  2. volatile 不对变量加锁,不会造成阻塞,synchronized 会加锁,造成阻塞。

  3. volatile 仅能实现变量的修改可见性,并不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。(synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改刷新到主存中)。

  4. volatile 禁止指令重排,标记的变量不会被编译器优化,synchroinzed 标记的变量可以被编译器优化。

3、 volatile 的作用

1. 禁止指令重排,使得前面的的操作都已经执行,后面的操作都没有执行。

2. 保证了不同线程对这个变量读取的可见性,即一个线程修改了该变量,其他线程都能立刻可见。

 volatile 关键字会强制将修改的值写入主存,线程1修改时,会导致其他线程的工作内存的该缓存变量无效。

原文地址:https://www.cnblogs.com/yumingxing/p/9542508.html