synchronized
了解synchronized
- synchronized关键字解决的是多个线程之间访问资源的同步性问题, synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
- dk1.6之前性能比较低,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
- JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized 实现原理
同步代码块的原理:
- synchronized同步语句块的实现使用的是monitorenter和monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
- 当执行monitorenter指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个ava对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。
- 当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
同步方法的原理:
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的却是ACC SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized1.6之后的性能优化
偏向锁
是指一段同步代码一直被一个线程所访问,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。降低获取锁的代价
轻量级锁
是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能(比如自旋锁)。
重量级锁
是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞(对象监视器Monitor),性能降低。
自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁"。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋
锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
锁粗粒化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,一直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁。那么会带来很多不必要的性能消耗。
synchronized 应用
synchronized 有如下四种方式修饰对象来实现代码同步:
- 修饰一个代码块,对象锁,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,对象锁,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
- 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
示例代码:
package com.muse.thread;
import java.util.concurrent.TimeUnit;
public class SynchronizedDemo {
public static void main(String[] args) {
/** case1:无Synchronized,乱序输出 */
// NoneSyncDemo noneSyncDemo = new NoneSyncDemo();
// Thread thread1 = new Thread(noneSyncDemo);
// Thread thread2 = new Thread(noneSyncDemo);
/** case2:synchronized修饰代码块, 对象锁 */
// 加锁有效
// SyncBlockDemo syncBlockDemo = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo);
// Thread thread2 = new Thread(syncBlockDemo);
// 加锁无效
// SyncBlockDemo syncBlockDemo1 = new SyncBlockDemo();
// SyncBlockDemo syncBlockDemo2 = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo1);
// Thread thread2 = new Thread(syncBlockDemo2);
/** case3:synchronized修饰方法,对象锁 */
// 加锁有效
// SyncMethodDemo syncMethodDemo = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo);
// Thread thread2 = new Thread(syncMethodDemo);
// 加锁无效
// SyncMethodDemo syncMethodDemo1 = new SyncMethodDemo();
// SyncMethodDemo syncMethodDemo2 = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo1);
// Thread thread2 = new Thread(syncMethodDemo2);
/** case4:synchronized修饰静态方法,类锁 */
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo);
// Thread thread2 = new Thread(syncStaticMethodDemo);
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo1 = new SyncStaticMethodDemo();
// SyncStaticMethodDemo syncStaticMethodDemo2 = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo1);
// Thread thread2 = new Thread(syncStaticMethodDemo2);
/** case5:synchronized修饰类,类锁 */
// 加锁有效
// SyncClassDemo syncClassDemo = new SyncClassDemo();
// Thread thread1 = new Thread(syncClassDemo);
// Thread thread2 = new Thread(syncClassDemo);
// 加锁有效
SyncClassDemo syncClassDemo1 = new SyncClassDemo();
SyncClassDemo syncClassDemo2 = new SyncClassDemo();
Thread thread1 = new Thread(syncClassDemo1);
Thread thread2 = new Thread(syncClassDemo2);
thread1.start();
thread2.start();
}
}
/**
* [case1:无Synchronized]
*/
class NoneSyncDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(100);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case2:synchronized修饰代码块]
* 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
**/
class SyncBlockDemo implements Runnable {
@Override
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
/**
* [case3:synchronized修饰方法]
* 一个线程访问一个对象中的synchronized修饰的方法时,其他试图访问该对象的线程将被阻塞。
**/
class SyncMethodDemo implements Runnable {
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case4:synchronized修饰静态方法]
* 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
**/
class SyncStaticMethodDemo implements Runnable {
@Override
public void run() {
method();
}
public synchronized static void method() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case5:synchronized修饰类]
* 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
**/
class SyncClassDemo implements Runnable {
@Override
public void run() {
synchronized(SyncClassDemo.class) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
volatile
volatile介绍
被volatile修饰的变量被修改之后所有的线程都能够获取到最新的值,避免数据的脏读。
volatile实现原理
如果对声明了volatile的变是进行写操作,JMM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过噢探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论;
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变呈数据。即可以获取当前最新值。
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
volatile可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性
volatile与内存屏障
jmm内存屏障分为四种:
volatile内存语义
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
在每个volatile读操作的前面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
CAS
cas原理
交换过程完全是原子的,基本流程如下:
- 在CPU上计算完结果后,都会对比内存的结果是否还是原先的值
- 如果不是原先的值,则认为已被其它线程修改,不能替换
- 如果是原先的值,则认为没有其它线程去修改,则可以修改,因为变量是volatile类型,所以最终写入的数据会被其它线程看到,所以一个线程修改成功后,其它线程就发现自己修改失败了重新尝试修改
CAS 优缺点
优点
- 高性能:无锁的方式实现原子操作
- 使用简单:调用compareAndSwapxx()方法即可实现
缺点
- ABA问题:由于CAS是检测值是否被改变,但一个变量的值原来是A,然后修改成B,然后又修改成A,这是实际上变量已被修改,但CAS检查的时候会发现它的值没有发生变化,还是能够被正常的CAS。解决思路就是通过对变量增加版本号,没修改一次版本号加1,所有原本ABA就是1A-2B-3A。在atomic包提供了一个类AtomicStampedReference就是采用类似的思路解决ABA问题
- 循环时间长开销大:在CAS更新失败会进入自旋,一旦长时间更新失败,就会占用较多的CPU
https://blog.csdn.net/qq_32828253/article/details/111998554?spm=1001.2014.3001.5502