浅谈Java多线程与并发原理

简介: ava的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

前序

线程安全问题的主要诱因

  1. 存在共享数据(也称临界资源)
  2. 存在多条线程共同操作这些共享数据

解决方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作

互斥锁的特征

互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应该获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
注:synchronized 锁的不是代码,锁的是对象

获取锁的分类:获取对象锁、获取类锁

获取对象锁的两种用法:

  • 同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号中的实例对象
  • 同步非静态方法(synchronized method) 锁是当前对象的实例对象

获取类锁的两种用法:

  • 同步代码块(synchronized(类.class)),锁是小括号中的类对象(Class对象)
  • 同步非静态方法(synchronized static method) 锁是当前对象的类对象(Class对象)

类锁和对象锁在锁同一个对象的时候表现行为是一样的,因为class也是对象锁,只是比较特殊,所有的实例共享同一个类(同一个class对象)

如果锁的是不同对象(同一个class的不同实例)表现就不一样了,类锁是全同步的,对象锁是按对象区分同步的。

类锁和对象锁互不干扰的,因为对象实例和类是两个不同的对象。

对象锁和类锁的终结

  • 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞
  • 若锁住的是同一个对象,一个线程在访问对象的同步方法时候另一个访问对象同步方法的线程会被阻塞
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个线程访问对象同步方法会被阻塞,反之亦然
  • 同一个类的不同对象锁互不干扰
  • 类锁由于是一种特殊的对象锁,因此表现和上述1、2、3、4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
  • 类锁和对象锁互不干扰

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

阻塞代价

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

  • 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
  • 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

深入理解synchronized底层实现原理:

Java对象头和Monitor是实现synchronized的基础

hotspot中对象在内存的布局是分3部分 :

  1. 对象头
  2. 实例数据
  3. 对其填充
    这里主要讲对象头:一般而言synchronized使用的锁对象是存储在对象头里的,对象头是由Mark Word和Class Metadata Address组成

要详细了解java对象的结构点击:https://blog.csdn.net/zqz_zqz/article/details/70246212

虚拟机位数 头对象结构 说明
32/64bit Mark Word 默认存储对象的hashCode,分代年龄、锁类型、锁标志位等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类型的数据

mark word存储自身运行时数据,是实现轻量级锁和偏向锁的关键,默认存储对象的hasCode、分代年龄、锁类型、锁标志位等信息。

mark word数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
image
由于对象头的信息是与对象定义的数据没有关系的额外存储成本,所以考虑到jvm的空间效率,mark word 被设计出一个非固定的存储结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间(轻量级锁和偏向锁是java6后对synchronized优化后新增加的)
Monitor:每个Java对象天生就自带了一把看不见的锁,它叫内部锁或者Monitor锁(监视器锁)。上图的重量级锁的指针指向的就是Monitor的起始地址。

每个对象都存在一个Monitor与之关联,对象与其Monitor之间的关系存在多种实现方式,如Monitor可以和对象一起创建销毁、或当线程获取对象锁时自动生成,当线程获取锁时Monitor处于锁定状态。

Monitor是虚拟机源码里面用C++实现的。
image

源码解读:_WaitSet_EntryList就是之前学的等待池和锁池,_owner是指向持有Monitor对象的线程。当多个线程访问同一个对象的同步代码的时候,首先会进入到_EntryList集合里面,当线程获取到对象Monitor后就会进入到_object区域并把_owner设置成当前线程,同时Monitor里面的_count会加一。当调用wait方法会释放当前对象的Monitor,_owner恢复成null,_count减一,同时该线程实例进入_WaitSet集合中等待唤醒。如果当前线程执行完毕也会释放Monitor锁并复位对应变量的值。

image
接下来是字节码的分析:

package interview.thread;

/**
 * 字节码分析synchronized
 * @Author: hankli
 * @Date: 2019/5/20 13:50
 */
public class SyncBlockAndMethod {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello Again");
    }
}

然后控制台输入 javac thread/SyncBlockAndMethod.java
然后反编译 javap -verbose thread/SyncBlockAndMethod.class
先看看syncsTask方法里的同步代码块:
image

从字节码中可以看出 同步代码块 使用的是 monitorenter 和 monitorexit ,当执行monitorenter指令时当前线程讲试图获取对象的锁,当Monitor的count 为0时将获的monitor,并将count设置为1表示取锁成功。如果当前线程之前有这个monitor的持有权它可以重入这个Monnitor。monitorexit指令会释放monitor锁并将计数器设为0。为了保证正常执行monitorenter 和 monitorexit 编译器会自动生成一个异常处理器,该处理器可以处理所有异常。主要保证异常结束时monitorexit(字节码中多了个monitorexit指令的目的)释放monitor锁

注:重入是从互斥锁的设计上来说的,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。就像如下情况:hello2也是会输出的,并不会锁住。

image
再看看syncTask同步方法:
image

解读:这个字节码中没有monitorenter和monitorexit指令并且字节码也比较短,其实方法级的同步是隐式实现的(无需字节码来控制)ACC_SYNCHRONIZED是用来区分一个方法是否同步方法,如果设置了ACC_SYNCHRONIZED执行线程将持有monitor,然后执行方法,无论方法是否正常完成都会释放调monitor,在方法执行期间,其他线程都无法在获得这个monitor。如果同步方法在执行期间抛出异常而且在方法内部无法处理此异常,那么这个monitor将会在异常抛到方法之外时自动释放。

java6之前Synchronized效率低下的原因:

在早期版本Synchronized属于重量级锁,性能低下,因为监视器锁(monitor)是依赖于底层操作系统的的MutexLock实现的。

而操作系统切换线程时需要从用户态转换到核心态,时间较长,开销较大

java6以后Synchronized性能得到了很大提升(hotspot从jvm层面做了较大优化,减少重量级锁的使用):

  • Adaptive Spinning 自适应自旋
  • Lock Eliminate 锁消除
  • Lock Coarsening 锁粗化
  • Lightweight Locking 轻量级锁
  • Biased Locking偏向锁
  • ……

自旋锁:

  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行while循环等待锁的释放,不让出CPU
  • java4就引入了,不过默认是关闭的,java6后默认开启的
  • 自旋本质和阻塞状态并不相同,如果锁占用时间非常短,那自旋锁性能会很好

缺点:若锁被其他线程长时间占用,会带来许多性能上的开销,因为自旋一直会占用CPU资源且白白消耗掉CPU资源。
如果线程超过了限定次数还没有获取到锁,就该使用传统方式挂起线程(可以设置VM的PreBlockSpin参数来更改限定次数)

目录
相关文章
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
23 3
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
8天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
24天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
18 3
|
24天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
24天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2