一、心路历程
在Java并发编程当中,synchronized关键字无疑是被问到频率较高的一个问题,在面试当中,很多面试官对你对synchronized关键字及对它底层的了解程度都是比较重视的。如果你能回答上来,无疑是比较加分的。以下是个人通过自己了解到的,已经书中的知识结合总结的内容,欢迎大家指正!
二、什么是synchronized关键字
在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而synchronized关键字则是用来保证线程同步的。
三、JMM及Java内存的可见性
那我们在了解synchronized关键字的底层实现原理之前,需要先首先了解一下Java内存模型(JMM),看看synchronized关键字是如何起作用的。
当然,这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,(JMM也更偏向于Java当中的一个规约)它包含了控制器、运算器、缓 存等。
同时Java内存模型规定,线程对共享变量的操作必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?
-
线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,这时主内存及本地内存中的X的值都为1。
-
线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。
-
线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。
该问题Java内存模型是通过synchronized关键字或volatile关键字就可以解决,那么synchronized关键字是如何解决的呢,其实进入synchronized块就是把在synchronized块内使用到的变量从线程的本地内存中擦除,这样在synchronized块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获取,解决了内存不可见问题。
四、synchronized和volatile的区别
上午我们说了synchronized都可以解决JMM中的问题,那么他们有什么区别呢?
-
volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即 保证了变量的可见性。
-
synchronized 解决的事执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且,synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证操作的内存可见性,同时也使得这个锁的线程的所有操作(基于happens-before规范)于随后获得这个锁的线程的操作。
主要区别:
-
volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的
-
volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性
-
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
-
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化(锁升级机制)。
五、Synchronized 和Lock的区别
-
Synchronized 内置的Java关键字,Lock 是一个Java类
-
Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
-
Synchronized 会自动释放锁,Lock 必须要手动释放锁,如果不释放锁,会造成死锁
-
Synchronized 假设A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待,Lock 锁 可以通过 tryLock 判断有没有锁
-
Synchronized 可重入锁、不可中断、非公平 ,Lock 可重入,可判断,可公平
-
Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
六、synchronized使用方式
-
修饰普通同步方法
public class synTest implements Runnable {
private volatile static int i = 0; //共享资源
private synchronized void add() {
i++;
}
public void run() {
for (int j = 0; j < 10000; j++) {
add();
}
}
public static void main(String[] args) throws Exception {
synTest syncTest = new synTest();
Thread t1 = new Thread(syncTest);
Thread t2 = new Thread(syncTest);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果:
20000
我们更改一下代码结构,再次查看结果:
public class synTest implements Runnable {
private volatile static int i = 0; //共享资源
private synchronized void add() {
i++;
}
public void run() {
for (int j = 0; j < 10000; j++) {
add();
}
}
public static void main(String[] args) throws Exception {
// synTest syncTest = new synTest();
Thread t1 = new Thread(new synTest());
Thread t2 = new Thread(new synTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果:
< 20000
第二个示例中的add() 方法虽然也使用synchronized关键字修饰了,但是因为两次new synTest() 操作建立的是两个不同的对象,也就是说存在两个不同的对象锁,线程t1和t2使用的是不同的对象锁,所以不能保证线程安全。那这种情况应该如何解决呢?因为每次创建的实例对象都是不同的,而类对象却只有一个,如果synchronized关键字作用于类对象,即用synchronized修饰静态方法,问题则迎刃而解了。
-
修饰静态方法
只需要在add()
方法前使用static修饰即可,即当synchronized作用于静态方法,锁的就是当前的class对象。
private static synchronized void add() {
i++;
}
结果:
2000
-
修饰同步代码狂
如果某些情况下,整个方法体比较大,需要同步的代码只是一小部分,如果直接对整个方法体进行同步,会使得代码性能变差,这时只需要对一小部分代码进行同步即可。代码如下:
public class synTest implements Runnable {
private static int i = 0; //共享资源
public void run() {
synchronized (this){ // this表示当前对象实例,这里还可以使用synTest.class;表示class对象锁
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws Exception {
synTest syncTest = new synTest();
Thread t1 = new Thread(syncTest);
Thread t2 = new Thread(syncTest);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果:
20000