线程和进程的区别是什么?
线程有些时候被称为轻量级进程,并且大多数据 现代操作系统把线程作为时序调度的基本单元,而不是进程。
对于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 括号里配置的对象
公平锁:多个线程申请获取同一个锁,按照线程的申请顺序,排队获取锁。公平锁的好处是等待的线程不会被饿死,相应的缺陷 就是整体吞吐量很低、效率很低,使用new ReentrantLock(true)可以构造一个公平锁。
非公平锁:多个线程申请获取同一个锁,获取锁的顺序不按照申请顺序,抢占式的获取。非公平锁的好处是整体效率很高,但是 可能会使有些线程一致在等待,造成饿死。使用Synchronized、new ReentrantLock()和new ReentrantLock(false)可 以 构建一 个非公平锁。
可重入锁:也称为递归锁,即线程在获取到某方法的锁之后,如果在该方法内部调用其它方法,这个方法也需要获取锁,那么 进入这个方法将自动获取锁,它可以在一定程度上避免死锁。
共享锁:简单的理解就是锁可以被多个线程持有。在实际使用过程中,线程A获取到了共享资源D的共享锁,其它线程只能获取 D的共享锁,不能获取独占锁。
独占锁:一次只能有一个线程获得锁,即只能被一个线程持有。在实际使用过程中,线程A获取到了共享资源D的独占锁,其它 线程不能获取D的任何类型锁。
互斥锁:即一次只能有一个线程持有的锁。ReentrantLock和synchronized都是互斥锁。
读写锁:一次只有一个线程(writer线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据 (reader 线程)。Java中的读写锁通过ReentrantReadWriteLock实现。ReentrantReadWriteLock.ReadLock是读锁,它 是共享锁。ReentrantReadWriteLock.WriteLock是写锁,它是独占锁。
悲观锁:认为对同一数据的并发操作一定会出现冲突问题,所以,在对数据操作之前一定要加锁。Java编码中悲观锁的实现有很 多种,for update就是一种。
乐观锁:认为对同一数据的并发操作不会出现冲突问题,数据操作不加锁,会在操作提交时检查数据的完整性。Java中常用的乐 观锁方式有版本号、时间戳
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为 Segment,它即类似于 HashMap(JDK7 与 JDK8 中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素既是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
这三个锁都是针对synchronized来说的,具体的实现细节比较复杂,这里不展开细说。
偏向锁:对于一段同步代码来说,锁偏向于第一次获取它的线程,如果继续执行的过程中,锁没有被其它线程持有,则持有偏向锁的线程将不需要同步,自动获取锁。
轻量级锁:当偏向锁被另一个线程持有的时候,偏向锁升级为轻量级锁,其它线程通过自旋转的方式尝试获取锁。
重量级锁:当轻量级锁被另一个线程持有的时候,轻量级锁升级为重量级锁。
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的所撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求相应速度,同步块执行速度非常块 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,相应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
Java线程在得不到锁时不会立即阻塞,而是执行一个循环,不断的去尝试获取锁,这种技术就是自旋锁。它可以减少在获取锁的过程中,因为线程上下文的切换而导致的额外消耗。
9 可重入锁
可重入锁:也称为递归锁,即线程在获取到某方法的锁之后,如果在该方法内部调用其它方法,这个方法也需要获取锁,那么进入这个方法将自动获取锁,它可以在一定程度上避免死锁。
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关系向程序员提供跨线程的内存可见性保证:
程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。
锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
3 在程序运行过程中,为了执行的效率,编译器和处理器是可以对程序进行一定的重排序,但是他们必须要满足两个条件:1 执行的结果保持不变,2 存在数据依赖的不能重排序。重排序是引起多线程不安全的一个重要因素。
4 同时顺序一致性是一个比较理想化的参考模型,它为我们提供了强大而又有力的内存可见性保证,他主要有两个特征:1 一个线程中的所有操作必须按照程序的顺序来执行;2 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原则执行且立刻对所有线程可见。
Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
方法区(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 方法相关,一般情况下,我们无需关心此区域。