1. 对象锁
1.1 什么是对象锁?
JVM在创建对象的时候为每一个对象关联唯一的一把锁。谁首先获得了这把锁,谁就可以访问这把锁控制的资源。例如:每一个房间里的门上都有一把锁,谁先进入了房间,谁就把房间的门反锁上,这时候别人想进房间就必须排队等待,直到房间里人把门打开之后才能进入。
2.线程安全
2.1 当程序中使用了多线程机制,并且多个线程之间需要访问相同的资源时,可能会导致数据混乱。这样是很不安全的。
例如: 用以下代码来模拟银行的存取款操作。对同一账户,首先存入1000元,然后取出1000元,最后查询余额,应该永远都是1000元才对。
首先来看单线程的情况。
package synchronize; import java.util.Random; public class BankProcess extends Thread{ private static int account = 3000; //存款 public void save(int money){ Random r = new Random(); try { this.sleep(100*r.nextInt(4)); } catch (InterruptedException e) { e.printStackTrace(); } account += money; } //取款 public void takeout(int money){ Random r = new Random(); try { this.sleep(100*r.nextInt(4)); } catch (InterruptedException e) { e.printStackTrace(); } account -= money; } //查询余额 public int getBalance(){ System.out.println("余额:"+account+"元"); return account; } @Override public void run() { save(1000); takeout(1000); getBalance(); } public static void main(String[] args){ new BankProcess().start();
}
}
当只要单个线程时,怎么着都不会有问题。
当存在多个线程时,问题就出现了。我们在main方法中启动多个线程来做试验:public static void main(String[] args){ for(int i=0;i<5;i++){ new BankProcess().start(); } }
输出结果:
余额:7000元 余额:6000元 余额:5000元 余额:4000元 余额:2000元
而且每次的运行结果都不一样。
为了避免这种情况的发生,线程同步机制应运而生。
3. 线程同步机制
线程同步机制的意思就是多个线程之间互斥的访问共享资源。
针对以上的例子就是说,在同一时间内,只允许一个线程使用共享资源。(这里的“使用”通常情况下都是指的修改操作,读取操作不会修改数据所以不会造成数据混乱的问题)
在第一个例子中我们看到了,单线程的情况下是怎么着都不会有问题的。所以线程同步其实就是强制构造出了一个单线程的安全环境。
4. synchronized 的使用方法
synchronized锁定的永远是对象,而不是代码。只有获取到对象锁的线程才可以执行synchronized控制的那段代码。要理解synchronized的用法关键在于理解锁定的是哪一个对象。
4.1 同步代码块
public void lockObject(Resource r){ synchronized(r){ //do something here } }
锁定的对象:r
若果一个线程获取到了对象r的锁,那么它就可以进入synchronized控制的代码块中。此时JVM会将此对象锁住,直到该线程离开这段代码块时才将锁释放。如果没有获取到对象r的锁,那么该线程只能在代码块外等待。直到对象的锁被释放才可进入。
此方法适用于多个线程需要访问到同一个Resource实例的情况。如果是多个线程访问Resource的不同的实例的话,则不会构成同步。
实例代码:
package synchronize; public class Process extends Thread{ Resource resource = null; Process(Resource resource){ this.resource = resource; } public void lockObject(Resource r){ synchronized(r){ System.out.println(this.getName() + " acquired the lock"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + " release the lock"); } } public static void main(String[] args){ Resource r = new Resource(); Process process1 = new Process(r); Process process2 = new Process(r); process1.start(); process2.start(); } @Override public void run() { System.out.println(this.getName()+" is running……"); this.lockObject(this.resource); System.out.println(this.getName()+" ends"); } }
输出结果:
Thread-1 is running…… Thread-1 acquired the lock Thread-0 is running…… Thread-1 release the lock Thread-1 ends Thread-0 acquired the lock Thread-0 release the lock Thread-0 ends
当Thread-1释放了实例对象r的锁之后,Thread-0才获得了锁。
4.2 锁定类对象
public class Process{ public void lockClass(Resource r){ synchronized(r.getClass()){ //do something here } //or synchronized(Resource.class){ //do something here } } }
锁定的对象:Resource的class对象
jvm在装在一个class文件时会生成一个class对象。
因为在java中一个类的class对象是唯一,此时若传入的参数都是Resource类型则需要互斥的访问这段代码。
如果A线程传入的是Resource类型的参数r,B线程传入到是Object类型的参数b,则A、B线程仍可以同时执行此段代码。
4.3 方法修饰符
4.3.1 synchronized作为方法修饰符修饰非静态方法时,锁定的对象是调用者,被同步的代码是整个方法体。
synchronized public void lock( ) { //do something here }
它等同于如下代码:
public void lock() { synchronized(this){ //do something here } }
如果调用不是同一个实例,则可以同时执行此段代码。
4.3.2 synchronized作为方法修饰符修饰static方法时有些特殊,它锁定的是调用者所属的类的class对象。
public class Process{ synchronized static public void lock() { //do something here } }
这样实际上就实现了代码的锁定。即同一时间内只有一个线程可以执行此段代码。因为能调用此方法的只有Process类对象、process实例对象以及其子类的类对象、实例对象。Process类对象和process实例对象在调用此方法时都会将Process类对象锁定,因而不能同时访问。Process子类的类如果没有继承lock方法,它在调用lock方法时实际上还是调用了父类的lock方法,调用者仍然是Process类;如果Process的子类继承了lock方法,则在调用lock方法时调用者就是Process子类,此时不能形成同步。
4.4 自定义锁对象如果没有明确的锁对象,而只是想要同步代码块。通常的做法是自定义锁对象。
public class Process{ static private byte[] lock = new byte[]; public void lock() { synchronized(lock){ //do something here } } }
自定义一个长度为0的字节数组对象,是系统开销最小的做法。