一、概述
在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,Java虚拟机采用了同步机制,以确保在某一时刻,只有一个线程访问临界资源。
二、synchronized关键字
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
三、使用
sychronized关键字可用于修饰:普通方法,静态方法,普通方法中的代码块,静态方法中的代码块。
- 修饰普通方法
public synchronized void increase(){
count++;
}
普通方法的同步是同步在该方法所属的具体对象(new对象)上。即不同线程访问不同对象的同步方法是不存在竞争关系的。或者说一个线程访问一个实例的普通同步方法不会阻塞另一个线程访问另外一个实例的对应的普通同步方法。
被synchronized关键字修饰的方法,在一个线程执行同一个对象的同步方法时,即在一个线程获取了该对象的锁之后,在该同步方法执行完之前,更准确的说,在它释放这个对象锁之前,其他线程无法执行该同步方法,或者说其他线程都处于阻塞状态,直至该对象锁被释放。
- 修饰静态方法
public static synchronized void increase(){
count ++;
}
静态方法的同步是同步在该方法所属的类对象(Class对象)上。即同时只允许一个线程执行同一个类中的静态同步方法。
被synchronized关键字修饰的静态方法,在当前获得该类的锁的线程释放该类锁之前,其他线程是无法执行该同步静态方法。
- 修饰普通方法的代码块
public void increase(){
synchronized(this){
count ++;
}
}
注意Java同步块构造器用括号将对象括起来。使用“this”,指调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。
- 修饰静态方法的代码块
public class TestClass {
public static synchronized void print1(String msg1, String msg2){
System.out.println(msg1);
System.out.println(msg2);
}
public static synchronized void print2(String msg1, String msg2){
System.out.println(msg1);
System.out.println(msg2);
}
}
public class TestClass {
public static void print1(String msg1, String msg2){
synchronized(MyClass.class){
System.out.println(msg1);
System.out.println(msg2);
}
}
public static void print2(String msg1, String msg2){
synchronized(MyClass.class){
System.out.println(msg1);
System.out.println(msg2);
}
}
}
以上MyClass类的print1,print2方法不允许被线程同时访问。即同步静态方法和静态同步代码块都是给Class对象加了类锁,某一时刻只允许一个线程执行两个方法中的任意一个。
对于同一个类A,线程1争夺A对象实例的对象锁,线程2争夺类A的类锁,这两者不存在竞争关系。也就说对象锁和类锁不存在互斥关系。
四、关于synchronized同步锁机制
如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。
持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。
使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。
类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。