深入了解多线程

synchronized关键字背后的原理:

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
重量级锁会阻塞,唤醒请求加锁的线程。他针对的是多个线程同时竞争同一把锁的情况(monitor机制)。JVM采用了自适应自旋来避免线程在面对非常小的synchronized代码块时,仍会被阻塞,唤醒的情况。重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段,他针对的是多个线程在不同时间申请同一把锁的情况(即在同一个时间点只有一个线程申请对象锁(如果这种情况出现频繁,jvm就会优化为轻量级锁的实现))。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址,在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回,他针对的是锁仅会被同一线程持有的情况(同一个线程已经获取到某个线程的锁以后(再次申请获取这个对象的锁(重入)
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

public static int SUM;

    public static synchronized void test1(){
        SUM++;
    }

    public static synchronized void test2(){
        test1();
        SUM++;
    }

如果我们创建一个线程调用test1()方法,那JVM就会将他优化为轻量级锁(同一个时间点只有一个线程竞争锁),如果调用test2()方法,就会优化为偏向锁(线程在进入test2就已经获取了锁,在进入test1()又再次申请获取已经获取的锁)。而如果创建多个线程调用test1()方法就是重量级锁

CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁
使用CAS,线程没有阻塞,一直处于运行态,不停调用方法(尝试修改值的操作)
ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。
没有使用版本号出现的问题,预期值是满足的,但是是经过其他线程修改过的,A->B->A,还是存在数据安全的问题
解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会出现老的值,每次修改值以后版本号+1

乐观锁和悲观锁

乐观锁(尝试修改数据的操作,内部通过版本号等一系列信息判断是否允许操作):乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是
否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
悲观锁(每次修改都加锁):总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样
别人想拿这个数据就会阻塞直到它拿到锁。
悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥

自旋锁(Spin Lock)

按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。
你可以简单的认为自旋锁就是下面的代码:

while (抢锁(lock) == 失败) {}

只要没抢到锁,就死等。

自旋锁的缺点:
缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。

可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

AQS(AbstractQueuedSynchronizer)抽象队列同步器

抽象队列同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现.

AQS(AbstractQueuedSynchronizer)抽象队列同步器
1.线程安全的队列(虚拟的双端队列):安全的存放元素,取出元素
2.内部实现原理:由CAS实现
3.java.util…concurrent包的很多API都是基于AQS实现的:如ReentrantLock,ThreadPoolExecutor,CountDownLacth,Semaphore

Lock简介

锁是⽤来控制多个线程访问共享资源的方式,⼀一般来说,一个锁能够防止多个线程同时访问共享资源。
在Lock接⼝口出现之前, java程序主要是靠synchronized关键字实现锁功能的,⽽而 JDK5 之后,并发包中增加了lock接⼝,它提供了了与synchronized⼀一样的锁功能。 虽然它失去了了像synchronize关键字隐式加锁解锁的便便捷性,但是却拥有了了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不不具备的同步特性。

public static void test1(){
        for(int i = 0; i < 20; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 10000; j++){
                        synchronized (LockTest.class){
                            SUM++;
                        }
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(SUM);
    }

此方法中我们通过创建20个线程,每个线程给SUM循环增加10000次,通过synchronized加锁,得到结果200000;但同样的我们也可以通过lock实现,只需要改一小部分,如下:

public static void test2(){
        final Lock lock = new ReentrantLock();
        for(int i = 0; i < 20; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 10000; j++){
                            try{
                                lock.lock();
                                SUM++;
                            }finally {
                                lock.unlock();
                            }
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(SUM);
    }

可以看出我们只需要在SUM++上一行通过lock.lock()加锁,然后在finally中释放锁,这样就能保证无论是否保存,锁都会被释放。

我们还有线程安全级别的n++,n–,即AtomicTnteger,先看代码:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicTntegerTest {

    private static AtomicInteger AI = new AtomicInteger();


    public static void main(String[] args) {
        for(int i = 0; i < 20; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 10000; j++){
                        AI.incrementAndGet(); //相当于++n
//                        AI.getAndIncrement(); //相当于n++
                    }
            }
        }).start();
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(AI.get());
    }
}
}

可以发现,我们不需要synchronized也不用lock,直接调用getAndIncrement()也能保证线程安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

运笔如飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值