一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
synchronized
synchronized是java中的一个关键字,简单来说,synchronized关键字以同步方法和同步代码块的方式,为方法和代码块上的对象加锁。使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行,本文将介绍为什么要加锁以及synchronized的用法以及使用上的区别。
为什么要加锁?
简单的说,加锁是为了避免多线程下并发冲突,锁是一种数据保护机制,可允许某一个线程(进程)进行操作锁,当文件锁上时,其他线程(进程)根据锁的性质(读写锁,阻塞非阻塞)。下面我们通过代码问题说明不加锁时存在的问题。 类SyncClass具有一个类变量elem=0,方法printElem(int count)与addElem(int count)方法使得分别用来对elem打印或执行加1 count次。
public static void testNoLock(int count) {
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
printElem(count);
}
});
Thread addThread = new Thread(new Runnable() {
@Override
public void run() {
addElemCount(count);
}
});
printThread.start();
addThread.start();
}
启动了两个线程分别执行打印与加1操作10次,结果如下,可以看到printThread线程打印的elem在第二次打印时已经被addThread线程所修改,如果要保证printThread打印时的elem都是相等不变的,我们需要加锁使得printThread打印elem时,资源elem不被其他线程所获取。
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
elem is 10
synchronized的作用
synchronized的下面三个作用:
- 原子性:一个事务的一个或多个操作在执行时,要么全部执行成功、要么执行失败。 被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得
- 可见性:在多线程环境下,该资源的状态、值信息等对其他线程都是可见的。synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。
- 有序性:程序执行的顺序按代码先后执行。synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。 另外synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
synchronized的用法
synchronized可以修饰静态方法、成员函数,同时还可以直接定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是类。其用法有以下三种:
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。例如
synchronized void method(int count)
. - 修饰静态方法:因为静态方法属于类成员,因此synchronized是给当前类加锁,会作用于类的所有对象实例,进入同步代码块要获得当前class的锁。例如
static synchronized void method(int count)
. - 修饰代码块:可以指定加锁的对象,对给定对象或类加锁
synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
下面我们通过printElem(int count)与addElem(int count)方法通过synchronized使用使得在线程情况下保证printElem(int count)打印的不受addElem(int count)所影响。
修饰实例方式
如下使用synchronize修饰printElem(int count)与addElem(int count)后,首先我们创建一个对象实例,然后开启两个线程进行调用:
/**
* 使用synchronized修饰实例方法addElem与printElem
*/
public class SyncClass {
public static int elem = 0;
public static void main(String[] args) {
SyncClass syncClass = new SyncClass();
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
syncClass.printElem(10);
}
});
Thread addThread = new Thread(new Runnable() {
@Override
public void run() {
syncClass.addElemCount(10);
}
});
printThread.start();
addThread.start();
}
// 对elem加1 count 次 饰静态方法
public synchronized void addElemCount(int count) {
for (int i = 0; i < count; i++) {
elem++;
System.out.println("elem 加1");
}
}
// 打印elem 10次。 synchronized修饰实例方法
public synchronized void printElem(int count) {
for (int i = 0; i < count; i++) {
System.out.println("elem is " + elem);
}
}
}
执行结果如下,由于synchronized对printElem与addElem修饰后,在执行printElem时,会对syncClass的实例对象加锁,当addElem执行时会去获取syncClass的实例对象信息(该对象的状态信息对所有线程是可见的),发现该对象已被加锁,此时会等待对象锁释放,因此addElem会在printElem执行完成后再执行。
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem is 0
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
elem 加1
修饰静态方法
如下代码,通过synchroized修饰静态方法,其执行结果与上面修饰实例方法结果一致。
/**
* synchronized修饰静态方法addElem与printElem
*/
// 对elem加1 count 次 饰静态方法
public synchronized static void addElem(int count) {
for (int i = 0; i < count; i++) {
elem++;
System.out.println("elem 加1");
}
}
// 打印elem 10次。 synchronized修饰实例方法
public synchronized static void printElem(int count) {
for (int i = 0; i < count; i++) {
System.out.println("elem is " + elem);
}
}
修饰代码块
如下synchronized也可以修饰代码块通过指定加锁对象,达到与修饰实例方法或静态方法的类似的效果。
public static void addElem(int count) {
synchronized (SyncStatic.class){
for (int i = 0; i < count; i++) {
elem++;
System.out.println("elem 加1");
}
}
}
synchronized修饰静态方法与修饰实例方法的区别
上面说到synchronized修饰静态方法是对当前类加锁,而修饰实例方式是对类的实例对象加锁,许多同学不是很理解这点,下面我们通过实例来说明二者的区别。首先我们需要了解下对象锁信息是是如何存储的。
对象头信息
对象头信息里保存了对象的加锁信息, java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、gc标记状态。对象头由Mark word和class pointer两部分组成,如果是数组,还包括数组长度.
而Mark Word中包含标记位lock与biased_lock,其表示含义如下:
下面我们看看一个未加锁的对象与加锁后其标记位的状态(引入jar包org.openjdk.jol:jol-core,使用ClassLayout打印对象头信息):
SyncClass syncClass = new SyncClass();
// 打印未加锁的对象头信息
System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
synchronized (syncClass){
// 打印加锁后的对象头信息
System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
}
输出结果如下,可对照上图可知,未加锁时biased_lock与lock为001表示无锁状态,而使用synchronized给对象加锁后变为000状态,说明对象syncClass上持有轻量级锁。
synchronized修饰静态方法VS修饰实例方法
我们实现一个类SyncClass提供以下两个方法testMethod1与testMethod2,分别为synchronized修饰实例方法与修饰静态方法,为了观察SyncClass实例对象与SyncClass类对象的状态,我们在这两个方法中死循环等待。
/**
* 使用synchronized修饰实例方法与静态方法
*/
public class SyncClass {
public static int elem = 0;
public static void main(String[] args) {
SyncClass syncClass = new SyncClass();
// 启动testMethod1或testMethod2
Thread thread= new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
syncClass.testMethod1();
}
});
thread.start();
System.out.println(ClassLayout.parseInstance(syncClass).toPrintable());
System.out.println(ClassLayout.parseInstance(SyncClass.class).toPrintable());
}
// synchronized修饰实例方法
public synchronized void testMethod1() throws InterruptedException {
while (true) {
Thread.sleep(1000);
}
}
// synchronized修饰静态方法
public static synchronized void testMethod2() throws InterruptedException {
while (true) {
Thread.sleep(1000);
}
}
}
执行修饰实例方法的结果如下,可以看到synchronized修饰实例方法时,实例对象syncClass的锁标记位为000,实例对象上被加上了轻量级锁。而类对象上无锁。
执行修饰静态方法的结果如下,可以看到synchronized修饰静态方法时,实例对象syncClass的锁标记位为001,表示无所,而类对象为010,表示类对象被加上了重量级锁(涉及到锁膨胀的问题,有兴趣的同学可自行了解下)。
故synchronized修饰实例方法与修饰静态方法区别如下:
- synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
- synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
结语
本文简单介绍了synchronized的用法,以及作用在实例方法与类方法上的区别,至于synchronized实现锁的原理,我们下篇文章再见。