一、synchronized的作用:
synchronized是Java 关键字,用于保证线程之间的同步,也就说同一时刻只能有一个线程访问同步方法或同步代码块。其主要有三种表现形式:
1.对于普通方法,其锁住的是当前的实例对象。(对于不同的实例对象是不起作用的)。
2.对于静态方法,锁住的是class类对象。
3.对于代码块,锁住的是括号里的对象。
synchronized底层实现原理:
synchronized修饰的方法:其是根据常量池中的acc_synchronized标识符来实现的,当线程访问同步方法时,查看是否有ACC_SYNCHRONIZED标识,如果有就获得监视器锁(加锁),执行方法体,执行完释放锁。
synchronized修饰的代码块:是根据monitorenter和monitorexit两个指令来实现的。线程执行到monitorenter指令时,就会获得监视器锁,执行代码块中的内容,当执行到monitorexit指令时,就释放监视器锁。每个对象维护着一个锁计数器。
二、关于Lock:
Lock的作用:
lock的作用就是将“多线程”单线程化,从而实现多线程按照一定的顺序执行。lock的原理就是编译器将lock转换成monitor,根据monitorenter和monitorexit指令来执行同步代码。只要锁定同一个对象就会顺序执行。相比较于synchronized,无论是功能还是性能上都有很大的提升,但是还是需要
Lock的定义及使用:
Lock本质上是一个接口,主要包含以下方法:
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
Lock接口有三个实现类,ReentrantLock类,以及ReentrantReadWriteLock类中的ReadLock和WriteLock两个静态内部类。
Lock的使用方式:在多线程访问共享资源时,访问时需要加锁,访问结束需要解锁,解锁一般放在finally块中。
Lock接口的基本思想:
实现锁的两个基本要素:一个是表示锁状态的变量,(0表示没有线程获取锁,1表示已有线程占用了锁),此变量必须声明为volatile类型;另一个是队列,队列中的节点表示未能获取到锁而阻塞的线程。(为了解决多核CPU多线程缓存不一致的问题,故而声明为volatile类型)
线程获取锁的大致过程:
线程释放锁的大致过程:当线程要释放锁的时候,就将状态变量置为1,并唤醒队列中的队首节点,然后去继续执行后面的代码。但是,如果是非公平锁,被唤醒线程可能会和未在队列中的线程一起竞争锁。
公平锁和非公平锁:
上边提到了公平锁和非公平锁,ReentrantLock默认是非公平锁。
公平锁意思就是严格按照顺序去获得锁。如果是公平锁,他想要获取锁,不仅要判断状态变量,还需要判断队列是否为空,如果队列为空,线程通过cas操作尝试获取锁;如果不为空,就进入等待队列,自身阻塞。
对于非公平锁,不需要判断队列是否为空,当状态变量为0时,就尝试获取锁。
三、关于volatile
宏观上理解:
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。
一旦一个变量被volatile所修饰,那么他就具有了两层语义:
一是保证了不同线程之间对共享变量的可见性,也就是说当前线程对共享变量进行了修改,其他线程是可见的。
二是有序,禁止指令重排。
volatile底层实现原理:
参考自:volatile底层实现原理 - myf008 - 博客园
首先是一个简单的例子:
public class volatileEx {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1");
for (;;){
if(flag){
System.out.println("跳出循环");
break;
}
}
}
}).start();
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2");
flag = true;
System.out.println("flag变化");
}
}).start();
}
}
结果如下:
此时是感知不到flag的变化的。
public class volatileEx {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1");
for (;;){
if(flag){
System.out.println("跳出循环");
break;
}
}
}
}).start();
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2");
flag = true;
System.out.println("flag变化");
}
}).start();
}
}
结果如下:
此时是可以感知的。
那么接下来说一下原理,就要从JMM开始说起。上篇博客已经介绍了JMM。
(转载自volatile底层实现原理 - myf008 - 博客园)
cpu和内存并不能直接交互数据,是通过总线。volatile会开启总线的MESI缓存一致性协议。
主要的流程:
1.当前线程修改值,经过总线,写入主内存。
2.其他线程通过总线嗅探机制感知到共享变量的变化,使自身工作内存中的贡献变量失效。
3.其他线程去主内存读取新值。
volatile底层是根据LOCK指令,保证其他线程读到最新值。在store之前对主内存加锁,知道修改完再释放锁。这期间其他线程不能访问。
四、说一说大家的区别:
首先synchronized是由JVM管理的,可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定。且当有线程占用锁时,其他线程只能阻塞等待。synchronized采用的是悲观锁机制,即线程获取的是独占锁,而cpu在转换线程阻塞时会引起上下文切换,导致效率低。
再次是lock是一个接口,由Java代码实现。lock需要手动释放锁,需要在finally块中写入释放锁的语句。每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS
操作。
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
volatile和synchronized区别:
1.volatile 仅能使用在变量级别;
synchronized 则可以使用在变量、方法、和类级别的;
2.volatile 仅能实现变量的修改可见性,并不能保证原子性;
synchronized 则可以保证变量的修改可见性和原子性;
3.volatile 不会造成线程的阻塞;
synchronized 可能会造成线程的阻塞;
4.volatile 标记的变量不会被编译器优化;
synchronized 标记的变量可以被编译器优化。