首先看一段简单的代码:
public class T001 {
private int count = 0;
private Object o = new Object();
public void m() {
//任何线程要执行下面这段代码,必须先拿到o的锁
synchronized (o) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
这段代码非常简单,o指向了堆内存中的一个对象,synchronized(o)起到的作用是,当有线程要执行其作用域中的代码的时候,需要先获取到o所指向的对象的锁,注意o是对象的引用,对象存在于堆内存中,锁的信息也是记录在堆内存中。只有拿到了锁的线程,才能执行,否则只有等待其他线程释放锁,所以这把锁叫做互斥锁。注意,synchronized锁定的不是一个代码块,而是一个对象。
将上面代码稍加修改,如下:
public class T002 {
private int count = 0;
public void m() {
//任何线程要执行下面这段代码,必须先拿到this的锁
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
synchronized (this)锁定的是自身这个对象,如果一段程序,开始的时候就要锁定自身,结束时才释放,那么有一种简单的写法:
public class T003 {
private int count = 0;
//任何线程要执行下面这段代码,必须先拿到this的锁
public synchronized void m() {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
synchronized关键字直接写在方法的声明中,它跟synchronized (this)是等价的,锁定的都是this对象(不是锁定一段代码哦)。
那么如果synchronized关键字写在静态方法的声明中,情况又会怎样呢?看如下代码:
public class T004 {
private static int count = 0;
//这里等同于 synchronized(T004.class)
public synchronized static void m() {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized (T004.class) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
实际上ynchronized关键字写在静态方法的声明中,等同于锁定的是T004.class这个对象,T004.class是Class类的对象。静态的方法,不需要创建对象来访问,所以这时候是不需要this的,所以m方法等同于mm方法。
synchronized声明的方法代码块,相当于一个原子操作:
public class T005 implements Runnable {
private static int count = 10;
@Override
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T005 t005 = new T005();
for(int i = 0; i < 5; i++) {
new Thread(t005, "thread_" + i).start();
}
}
}
上面代码中,因为run方法由synchronized关键字,所以该方法的代码块,就相当于一个原子操作,尽管在main方法中启动了5个线程都对t005对象的count进行了修改,但打印的结果顺序完好。
假如同时存在同步方法(方法声明中有synchronized关键字)和非同步方法,那么在执行同步方法的过程中,非同步方法能否被其他线程执行呢?看下面这段代码:
public class T006 {
private static int count = 10;
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end.");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2");
}
public static void main(String[] args) {
T006 t006 = new T006();
new Thread(()->t006.m1()).start();
new Thread(()->t006.m2()).start();
}
}
上面代码启动了两个线程,线程1执行m1方法,线程2执行m2方法,在m1执行的过程中睡眠了10秒,在此期间,m2被执行了,执行结果如下:
Thread-0 m1 start...
Thread-1 m2
Thread-0 m1 end.
这说明,在同步方法在被执行的过程中,非同步方法是可以被其他线程执行的。有一个经典的问题,那就是写方法同步,读方法非同步的时候,容易产生脏读,下面是一个例子:
public class T007 {
private String name;
private double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance() {
return this.balance;
}
public static void main(String[] args) {
T007 t007 = new T007();
new Thread(()->t007.set("zhangsan", 100.0)).start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("balance:" + t007.getBalance());
}
}
显然打印出来的并不会是set进去的100.0,而是0,这就是脏读问题。业务代码只对写加锁,而读不加锁,写方法在被执行的时候,读方法可以被其他线程执行。因此,在做业务的时候要考虑清楚,是否允许脏读。
synchronized获得的锁,是可重入的。即一个同步方法,可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请时,仍然会得到该对象的锁。下面是一个小例子:
public class T008 {
public synchronized void m1() {
System.out.println("m1 start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end.");
}
public synchronized void m2() {
System.out.println("m2 start...");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("m2 end.");
}
public static void main(String[] args) {
T008 t008 = new T008();
new Thread(t008::m1).start();
}
}
打印结果如下:
m1 start...
m2 start...
m2 end.
m1 end.
用一句话归纳,就是,同一个线程,同一把锁,则可重入。另一种情形与此相似,子类调用父类的方法:
public class T009 {
public synchronized void m() {
System.out.println("m start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("m end.");
}
public static void main(String[] args) {
TT009 tt009 = new TT009();
new Thread(tt009::m).start();
}
}
class TT009 extends T009 {
@Override
public synchronized void m() {
System.out.println("child m start...");
super.m();
System.out.println("child m start...");
}
}
打印结果如下:
child m start...
m start...
m end.
child m start...
在程序执行过程中,如果出现异常,默认情况锁会被释放,所以,在并发处理过程中,有异常要多加小心,不然可能发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这是如果异常处理不合适,在第一个线程抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心地处理同步业务逻辑中的异常。
public class T010 {
private int count = 0;
public synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start...");
while (true) {
count++;
System.out.println("m start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if(5 == count) {
//此处必定会抛出异常,要想锁不被释放,需要进行catch,让循环继续
int i = 1/0;
}
}
}
public static void main(String[] args) {
T010 t010 = new T010();
Runnable r = new Runnable() {
@Override
public void run() {
t010.m();
}
};
new Thread(r, "thread_1").start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(r, "thread_2").start();
}
}
在上面这段代码中,thread_1抛出异常时,thread_2马上开始执行,这说明,thread_1抛出异常的时候,也释放了锁。