Java并发

 线程和进程的区别是什么?

线程有些时候被称为轻量级进程,并且大多数据 现代操作系统把线程作为时序调度的基本单元,而不是进程。

对于CUP资源比较特殊,线程才是CPU分配的基本单位

线程是进程中的一个实体,线程是不会独立存在的!所以说,没有进程就没有线程

1.简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2.线程的划分尺度小于进程,使得多线程程序的并发性高。

3.另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

4.线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5.从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度

优缺点

线程和进程在使用上各有优缺点:

 1  线程执行开销小,但不利于资源的管理和保护;而进程正相反。

  2 线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

  3 恰当地使用线程时,可以降低开发和维护的开销,并且能够提高杂应用的性能。

Volatile  变量:一种同步的弱形式。它确保参一个变量的更新以可预见的方式告知其他的线程。
1 Volatile 变量的操作不会加锁。也就不会引起执行线程的阻塞
2 Volatile 变量地可见性的影响所产生的价值远远高于变量本身
3 加锁可以保证可见性与原子性,volatile 变量只能可见性

4 volatile 关键字通过添加内存屏障(Memory Barriers)的方式来禁止特定类型的处理器重排序,即重排序时不能把后面的指令放到内存屏障之前
满足下面所有的标准后,你才能使用Volatile
1写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2变量不需要与其他的状态变量共同参与不变约束
3而且访问变量时,没有其他的原因需要加锁

synchronized 的实现原理与应用

synchronized 称为 重要级锁

synchronized 保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

java 中的每一个对象都可以作为锁 表现以下3种形式
1 对于普通同步方法,锁是当前实例对象
2对于静太同步方法,锁是当前类的Class 对象。
3对于同步方法块,锁是Synchonized 括号里配置的对象

1 公平锁/非公平锁 

公平锁:多个线程申请获取同一个锁,按照线程的申请顺序,排队获取锁。公平锁的好处是等待的线程不会被饿死,相应的缺陷               就是整体吞吐量很低、效率很低,使用new ReentrantLock(true)可以构造一个公平锁。

非公平锁:多个线程申请获取同一个锁,获取锁的顺序不按照申请顺序,抢占式的获取。非公平锁的好处是整体效率很高,但是                   可能会使有些线程一致在等待,造成饿死。使用Synchronized、new ReentrantLock()和new ReentrantLock(false)可                    以 构建一  个非公平锁。

2 可重入锁

  可重入锁:也称为递归锁,即线程在获取到某方法的锁之后,如果在该方法内部调用其它方法,这个方法也需要获取锁,那么                       进入这个方法将自动获取锁,它可以在一定程度上避免死锁。

3 独享锁/共享锁

  共享锁:简单的理解就是锁可以被多个线程持有。在实际使用过程中,线程A获取到了共享资源D的共享锁,其它线程只能获取                 D的共享锁,不能获取独占锁。

  独占锁:一次只能有一个线程获得锁,即只能被一个线程持有。在实际使用过程中,线程A获取到了共享资源D的独占锁,其它                线程不能获取D的任何类型锁。

4 互斥锁/读写锁

互斥锁:即一次只能有一个线程持有的锁。ReentrantLock和synchronized都是互斥锁。

读写锁:一次只有一个线程(writer线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据                           (reader 线程)。Java中的读写锁通过ReentrantReadWriteLock实现。ReentrantReadWriteLock.ReadLock是读锁,它               是共享锁。ReentrantReadWriteLock.WriteLock是写锁,它是独占锁。

5 乐观锁/悲观锁

悲观锁:认为对同一数据的并发操作一定会出现冲突问题,所以,在对数据操作之前一定要加锁。Java编码中悲观锁的实现有很                多种,for update就是一种。

乐观锁:认为对同一数据的并发操作不会出现冲突问题,数据操作不加锁,会在操作提交时检查数据的完整性。Java中常用的乐               观锁方式有版本号、时间戳

6 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为 Segment,它即类似于 HashMap(JDK7 与 JDK8 中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素既是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

7 偏向锁/轻量级锁/重量级锁

这三个锁都是针对synchronized来说的,具体的实现细节比较复杂,这里不展开细说。

偏向锁:对于一段同步代码来说,锁偏向于第一次获取它的线程,如果继续执行的过程中,锁没有被其它线程持有,则持有偏向锁的线程将不需要同步,自动获取锁。

轻量级锁:当偏向锁被另一个线程持有的时候,偏向锁升级为轻量级锁,其它线程通过自旋转的方式尝试获取锁。

重量级锁:当轻量级锁被另一个线程持有的时候,轻量级锁升级为重量级锁。

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的所撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求相应速度,同步块执行速度非常块
重量级锁 线程竞争不适用自旋,不会消耗CPU 线程阻塞,相应时间缓慢 追求吞吐量,同步块执行速度较长

8 自旋锁

Java线程在得不到锁时不会立即阻塞,而是执行一个循环,不断的去尝试获取锁,这种技术就是自旋锁。它可以减少在获取锁的过程中,因为线程上下文的切换而导致的额外消耗。

9 可重入锁

可重入锁:也称为递归锁,即线程在获取到某方法的锁之后,如果在该方法内部调用其它方法,这个方法也需要获取锁,那么进入这个方法将自动获取锁,它可以在一定程度上避免死锁。

 

java 内存模型


1 Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。

原子性:一个操作或者多个操作要么全部执行要么全部不执行;
可见性:当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变;
有序性:程序的执行要按照代码的先后顺序执行;
在并发编程模式中,势必会遇到上面三个概念,JMM对原子性并没有提供确切的解决方案,但是JMM解决了可见性和有序性,至于原子性则需要通过锁或者Synchronized来解决了。

2 happens-before

   happens-before是JMM最核心的概念。对于JAVA程序员来说,理解happens-before是理解JMM的关键

    原则是JMM中非常重要的一个原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以解决在并发环境下两个操作之间是否存在冲突的所有问题。JMM规定,两个操作存在happens-before关系并不一定要A操作先于B操作执行,只要A操作的结果对B操作可见即可。

JMM通过happens-before关系向程序员提供跨线程的内存可见性保证:

  1. 程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

  2. 锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

  3. volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

  4. 传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

  5. 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

  6. 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

 

3 在程序运行过程中,为了执行的效率,编译器和处理器是可以对程序进行一定的重排序,但是他们必须要满足两个条件:1 执行的结果保持不变,2 存在数据依赖的不能重排序。重排序是引起多线程不安全的一个重要因素。


4 同时顺序一致性是一个比较理想化的参考模型,它为我们提供了强大而又有力的内存可见性保证,他主要有两个特征:1 一个线程中的所有操作必须按照程序的顺序来执行;2 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原则执行且立刻对所有线程可见。

 

Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

内存模型

方法区(Method Area):
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。
JVM堆(Java Heap):
Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
程序计数器(Program Counter Register):
字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
多线程中,为了让线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响、独立存储,因此这块内存是线程私有的。
虚拟机栈(Java Virtual Machine Stacks):
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口信息等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈(Native Method Stacks):
本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

 

 

你可能感兴趣的:(JAVA)