volatile、synchronized、ReentrantLock、Atomic原子类、CountDownLatch等

volatile关键字

volatile就是表示某人或某物是不稳定的、易变的

volatile与普通变量的区别

volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新

volatile保证内存可见性

1、可见性是指: 当一个线程修改了共享变量的值时,其他线程能够立即得到这个修改。

2、volatile实现内存可见性原理:

通过这条规则线程T对变量V的use动作是和load、read动作相关联的,必须连续且一起出现,使得每次使用V前必须先从主内存刷新最新的值。

通过这条规则线程T对变量V的assign动作是和store、write动作相关联的,必须连续且一起出现,使得更改后的值能立刻同步回主内存。

有序性-禁止指令重排序

volatile能在一定程度上保证有序性。

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

例1:

//x、y为非volatile变量

//flag为volatile变量

x = 2;        //语句1

y = 0;        //语句2

flag = true;  //语句3

x = 4;         //语句4

y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

例2:

//线程1:

context = loadContext();   //语句1

inited = true;             //语句2

//线程2:

while(!inited ){

  sleep()

}

doSomethingwithconfig(context);

而有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

不能保证原子性

volatile无法保证对变量的任何操作都是原子性的,比如i++。

synchronized关键字

synchronized:关键词,它依赖于JVM,保证了同一时刻只能有一个线程作用对象作用范围内进行操作。

如何保证线程安全

1、内存可见性:

同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的。

2、操作的原子性:

共享资源代码段又称为临界区(critical section),保证临界区互斥,是指执行临界区(critical section)的只能有一个线程执行,其他线程阻塞等待,达到排队效果。

原子性提供了程序的互斥操作,同一时刻只能有一个线程能对某块代码进行操作。

Synchronized的使用方式有三种

  • 修饰普通方法,监视器锁(monitor)便是对象实例(this)

  • 修饰静态方法,监视器锁(monitor)便是对象的Class实例(每个对象只有一个Class实例)

  • 修饰代码块,监视器锁(monitor)是指定对象实例

3、有序性

synchronized由”一个变量在同一时刻只允许一条线程对其进行lock操作“,来保证有序。
  
互斥区或临界区
synchronized(锁){

临界区代码

}

monitor锁

任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者
  • 如果线程已经占有该monitor,重新进入,则monitor的进入数加1
  • 线程执行monitorexit,monitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit
  • 如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

一个线程执行临界区代码过程:


    1 获得同步锁 
    2 清空工作内存 
    3 从主存拷贝变量副本到工作内存 
    4 对这些变量计算 
    5 将变量从工作内存写回到主存 
    6 释放锁

可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性

Synchronized优化

1、锁粗化

将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

2、锁消除

3、锁升级

Java1.5以后为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,Synchronized的升级顺序是 「无锁–>偏向锁–>轻量级锁–>重量级锁,只会升级不会降级」

偏向锁

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁,其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能。

线程执行同步代码或方法前,线程只需要判断对象头的Mark Word中线程ID与当前线程ID是否一致,如果一致直接执行同步代码或方法。

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,持有锁时间也不长的场景。因为阻塞线程需要C P U从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失,所以干脆不阻塞这个线程,让它自旋一段时间等待锁释放。

当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?

线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。

反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

重量级锁

轻量级锁膨胀之后,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,这个成本非常高,这就是为什么Java1.6之前Synchronized效率低的原因。

升级为重量级锁时,锁标志位的状态值变为10,此时Mark Word中存储内容的是重量级锁的指针,等待锁的线程都会进入阻塞状态

synchronized和volatile的比较

区别Synchronizedvolatile
内存可见性可以可以
原子操作性可以不可以
是否会阻塞线程synchronized可能会造成线程的阻塞volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程
是否会被编译器优化可以volatile标记的变量不会被编译器优化
修饰synchronized是一个方法或块的修饰符volatile是变量修饰符,仅能用于变量

ThreadLocal

ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

CAS与AQS

CAS与AQS

ReentrantLock: CAS+AQS(非公平/公平)

ReentrantLock详细介绍

synchronized与ReentrantLock对比

1、可重入:

两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

2、锁的实现

对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

3、性能的区别:

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

4、功能区别:

便利性:
Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放;
而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:ReenTrantLock优于Synchronized

5、ReenTrantLock独有的能力:

  1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

CountDownLatch(倒计时锁): CAS+AQS

适用于一个线程,等待N个线程完成某个事情之后才能执行.

  • 用于控制线程的执行顺序
  • await():
    阻塞当前线程,并监视计数器的值,如果计数器值不为0则一直阻塞,如果计数器值变为0则自动让行,线程继续执行
  • countDown():
    计数器值减1

Semaphore(信号量): CAS+AQS(非公平/公平)

Semaphore信号量主要用于两个目的:
一个是用于多个资源的互斥作用,另一个用于并发线程数的控制。

无论是Synchroniezd还是ReentrantLock,一次都只允许一个线程访问一个资源,但是Semaphore可以指定多个线程同时访问某一个资源。

semaphore.acquire();
semaphore.release();

CyclicBarrier:ReentrantLock

让一组线程等待至某个状态之后再全部同时执行。

  • 线程间同步阻塞是使用的是ReentrantLock,可重入锁;
  • 线程间通信使用的是Condition,Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。

AtomicInteger:CAS+volatile

unsafe实例

java.util.concurrent包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

 // setup to use Unsafe.compareAndSwapInt for updates

  //unsafe实例采用Unsafe类中静态方法getUnsafe()得到,但是这个方法如果我们写的时候调用会报错,
  //因为这个方法在调用时会判断类加载器,我们的代码是没有“受信任”的,而在jdk源码中调用是没有任何问题的

    private static final Unsafe unsafe = Unsafe.getUnsafe();

    private static final long valueOffset;

 

    static {

      try {

        valueOffset = unsafe.objectFieldOffset

            (AtomicInteger.class.getDeclaredField("value"));

      } catch (Exception ex) { throw new Error(ex); }

    }

    private volatile int value;//volatile关键字保证了在多线程中value的值是可见的,任何一个线程修改了value值,会将其立即写回内存当中

getAndIncrement 方法,该方法的作用相当于i++操作

getAndIncrement的功能为:
i与current比较,如果相等则把i的值变为next;
这时候可以保证在int next = current + 1;与if();之间不会被其他线程抢占(因为i的值在这段时间内没有变),如果被抢占则会做自旋操作。这就在某种程度上可以实现原子性操作。

这是一种不加锁而实现操作原子化的一种巧妙的编程方式,不仅在java的jvm种,甚至在操作系统的底层并发实现机制中也有CAS的大量应用。

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值