并发编程三概念
- 原子性: 一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性: 程序执行的顺序按照代码的先后顺序执行。(指令优化,指令重排是指互不依赖的指令会进行重排,优化计算)
-
补充: Java内存模型具备一些先天的
有序性,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before原则(先行发生原则)。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。-----出自《深入理解Java虚拟机》
如何保证线程安全
- 保证线程安全以是否需要同步手段分类,分为
同步方案和无需同步方案。
互斥同步
-
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
-
synchronized关键字:最基本的互斥同步手段,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。 -
ReentrantLock:在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。 -
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
非阻塞同步
-
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
-
非阻塞的实现
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带来非常大的执行开销。
无需同步方案
- 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1. 可重入代码
-
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
-
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
-
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
2. 线程本地存储
-
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
-
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。
synchronized关键字
- synchronized能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。以此可以保证可见性。
- 对象在内存中的布局包括三部分:对象头、实例数据、对齐填充数据。
其中,对象头是实现Synchronized的锁对象的基础。当给对象加锁的时,数据是存储在对象头中。当执行Synchronized同步方法或者同步代码块的时候,会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管道或者监视器锁)。
-
同步代码块 的实现是基于虚拟机的指令
monitorenter和monitorexit指令。monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁.。 -
同步方法 的实现则是通过对象头实现的访问标志位为基础。synchronized方法会被翻译成普通的方法调用和返回指令,如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,
而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象。
lock锁
Java中的锁大致分为:偏向锁、轻量级锁、重量级锁、自旋锁(锁只能升级,不能降级)
-
偏向锁、轻量级锁、重量级锁:
这三种锁是指锁的状态,并且是针对Synchronized,在Java 5通过引入锁升级的机制来实现高效Synchronized,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价;轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 -
自旋锁:
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 -
可重入锁:
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。可重入锁最大的作用是避免死锁。 -
乐观锁、悲观锁:
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
voliate关键字
-
synchronized是阻塞同步的,在线程竞争激烈的情况下会升级为重量级锁。而voliate就可以说是java虚拟机提供的最轻量级的同步锁。 - voliate关键字主要有两个作用:
1. 保证可见性
2. 禁止进行指令重排序。 -
可见性如何保证: 各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内容中的数据进行操作处理。那么线程在工作内存进行操作后何时会写到主内存中?这个时机对于普通变量是没有规定的,而针对
voliate修饰的变量给java虚拟机特殊的约定,线程对voliate变量的修改会立即被其他线程感知,在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock指令。也就是说,我一旦修改了该变量,就会立马通知其他线程,修改后的值,具体就是这个写回内存的操作会使得其他缓存了该内存地址的数据无效。 - 有序性如何保证:
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
-
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
notyfy和notifyAll的区别
- notify: 只随机唤醒一个 wait 线程,被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池。
- notifyAll: 将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- notify可能会导致死锁,而notifyAll则不会。
- 所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。