多线程常见问题

并发编程三概念

  • 原子性: 一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性: 程序执行的顺序按照代码的先后顺序执行。(指令优化,指令重排是指互不依赖的指令会进行重排,优化计算)
  • 补充: Java内存模型具备一些先天的有序性,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则(先行发生原则)。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。-----出自《深入理解Java虚拟机》

如何保证线程安全

  • 保证线程安全以是否需要同步手段分类,分为同步方案无需同步方案

在这里插入图片描述

互斥同步

  • 互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

  • synchronized关键字:最基本的互斥同步手段,synchronized关键字编译之后,会在同步块的前后分别形成monitorentermonitorexit这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。

  • ReentrantLock:在基本用法上,ReentrantLocksynchronized很相似,他们都具备一样的线程重入特性。

  • 互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

非阻塞同步

  • 随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。

  • 非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。

  • CAS缺点:
    ABA问题: 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

    使用版本号解决ABA问题: 在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    性能消耗大: 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

  • JUC对原子类有一个优化,LongAdder,热点分散,类似于分治。是CAS优化的地方。

无需同步方案

  • 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。

1. 可重入代码

  • 可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

  • 可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。

  • (类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)

2. 线程本地存储

  • 如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。

  • 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。

synchronized关键字

  • synchronized能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。以此可以保证可见性。
  • 对象在内存中的布局包括三部分:对象头实例数据对齐填充数据

在这里插入图片描述

其中,对象头是实现Synchronized的锁对象的基础。当给对象加锁的时,数据是存储在对象头中。当执行Synchronized同步方法或者同步代码块的时候,会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管道或者监视器锁)。

  1. 同步代码块 的实现是基于虚拟机的指令monitorentermonitorexit指令。monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁.。

  2. 同步方法 的实现则是通过对象头实现的访问标志位为基础。synchronized方法会被翻译成普通的方法调用和返回指令,如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象。

  3. 锁升级的过程
    在jdk1.6的时候有了锁升级,

  • 无锁: 没有锁
  • 偏向锁: 有一个资源,线程去申请,并且申请到了这个锁,并且没有其他线程去争抢这个锁,当他执行同步代码块的时候,就没有必要去释放这个锁了,因为释放锁的过程需要从用户态转到内核态,是需要消耗很多资源。为了提高性能就引入了这个偏向锁。如果有两个资源去争夺这个锁,他就会升级到轻量级锁。
  • 轻量级锁:
    用CAS,先去判断能否将这个锁抢到,如果抢不到,就进行自旋操作,还是没有办法获取到这个锁,那么就升级到重量级锁。
  • 重量级锁:
    用管程实现的,就完全阻塞了。
  1. 锁升级的原理

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

lock锁

  • synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的。

  • Java中的锁大致分为:偏向锁、轻量级锁、重量级锁、自旋锁(锁只能升级,不能降级)

  • 偏向锁、轻量级锁、重量级锁
    这三种锁是指锁的状态,并且是针对Synchronized,在Java 5通过引入锁升级的机制来实现高效Synchronized,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价;轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

  • 自旋锁:
    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

  • 可重入锁:
    可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。可重入锁最大的作用是避免死锁。

  • 乐观锁、悲观锁:
    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

在这里插入图片描述

voliate关键字

  • synchronized是阻塞同步的,在线程竞争激烈的情况下会升级为重量级锁。而voliate就可以说是java虚拟机提供的最轻量级的同步锁
  • voliate关键字主要有两个作用:
    1. 保证可见性
    2. 禁止进行指令重排序。
  • 可见性如何保证: 各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内容中的数据进行操作处理。那么线程在工作内存进行操作后何时会写到主内存中?这个时机对于普通变量是没有规定的,而针对voliate修饰的变量给java虚拟机特殊的约定,线程对voliate变量的修改会立即被其他线程感知,在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock指令。也就是说,我一旦修改了该变量,就会立马通知其他线程,修改后的值,具体就是这个写回内存的操作会使得其他缓存了该内存地址的数据无效。
  • 有序性如何保证:
  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

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

interrupt,interrupted,isInterrupted

  • interrupt方法用于中断线程。 调用interrupt()方法来停止线程,不会马上终止
    注意: 它仅仅是在当前的线程中打了一个停止的标记。即不会影响线程的正常运行,只是该线程多了一个停止的标记而已。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为中断状态,就会抛出中断异常。
 public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
  • interrupted 和 isInterrupted
  1. 先看interrupted ()方法,该方法就是直接调用当前线程的isInterrupted(true)方法。
public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
  1. 再来看isInterrupted(true)方法
 private native boolean isInterrupted(boolean ClearInterrupted);

区别: interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)这两个方法最终都会调用同一个方法,只不过参数一个是true,一个是false。

notyfy和notifyAll的区别

  • notify: 只随机唤醒一个 wait 线程,被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池。
  • notifyAll: 将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • notify可能会导致死锁,而notifyAll则不会。
  • 所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

ThreadLocal详解

  • 既然是多个线程共享同一个变量所造成的线程安全问题,那我们就不让他共享了, ThreadLocalMap 的作用就是:让每个线程独立保存一份自己的变量副本,这样就不会影响到其他线程了。
  • ThreadLocal是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value
  • 这个对象由ThreadLocalMap 内部类创建,这个类才是实现线程安全的关键,Map的key是ThreadLocal类的实例对象,value为用户的值。它本身是不存储值的,他只提供一个可以查到这个值的key
  • ThreadLocal线程之间是不可以继承,因为当线程中开启了其他的线程,此时ThreadLocal里面的数据将会出现无法获取/读取错乱,甚至还可能会存在内存泄漏等问题,但是通过InheritableThreadLocalThreadLocal的子类) 这个组件可以实现父子线程之间的数据传递,在子线程中能够父线程中的ThreadLocal本地变量。
  • 经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就 可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

1. JDK 的实现里面这个 Map 是属于 Thread,而非属于 ThreadLocal。
       ThreadLocal 仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。ThreadLocalMap 属于 Thread 也更加合理。
2. 内存泄露问题。
       -存在内存泄漏的问题,因为他的key是弱引用,value是强引用,在GC的时候,有可能会把key回收掉置为nullvalue值不会被回收,也一直不会消失,也不能通过key来找到这个value。所以在get(),set(),remove()的时候,都会对keynullentry进行清除。也就是将vlaue值置为null,这样在下一次GC的时候,就会回收掉,所以使用的时候,要手动调用一下remove()方法就可以了。

ReentrantLock独占锁

ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

  1. synchronized是独占锁,可重入,由于加锁和解锁的过程自动进行,所以不必担心最后是否释放锁,易于操作,但不够灵活。
    ReentrantLock是独占锁,可重入,由于加锁和解锁的过程需要手动进行,且次数需要一样,否则其他线程无法获得锁。不易操作,但非常灵活。

  2. synchronized不可响应中断,一个线程获取不到锁就一直等着。
    ReentrantLock可以响应中断。

  3. ReentrantLock 底层调用的是 Unsafepark 方法加锁,synchronized 操作的应该是对象头中 mark word

  4. ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。

这样看来,好像也好不到哪里去吧,but~~ 一个最主要的就是ReentrantLock还可以实现公平锁机制。(公平锁: 锁上等待时间最长的线程将获得锁的使用权)

ReadWriteLock读写锁

  • ReentrantLock某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock

  • ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术 ReentrantReadWriteLockReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值