Java中Synchronized关键字详解及使用
synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。synchronized属于独占式 悲观锁,同时也属于 可重入锁。
在使用synchronized修饰 对象时,同一时刻只能有一个线程对该对象进行访问;
在使用synchronized修饰 方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源后才能访问该对象或执行同步代码块。
synchronized的作用范围
- synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
- synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象。
- synchronized作用于一个代码块时,锁住的是在所有代码块中配置的对象。
synchronized的用法
作用于成员变量和非静态方法
synchronized作用于成员变量和非静态方法时,锁住的是当前对象的实例,代码如下:
/**
* @author CaveWang
* @date 2022/10/7 11:23
*/
public class SynchronizedDemo {
public static void main(String[] args) {
final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.testMethod2();
}
}).start();
}
public synchronized void testMethod1(){
try {
for (int i = 1; i < 3; i++) {
System.out.println("testMethod1第"+i+"次执行!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void testMethod2(){
try {
for (int i = 1; i < 3; i++) {
System.out.println("testMethod2第"+i+"次执行!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
因为synchronized锁住了对象实例,所以线程2等待线程1执行完成才能执行。运行结果如下:
testMethod1第1次执行!
testMethod1第2次执行!
testMethod2第1次执行!
testMethod2第2次执行!
那么如何让线程1和线程2并发执行呢?只需要定义两个对象实例分别调用两个方法就可以了。
修改main方法中的代码
public static void main(String[] args) {
final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo1.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo2.testMethod2();
}
}).start();
}
运行结果如下:
testMethod1第1次执行!
testMethod2第1次执行!
testMethod1第2次执行!
testMethod2第2次执行!
作用于静态方法
synchronized作用于静态同步方法时,锁住的是当前类的Class对象,示例代码如下所示,只需在以上的方法加上static关键字即可。
package synchronizedLocks;
/**
* @author CaveWang
* @date 2022/10/7 11:23
*/
public class SynchronizedDemo {
public static void main(String[] args) {
final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo1.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo2.testMethod2();
}
}).start();
}
public static synchronized void testMethod1(){
try {
for (int i = 1; i < 3; i++) {
System.out.println("testMethod1第"+i+"次执行!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void testMethod2(){
try {
for (int i = 1; i < 3; i++) {
System.out.println("testMethod2第"+i+"次执行!");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
此时synchronized锁住的为Class对象,即使我们创建了两个对象实例也不能使线程1和线程2并发执行,执行结果如下所示
testMethod1第1次执行!
testMethod1第2次执行!
testMethod2第1次执行!
testMethod2第2次执行!
作用于代码块
synchronized作用于一个代码块时,锁住的是在代码块中配置的对象,实例代码如下:
/**
* @author CaveWang
* @date 2022/10/7 13:35
* synchronized作用于代码块
*/
public class SynchronizedDemo2 {
final String lock = "lockA";
public static void main(String[] args) {
SynchronizedDemo2 synchronizedDemo2 = new SynchronizedDemo2();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo2.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo2.testMethod2();
}
}).start();
}
public void testMethod1(){
try {
synchronized (lock){
for (int i = 1; i < 3; i++) {
System.out.println("testMethod1第"+i+"次执行!");
Thread.sleep(3000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testMethod2(){
try {
synchronized (lock){
for (int i = 1; i < 3; i++) {
System.out.println("testMethod2第"+i+"次执行!");
Thread.sleep(3000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果相信大家也都猜到了,由于两个方法都需要获取名为lock的锁,所以线程2会等待线程1执行完成后才能获取该锁并执行
testMethod1第1次执行!
testMethod1第2次执行!
testMethod2第1次执行!
testMethod2第2次执行!
而我们在实际开发过程中可能会遇到A线程依赖B线程的资源,而B线程又依赖A线程中资源的情况,这时就可能会出现死锁,以下代码就是一段典型的死锁代码
/**
* @author CaveWang
* @date 2022/10/7 14:14
* 死锁
*/
public class SynchronizedDemo3 {
final String lockA = "LockA";
final String lockB = "LockB";
public static void main(String[] args) {
final SynchronizedDemo3 synchronizedDemo3 = new SynchronizedDemo3();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo3.testMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo3.testMethod2();
}
}).start();
}
public void testMethod1(){
try {
synchronized (lockA){
for (int i = 1; i < 3; i++) {
System.out.println("testMethod1第"+i+"次执行!");
Thread.sleep(3000);
synchronized (lockB){}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testMethod2(){
try {
synchronized (lockB){
for (int i = 1; i < 3; i++) {
System.out.println("testMethod2第"+i+"次执行!");
Thread.sleep(3000);
synchronized (lockA){}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上代码可以看出,出现了testMethod1等待testMethod2释放lockA,而testMethod2等待testMethod1释放lockB的情况,这样就出现了死锁,执行结果是两个线程都挂起,等待对方释放资源。
synchronized原理
在synchronized内部有以下六个区域,这些区域的数据分别代表了锁的不同状态
- ContentionList: 锁竞争队列,所有请求锁的线程都被放在锁竞争队列中
- EntryList: 竞争候选队列,在ContentionList中有资格成为候选者来竞选锁资源的线程被移动到了EntryList中
- WaitSet: 等待集合,调用wait方法后被阻塞的线程被放在WaitSet中
- OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck
- Owner:竞争到锁资源的线程为成为Owner线程
- !Owner:在Owner线程释放锁后,会从Owner状态变成!Owner状态
①synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取到锁资源,则将被放入ContentionList中。
②为了防止锁竞争时ContentionList尾部元素被大量的并发线程进行CAS访问而影响性能,Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中。
③Owner线程会指定EntryList中的某个线程(一般是最先进入的线程)为OnDeck线程。
④Owner线程并没有直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,让OnDeck线程重新竞争锁(这种行为在Java中叫“竞争切换”)。获取到锁资源的OnDeck线程会变为Owner线程,而未获取到锁资源的线程仍然停留在EntryList中。
⑤Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,直到某个时刻被notify方法或者notifyAll方法唤醒,会再次进入EntryList中。
⑥Owner线程在执行完毕后会释放锁资源并变成!Owner状态。
之前为什么说synchronized是非公平锁呢?原因就在这里,在线程进入到ContentionList之前,等待的线程会先尝试以自旋的方式获取锁,如果获取不到就进入ContentionList,该做法对已经进入队列的线程是不公平的。
另外JDK1.6对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级的过程叫做锁膨胀。