Synchronized原理图解
为什么要使用Synchronized
//两个线程对初始值为0的静态变量,一个做自增,一个做自减,各自5000次,结果却不是0.
public class Test {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
counter++;
}
}, "线程一");
Thread t2 = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
counter--;
}
}, "线程二");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
问题分析:以上的结果可能是正数,负数,0的原因在于Java中对静态变量的自增自减并不是原子性操作,如果要彻底理解,必须从字节码来进行分析,例如对于i++而言(i为静态变量),实际产生的JVM字节码指令为:
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i
而对应i--也是类似
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
isub //自减
putstatic i //将修改后的值存入静态变量i
而java的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换。
临界区
一个程序运行多个线程本身是不存在问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,但在多个线程对共享资源读写时指令交错,就会出现问题,一段代码内如果存在对共享资源的多线程读写操作,那么这段代码被称为临界区。例如:
static int count = 0;
static void increment()
//临界区
{
count++;
}
static void decrement()
//临界区
{
count--;
}
当多个线程在临界区
内执行,由于代码的执行序列不可预测而导致结果无法预测,从而发生了竞态条件
。
Synchronized的使用
为了避免临界区的竞态条件产生,有以下两种解决方案:
阻塞式:synchronized(对象锁), Lock
非阻塞式:原子变量
Synchronize被称为对象锁,它采用互斥的方式让同一时刻最多只有一个线程能持有对象锁,其它线程要是想获取这个对象锁时就会被阻塞住,这样就能保证有锁的线程可以安全的执行临界区的代码块,同时也避免了线程切换的问题。
synchronized(对象){
//临界区
}
//1.修饰实例方法,此时,synchronized加锁的对象就是这个方法所在实例的本身。
public synchronized void add(){
i++;
}
//2.修饰静态方法,此时,synchronized加锁的对象为当前静态方法所在类的Class对象。
public static synchronized void add(){
i++;
}
//3.修饰代码块,此时synchronized加锁对象即为传入的这个对象实例。
public void add() {
synchronized (this) {
i++;
}
}
案例:
public class Test {
static int counter = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj){
counter++;
}
}
}, "线程一");
Thread t2 = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (obj){
counter--;
}
}
}, "线程二");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
//执行结果: 0