synchronized详解
synchronized关键可以修饰函数、函数内语句。无论它加上方法还是对象上,它取得的锁都是对象,而不是把一段代码或是函数当作锁。
1,当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一段时间只能有一个线程得到执行,而另一个线程只有等当前线程执行完以后才能执行这块代码。
2,当一个线程访问object中的一个synchronized(this)同步代码块时,其它线程仍可以访问这个object中是其它非synchronized (this)代码块。
3,这里需要注意的是,当一个线程访问object的一个synchronized(this)代码块时,其它线程对这个object中其它synchronized (this)同步代码块的访问将被阻塞。
4,以上所述也适用于其它的同步代码块,也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,这个线程就获得了object的对象锁。而且每个对象(即类实例)对应着一把锁,每个synchronized(this)都必须获得调用该代码块儿(可以函数,也可以是变量)的对象的锁才能执行,否则所属线程阻塞,方法一旦执行就会独占该锁,直到从方法返回时,也释放这个锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个对象,其所有声明为synchronized的成员函数中至多只有一个处于可执行状态(因为至多只有一个线程可以获取该对象的锁),从而避免了类成员变量的访问冲突。
synchronized方式的缺点:
由于synchronized锁定的是调用这个同步方法的对象,也就是说,当一个线程P1在不同的线程中执行这个方法时,它们之间会形成互斥,从而达到同步的效果。但这里需要注意的是,这个对象所性的Class的另一个对象却可以任意调用这个被加了synchronized关键字的方法。同步方法的实质是将synchronized作用于object reference,对于拿到了P1对象锁的线程才可以调用这个synchronized方法,而对于P2来说,P1与它毫不相干,程序也可能在这种情况下摆脱同步机制的控制,造成数据混乱。以下我们将对这种情况进行详细地说明:
首先我们先介绍synchronized关键字的两种加锁对象:对象和类——synchronized可以为资源加对象锁或是类锁,类锁对这个类的所有对象(实例)均起作用,而对象锁只是针对该类的一个指定的对象加锁,这个类的其它对象仍然可以使用已经对前一个对象加锁的synchronized方法。
在这里我们主要讨论的一个问题就是:“同一个类,不同实例调用同一个方法,会产生同步问题吗?”
同步问题只和资源有关系,要看这个资源是不是静态的。同一个静态数据,你相同函数分属不同线程同时对其进行读写,CPU也不会产生错误,它会保证你代码的执行逻辑,而这个逻辑是否是你想要的,那就要看你需要什么样的同步了。即便你两个不同的代码,在CPU的不同的两个core里跑,同时写一个内存地址,Cache机制也会在L2里先锁定一个。然后更新,再share给另一个core,也不会出错,不然intel,amd就白养那么多人了。
因此,只要你没有两个代码共享的同一个资源或变量,就不会出现数据不一致的情况。而且同一个类的不同对象的调用有完全不同的堆栈,它们之间完全不相干。
以下我们以一个售票过程举例说明,在这里,我们的共享资源就是票的剩余张数。
package com.test;
public class ThreadSafeTest extends Thread implements Runnable {
private static int num = 1; |
public ThreadSafeTest(String name) {
setName(name);
}
public void run() {
sell(getName());
}
private synchronized void sell(String name){
if (num > 0) {
System. out.println(name + ": 检测票数大于0" );
System. out.println(name + ": \t正在收款(大约5秒完成)。。。" );
try {
Thread. sleep(5000);
System. out.println(name + ": \t打印票据,售票完成" );
num--;
printNumInfo();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System. out.println(name+": 没有票了,停止售票" );
}
}
private static void printNumInfo() { |
System. out.println("系统:当前票数:" + num);
if (num < 0) {
System. out.println("警告:票数低于0,出现负数" );
}
}
public static void main(String args[]) {
try {
new ThreadSafeTest("售票员李XX" ).start();
Thread. sleep(2000);
new ThreadSafeTest("售票员王X" ).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行上述代码,我们得到的输出是:
售票员李XX: 检测票数大于0
售票员李XX: 正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X: 正在收款(大约5秒完成)。。。
售票员李XX: 打印票据,售票完成
系统:当前票数:0
售票员王X: 打印票据,售票完成
系统:当前票数:-1
警告:票数低于0,出现负数
|
根据输出结果,我们可以发现,剩余票数为-1,出现了同步错误的问题。之所以出现这种情况的原因是,我们建立的两个实例对象,对共享的静态资源static int num = 1同时进行了修改。那么我们将上面代码中方框内的修饰词static去掉,然后再运行程序,可以得到:
售票员李XX: 检测票数大于0
售票员李XX: 正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X: 正在收款(大约5秒完成)。。。
售票员李XX: 打印票据,售票完成
系统:当前票数:0
售票员王X: 打印票据,售票完成
系统:当前票数:0
|
对程度修改之后,程序运行貌似没有问题了,每个对象拥有各自不同的堆栈,分别独立运行。但这样却违背了我们希望多线程同时对共享资源的处理(去static后,num就从共享资源变成了每个实例各自拥有的成员变量),这显然不是我们想要的。
在以上两种代码中,采取的主要是对对象的锁定。由于我之前谈到的原因,当一个类的两个不同的实例对同一共享资源进行修改时,CPU为了保证程序的逻辑会默认这种做法,至于是不是想要的结果,这个只能由程序员自己来决定。因此,我们需要改变锁的作用范围,若作用对象只是实例,那么这种问题是无法避免的;只有当锁的作用范围是整个类的时候,才可能排除同一个类的不同实例对共享资源同时修改的问题。
package com.test;
public class ThreadSafeTest extends Thread implements Runnable {
private static int num = 1;
public ThreadSafeTest(String name) {
setName(name);
}
public void run() {
sell(getName());
}
private synchronized static void sell(String name){ |
if (num > 0) {
System. out.println(name + ": 检测票数大于0" );
System. out.println(name + ": \t正在收款(大约5秒完成)。。。" );
try {
Thread. sleep(5000);
System. out.println(name + ": \t打印票据,售票完成" );
num--;
printNumInfo();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System. out.println(name+": 没有票了,停止售票" );
}
}
private static void printNumInfo() {
System. out.println("系统:当前票数:" + num);
if (num < 0) {
System. out.println("警告:票数低于0,出现负数" );
}
}
public static void main(String args[]) {
try {
new ThreadSafeTest("售票员李XX" ).start();
Thread. sleep(2000);
new ThreadSafeTest("售票员王X" ).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
将程序做如上修改,可以得到运行结果:
售票员李XX: 检测票数大于0
售票员李XX: 正在收款(大约5秒完成)。。。
售票员李XX: 打印票据,售票完成
系统:当前票数:0
售票员王X: 没有票了,停止售票
|
对sell()方法加上了static修饰符,这样就将锁的作用对象变成了类,当该类的一个实例对共享变量进行操作时将会阻塞这个类的其它实例对其的操作。从而得到我们如期想要的结果。
总结:
1,synchronized关键字有两种用法:synchronized方法和synchronized块。
2,在Java中不单是类实例,每一个类也可以对应一把锁
在使用synchronized关键字时,有以下几点儿需要注意:
1,synchronized关键字不能被继承。虽然可以用synchronized来定义方法,但是synchronized却并不属于方法定义的一部分,所以synchronized关键字并不能被继承。如果父类中的某个方法使用了synchronized关键字,而子类中也覆盖了这个方法,默认情况下子类中的这个方法并不是同步的,必须显示的在子类的这个方法中加上synchronized关键字才可。当然,也可以在子类中调用父类中相应的方法,这样虽然子类中的方法并不是同步的,但子类调用了父类中的同步方法,也就相当子类方法也同步了。如,
在子类中加synchronized关键字:
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method () { }
}
调用父类方法:
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
2,在接口方法定义时不能使用synchronized关键字。
3,构造方法不能使用synchronized关键字,但可以使用synchronized块来进行同步。
4,synchronized位置可以自由放置,但是不能放置在方法的返回类型后面。
5,synchronized关键字不可以用来同步变量,如下面代码是错误的:
public synchronized int n = 0;
public static synchronized int n = 0;
6,虽然使用synchronized关键字是最安全的同步方法,但若是大量使用也会造成不必要的资源消耗以及性能损失。从表面上看synchronized锁定的是一个方法,但实际上锁定的却是一个类,比如,对于两个非静态方法method1()和method2()都使用了synchronized关键字,在执行其中的一个方法时,另一个方法是不能执行的。静态方法和非静态方法情况类似。但是静态方法和非静态方法之间不会相互影响,见如下代码:
public class MyThread1 extends Thread {
public String methodName ;
public static void method(String s) {
System. out .println(s);
while (true );
}
public synchronized void method1() {
method( "非静态的method1方法" );
}
public synchronized void method2() {
method( "非静态的method2方法" );
}
public static synchronized void method3() {
method( "静态的method3方法" );
}
public static synchronized void method4() {
method( "静态的method4方法" );
}
public void run() {
try {
getClass().getMethod( methodName ).invoke( this);
}
catch (Exception e) {
}
}
public static void main(String[] args) throws Exception {
MyThread1 myThread1 = new MyThread1();
for (int i = 1; i <= 4; i++) {
myThread1. methodName = "method" + String.valueOf (i);
new Thread(myThread1).start();
sleep(100);
}
}
}
运行结果为:
非静态的method1方法
静态的method3方法
|
从上面的运行结果可以看出,method2和method4在method1和method3运行完之前是不会运行的。因此,可以得出一个结论,如查在类中使用synchronized来定义非静态方法,那么将影响这个类中的所有synchronized定义的非静态方法;如果定义的静态方法,那么将影响这个类中所有以synchronized定义的静态方法。这有点儿像数据表中的表锁,当修改一条记录时,系统就将整个表都锁住了。因此,大量使用这种同步方法会使程序的性能大幅度地下降。
对共享资源的同步访问更加安全的技巧:
1,定义private的instance变量+它的get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象可以在外界绕过同步方法的控制而直接取得它,并且改动它。这也是JavaBean的标准实现之一。
2,如果instance变量是一个对象,如数组或ArrayList等,那上述方法仍然不安全,因为当外界通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且只返回这个private对象的clone()。这样,调用端得到的就只是对象副本的一个引用了。