锁代码还是锁对象?
很多使用synchronized关键字的同学很容易混淆这个概念。我们很常见到用synchronized 修饰一个方法,那么究竟synchronized锁定的是方法,还是对象呢?
答案是:synchronized锁定的是对象!是堆中实际的对象,并不是栈中的引用。
锁对象?
想要证明是锁对象其实非常简单,下面这个demo可以看出synchronized锁定的是同一个共享对象,而不是这个对象的某一个加锁方法。
import java.util.concurrent.TimeUnit;
/**
* 此例证明了synchronized锁是锁定的对象而不是代码片段 <br>
* 类名:ObjectLockDemo<br>
* 作者: mht<br>
* 日期: 2018年8月27日-下午1:25:47<br>
*/
public class ObjectLockDemo {
public static void main(String[] args) {
T t = new T();
new Thread(() -> t.doSomthing(), "t1").start();
new Thread(() -> t.doOtherthing(), "t2").start();
}
}
class T {
synchronized void doSomthing() {
System.out.println(Thread.currentThread().getName() + " do something start...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " do something end...");
}
synchronized void doOtherthing() {
System.out.println(Thread.currentThread().getName() + " do otherthing...start");
System.out.println(Thread.currentThread().getName() + " do otherthing...end");
}
}
上面这个例子非常简单。首先,我们为T类创建了两个加锁的方法doSomthing()和doOtherthing(),在主线程中,我们创建了一个 t 对象,然后通过线程 t1 调用 对象 t 的doSomthing()加锁方法,紧接着线程 t2 去调用 对象 t 的另一个加锁方法doOtherthing(),如果synchronized锁住的是方法,那么在 t1 执行的5秒内,t2 可以自由的执行doOtherthing()方法而不受限制,但是如果是锁住了 对象那么线程 t2 只有当 t1 执行完成后释放了对象锁,才能去执行 doOtherthing()方法,也就证明了synchronized锁住的是 t 对象。
执行结果如下,很明显,t2 等到 t1 完成后才执行了线程,synchronized锁住的是对象。
synchronized用法
synchronized不论如何使用,都是直接锁住了对象,但是在写法上,却存在很多变式:
用法一:
public class A {
private Object o = new Object();
private int count = 10;
public void m() {
synchronized (o) {// 任何线程若想执行代码块中的语句,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
用法二:
public class A {
private int count = 10;
public void m() {
synchronized (this) {// 任何线程若想执行代码块中的语句,必须先拿到this的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
synchronized锁定的是一个对象,任何代表对象的事物都可以作为synchronized参数。
用法三:
public class A {
private int count = 10;
public synchronized void m() { // 等同于在方法的代码执行时要synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
上面代码就是平时比较常见的加锁方式,虽然synchronized修饰了方法,但是一定不能理解为锁定了代码块,而是执行这段代码的当前对象 this
用法四(修饰静态方法):
public class A {
private static int count = 10;
public synchronized static void m() { // 这里相当于synchronized(thingking.com.A.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized (A.class) {// 这是不可以写this,this指代对象,而静态方法是通过类来调用的,没有this对象
count--;
}
}
}
synchronized作用
synchronized作用可以用一句话来概括:将被修饰的代码块中的语句变为原子操作,避免并发情况下的线程重入问题。
public class RR implements Runnable{
private int count = 10;
@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
RR rr = new RR();
for (int i = 0; i < 5; i++) {
new Thread(rr, "THREAD" + i).start();
}
}
}
上述代码中,如果run方法不加synchronized修饰,因为count-- 不是原子性操作,那么可能发生多线程重入问题,加上synchronized关键字后,代码块中的操作属于一个不可分割的原子操作。
运行结果对比,左为不加synchronized 右边为加上之后的结果。
非同步方法的执行
虽然我们使用synchronized修饰了成员方法,并称这些方法为同步方法,未使用synchronized修饰的方法为非同步方法。在调用同步方法时,由于synchronized锁住了对象,因此是不是就不能执行这个对象的任何方法了呢?
不是的,非同步方法在一个线程持有某个对象的同步锁后依然可以被其他线程调用,实际上锁定对象这个说法并不准确,且容易给人一种这个对象被占用中,无法被其他线程使用的错觉。而实际上,线程调用对象的同步方法,并持有了这个对象的同步锁,仅仅是对同步方法奏效(因为对象只有一把锁,它是一个可以记录对象持有线程重入次数的计数器,当值为0时,就代表没有任何线程持有这把锁,具体细节可以参考synchronized实现原理),对于不需要持有同步锁的非同步方法,其他线程是完全可以自由执行的。
所以,当一个线程在执行某一对象的同步方法的过程中,其他线程是可以执行这个对象的非同步方法的。
脏读(Dirty Read)
脏读,指的是读取了无效的数据。
对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题,也就是读取了在写的过程中还没有完成的数据。
可重入锁?
synchronized锁是可重入锁,一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。也就是说synchronized获得的锁是可重入的。
对于synchronized锁的可重入性,我在之前的一篇博客里专门对《Java Concurrency In Practice》中的重入性做了一个比较全面的剖析:《Java并发编程实战————可重入内置锁》欢迎赐教。
很多同学都会问一个问题那就是重入性有什么用?试想一下,如果一个线程调用一个同步方法,并锁定了这个对象,而在这个同步方法中又调用了这个对象的另一个同步方法,同样需要获得这个对象的锁,那么如果synchronized不具备重入性,线程将永远无法获取由它自己持有的这个对象的锁,这就导致了程序死锁。
public class T {
synchronized void m() {
System.out.println("m start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end...");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T {
@Override
synchronized void m() {
System.out.println("child m start...");
super.m();
System.out.println("child m end...");
}
}
上述代码是重入性的一种典型应用,子类中的同步方法调用父类中的同步方法,也是允许重入的。当调用子类的同步方法时,实际上锁定的是this,而子类的对象同样也可以看做是一个父类对象,因此这个锁是允许重入的,不会造成死锁。
执行结果:
线程通讯模型
线程之间的通讯有两种方式,一种是线程之间通过访问同一块内存区域达到数据的共享和传递;另一种方式是线程之间直接发消息。在Java中线程之间的通讯是使用了第一种通讯模型。
注意异常!
在默认情况下,synchronized锁在发生异常时会自动释放锁。所以在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet 线程共同访问同一个资源,这是如果如果异常处理不合适,在第一个线程中抛出异常,其他线程会进入同步代码块,有可能会访问到异常产生的数据,因此要非常小心的处理同步业务逻辑中的异常。
public class T {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start...");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1 / 0;// 此处抛出异常,锁将被释放,要想不释放,可以在这里进行catch,然后让循环继续
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
执行结果: