摘要:在多线程的问题上面概念比较多,也需要慢慢理解,其实也在多线程的锁的上面做了很多优化,还有互斥同步和非互斥同步,还有很多概念,什么是自旋和自适应自旋,锁消除顺便提一下,上面的字符串拼接的例子就是用到了这种优化方式,锁粗化,我们下次再继续分享。
在我们平常的开发工作中,或多或少的都能接触到多线程编程或者一些并发问题,随着操作系统和系统硬件的升级,并发编程被越来越多的运用到我们的开发中,我们使用多线程的最初的想法是能够更大程度的利用系统资源,但是我们在使用多线程的时候,也会有一些问题的存在,我们先来看一段代码。
private static int i = 0; private static void increse(){ i++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for (int i = 0; i < threads.length; i++){ threads[i] = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++){ increse(); } } }); threads[i].start(); } while (Thread.activeCount() > 1){ Thread.yield(); } System.out.println(i); }
首先看看这段代码是没有问题的,但是如果在多线程的环境中,这段代码运行的结果基本是都不一样的,这里是开启20个线程,然后每一个线程调用increse()方法对变量i进行一个赋值操作,预期的一个输出应该是200000,但是为什么会每一次的输出都不太一样呢?原因就在于这个地方i++,这里就是产生并发问题的根本原因,那看起来很简单的一个i++为什么会有这种问题?这里就简单的从java内存模型(JMM)来了解一下,首先JMM规定,每一个线程运行时都会有一个工作内存,然后变量i是存储在主内存的,每次线程在计算数据的时候都要去主内存中获取当前变量的值,那么简单的来说,就是一个线程将变量i计算得到结果后,还没有将这个数据刷新到主内存,在这个时候,其他的线程已经获取到了原来的值,换句话说,本线程中获取到的数据是否是最新的,这个是不知道的。但是这只是从JMM角度来简单的说一下,i++这个看似简单的操作其实包含了三个操作,获取i的值,对i进行自增,然后对i进行赋值。就是在这几个操作中,其他的线程会有很多的时间来做很多的事情,那有人会问,是不是将这个i++操作同步就可以了?是的,那该如何同步呢?
有人说用volatile来修饰一下变量i不就可以了么?是的,java中volatile这个关键字确实是提供了一个同步的功能,但是为什么在这里修改一下还是没有效果呢?原因就在于如果一个变量用volatile修饰之后,只是会让其他的线程立即能够知道当前变量的值是多少,这里就叫做可见性,但是还是解决不了i++这几个操作的问题,那如何处理,又有人提出用synchronized这个关键字,不得不承认,这个关键字确实很强大,是能够解决这个问题,那我们有没有考虑过这个为什么可以解决这个问题,是如何解决的,那还是简单的说一下,首先synchronized是属于JVM级别的,有这个关键字的方法或者代码块,最后会被解释成monitorenter和monitorexit指令,这两个字节码都明确需要一个reference类型的参数来指出要锁定或者解锁的对象,像这样:
public synchronized String f(){ //code } synchronized(object){ //code }
看到这里,我们应该能明白synchronized为什么可以解决上面程序的问题,但是我们还应该要明确一个概念就是原子性,换句话说,就是我们在处理一些多线程的问题的时候,应该保证一些共享数据的操作是原子性的,这样才能保证正确性,看到这里,相信你也有了一个大概的理解,那我们来总结一下,在处理多线程的问题的时候,哪些点是值得注意的,可见性,原子性,有序性,这几个点是保证多线程能够正确的一个前提条件,至于什么是有序性,这里涉及到内存指令的重排序,不在讨论范围内,以后再来讨论。
这里还要指出一个问题,就是是否我们在处理多线程问题的时候,一定要同步,或者说一定要加锁,这个也不是一定的,之前网上有一个说笑的方式,就是我们在处理多线程的问题的时候,有时候就会发现,代码又被写成了单线程,当然这只是一个玩笑话,但是这里我们也能看出来,是不是单线程的程序就不会有这些问题?答案是肯定的,因为单线程不存在资源竞争的问题,也就不需要再讨论了。
那么我们什么时候需要使用同步,什么时候又不需要呢?我们来看一段代码
public String f(String s1, String s2, String s3){ return s1 + s2 +s3; }
这是一个字符串拼接的一个方法,我们来反编译看一下,这里JVM到底是怎么做的?
这里很明显的能够看出来,最后是通过StringBuilder来为我们生成了最后的结果,那有人会问,这里线程安全么?是的,这里是线程安全的,因为在这个方法中,虽然也有变量的使用,但是都是属于线程内部在使用,其他的线程根本不会访问到或者说这些变量也不会让其他线程访问到,我们称其为没有方法逃逸,也就是说只能在本线程中使用这些变量,这里是线程安全的,至于什么是逃逸分析,简单的提一下就是这是JVM的高级优化的一种方式,说的再简单一点,就是别的线程访问不到这个变量,这样的代码是不需要同步的。
在多线程的问题上面概念比较多,也需要慢慢理解,其实JVM也在多线程的锁的上面做了很多优化,还有互斥同步和非互斥同步,还有很多概念,什么是自旋和自适应自旋,锁消除(顺便提一下,上面的字符串拼接的例子就是用到了这种优化方式),锁粗化,我们下次再继续分享。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76658.html
摘要:物理计算机并发问题在介绍内存模型之前,先简单了解下物理计算机中的并发问题。基于高速缓存的存储交互引入一个新的问题缓存一致性。写入作用于主内存变量,把操作从工作内存中得到的变量值放入主内存的变量中。 物理计算机并发问题 在介绍Java内存模型之前,先简单了解下物理计算机中的并发问题。由于处理器的与存储设置的运算速度有几个数量级的差距,所以现代计算机加入一层读写速度尽可能接近处理器的高速缓...
摘要:第一个字被称为。经量级锁的加锁过程当一个对象被锁定时,被复制到当前尝试获取锁的线程的线程栈的锁记录空间被复制的官方称为。根据锁对象目前是否处于被锁定状态,撤销偏向后恢复到未锁定或经量级锁定状态。 Synchronized关键字 synchronized的锁机制的主要优势是Java语言内置的锁机制,因此,JVM可以自由的优化而不影响已存在的代码。 任何对象都拥有对象头这一数据结构来支持锁...
摘要:内存模型是围绕着在并发过程中如何处理原子性可见性和有序性这个特征来建立的,我们来看下哪些操作实现了这个特性。可见性可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的,我们来看下哪些操作实现了这3个特性。 原子性(atomicity): 由Java内存模型来直接保证原子性变量操作包括...
摘要:这两种策略的区别就在于,公平策略会让等待时间长的线程优先执行,非公平策略则是等待时间长的线程不一定会执行,存在一个抢占资源的问题。 之前有一篇文章我们简单的谈到了Java中同步的问题,但是可能在平常的开发中,有些理论甚至是某些方式是用不到的,但是从程序的角度看,这些理论思想我们可以运用到我们的开发中,比如是不是应该一谈到同步问题,就应该想到用synchronized?,什么时候应该用R...
摘要:并发需要解决的问题功能性问题线程同步面临两个问题,想象下有两个线程在协作工作完成某项任务。锁可用于规定一个临界区,同一时间临界区内仅能由一个线程访问。并发的数据结构线程安全的容器,如等。 并发指在宏观上的同一时间内同时执行多个任务。为了满足这一需求,现代的操作系统都抽象出 线程 的概念,供上层应用使用。 这篇博文不打算详细展开分析,而是对java并发中的概念和工具做一个梳理。沿着并发模...
阅读 2780·2021-10-14 09:50
阅读 1194·2021-10-08 10:21
阅读 3625·2021-10-08 10:16
阅读 3005·2021-09-27 14:02
阅读 3114·2021-09-23 11:21
阅读 2049·2021-09-07 10:17
阅读 373·2019-08-30 14:00
阅读 2069·2019-08-29 17:26