一、引言
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问的问题。
这套机制就是synchronized关键字,它包括两种用法:synchronized 方法(同步方法)和synchronized语句块(同步语句块)。
二、synchronized不同的修饰情况
1、synchronized方法(同步方法):synchronized修饰类中的方法,如下所示:
class P implements Runnable {
public synchronized void methodPA() {
//...
}
}
2、synchronized语句块(同步语句块):带有某具体对象的synchronized修饰类中方法内的语句,如下所示:
class P implements Runnable {
public void methodPA(SomeObject so) {
synchronized(so) {//so为对象锁得引用
//...
}
}
}
三、几个重要概念
①对象锁:每个对象都只有一个锁,称为对象锁;
②synchronized锁定的是哪个对象:调用该synchronized方法的对象或者synchronized指定的对象;
③线程拿到锁(对象锁):同上;
④取得对象锁的线程:某个对象有可能在不同的线程中调用同步方法,只有拿到对象锁得线程,才可以使用该对象来调用同步方法。
四、对象锁的理解
①无论synchronized加在方法上还是加在对象上,线程拿到的锁都是对象(关键要分析出synchronized锁定的是哪个对象),不能把某个函数或一段代码当作锁;
②每个对象只有一个锁;
③实现同步需要很大的系统开销,甚至可能造成死锁,所以尽量避免无谓的同步。
五、理解线程同步(synchronized)
用个例子来讲解比较清晰:假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步方法或同步块,P1对象、P2对象能够执行它们。
1、synchronized方法(同步方法)
class P implements Runnable {
public synchronized void methodPA() {
//...
}
}
此时synchronized锁定的是哪个对象呢(对象锁)?
synchronized锁定的是调用该同步方法的对象,即对象P1在不同线程中调用该同步方法,这些线程间会形成互斥关系,只有拿到P1对象锁的线程,才能够调用该同步方法。
但是对于P1对象所属类所产生的另一对象P2而言,还是能够任意调用这个被该同步方法,因此程式也可能在这种情形下摆脱同步机制的控制,造成数据混乱。
上边的示例代码等同于如下代码:
class P implements Runnable {
public synchronized void methodPA() {
synchronized(this) {
//...
}
}
}
上述代码中的this指的是调用这个方法的对象即P1。
2、synchronized语句块(同步语句块)
class P implements Runnable {
public void methodPA(SomeObject so) {
synchronized(so) {
//...
}
}
}
此时锁就是so这个对象,只有拿到这个锁的线程,才能够运行该锁对象控制的这段代码。当有一个明确的对象作为锁时,就能够这样写程式,但当没有明确的对象作为锁,只是想让一段代码同步时,能够创建一个特别的instance变量对象来充当锁:
class P implements Runnable {
private byte[] lock = new byte[0];//特别的instance变量
public void methodPA(){
synchronized(lock){
//...
}
}
}
3、synchronized修饰static函数
class P implements Runnable {
public synchronized static void methodPA() {
//...
}
public void methodPB(){
synchronized(P.class){
//...
}
}
}
六、线程访问对象锁的规则—这里比较特殊使用的是synchronized(this)
①当两个线程访问同一个对象的synchronized(this)代码块时,同一时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块;
②然而,当一个线程访问对象的一个synchronized(this)代码块时,其他线程仍然可以访问该对象的非synchronized(this)的代码;
③尤其关键的是,当一个线程访问对象的一个synchronized(this)代码块时,其他线程对该对象中其它所有synchronized(this)代码块的访问将被阻塞(因为前一个线程获得了该对象的锁,导致其他线程无法获得该对象的锁)。
例如下面的程序:当有线程进入同步代码1时,其他所有的线程都不能进入同步代码1和同步代码2,因为此时锁1处于锁上的状态,但可以进入同步代码3,因为同步代码3使用的是锁2,与锁1的状态没有关系。
synchronized(锁1) {
同步代码1
}
synchronized(锁1) {
同步代码2
}
synchronized(锁2) {
同步代码3
}
使用以下代码来讲解一下线程同步的执行步骤:
public class MyThreadDemo {
public static void main(String[] args) {
//任意定义一个对象,传给线程作为锁,可以是任意对象,例如int myLock = 1;也可以是内存中已经存在的对象,例如Object.class
String myLock = "hi";
Thread t1 = new LockThread("t1",myLock);
Thread t2 = new LockThread("t2",myLock);
Thread t3 = new LockThread("t3",myLock);
t1.start();
t2.start();
t3.start();
}
}
class LockThread extends Thread {
private Object MyLock;
public LockThread(String name, Object MyLock){
super(name);
this.MyLock = MyLock;
}
public void run(){
System.out.println(currentThread().getName()+":我获得cpu了,准备进入同步代码块.....");
synchronized(MyLock){
System.out.println(currentThread().getName()+":我进来了!");
System.out.println(currentThread().getName()+":歇会");
try {
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(currentThread().getName()+":走了!");
}
}
}
执行结果如下,可以看到当t1先进入同步代码块后,即使t3,t2获得了cpu,也没有办法进入到同步代码块中,直到t1出来,并开锁后t2才能进入:
t1:我获得cpu了,准备进入同步代码块.....
t1:我进来了!
t1:歇会
t2:我获得cpu了,准备进入同步代码块.....
t3:我获得cpu了,准备进入同步代码块.....
t1:走了!
t3:我进来了!
t3:歇会
t3:走了!
t2:我进来了!
t2:歇会
t2:走了!