Java中的synchronized

为什么要线程同步?

多个线程同时对共享资源进行操作的时候,可能会出现线程干扰或者内存不一致的情况。为了避免这些异常情况,就需要线程同步。

什么是线程干扰?
class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

如果是多个线程同时对同一个 Counter 对象进行数据操作,可能会因为多个线程的最终执行顺序不同而导致最终结果不同。

代码中对变量 c+1 操作(c++),在JVM执行时,会被解析成多个步骤:

  1. 获取当前 c 的值
  2. c 进行 +1 操作
  3. 将计算后的结果写入内存

c-- 操作与 c++ 操作是一样的拆分步骤,只是第二步的 +1 变成了 -1

多个线程同时进行加减操作,可能会被JVM最终解析成:

  1. 线程A获取到c
  2. 线程B获取到c
  3. 线程A对c进行-1操作
  4. 线程B对c进行+1操作
  5. 线程B将结果写回内存,此时c的值是1
  6. 线程A将结果写回内存,此时c的值是-1

这和预期的结果不相同,预期的结果是:线程 A 对 c+1,c 修改后为 1;线程B对 c-1,c修改后结果为 0

上面所列的只是其中一种情况,也有可能JVM解析后,结果并没有出错。

所以线程干扰是不可预期的。

什么是内存一致性错误?

当两个或者多个线程对同一个共享资源进行操作时,各线程间对共享资源的最新信息并不是即刻知道。

例如:

线程A 对变量 c 进行了 +1 操作;同时线程B在读取变量 c 的值,线程B读取到的值不一定是 1,也可能是 0。

想要避免这个问题,就需要共享资源对所有请求它的线程都是可见的。

synchronized 

为了解决上面所说的问题,设计了synchronized 方法和synchronized 代码块。

它可以同时保证不会出现线程操作的原子性和数据的一致性问题。

synchronized 修饰方法

修饰方法时,多个线程同时调用被修饰的方法:

  1. 只有一个线程可以获得执行这个方法的许可,其他的线程都会因为请求不到执行而被阻塞,直到当前执行的线程结束。
  2. 所有线程对于共享资源都是可见的,某一个线程的操作对其他线程而言都是可见的。
  3. 当同一个线程在调用同步代码时,直接或者间接调用了另一个包含同步代码的方法,只要他们持有的是同一个对象锁,就可以重复调用。
修饰静态方法

当 synchronized 用于修饰静态方法时,synchronized 锁住的对象是当前类Class对象

public static synchronized void staticMethod() {
    // 同步静态方法
}
修饰实例方法

当 synchronized 用于修饰实例方法时,synchronized 锁住的对象是当前的实例对象

public synchronized void increment() {
    count++;
}
修饰代码块

可以自己指定锁对象,获得括号中的对象锁才能执行其中的代码块

public void method() {
    synchronized (lockObject) {
        // 同步的代码
    }
}

在使用同步代码块时,要注意锁的粒度。只需要锁住多线程操作中需要同步的部分即可。

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

使用实例对象作为锁时,多个线程同时调用添加 synchronized 同步代码块的方法时,只有一个线程可以获得当前实例对象锁,执行同步代码块

使用当前类Class对象作为锁时,即使实例对象在不同的线程中执行,也只会有一个线程可以执行锁住的代码块(JVM为每一个类管理一个唯一的Class对象)

 注意事项

当调用同步方法时,在方法返回时就会释放锁。就算是因为抛出未捕获的异常,也会释放锁。

死锁

两个或多个线程被永久阻塞,彼此等待对方释放锁以继续执行

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

图解如下:

线程A和线程B都在自己已获得锁的方法中执行代码,去等待对象释放锁以继续执行。使用锁时要避免这种情况的发生

为什么加锁的时候要注意粒度? 

一个应用如果所有需要同步操作的地方,都加同一把锁。那么多线程因不同的业务需要同步操作的时候,都要去请求获取该锁。

这些线程,只有一个能获得执行的权限,其他的线程就会一直阻塞,直到上一个线程释放锁后。再去竞争获得锁处理业务。

那么应用要么就是响应特别慢,要么直接卡死。

所以在使用的时候需要注意锁的粒度。不同的业务模块中,自身业务需要加锁进行同步操作的时候,根据具体情况区分哪些代码块需要加锁,哪些代码块需要加同一把锁来避免多线程操作引起的数据问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值