volatile,synchronized和Lock
相关特点:
可见性(visibility):一个线程对于共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的修改。
Java内存模型是通过将工作区内存中的变量修改后的值同步到主内存中,在读取变量前从主内存刷新最新的值到工作内存中,这种依赖主内存的方式来实现可见性。
原子性(atomicity):一次操作不能被打断,要么全部执行,要么都不执行。
有序性:在本线程中观察,操作都是有序的;如果在另外一个线程中观察本线程,所有的操作都是无序的。
Java内存模型保证的是,同线程内,所有的操作都是由上到下的,但是多个线程并行的情况下,则不能保证操作的有序性。计算机在执行程序时,为了提高性能,编译器常常会对指令进行重排。通常先是编译器优化重排,再是指令并行重排,再是内存系统重排,最终成为实行指令。
在单线程环境里面确保程序最终运行结果和代码执行结果一致,处理器在进行重排时必须考虑到指令之间的数据依赖性。
在多线程环境中线程交替执行,由于编译器优化重排的存在,多个线程使用的变量能否保证变量一致是无法确定的,结果是无法预测的。
线程安全性保证
常见的线程安全问题:工作内存与主内存同步延时现象导致数据可见性问题。
当一个变量在两个线程中操作时,当一个线程正在对变量进行操作,这时第二个线程突然抢占到执行权,也对该变量进行操作,主内存中的数据还没来得及更新,这时将会出现不想得到的结果。
保证可见性的操作:
volatile,synchronized,final(一旦初始化完成其他线程就可见)
volatile
类型修饰符,被用来修饰被不同线程访问和修改的变量,保证本条指令不会因为编译器的优化被省略,就是能够使变量的值发生改变时能尽快让其他线程知道。
当编译器为了加快程序的运行速度,对一些变量的写操作会先寄存在寄存器或者CPU的缓存上进行,最后才写入主内存。而在此过程中,变量的改变新值对其他线程来说是不可见的。
这样会导致其他线程在进行同一操作时,依然从主内存中读取变量的旧值。当对 volatile 修饰的变量进行操作时,会将其他缓存中储存的修改前的变量清除,然后重新读取赋新值。一般来说是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这是储存新值的缓存A获取到消息,将新值传送给B,最后将新值写入主内存。当变量需要更新时,都会重复此步骤。
volatile除了解决变量可见性问题,另一个作用是禁止指令重排顺序优化。从而避免多线程环境下程序出现乱序现象。
内存屏障(Memory Barrier):是一个CPU指令,其作用有两个:一个是保证特定操作的执行顺序和保证某些变量的可见性。由于编译器和处理器都能进行指令优化重排,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,禁止对在内存屏障前后的指令进行执行指令优化重排。第二个是强制刷出各种CPU缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版。

synchronized
Java语言的关键字,可以用来给对象,方法或者代码块加锁。当他锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码,其他线程必须等待当前线程执行完后才可执行此加锁代码块,对于其他加锁代码块的访问也将被阻塞,但是可以执行非加锁代码块。
方法声明:
public synchronized void 方法名(){
方法体;
}
代码块声明:
synchronized(变量){
方法;
}
wait():
wait():使线程进入阻塞状态,释放CPU执行权,并且释放占有的对象锁。其他正在等待的线程即可抢占CPU执行权,即可抢占对象锁,获得的此锁的线程即可执行程序。只有当线程调用 notify()方法才可跳出阻塞状态。
与sleep()的不同:
线程调用 sleep()方法,线程会在一段时间内进入阻塞状态,这段时间内,会暂时释放CPU的执行权,但是不会释放对象锁。其他线程在此期间仍然无法进入锁定代码块内,阻塞时间一过,重新抢占CPU执行权,继续执行代码块,这样很容易造成大量线程阻塞。
notify():
此方法会唤醒因为调用 wait() 方法而进入阻塞的线程,其实就是对对象锁的唤醒,从而使得阻塞的线程有机会获取对象锁。当线程调用 notify()被唤醒后,不会立刻释放对象锁,而是将 synchronized 中的代码块继续执行完毕后,才会释放对象锁。
注:
1) notifyAll() 方法是唤醒所有都在等待的线程。
2) wait() 和 notify() 必须在 synchronized 代码块中调用。
synchronized的缺陷:
synchronized 是Java中的关键字,属于Java语言内置的属性,但是一个代码块被synchronized修饰了,当一个线程获取对应的对象锁,并执行该代码块时,其他线程只能一直等待,等待获取对象锁的线程释放对象锁,而线程释放对象锁只有两种情况:
1)获取对象锁的线程执行完该代码,然后线程释放对象锁的占有。
2)线程执行出现异常,此时JVM会让线程自动释放对象锁。
但是如果获取的线程由于要等待IO方法返回参数或者等待某种通知或其他原因(如调用 sleep()方法)被阻塞了,并且这时线程也没有释放对象锁,那么其他线程便只能一直等待,这很影响线程执行效率。
通过 Lock 就可以不让等待的线程一直无限制的等待下去或者只等待一定的时间,再或者能够响应中断。
举个例子:当多线程读写文件时,写和写操作会冲突,写和读操作也会冲突,但是读和读不会冲突。当采用 synchronized来实现线程同步的话,会出现一个问题:如果多个线程只进行读操作,当一个线程进行读操作时,虽然不会发生冲突,但是其他线程还是会等待无法进行操作,这就影响程序的效率了。这时就需要一种机制来使得多个线程都只进行读操作时,多个线程可一起执行且线程之间不会发生冲突, Lock就可办到。
Lock
Lock 不是Java语言的内置属性,Lock 是一个接口,通过该接口可以实现同步访问。Lock 必须要用户亲自手动释放对象锁,如果没有主动释放锁,就可能导致出现死锁现象。而 synchronized 中的代码执行完后,系统会让线程自动释放锁。
ReentrantLock,“可重入锁”是Lock接口的唯一实现类

注意:
如果锁具有可重入性,则称为可重入锁。 synchronized 和 ReentrantLock 都是可重入锁,可重入性实际上就是表明了锁的分配机制:基于线程的分配,而不是基于方法的分配。举个例子,当一个线程执行到 synchronized 方法时,在此方法中又调用了另外一个 synchronized 方法,此时线程不必重新申请锁,而是直接执行所调用的方法。
volatile 与 synchronized 的区别
1)volatile 本质是告诉JVM当前在寄存器中的值是不确定的,需要重新从主内存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
2)volatile 仅能使用在变量级别,synchronized 则可以使用在变量,方法上。
3)volatile 仅能实现修改变量的可见性,而synchronized 则可以实现修改变量的可见性和原子性。
4)volatile 不会造成线程的阻塞,而synchronized 可能会造成线程的阻塞。
5)当一个域(接口中的变量,属性等)的值依赖于它之前的值时,volatile 就无法工作了,如 n++。当某个域的值受到其他域的值的限制,volatile 也无法工作,如 Range类的 lower 和 upper 边界,必须遵循 lower<=upper 的限制。
6)使用 volatile 而不是 synchronized 的唯一安全情况是类中只有一个可变的域。
synchronized 和 Lock 的区别
1)Lock 是一个接口,而synchronized 是Java中的关键字,synchronized 是内置的语言实现。
2) synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象的发生;而 Lock 在发生异常时,如果没有主动通过 unlock() 去手动释放,则可能造成死锁现象,因此使用 Lock 时需要在try-catch-finally 块中释放锁。
3)Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,等待的线程会一直等待下去,不能够响应中断。
4)Lock 中的 tryLock() 可以知道有没有成功获取锁,如果获取到锁则放回true反之false,这样可以避免线程阻塞问题,而synchronized 无法办到。
5)Lock 可以提高多个线程进行读操作的效率。

本文详细介绍了Java并发编程中的volatile、synchronized和Lock的特点与区别。volatile保证了变量的可见性和禁止指令重排,但不保证原子性。synchronized提供线程安全保证,但可能导致阻塞。Lock接口提供了更灵活的锁机制,支持响应中断和尝试获取锁,且可提高多线程读操作的效率。
844

被折叠的 条评论
为什么被折叠?



