一、Synchronized 的意义
Synchronized
关键字是 Java 用来解决多线程之间访问共享变量的线程安全问题的,它是一种阻塞式的解决方案,可以保证只有一个线程来访问临界区,进而确保线程安全。
下面是线程不安全的例子:
/**
* 共享带来的问题
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test17")
public class Test17 {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
一共有两个线程,一个线程对共享变量 counter
自增,另外一个线程自减。在多次运行后可以发现,结果是不稳定的,有时是整数,有时是负数或者0。之所以出现这个现象,是因为共享变量 counter
的自加或自减不是原子性的。下面是变量 counter++
在 JVM 的字节码:
getstatic counter // 获取静态变量counter的值
iconst_1 // 准备常量1
iadd // 自增
putstatic counter // 将修改后的值存入静态变量counter
如果 t1
线程先执行,获取到 counter
的值(0),执行了自增操作,还没有来得及写入,此时发生线程上下文切换。那么 t2
线程获取到 counter
的值是 0,执行自减操作,并写入 counter
,即 counter = -1
,t2
线程执行完毕。轮到 t1
线程执行,根据程序计数器找到需要执行的语句,把上次的自增结果 1 写入,执行完毕。我们可以发现,此时的结果是 1 并不是预料中的 0。出现负数的情况也类似,这里就不叙述了。
为什么会出现这种结果呢?聪明的你一定能猜到,不就是因为访问共享变量的操作不是原子性的吗?我们只要确保两个线程访问共享变量的操作是同步的就行,就好像有一间房子,两个人都想入住,但房间只能给一个人住。谁也不想被打扰,我们只要在门口加把锁就行。等我们不住了,就把锁开了,给其他人住。而 Java 实现这种机制正是靠关键字 syschronized
,锁的对象是任意的,但是要确保访问同一个共享变量的所有线程用的都是同一把锁。
二、Synchronized 的使用以及注意事项
使用
基本语法:
java synchronized(对象) { 临界区 }
三种使用方式:
-
加在同步代码块
class Test{ public void test() { synchronized(obj) { } } }
-
加在普通方法上
class Test { public synchronized void test() { } } 等价于 class Test{ public void test() { synchronized(this) { } } }
-
加在静态方法上
class Test{ public synchronized static void test() { } } 等价于 class Test{ public static void test() { synchronized(Test.class) { } } }
注意事项
当我们使用 synchronized
时,要注意线程使用的锁对象是不是同一个。如果不是同一个,那线程的安全是无法保证的,这点务必牢记。我们也可以通过这个特点,快速地判断出一段程序是不是线程安全的。
三、字节码的反编译
首先准备好需要反编译的 java 源文件,可以直接通过 java 的命名实现字节码的反编译(需要配置 JDK 环境变量)
-
编译 java 源文件
javac fileName.java
-
反编译生成的字节码
javap -c fileName
如果需要把反编译的结果保存到文件中的话,可以使用重定向技术。
cmd javap -c fileName > testFile.txt