在多线程环境中,对同一个数据进行操作时,很容易出现同步问题。为了解决该问题,java提供了Synchronized手段。
Synchronized主要在这三处使用
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
2. 修饰一个非静态方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象,所有的静态同步方法用的也是同一把锁——类本身。
我们可以从字节码来看下Synchronized如何运行。
public class Run{
public synchronized void m() {
System.out.printf("M 干了一堆事");
}
public static synchronized void m1() {
System.out.printf("M1 干了一堆事");
}
public void m2() {
synchronized(this) {
System.out.printf("M2 干了一堆事");
}
}
}
对应字节码如下
public synchronized void m();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String M 干了一堆事
5: iconst_0
6: anewarray #2 // class java/lang/Object
9: invokevirtual #15 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
12: pop
13: return
LineNumberTable:
line 5: 0
line 6: 13
public static synchronized void m1();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String M1 干了一堆事
5: iconst_0
6: anewarray #2 // class java/lang/Object
9: invokevirtual #15 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
12: pop
13: return
LineNumberTable:
line 9: 0
line 10: 13
public void m2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorente
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #23 // String M2 干了一堆事
9: iconst_0
10: anewarray #2 // class java/lang/Object
13: invokevirtual #15 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
16: pop
17: aload_1
18: monitorexit
19: goto 27
22: astore_2
23: aload_1
24: monitorexit
25: aload_2
26: athrow
27: return
我们可以看出m方法中flags标记包含ACC_SYNCHRONIZED,m1方法中除了ACC_SYNCHRONIZED还包含ACC_STATIC,而m2中flags不包含ACC_SYNCHRONIZED。
synchronized修饰的方法在字节码中添加了一个ACC_SYNCHRONIZED的flags,而同步代码块则是在同步代码块前插入monitorenter,在同步代码块结束后插入monitorexit。
每一个对象,对象头信息中包含一个monitor,就是监视器,当线程获取monitor成功的时候,这个对象就被锁住了。
synchronized修饰的方法情况下:
当线程执行到某个方法时,JVM会去检查该方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了那线程会去获取这个对象所对应的monitor对象(每一个对象都有且仅有一个与之对应的monitor对象),获取成功后才执行方法体,方法执行完再释放monitor对象,在这一期间,任何其他线程都无法获得这个monitor对象。
同步代码块情况下:
monitorenter指令,
执行monitorenter就是为了尝试获取monitor的拥有权,如果一个monitor的数值是0,那么线程直接进入monitor,并且将monitor置为1,并将对象的抢占该线程置为该线程。
如果该线程已经占用monitor,则直接进入,monitor数+1 (可重入锁)
如果该对象的抢占线程不是该线程,那么该线程就会被阻塞,直到monitor的数值变成0,再次抢占。
monitorexit指令,
执行monitorexit的线程必须是已经拥有该对象monitor的线程,执行monitorexit命令后,monitor的进入数会减1,当减为0时,其它线程就可以尝试获得monitor的所有权。
我们看到其中有两个monitorexit,这其实是和try/finally是一个道理,我们一般编程的时候,都要在finally里加一个解锁unlock,防止异常情况的发生,那么这里也是一样的,为了防止代码出现异常,最后在进行monitorexit一下。
可重入性 是 锁 的 一 个 基 本 要 求 , 是 为 了 解 决 自 己 锁 死 自 己 的 情 况,比如一个类中出现一个同步方法调用另一个同步方法,在调用method1方法中monitor会被占用,那再调用method2方法,在method2尝试获取锁的过程中,假如Synchronized不支持可重入性,那就一直获取不到锁,直到method1运行完毕,释放锁。这样就会导致自己阻塞自己。
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争 到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象.
锁消除。是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。比如在方法中创建局部变量StringBuffer,进行append操作,append操作用了synchronized,是线程安全的。但是这个时候不会存在线程安全问题,这个时候加锁无意义且耗时。
这个时候我们可以通过编译器将其优化,将锁消除,同时必须开启逃逸分析:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
锁粗化。原则上 ,同步块的作用范围要尽量小,避免不必要耗时操作在作用域中。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁地进行互斥同步操作也会 导致不必要的性能损耗,这种情况,可以考虑扩大锁作用域。
Synchronized是一个悲观锁,因为它的并发策略是悲观的,不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换 、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作 。
乐观锁的核心算法是CAS(比较&替换),它涉及到三个操作数 :内 存 值、预期值、新值。当且仅当预期值和内存值相等时才将 内存值修改为新值 。
java中的Atomic包下的一系列类就是使用了乐观锁机制。
首先检查某块内存的值是否跟之前我读取时的一样,如不一样,则期间此内存值已经被别的线程更改过,舍弃本次操作,否则说 明期间没有其他线程对此内存值操作,可以把新值设置给此块内存 。
但是这可能会出现ABA问题,举个例子,你看到桌子上有100块钱,然后你去干其他事了,回来之后看到桌子上依然是100块钱,你就认为这100块没人动过,其实在你走的那段时间,别人已经拿走了100块,后来又还回来了。这就是ABA问题。这么看好像没毛病。
我们再举个例子。
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
解决方案:
乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题。在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:
我们可以来个代码看看。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Run {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
thread1.start();
thread2.start();
}
}
打印true,这个代码就出现ABA问题,执行thread2前,atomicInt已经被修改过又复原,在执行thread2时,该线程认为没有变化,实际是有变化的。
我们再用AtomicStampedReference来看看
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
打印false,解决了ABA问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。