对于多线程共享资源的情况需要进行同步,以避免一个线程的改动被另一个线程的改动所覆盖。最普遍的同步方式就是synchronized。把代码声明为synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
1、原子性强调的是执行,意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。
2、可见性强调的是结果,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。
同步方法
看一个例子就明白了:
import java.util.Random;
public class TestSyncClass {
private int num=0;
private Random random=new Random();
public synchronized void testAdd1(){
System.out.println("testAdd1--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1-result-->>"+num);
}
public synchronized void testAdd2(){
System.out.println("testAdd2--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2-result-->>"+num);
}
public void testAdd3(){
System.out.println("testAdd3--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("3-result-->>"+num);
}
public static void main(String[] args) {
final TestSyncClass syncClass=new TestSyncClass();
Thread thread1=new Thread(){
@Override
public void run() {
syncClass.testAdd1();
super.run();
}
};
Thread thread2=new Thread(){
@Override
public void run() {
syncClass.testAdd2();
super.run();
}
};
Thread thread3=new Thread(){
@Override
public void run() {
syncClass.testAdd3();
super.run();
}
};
thread1.start();
thread2.start();
thread3.start();
}
}
代码运行结果:
testAdd1--->>
testAdd3--->>
1-result-->>2
3-result-->>2
testAdd2--->>
2-result-->>3
代码中testAdd1、testAdd2是被synchronized声明的方法,testAdd3没有声明。在运行的时候由于testAdd3没有被声明,所以在紧跟着开始运行testAdd1的时候也运行了testAdd3,结果testAdd1运行的结果被testAdd3的结果覆盖了,打印了相同的值3。这个主要是可见性的问题。由于testAdd2也是被声明过的,所以testAdd2并没有立即执行,而是等testAdd1执行完之后才开始执行。
所有对象都自动含有单一的锁(也称为监视器monitor object)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static 方法可以在类的范围内防止对static数据的并发访问。
同步块
无论何种情况,要想锁住代码,必须使用相同的锁,例如把testAdd2改成
private Object object=new Object();
public void testAdd2(){
synchronized(object){
System.out.println("testAdd2--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2-result-->>"+num);
}
}
则testAdd2和testAdd1就不会相互等待了。结果如下:
testAdd2--->>
testAdd3--->>
testAdd1--->>
3-result-->>3
2-result-->>3
1-result-->>3
其实synchronized(object)是更安全的上锁方式,因为直接声明方法的形式用的是类的锁,而声明代码块的形式用的是私有属性的锁,尤其是在服务器开发的时候,外面类的锁很容易被黑客获取,从而获取了攻击服务器的入口,而私有属性的私有性让黑客难以获取,所以它的锁就相对安全的多。
类同步
上面的main方法的三个线程用的是同一个TestSyncClass syncClass对象,如果每个线程都各自创建一个对象就不能达到锁定代码的目标了。要想达到同步的目的,代码需要修改成如下:
import java.util.Random;
public class TestSyncClass {
private int num = 0;
private static Object object = new Object();
private Random random = new Random();
public void testAdd1() {
synchronized (object) {
System.out.println("testAdd1--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1-result-->>" + num);
}
}
public void testAdd2() {
synchronized (object) {
System.out.println("testAdd2--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2-result-->>" + num);
}
}
public void testAdd3() {
System.out.println("testAdd3--->>");
num++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("3-result-->>" + num);
}
public static void main(String[] args) {
Thread thread1 = new Thread() {
@Override
public void run() {
TestSyncClass syncClass = new TestSyncClass();
syncClass.testAdd1();
super.run();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
TestSyncClass syncClass = new TestSyncClass();
syncClass.testAdd2();
super.run();
}
};
Thread thread3 = new Thread() {
@Override
public void run() {
TestSyncClass syncClass = new TestSyncClass();
syncClass.testAdd3();
super.run();
}
};
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果:
testAdd1--->>
testAdd3--->>
3-result-->>1
1-result-->>1
testAdd2--->>
2-result-->>1
其实使用synchronized (TestSyncClass.class)类的锁也能达到类似的效果,但是考虑到私有属性的安全性就直接使用上面代码做实例了。
注意:synchronized是不能继承的,父类中synchronized的声明在子类的继承过程中需要再次声明,否则synchronized将会丢失。
wait(), notify(),notifyAll()
基类不光有锁,还有这三个方法,wait()会让获取锁的线程等待并释放锁,直到notify()或notifyAll()唤醒并重新获取锁。先看一个例子:
public class TestSyncClass {
private int num = 0;
private Object object = new Object();
private Object object1 = new Object();
public void testAdd(int index) {
System.out.println("testAdd" + index + "--->>");
synchronized (object) {
num++;
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(index + "-result-->>" + num);
}
}
public void release() {
synchronized (object) {
object.notifyAll();
System.out.println("-release-->>");
}
}
public static void main(String[] args) {
final TestSyncClass syncClass = new TestSyncClass();
Thread thread1 = new Thread() {
@Override
public void run() {
syncClass.testAdd(1);
super.run();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
syncClass.testAdd(2);
super.run();
}
};
Thread thread3 = new Thread() {
@Override
public void run() {
syncClass.testAdd(3);
super.run();
}
};
thread1.start();
thread2.start();
thread3.start();
Thread thread4 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
syncClass.release();
super.run();
}
};
thread4.start();
}
}
运行结果:
testAdd1--->>
testAdd2--->>
testAdd3--->>
-release-->>
3-result-->>3
2-result-->>3
1-result-->>3
调用object的wait(), notify(),notifyAll()法前,必须获得object锁,也就是这三个方法必须写在synchronized(obj) {…} 代码段内,否则跑出异常java.lang.IllegalMonitorStateException。
调用object.wait()后,线程A就释放了object的锁,否则syncClass.release()无法获得object锁,等待的线程。
当object.wait()方法返回后,各个线程需要再次获得object锁,才能继续执行。
notify()只能唤醒线程,notifyAll()则能全部唤醒,但是个线程需要重新竞争object的锁。