多线程三个特征:原子性、可见性以及有序性.
同步锁 /并发锁/ 读写锁,显示锁, ReentrantLock与Condition.
> 线程的同步与互斥 (同步线程与异步线程,线程同步和异步问题)
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的.
1.同步:假设现有线程A和线程B,线程A需要往缓冲区写数据,线程B需要从缓冲区读数据,但他们之间存在一种制约关系,即当线程A写的时候,B不能来拿数据;B在拿数据的时候A不能往缓冲区写,也就是说,只有当A写完数据(或B取走数据),B才能来读数据(或A才能往里写数据)。这种关系就是一种线程的同步关系。
同步关系则是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用。
同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
2.互斥:对于线程A和线程B来讲,在同一时刻,只允许一个线程对临界资源进行操作,即当A进入临界区对资源操作时,B就必须等待;当A执行完,退出临界区后,B才能对临界资源进行操作。
所谓互斥,就是不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止访问冲突,在有限的时间内只允许其中之一独占性的使用共享资源。如不允许同时写;
3.总的来说,两者的区别就是:
互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。
同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
> synchronized在静态方法上表示调用前要获得类的锁,而在非静态方法上表示调用此方法前要获得对象的锁。
public class StaticSynDemo {
private static String a="test";
public void print2(String b){
synchronized (this) {//取得StaticSynDemo实例化后对象的锁
System.out.println(b+a);
}
}
public static void print4(String b){
synchronized (StaticSynDemo.class) { //取得StaticSynDemo.class类的锁
System.out.println(b+a);
}
}
}
同步代码块或同步对象:synchronized 方法和 synchronized 块。synchronized(this)、synchronized(*.class)与synchronized(任意对象)这几种类型同步方法:
同步synchronized(*.class)代码块的作用其实和synchronized static方法作用一样。Class锁对类的所有对象实例起作用。
this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。
Synchronized和Static Synchronized区别:
一个是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),一个是全局锁(该锁针对的是类,无论实例多少个对象,那么线程都共享该锁)。
实例锁对应的就是synchronized关键字,而类锁(全局锁)对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
synchronized(this)与synchronized(static XXX)的区别了,synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。
synchronized是通过同一时刻只有一个线程执行共享代码来保证多线程三个特征的;
volatile 变量具有 synchronized 的可见性特性,禁止指令重排,但是不具备原子特性。使用volatile变量,必须同时满足下面两个条件:
1.对变量的写操作不依赖于当前值。
2.该变量没有包含在具有其他变量的不变式中。
> 线程安全
造成线程安全问题的主要诱因有两点:
一是存在共享数据(也称临界资源),
二是存在多条线程共同操作共享数据。
在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:
由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。这里面,这个资源被称为:临界资源(也有称为共享资源)。
也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。不过,当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
-- 如何解决线程安全问题的呢?
基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达到互斥访问目的的锁。举个简单的例子:如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。
在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
> 同步锁 /并发锁/ 读写锁
互斥锁的本质: 首先需要明确一点,互斥锁实际上是一种变量,在使用互斥锁时,实际上是对这个变量进行置0置1操作并进行判断使得线程能够获得锁或释放锁。
-- 死锁产生的条件:
1、互斥属性:即每次只能有一个线程占用资源。
2、请求与保持:即已经申请到锁资源的线程可以继续申请。在这种情况下,一个线程也可以产生死锁情况,即抱着锁找锁。
3、不可剥夺:线程已经得到所资源,在没有自己主动释放之前,不能被强行剥夺。
4、循环等待:多个线程形成环路等待,每个线程都在等待相邻线程的锁资源。
-- 死锁的避免:
1、既然死锁的产生是由于使用了锁,那么在能不使用锁的情况下就尽量不使用,如果有多种方案都能实现,那么尽量不选用带锁的这种方案
2、尽量避免同时获得多把锁,如果有必要,就要保证获得锁的顺序相同。
> Java锁:无锁状态、偏向锁、轻量级锁和重量级锁
-- 锁的使用场景
由于偏向锁在线程存在竞争的时候会带来额外的性能开销,所以偏向锁适用于只有一个线程方法同步快的情况;轻量级锁在线程竞争锁的情况下不会导致线程阻塞,但是会通过自旋消耗CPU,所以轻量级锁适用于追求响应时间的情况。重量级锁线程竞争不会使用自旋,但是线程竞争会导致阻塞,所以响应时间比较慢,重量级锁一般使用在追求吞吐量的情况。
-- 当前常用的多线程同步机制可以分为下面三种类型:
1.volatile 变量:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性保证,不提供原子性。
2.CAS 原子指令:轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性和原子化更新保证。
3.内部锁和显式锁:重量级多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性和原子性。
-- java同步机制: volatile、synchronized和final.
Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级.读写锁特点:
1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
互斥锁特点:一次只能一个线程拥有互斥锁,其他线程只有等待
-- 使用同步机制获取互斥锁的情况,进行几点说明:
1、如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
2、类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
3、访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
4、持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。
5、持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。
6、使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。
7、类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
8、互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。
-- 无锁、偏向锁、轻量级锁和重量级锁:
在JDK 1.6中引入了“偏向锁”和“轻量级锁“。锁一共有四种状态:无锁、偏向锁、轻量级锁和重量级锁。锁只能升级,不能降级。当对锁的竞争加剧的时候,锁会发生升级。
1.偏向锁
之所以引入偏向锁,是为了让线程获得锁的代价更低。当一个线程访问同步块并获取锁的时候,会在对象的对象头(对象头包括两部分的信息:一部分是”Mark Word“,主要存放的是哈希码、对象的分代年龄、锁的标记等信息;另一部分是对象的类型指针)和栈帧中的锁记录中存储锁偏向的ID,以后该线程在进入方法的同步块的时候,就检查这个ID(可以理解为一种标记,是一种身份的标识),如果测试成功,表明对象已经获得了锁;如果测试失败,继续测试偏向锁的标识是否设置为1(1的话就是偏向锁),如果没有则使用CAS(Compare And Swap)锁。
2.轻量级锁
分为加锁和解锁。当线程执行到同步块之前,JVM会首先检查当前线程的栈帧中创建用于存储记录锁记录的空间,并将对象头中Mark Word复制到锁记录中,也称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则线程获得锁,否则当前线程尝试使用自旋来获取锁。这就是加锁的过程。
这里多次提到CAS,那么CAS是个什么鬼?CAS是Compare and swap(比较和替换)的简写,具体而言就是:当进行CAS操作的时候,需要输入两个数值,一个是旧值,该旧值是原来的值,另一个是新值,也就是发生改变的值,得到这两个值后,在CAS操作期间会去比较旧值是否发生变化,如果没有发生变化就用新值进行替换,如果发生了变化就不进行替换。
那么解锁的过程又是怎样的呢?就是使用CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀,膨胀的结果是导致锁的升级,并进入阻塞状态。直到需要释放锁的线程释放锁并唤醒其他等待的线程。
Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制-- http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
Java 语言包括了跨线程传达并发性约束的构造 —— synchronized 和 volatile 。把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
Condition是配合Lock使用的,而wait/notify是配合synchronized使用的。比较两种实现方式,其实就是比较Lock和synchronized两种同步机制的区别。 ReentrantLock 和 synchronized 的可伸缩性.
-- 可重入代码, 重入锁ReetrantLock
可重入代码又称为“纯代码”,是一种允许多个进程访问的代码,因此,可重入代码是一种不允许任何进程对它进行修改的代码 。为了能修改,访问纯代码的
进程,把执行中可能改变的部分拷贝到该数据区,只需对该数据区中的内容进行修改,并不去改变共享的代码,这时的可共享代码即成为可重入码。
synchronized一般都是配合wait()和notify()一起使用的;通过重入锁 的newCondition()创建一个Condition, Condition 的await()和signal()
并发编程都是离不开 同步的,而同步最简单的莫过于synchronized关键字了;ReentrantLock重入锁
Condition condition = lock.newCondition(); condition.await(); condition.signal();
ReentrantLock lock = new ReentrantLock();lock.lock();lock.unlock();lock.tryLock() lock.tryLock(10, TimeUnit.SECONDS);
若一个程序或子程序可以安全的被并行执行,则称其为可重入(reentrant或re-entrant)的;即,当该子程序正在运行时,可以再次进入并执行它。若一个函数是可重入的,则该函数:
不能含有静态(全局)非常量数据。
不能返回静态(全局)非常量数据的地址。
只能处理由调用者提供的数据。
不能依赖于单实例模式资源的锁。
不能调用不可重入的函数。
多用户/对象/进程优先级'以及多进程一般会使得对可重入代码的控制变得复杂。同时,IO代码通常不是可重入的,因为他们依赖于像磁盘这样共享的、单独的资源。
要提供可重入性代码方法是:编写的函数都只会影响到局部变量,而不能改变全局的数据结构。这样的函数都被称作可重入函数。但是一个可重入内核不仅有这些可重入函数。Linux内核都是可重入的。由于在访问I/O等共享资源,内核还要有不可重入函数。此时,Linux内核就要使用锁机制,来保证在某一时间内只有一个进程可执行该不可重入代码,也就是分时共享的办法,来实现可重入的内核。
重入锁ReetrantLock,JDK 1.5新增的类,实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。synchronized在等待获取锁时是不可中的。
AQS,AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的。从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作。
在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock.
加Java架构师群获取Java工程化、高性能及分布式、高性能、深入浅出。高架构。性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的直播免费学习权限 都是大牛带飞 让你少走很多的弯路的 群..号是:855801563 对了 小白勿进 最好是有开发经验
注:加群要求
1、具有工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。
2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。
3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。
4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。
5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!