由之前的知识我们了解到,Java中是存在线程并发安全性问题的,主要原因是内存可见性和指令重排序。而synchronized关键字可以使得线程之间以此排队去操作共享变量,保证线程的安全性。但是这种方式也会导致效率比较低,并发程度低。
静态方法: 当synchronized关键字修饰静态方法时,保证了同一个类的所有对象中中,只能有一个对象的一个线程同时访问该静态方法。
public class Thread1 extends Thread{
public synchronized static void test1(){
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"->"+i);
}
}
public static void main(String[] args){
Thread1 thread1 = new Thread1(){
@Override
public void run(){
Thread1.test1();
}
};
Thread1 thread2 = new Thread1(){
@Override
public void run(){
Thread1 curThread = new Thread1();
curThread.test1();
}
};
thread1.start();
thread2.start();
}
}
非静态方法: 当synchronized关键字修饰非静态方法时,保证了同一个对象中,只能有一个线程同时访问该非静态方法。
public class Thread2 {
public synchronized void test1(){
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"->"+i);
}
}
public static void main(String[] args) {
final Thread2 thread = new Thread2();
Thread thread1 = new Thread(){
@Override
public void run() {
thread.test1();
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
thread.test1();
}
};
thread1.start();
thread2.start();
}
}
对象锁: 当synchronized使用this关键字锁住代码块,只能保证同一个对象中,只能由一个线程同时访问代码块的内容。
public class Thread3 {
public void test(){
synchronized (this){
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"->"+i);
}
}
}
public static void main(String[] args) {
final Thread3 thread = new Thread3();
Thread thread1 = new Thread(){
@Override
public void run() {
thread.test();
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
thread.test();
}
};
thread1.start();
thread2.start();
}
}
类锁: 当synchronized使用类名.class锁住代码块,保证了同一个类的所有对象, 只能有一个对象的一个线程访问代码块。
public class Thread4 {
public void test(){
synchronized (Thread4.class){
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"->"+i);
}
}
}
public static void main(String[] args) {
final Thread4 thread1 = new Thread4();
final Thread4 thread2 = new Thread4();
Thread thread3 = new Thread(){
@Override
public void run() {
thread1.test();
}
};
Thread thread4 = new Thread(){
@Override
public void run() {
thread2.test();
}
};
thread3.start();
thread4.start();
}
}
public class SynchronizedDemo1 {
public SynchronizedDemo1() {
}
public static void main(String[] args) {
method();
Class var1 = SynchronizedDemo1.class;
synchronized(SynchronizedDemo1.class) {
;
}
}
public static synchronized void method() {
System.out.println("ok");
}
}
我们根据上面的例子来分析synchronized的实现原理,使用javap -v SynchronizedDemo1来对字节码文件进行分析:分析结果如下图所示:
对于上图中我们有几点需要注意:
此外,synchronized关键字具有重入性,如果多层嵌套synchronized关键字,那就会有多个monitorenter,也会有对应的monitorexit。这是因为每个对象都拥有一个计数器,当线程获取该对象锁之后,计数器就会加一,释放的时候就会减一。如下所示:
public class SynchronizedDemo2 {
public static void main(String[] args) {
method();
synchronized (SynchronizedDemo2.class){
synchronized (SynchronizedDemo2.class){
}
}
}
public synchronized static void method(){
System.out.println("ok");
}
}
最后,我们来总结下synchronized的工作流程,如下图所示:
使用synchronized进行同步,其关键就是要获得对象监视器monitor,当线程获取了monitor才能继续往下执行,否则就只能进行等待。获取的过程是互斥的,即同一时刻只能由一个线程获取到monitor。
我们来看下面这段代码:
public class Thread5 {
private int answer = 0;
public synchronized void A() throws InterruptedException { // 1
System.out.println("AAAA");
Thread.sleep(2000);
answer++; // 2
System.out.println("A"+answer);
} // 3
public synchronized void B(){ // 4
System.out.println("BBBB");
int i = ++answer; // 5
System.out.println("B"+i);
} // 6
public static void main(String[] args) throws InterruptedException {
final Thread5 thread = new Thread5();
Thread threadA = new Thread(){
@Override
public void run() {
try {
thread.A();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread threadB = new Thread(){
@Override
public void run() {
thread.B();
}
};
threadA.start();
threadB.start();
}
}
由于synchronized作用于同一个类的不同方法中,同一个对象只会有一个monitor,所以当一个线程进入synchronized修饰的方法中时,其他线程也无法进入其他synchronized修饰的方法中。 因此,根据以上的代码中的序号,我们可以推断出下面的图:
其中黑色的箭头是通过程序顺序执行推导的,蓝色箭头则是通过监视器规则推导出来的happens-before原则。
监视器锁的获取和释放的流程如下:
锁实际上是可以分为两大类的:乐观锁和悲观锁。
悲观锁:假设每一次执行临界区代码都会发生冲突,所以在每一个时刻都只能有一个线程获取锁,其余线程只能等待。
乐观锁:假设所有线程访问临界区的资源都不会发生冲突,所以不会添加锁,只在更新数据的时候去判断有没有别的线程更新了这个数据。如果这个数据没有被更新,那就成功写入数据。如果发现被其它线程更新过,则进行重试活报错。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS的全称是compare and swap(比较与交换),是一种无锁算法,也就是乐观锁,在不使用锁的情况下实现多线程之间的变量同步。
CAS(V,O,N)操作主要包括:V是内存地址中存放的实际值;O是预期值(旧值);N是将要更新的值。当V和O相同时,说明该值没有被其它线程修改过,是安全的,所以可以将值N赋值给V。如果V和O不同,说明该值已经被其它线程修改过,就不能将N赋值给V。
CAS和synchronized的主要区别:如果使用synchronized关键字,当有多个线程竞争临界区资源的时候,当一个线程获得监视器锁,其它线程就会被挂起。而对于CAS操作,当CAS操作失败,并不是立即挂起,而是会进行重试。
CAS操作也会存在一些问题,主要如下:
因为原始的synchronized会造成并发程度低的问题,所以对此做了些优化,也就是我们常说的四种锁的状态:无锁,偏向锁,轻量级锁,重量级锁。
在同步的时候是获取对象的monitor,即获取到对象的锁。对象的锁的本质其实就是对象中的一个标志,这个标志就是存放在对象头里面,对象头里面的Mark Word中就存放有锁状态的标志位:
无锁就是没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能够修改成功。
无锁的特点就是修改操作在循环内执行,线程会不断尝试修改共享资源,如果没有冲突就修改成功并退出,否则就进行循环尝试。上面讲到CAS就是无锁的具体实现。
偏向锁顾名思义,它会偏向于第一个访问锁的进程。 如果在代码运行过程中,同步代码只有一个线程访问,不存在线程争用的情况,则不需要触发同步机制,那么就会给该线程偏向锁。
偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销需要等待全局安全点,也就是需要stop the world,变为无锁的状态。
偏向锁适用于只有一个线程在执行同步代码块,不存在锁争用问题。如果存在锁竞争,则需要禁用偏向锁,否则stop the world很消耗资源。下图时偏向锁获得和撤销的过程:
轻量级锁是在偏向锁的时候,被另外的线程访问,偏向锁就会升级轻量级锁,其它线程会通过自旋的方式来获取锁,不会阻塞,从而提高性能。
轻量级锁的加锁过程:JVM会在当前线程栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录空间中,官方称为Displaced Mark Word。然后尝试使用CAS将对象头中的Mark Word替换为锁记录空间的地址。如果成功,则当前线程获得锁。如果失败,则通过自旋来尝试获得锁。
轻量级锁的解锁过程:会用CAS操作将锁记录的空间地址替换为Mark Word。如果成功,则释放锁。如果失败,则表示在它持有锁之前有其它线程尝试获取过锁,并且对Mark Word进行了修改,两者不一致,则切换重量级锁。
若当前只有一个等待线程,则该线程通过自旋操作进行等待。但是当自旋超过一定次数,或者一个线程持有锁,一个在自旋,又有第三个线程来访,则会升级为重量级锁。 重量级锁则是只有一个线程能够进入同步代码块。和原始的synchronized一样。
这篇文章参考了:
彻底理解synchronized
java中的各种锁详细介绍