Java synchronized 关键字详解
前置技能点
- 进程和线程的概念
- 线程创建方式
- 线程的状态状态转换
- 线程安全的概念
synchronized 关键字的几种用法
-
修饰非静态成员方法
synchronized public void sync(){ }
-
修饰静态成员方法
synchronized public static void sync(){ }
-
类锁代码块
synchronized (类.class){ }
-
对象锁代码块
synchronized (this|对象){ }
synchronized 修饰非静态方法时可以看做是锁 this 对象,修饰静态方法时可以看做是锁方法所在的类。
synchronized 关键字的根本机制
各个线程想要访问被 synchronized 修饰的代码块,就要取得 synchronized 声明的锁。如果两个线程的目标是同一个锁,就会出现阻塞的现象,所以两个线程不能同时访问同一个锁下的代码,保证了多线程在执行时最终结果不会出错。这与共享变量是否为静态无关。
几个例子
对象锁
public class ThreadDemo extends Thread {
@Override
public synchronized void run() {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("执行完成");
}
}
直接将继承的 run() 方法标记为 synchronized ,作用是对 Main 类中的 i 变量做 10000 次累加操作。
public class Main {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo=new ThreadDemo();
Thread t1 = new Thread(threadDemo);
Thread t2 = new Thread(threadDemo);
Thread t3 = new Thread(threadDemo);
Thread t4 = new Thread(threadDemo);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
}
//输出结果:
//执行完成
//执行完成
//执行完成
//执行完成
//40000
可以看到当4个线程全部执行完毕之后,变量 i 成功的累加了 40000 次,没有出现丢失操作的情况。
如果我们将 main() 方法修改如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new ThreadDemo();
Thread t2 = new ThreadDemo();
Thread t3 = new ThreadDemo();
Thread t4 = new ThreadDemo();
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
//输出结果:
//执行完成
//执行完成
//执行完成
//执行完成
//27579
可以看到丢失了不少的累加操作。观察前后两个 main() 方法创建线程的方式可以发现,前面的 main() 方法是使用了同一个对象来创建了4个不同的线程,而后一个 main() 方法使用了4个不同的 ThreadDemo 对象创建了4个线程。我们用 synchronized 修饰的是一个非静态成员函数,相当于对该方法创建了 this 的对象锁。在第一个 main() 方法中使用同一个对象来创建 4 个不同线程就会让 4 个线程争夺同一个对象锁,这样,在同一时间内,仅能有一个线程能访问 synchronized 修饰的方法。而在第二种 main() 方法中,4 个线程各自对应一个对象锁,4 个线程之间没有竞争关系,对象锁自然无法生效。
类锁
public class ThreadDemo extends Thread {
@Override
public void run() {
synchronized (ThreadDemo.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("执行完成");
}
}
}
将修饰方法的 synchronized 改为对 ThreadDemo.class 上锁的代码块
public class ThreadDemo2 extends Thread {
@Override
public void run() {
synchronized (ThreadDemo2.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("执行完成");
}
}
}
再创建一个相同的类命名为 ThreadDemo2 ,与 ThreadDemo 不同的是,ThreadDemo2 中,synchronized 对 ThreadDemo2.class 上锁。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new ThreadDemo();
Thread t2 = new ThreadDemo();
Thread t3 = new ThreadDemo2();
Thread t4 = new ThreadDemo2();
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
//输出结果:
//执行完成
//执行完成
//执行完成
//执行完成
//33054
4 个线程分别由 ThreadDemo 和 ThreadDemo2 来创建,显然得到的结果与预期的 40000 不符。如果我们将 ThreadDemo2 中的 synchronized 改为对 ThreadDemo.class 上锁:
public class ThreadDemo2 extends Thread {
@Override
public void run() {
synchronized (ThreadDemo.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("执行完成");
}
}
}
//输出结果:
//执行完成
//执行完成
//执行完成
//执行完成
//40000
可以看到,虽然是声明在两个不同的类中的 synchronized 代码块,但是由于都是对 ThreadDemo.class 上锁,所以 4 个线程之间还是建立了竞争关系,同时只能有一个线程访问被 synchronized 修饰的代码。
总结
所以 synchronized 关键字的本质是限制线程访问一段代码,而限制的条件就是,在所有被加上相同锁的代码上,同一时间,只能有一个线程在运行。这与你要修改什么样的共享变量无关。在我刚接触到的时候以为类锁和对象锁是分别针对静态共享变量和非静态共享变量的,但事实上锁的是要执行的代码块,而不是代码块将要访问的共享变量。