多线程3:Java内置锁与synchronized关键字
Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized block),同步代码块包含2个部分:1,作为锁的对象引用;2,该锁所保护的代码块
synchronized(lock){ ...... }
public class Status { private int count = 0; public int getNum(){ synchronized(this){ ++count; } return count; } }
方法的执行过程为:
0: aload_0 1: dup 2: astore_1 3: monitorenter //获得对象锁 4: aload_0 5: dup 6: getfield #12; //Field count:I 9: iconst_1 10: iadd 11: putfield #12; //Field count:I 14: aload_1 15: monitorexit //释放对象锁 16: goto 22 19: aload_1 20: monitorexit 21: athrow 22: aload_0 23: getfield #12; //Field count:I 26: ireturn
可以看到比起正常的方法,增加了加锁和解锁的字节码指令(为什么会有个goto语句?)
每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
内置锁为互斥锁,即线程A获取到锁后,线程B必须等待或阻塞直到线程A退出同步代码块,线程B才能获取到同一个锁,由于同一时刻只能有一个线程进入同步代码块,故同步代码块中的操作可以保证原子性
public class Status { private int num = 0; public final int getNum(){ return num; } public final void setNum(int num){ this.num = num; } }
public class Task implements Runnable { private Status status = new Status(); public void run() { synchronized (status) { int num = status.getNum(); status.setNum(num + 1); System.out.println(Thread.currentThread().getName() + "|" + status.getNum() + "|" + status.hashCode()); } } public static void main(String[] args) { Task task = new Task(); Thread t1 = new Thread(task); Thread t2 = new Thread(task); Thread t3 = new Thread(task); Thread t4 = new Thread(task); t1.start(); t2.start(); t3.start(); t4.start(); } }
运行结果为:
Thread-0|1|22971385 Thread-2|2|22971385 Thread-3|3|22971385 Thread-1|4|22971385
修改一下上面的代码,每个线程运行时new一个新的Status对象,而不是像上面的代码,4个线程共用同一个Status对象:
public class Task implements Runnable { private Status status; public void run() { status = new Status(); synchronized (status) { int num = status.getNum(); status.setNum(num + 1); System.out.println(Thread.currentThread().getName() + "|" + status.getNum() + "|" + status.hashCode()); } } ......
由于充当锁的对象不一定是同一个对象(hashcode不同),同步失效:
Thread-0|1|27744459 Thread-3|1|28737396 Thread-1|2|28737396 Thread-2|1|6927154
因此同步代码块中充当锁的对象必须为同一个对象
public class Task implements Runnable { private Status status; public Task(Status status){ this.status = status; } public void run() { synchronized (status) { System.out.println("Thread lock"); System.out.println("Thread:" + status.getNum()); System.out.println("Thread over"); } } public static void main(String[] args) { Status status = new Status(); Task task = new Task(status); Thread t = new Thread(task); t.start(); //synchronized(status){ System.out.println("Main"); status.setNum(1); System.out.println("Main:" + status.getNum()); //} } }
运行结果为:
Main Thread lock Main:1 Thread:1 Thread over
从运行结果可以看出,在Thread线程锁定status对象的时候,Main线程在Thread线程释放锁对象前依然能够修改status对象的num域,说明锁没有生效
Main线程中没有对status对象进行同步,故在Thread线程锁定status对象的时候不需要等待或阻塞,可以直接操作status对象,因此所有使用同步对象的地方都必须进行同步
修改方式为:Task类的main方法中,在操作status对象时进行同步(去掉代码中的注释部分)
用synchronized关键字修饰的方法是一个横跨整个方法体的同步代码块,锁为方法所在的对象,如果该方法为静态方法,则锁为Class类,当然这里的锁也必须为同一个对象
特别需要注意的是所有访问状态变量的方法都必须进行同步:
public class Task implements Runnable { private int count = 0; public void run() { for(int i = 0 ; i < 5 ; i++){ this.reset(); this.add(); } } public synchronized void reset(){ if(count == 5){ count = 0; System.out.println(Thread.currentThread().getName() + "[count reset]"); } } public synchronized void add(){ count++; System.out.println(Thread.currentThread().getName() + "[count:" + count + "]"); } public static void main(String[] args){ Task t = new Task(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); } }
运行结果:
Thread-0[count:1] Thread-0[count:2] Thread-0[count:3] Thread-0[count:4] Thread-1[count:5] Thread-1[count reset] Thread-1[count:1] Thread-1[count:2] Thread-1[count:3] Thread-1[count:4]