多线程并发运行的时候,有可能会出现多个线程同时访问一个资源的情况。(这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等)。这时就会存在一个问题:由于每个线程执行的过程是不可控的,所以很可能导致运行结果和预期结果不一致,或者直接导致程序出错。这就是线程安全问题。
那么要解决线程安全问题,就要保证在同一时刻,只能有一个线程访问临界资源,也称同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在 Java 中,提供了几种方式来实现同步互斥访问:
-
通过锁的方式实现:synchronized 和 Lock
-
不通过锁机制实现:CAS
一.synchronized :
-
同步代码块
synchronized(object)//表示线程在执行的时候会将 object 对象上锁 { }
synchronized 块 比 synchronized 方法更加细粒度地控制了多个线程的访问, 一个方法中可能有几行代码会涉及到线程同步问题,那么我们将这些方法放入到synchronized 代码块中,就不能同时被多个线程所访问了,方法中的其他语句仍然 可以同时被多个线程所访问(包括 synchronized 块之前的和之后的)。
-
修饰非静态的方法 当 synchronized 关键字修饰一个非静态方法的时候,该方法叫做同步方法。 Java 中的每个对象都有一个锁(lock),或者叫做监视器(monitor), 当一个线程访问某个对象的 synchronized 方法时,将该对象上锁,其他任何 线程都无法再去访问该对象的 synchronized 方法了,直到之前的那个线程执行方法完毕后(或者是 抛出了异常),该对象的锁释放掉,其他线程才有可能再去访问该对象的 synchronized 方法。 注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制 关系。 注意,如果一个对象有多个 synchronized 方法,某一时刻某个线程已经进入 到了某个 synchronized 方法,那么在该方法没有执行完毕前,其他线程是无法访 问该对象的任何 synchronized 方法的。
-
修饰静态的方法 当一个 synchronized 关键字修饰一个静态方法对象上锁,但是静态方法不属于对象,而是属于类,它 会将这个方法所在的类的 Class 对象上锁。一个类不管生成多少个对象,它们 所对应的是同一个 Class 对象。 因此,当线程分别访问同一个类的两个对象的两个 static,synchronized 方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行 完毕后另一个线程才开始。 结论: synchronized 方法是一种粗粒度的并发控制,某一时刻,只能有一个线 程执行该 synchronized 方法。** synchronized 块则是一种细粒度的并发控制,只会将块中的代码同步, 位于方法内,synchronized 块之外的其他代码是可以被多个线程同时访问到 的。**
二.Lock
Lock 是一个接口,,使用 Lock 必须在 try-catch-finally 块中进行,并且释放锁的操作是在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用 Lock 来 进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock();//释放锁
}
Lock 和 synchronized 的区别:(synchronized 不会发生死锁) 1)Lock 是一个接口,而 synchronized 是 Java 中的关键字, 2)Lock 在发生异常时,如果没有主动通过 unLock()去释放 锁,**则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导 致死锁现象发生; 3)Lock 可以让等待锁的线程响应中断(可中断锁),使用 synchronized 时,等待的线程会一直等待下去,不能够响应中 断(不可中断锁); 4)通过 Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取 了锁,则返回 true;否则返回 false,也就说这个方法无论如何都会立即返回。 在拿不到锁时不会一直在那等待。而 synchronized 却无法办到。 5)Lock 可以提高多个线程进行读操作的效率(读写锁)。 6)Lock 可以实现公平锁,synchronized 不保证公平性。 在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 **当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。**所以说,在具体使用时要根据适当情况选择。
扩展1: volatile 和 synchronized 区别。
首先需要理解线程安全的两个方面:执行控制和内存可见控制。
-
执行控制:目的是控制代码执行(顺序)及是否可以并发执行。
-
内存可见控制:目是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
synchronized关键字解决的是执行控制的问题。**volatile
关键字解决的是内存可见性的问题,会使得所有对volatile
变量的读写都会直接刷到主存,即保证了变量的可见性,这个写会操作会导致其他线程中的缓存无效。。**这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。(注意:使用volatile
关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,不能保证复合操作的原子性,如i++
)
比如线程A将变量status修改为true这个动作发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B可能不知道status的值被修改了,就缓存的还是status的初始值false。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了(开销庞大),有点炮打蚊子的意思。比较合理的方式其实就是volatile
- volatile 是变量修饰符,而 synchronized 则作用于代码块或方法。
- volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会 对变量加锁,可能会造成线程的阻塞。
- volatile 仅能实现变量的修改可见性,并不能保证原子性;而 synchronized 则 可 以 保 证 变 量 的 修 改 可 见 性 和 原 子 性 。 (synchronized 有两个重要含义:它确保了一次只有一个线程可以执 行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于 其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改 刷新到主存中)。
- volatile 标记的变量不会被编译器优化,禁止指令重排序; synchronized 标记的变量可以被编译器优化。
扩展 2:什么场景下可以使用 volatile 替换 synchronized?
只需要保证共享资源的可见性的时候可以使用 volatile 替代, synchronized 保证可操作的原子性,一致性和可见性。
三.CAS:
Synchronized 和 Lock 都是悲观锁。CAS是乐观锁
悲观锁:悲观锁是认为肯定有其他线程来争夺资源,因此不管到底会不会发生争夺, 悲观锁总是会先去锁住资源,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放 锁。 乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直 到成功为止。就是当去做某个修改或其他操作的时候它认为不会有其他线程来做同样的操 作(竞争),这是一种乐观的态度,通常是基于 CAS 原子指令来实现的。CAS 通常不会将 线程挂起,因此有时性能会好一些。
CAS是一种非阻塞的同步方式。CAS实现线程安全不在代码层面处理,而是交给硬件-CPU和内存,利用CPU的多处理能力,实现硬件层面的阻塞;CAS 不适合竞争十分频繁的场景。
现在 几乎所有的 CPU 指令都支持 CAS 的原子操作, 当多个线程尝试使 用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线 程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再 次尝试。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改后的新值 B。 当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。 CAS 的关键点在于,系统在硬件层面保证了比较并交换操作的原子性, 处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。 一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作 内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从 主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该 线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用 CAS 刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果 一致,就可以更新成功。 Atomic 包提供了一系列原子类。这些类可以保证多线程环境下,当某个 线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一 样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。 Atomic 类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关 的指令来保证的。 hronized volatile 的区别;可重入锁在过期前续期失败会发生什么(说了事务回滚和yeid让出);读写锁**
未完待续。。。。。