概念
是利用锁的机制来实现同步的。
**互斥性:**即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
**可见性:**必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
使用方式
在应用Sychronized关键字时需要把握如下注意点:
-
一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
-
每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁
-
synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
对象锁
包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)
public class SynchronizedDemo implements Runnable {
//两个线程访问同一个实例的同步代码块的时候,一个时间内只有一个线程得到执行,另外一个线程必须等到之前的线程结束 this指向的是当前的实例
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo);
a.start();
b.start();
}
@Override
public void run() {
synchronized (this) {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
}
public class SynchronizedDemo1 implements Runnable {
Object block1 = new Object();
Object block2 = new Object();
//虽然使用的是一个实例,同步代码块中,使用的是两把锁,
public static void main(String[] args) {
SynchronizedDemo1 synchronizedDemo = new SynchronizedDemo1();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo);
a.start();
b.start();
}
@Override
public void run() {
synchronized (block1) {
System.out.println("block1 我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1"+Thread.currentThread().getName() + "结束");
}
synchronized (block2) {
System.out.println("block2-我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block2"+Thread.currentThread().getName() + "结束");
}
}
}
public class SynchronizedDemo2 implements Runnable {
Object block1 = new Object();
Object block2 = new Object();
public static void main(String[] args) {
//两个线程获取同一个实例,谁先获取锁,谁先执行同步方法,另一个未获取的只能等待前一个线程执行结束
SynchronizedDemo2 synchronizedDemo = new SynchronizedDemo2();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo);
a.start();
b.start();
}
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
类锁
指synchronize修饰静态的方法或指定锁对象为Class对象
public class SynchronizedDemo2 implements Runnable {
Object block1 = new Object();
Object block2 = new Object();
public static void main(String[] args) {
//修饰同步方法的指向当前this.两个实例不同的this.无需等前一个线程释放锁。
SynchronizedDemo2 synchronizedDemo = new SynchronizedDemo2();
SynchronizedDemo2 synchronizedDemo2 = new SynchronizedDemo2();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo2);
a.start();
b.start();
}
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public class SynchronizedDemo3 implements Runnable {
public static void main(String[] args) {
//修饰静态同步方法,当前锁就是Class. 无论哪个线程访问,只有一把锁,一但被获取,后一个线程只能等待前一个线程释放锁
SynchronizedDemo3 synchronizedDemo = new SynchronizedDemo3();
SynchronizedDemo3 synchronizedDemo2 = new SynchronizedDemo3();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo2);
a.start();
b.start();
}
@Override
public void run() {
method();
}
public static synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public class SynchronizedDemo4 implements Runnable {
public static void main(String[] args) {
//修饰同步代码块,锁是当前类的class。所有线程获取的锁都是同一把,后面线程需要等待前面线程执行结束才可以
SynchronizedDemo4 synchronizedDemo = new SynchronizedDemo4();
SynchronizedDemo4 synchronizedDemo2 = new SynchronizedDemo4();
Thread a = new Thread(synchronizedDemo);
Thread b = new Thread(synchronizedDemo2);
a.start();
b.start();
}
@Override
public void run() {
synchronized (SynchronizedDemo4.class){
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
}
实现机制
加锁和释放锁的原理。编译JVM的字节码
public class SynchronizedTest {
Object object = new Object();
public void method1() {
synchronized (object) {
}
method2();
}
private static void method2() {
}
}
javac SynchronizedTest.java
javap -verbose SynchronizedTest.class
public void method1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter //
7: aload_1
8: monitorexit //
9: goto 17
12: astore_2
13: aload_1
14: monitorexit //
15: aload_2
16: athrow
17: invokestatic #4 // Method method2:()V
20: return
Monitorenter
和Monitorexit
指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
-
monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
-
如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
-
这把锁已经被别的线程获取了,等待锁释放
monitorexit指令
:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。出现异常的话也会释放锁,所有会有两个monitorexit指令
任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
底层语义
synchronized 锁机制在 Java 虚拟机中的同步是基于进入和退出监视器锁对象 monitor
实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor
对象,当一个 monitor
被某个线程持有后,它便处于锁定状态。在 HotSpot
虚拟机中,monitor 是由 ObjectMonitor
实现的,每个等待锁的线程都会被封装成 ObjectWaiter
对象,ObjectMonitor
中有两个集合,_WaitSet
和 _EntryList
,用来保存 ObjectWaiter
对象列表 ,owner 区域指向持有 ObjectMonitor
对象的线程。
当多个线程同时访问一段同步代码时,首先会进入 _EntryList
集合尝试获取 moniter,当线程获取到对象的 monitor
后进入 _Owner
区域并把 _owner
变量设置为当前线程,同时 monitor
中的计数器 count 加1;若线程调用 wait()
方法,将释放当前持有的 monitor
,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放 monitor
并复位变量的值,以便其他线程获取 monitor
。
_EntryList
:存储处于Blocked
状态的ObjectWaiter
对象列表。_WaitSet
:存储处于wait
状态的ObjectWaiter
对象列表。
可重入原理
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method2();
}
private synchronized static void method2() {
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class tech/pdai/test/synchronized/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
15: invokestatic #3 // Method method2:()V
Exception table:
from to target type
5 7 10 any
10 13 10 any
上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗? 答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
保证可见性的原理
内存模型和happens-before规则
Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:
public class MonitorDemo {
private int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5。
根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。
锁优化
在 JVM中,对象在堆内存中的布局分为三块区域:对象头、实例变量和填充数据。
- 实例数据: 存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
- 对齐填充: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头: 它实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的其主要结构是由Mark Word 和 Class Metadata Address 组成。如果是数组还会记录数组的长度。
- class pointer(存储对象的类型指针): 该指针指向它的类元数据(JVM通过该指针找到该对象是那个类的事例)。值得注意的是,如果应用的对象过多,使用 64 位的指针将浪费大量内存。64 位的 JVM 比 32 位的 JVM 多耗费 50% 的内存。现在使用的 64 位 JVM 会默认使用选项+UseCompressedOops(普通对象的指针指的是类成员变量的属性(引用类型))开启指针压缩,将指针压缩至 32 位。+UseCompressedClassPointers(类指针压缩)也会压缩。
- mark word(对象自身的运行时数据): 存储 hashcode、GC 分代年龄、锁类型标记、偏向锁线程 ID、CAS 锁指向线程 LockRecord 的指针等,synchronized 锁的机制与这个部分(markwork)密切相关,用 markword 中最低的三位代表锁的状态,其中一位是偏向锁位,另外两位是普通锁位。
JVM中的Monitorenter
和Monitorexit
字节码是依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用了Mutex Lock需要将当前线程挂起并从用户态切换到内核态,这种切换的代价是非常昂贵的;然后在现实中大部分情况下,同步方法是运行在单线程中的(无锁竞争环境),如果每次都Mutex Lock肯定严重影响程序的性能。在JDK1.6之后对锁进行了大量的优化,从而减少锁操作的开销。
锁优化的技术
锁粗化
锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。举个例子,一个循环体内有一个代码同步块,每次循环都会执行加锁解锁操作。那么经过锁粗化就会变成加锁的方式在循环体外。
public static String test04(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
在上述的连续append()操作中就属于这类情况。JVM会检测到这样一连串的操作都是对同一个对象加锁,那么JVM会将加锁同步的范围扩展(粗化)到整个一系列操作的 外部,使整个一连串的append()操作只需要加锁一次就可以了。
锁消除
锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。比如:在操作String类型数据时,由于String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。StringBuilder不是安全同步的,但是在上述代码中,JVM判断该段代码并不会被其他线程访问,则将该代码默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。
public static String test03(String s1, String s2, String s3) {
String s = s1 + s2 + s3;
return s;
}
StringBuilder不是安全同步的,但是在上述代码中,JVM判断该段代码并不会逃逸,则将该代码带默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。(还有Vector中的各种操作也可实现锁消除。在没有逃逸出数据安全防卫内)
锁消除的依据是逃逸分析的数据支持
内存逃逸
锁消除,前提是java必须是server模式(server比client模式有更多的优化),同时必须开启逃逸分析
-XX:+(+开启,-关闭)DoEscapeAnalysis 开启逃逸分析
-XX:+EliminateLocks 开启锁消除
- 同步忽略,如果一个对象发现只能从一个线程被访问到,那么对于这个对象的操作可以不做同步考虑
- 将堆分配转化为栈分配。如果一个对象在子进程中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 分离对象(聚合量)或标量(8大基本数据类型)替换,有的对象可能不需要作为一个连续的内存机构存在也可以被访问到,那么对象的部分或者全部可以不存储在内存,而是存储在CPU寄存器中
JDK1.7之后默认开启逃逸分析。
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i=0;i<500000;i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println("cost-time "+(end-start)+" ms");
try{
Thread.sleep(1000000);
}catch (Exception e){
e.printStackTrace();
}
}
private static Student alloc() {
/**
* JIT对编译时会对代码进行逃逸分析
* 并不是所有对象都是存放在堆里面的,有的一部分存在线程栈空间
*/
/**
* 逃逸分析会认为没必要创建这么多对象,因为这些对象只会在当前线程引用使用
所以ß直接将对象的属性name和age放入到栈中
*/
Student student = new Student();
return student;
}
static class Student {
private String name;
private Integer age;
}
//通过开启和关闭内存逃逸进行分析
//使用jps找到进程
//通过 jmap -histo pid进行分析
锁升级
在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁、偏向锁、轻量级所、重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。
无锁
无锁状态其实就是乐观锁
偏向锁
偏向锁(Biased Locking)是jdk1.6之后增加的。是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
偏向锁的撤销
偏向锁使用了一种等待竞争出现才会释放锁的机制。所以当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放锁。但是偏向锁的撤销需要等到全局安全点(就是当前线程没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,让你检查持有偏向锁的线程是否活着。如果线程不处于活动状态,直接将对象头设置为无锁状态。如果线程活着,JVM会遍历栈帧中的锁记录,栈帧中的锁记录和对象头要么偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁。
在JDK15的版本禁止了偏向锁,并在以后打算删除
- 为了支持偏向锁使得代码复杂度大幅度提升,并且对 HotSpot 的其他组件产生了影响,这种复杂性已成为理解代码的障碍,也阻碍了对同步系统进行重构
- 在更高的 JDK 版本中针对多线程场景推出了性能更高的并发数据结构,所以过去看到的性能提升,在现在看来已经不那么明显了。
- 围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏向锁的情况下变得更好。
轻量级锁
加锁过程
在线程执行同步块之前,JVM会先在当前线程的栈帧中创建一个名为锁记录(Lock Record
)的空间,用于存储锁对象目前的Mark Word
的拷贝(JVM会将对象头中的Mark Word
拷贝到锁记录中,官方称为Displaced Mark Ward
)这个时候线程堆栈与对象头的状态如图:
如上图所示:如果当前对象没有被锁定,那么锁标志位位01状态,JVM在执行当前线程时,首先会在当前线程栈帧中创建锁记录Lock Record
的空间用于存储锁对象目前的Mark Word
的拷贝。
然后,虚拟机使用CAS操作将标记字段Mark Word拷贝到锁记录中,并且将Mark Word
更新为指向Lock Record
的指针。如果更新成功了,那么这个线程就有用了该对象的锁,并且对象Mark Word的锁标志位更新为(Mark Word
中最后的2bit)00,即表示此对象处于轻量级锁定状态,如图:
如果这个更新操作失败,JVM会检查当前的Mark Word
中是否存在指向当前线程的栈帧的指针,如果有,说明该锁已经被获取,可以直接调用。如果没有,则说明该锁被其他线程抢占了,如果有两条以上的线程竞争同一个锁,那轻量级锁就不再有效,直接膨胀位重量级锁,没有获得锁的线程会被阻塞。此时,锁的标志位为10。Mark Word
中存储的时指向重量级锁的指针。
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word
替换回到对象头中,如果成功,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁。两个线程同时争夺锁,导致锁膨胀的流程图如下:
- 自旋锁:在JDK1.4引入,当时默认关闭,jdk1.6默认开启。自旋锁本质上与阻塞并不相同,先不考虑其对多处理器的要求,如果锁占用的时间非常的短,那么自旋锁的性能会非常的好,相反,其会带来更多的性能开销(因为在线程自旋时,始终会占用CPU的时间片,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源)。因此自旋等待的时间必须要有一定的限度,如果自选超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。但是自旋锁无法在知道自己要自旋多少次才可以获取到锁,可能一直要自旋,也可能只需要自旋几次就可以。
- 自适应自旋锁:jdk1.6引入;自旋的时间是由上一次在同一个锁的自旋时间及锁的拥有者决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能 | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块执行速度较长 |
其他知识
Synchronized与Lock
synchronized的缺陷
效率低
:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时不够灵活
:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活无法知道是否成功获得锁
,相对而言,Lock可以拿到状态,如果成功获取锁,…,如果获取失败,…
Lock解决相应问题
lock()
: 加锁unlock()
: 解锁tryLock()
: 尝试获取锁,返回一个boolean值tryLock(long,TimeUtil)
: 尝试获取锁,可以设置超时
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合
解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
使用Synchronized有哪些要注意的?
- 锁对象不能为空,因为锁的信息都保存在对象头里
- 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。