Java并发机制和Java中提供的锁介绍

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mingC0758/article/details/81779293

参考

https://www.cnblogs.com/dolphin0520/p/3920373.html

Java中的非阻塞算法
https://blog.csdn.net/lifuxiangcaohui/article/details/8051687

并发中的概念

并发编程中常遇到的三个问题

原子性问题

原子性操作是指程序执行的最小单位,也就是一个线程执行该操作时不能切换到其他线程。非原子性操作在多线程中可能会造成原子性问题,比如典型的i++:

i++ 是一个语法糖,在编译阶段会编译成: t=i+1; i=t;

试想如果两个线程同时执行到了t=i+1;然后后执行的i=t就会覆盖先执行的i=t

可见性问题

学过计算机原理的都知道,由于CPU的速度跟内存读写速度不匹配,所以在CPU跟内存之间加了一层高速缓存,使得CPU不能直接读写内存,只能读写高速缓存。

image

JVM的内存模型:

image

线程只能操作工作内存,在多线程情况下,由于每个工作线程对持有同一个变量的不同备份,导致一个线程改变了该变量的值,而另外一个线程得到的变量还是原来的值。这就是所谓的可见性的问题。

有序性问题

有序性问题是由CPU的指令重排引起的。CPU会对没有依赖关系的指令进行重排,这在单线程状态下是完全没有问题的,而多线程下可能会出现问题。

典型的问题是对象的实例化:

Object o = new Object;

实际上在CPU分为三个指令:

(1)开辟内存空间
(2)初始化
(3)赋值给o

由于CPU指令重排,真正执行的时候的顺序有可能是(1)(3)(2);

这样可能导致其他线程判断o != null 而使用o对象,而实际上o对象还没完成初始化,程序会报错。

CAS(Compare and Swap)

CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

java一些并发类就是应用了CAS代替阻塞的思想,少量的循环比短暂的阻塞效率更好(轻度和中度的争用情况),因为阻塞意味着需要进行线程的挂起和唤醒,涉及到上下文的切换。所以失败重试的非阻塞算法在某些情况下效率更高。

自旋锁和互斥锁

自旋锁是让当前线程不断地循环,当循环条件不满足(获得锁)时进入临界区。
互斥锁是指如果不能马上获得锁,就进入阻塞状态,需要被唤醒。

自旋锁的好处是不需要改变线程的状态,响应速度快,但是不断地循环会耗费CPU资源。
互斥锁的好处是阻塞等待时不占用cpu资源,但是需要耗费线程阻塞和恢复的时间。

使用情况 在资源竞争激烈的情况下使用互斥锁,否则使用自旋锁。

可重入锁的概念

可重入锁只针对单个线程而言的,如果单个线程在获得对象锁后,再次访问这个对象的其他同步方法,如果不需要等待就称该锁为可重入锁,否则称为不可冲入锁。

synchronized关键字

synchronized修饰的代码块/方法时,任何线程进入同步块时都需要获得该对象的锁,也就是多个线程不能同时进入一个或多个同步块。

举个例子:

public class Test {

    synchronized void synMethod(){
        System.out.println("同步方法1");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void synMethod2(){
        System.out.println("同步方法2");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当线程一正在执行synMethod时,线程二不能马上执行synMethod2,必须要等待线程一释放Test的对象锁,同时线程二获得该锁之后,线程二才能进入synMethod2。

  1. synchronized提供的是非公平锁,可重入锁。
  2. synchronized在等待锁期间是不可以中断的。
  3. JDK1.6之后,对synchronized优化,根据不同情形出现了偏向锁、轻量锁、对象锁,自旋锁(或自适应自旋锁)等,因此,现在的synchronized可以说是一个几种锁过程的封装。

ReentrantLock 可重入锁

使用:

public class Test {
    ReentrantLock mLock = new ReentrantLock(true);

    void lockMethod() {
        mLock.lock();
        try {
            System.out.println("同步方法1");
            lockMethod2(); //在同步方法1中调用同步方法2不会出现死锁
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }

    void lockMethod2() {
        mLock.lock();
        try {
            System.out.println("同步方法2");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }
}
  1. ReentrantLock是可重入锁,通过其构造方法可以设定为公平锁或者非公平锁。
  2. 为了保证锁的正确释放,编写代码时要把互斥区(临界区)放在try块中,并且在finally块中释放锁。
  3. ReentrantLock的lockInteruptibly是等待可中断的,在等待锁的过程中是可以调用Thread.interupt中断线程。
  4. tryLock会打破“公平锁”,如果想要继续保持锁的公平,可以用tryLock(0, TimeUnit.SECONDS)

Condition

Condition提供了类似wait和notify的机制,一个lock可以产生多个Condition,await方法和signal要在lock()和unLock()之间被调用。

ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
try {
    lock.lock();
    notFull.await();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

调用await后,当前线程会阻塞;直到其他线程调用了signal之后,会唤醒其中一个处于await状态的线程。

Condition跟Object提供的wait和notify的区别
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持

volatile关键字

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  • Java提供了volatile关键字来保证可见性
  • 普通的变量不能保证可见性

volatile能保证一定的有序性
- volatile前面的语句保证在volatile操作前
- volatile后面的语句保证在volatile操作后

原理

《深入理解Java虚拟机》

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
 

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
  
volatile不能替代synchronized,因为volatile不能保证操作的原子性

猜你喜欢

转载自blog.csdn.net/mingC0758/article/details/81779293