为什么要用锁?
在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的。注意这里 局部变量是不存在脏读的情况
实例:
定义一个类,通过不同的传入返回不同的结果 SynchronizedLock.java
1 /** 2 * 使用synchronized关键字加锁 3 * 4 */ 5 public class SynchronizedLock { 6 private int num = 0; 7 8 public void addNum(String userName) { 9 try { 10 if("a".equals(userName)){ 11 num=100; 12 System.out.println("a set over"); 13 Thread.sleep(2000); 14 }else { 15 num=200; 16 System.out.println("b set over"); 17 } 18 System.out.println(userName+",num="+num); 19 } catch (InterruptedException e) { 20 } 21 } 22 public static void main(String[] args) { 23 SynchronizedLock sl=new SynchronizedLock(); 24 MyThread01 mt1=new MyThread01(sl); 25 MyThread02 mt2=new MyThread02(sl); 26 27 mt1.start(); 28 mt2.start(); 29 } 30 31 }
自定义两个线程,MyThread01.java 和 MyThread02.java
1 public class MyThread01 extends Thread { 2 private SynchronizedLock sl; 3 4 public MyThread01(SynchronizedLock sl) { 5 this.sl = sl; 6 } 7 8 @Override 9 public void run() { 10 sl.addNum("a"); 11 } 12 }
1 public class MyThread02 extends Thread { 2 private SynchronizedLock sl; 3 4 public MyThread02(SynchronizedLock sl) { 5 this.sl = sl; 6 } 7 8 @Override 9 public void run() { 10 sl.addNum("b"); 11 } 12 }
运行SynchronizedLock的结果为:
a set over b set over b,num=200 a,num=200
如果按照代码执行的话,应该打印 “a,num=100”和“b,num=200”,但是结果却出现了问题。这就是线程的安全问题。过程如下:
1、mt0先运行,给num赋值100,然后打印出"a set over!",开始睡觉
2、mt0在睡觉的时候,mt1运行了,给num赋值200,然后打印出"b set over!",然后打印"b num = 200"
3、mt1睡完觉了,由于mt0的num和mt1的num是同一个nu
使用synchronized关键字
1 public synchronized void addNum(String userName) { 2 try { 3 if("a".equals(userName)){ 4 num=100; 5 System.out.println("a set over"); 6 Thread.sleep(2000); 7 }else { 8 num=200; 9 System.out.println("b set over"); 10 } 11 System.out.println(userName+",num="+num); 12 } catch (InterruptedException e) { 13 } 14 }
结果为:
a set over a,num=100 b set over b,num=200
多个对象
如果在有Synchronized锁的时候,使用多个对象呢?
1 public static void main(String[] args) { 2 SynchronizedLock sl=new SynchronizedLock(); 3 SynchronizedLock sl2=new SynchronizedLock(); 4 MyThread01 mt1=new MyThread01(sl); 5 MyThread02 mt2=new MyThread02(sl2); 6 7 mt1.start(); 8 mt2.start(); 9 }
结果为:
a set over b set over b,num=200 a,num=100
原因:
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,这里如果是把一段代码或方法(函数)当作锁,其实获取的也是对象锁,只是监视器(对象)不同而已,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象。
如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁,就像上面的例子一样,创建了两个ThreadDomain13对象,就产生了2个锁。既然两个线程持有的是不同的锁,自然不会受到"等待释放锁"这一行为的制约,可以分别运行addNum(String userName)中的代码。
1、A线程持有Object对象的Lock锁,B线程可以以异步方式调用Object对象中的非synchronized类型的方法
2、A线程持有Object对象的Lock锁,B线程如果在这时调用Object对象中的synchronized类型的方法则需要等待,也就是同步
synchronized锁重入
关键字synchronized拥有锁重入的功能。所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的
1 /** 2 * 多个synchronized关键字 3 * 4 */ 5 public class SynchronizedLocks { 6 public synchronized void print1() { 7 System.out.println("pirnt 1 method"); 8 print2(); 9 } 10 11 private synchronized void print2() { 12 System.out.println("print 2 method"); 13 print3(); 14 } 15 16 private synchronized void print3() { 17 System.out.println("print 3 method"); 18 } 19 public static void main(String[] args) { 20 MyThread03 mt3=new MyThread03(); 21 mt3.start(); 22 } 23 24 } 25 class MyThread03 extends Thread{ 26 @Override 27 public void run() { 28 SynchronizedLocks sls=new SynchronizedLocks(); 29 sls.print1(); 30 } 31 }
结果:
pirnt 1 method print 2 method print 3 method
证明了对象可以再次获取自己的内部锁。这种锁重入的机制,也支持在父子类继承的环境中。
异常自动释放锁
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
模拟的是把一个long型数作为除数,从MAX_VALUE开始递减,直至减为0,从而产生ArithmeticException。看一下例子:SynchronizedLockException.java
1 /** 2 * 异常释放锁 3 */ 4 public class SynchronizedLockException { 5 public static void main(String[] args) { 6 SynchronizedLockException sle = new SynchronizedLockException(); 7 MyThread04 mt1=new MyThread04(sle); 8 MyThread04 mt2=new MyThread04(sle); 9 mt1.start(); 10 mt2.start(); 11 } 12 13 public synchronized void testMethod(){ 14 try { 15 System.out.println("testMethod(),currentThread:"+Thread.currentThread().getName()); 16 long l=Integer.MAX_VALUE; 17 while(true){ 18 long lo=2/l; 19 l--; 20 } 21 } catch (Exception e) { 22 e.printStackTrace(); 23 } 24 } 25 26 } 27 class MyThread04 extends Thread{ 28 private SynchronizedLockException sle; 29 public MyThread04(SynchronizedLockException sle){ 30 this.sle=sle; 31 } 32 @Override 33 public void run() { 34 sle.testMethod(); 35 } 36 }
打印结果:
testMethod(),currentThread:Thread-0 java.lang.ArithmeticException: / by zero testMethod(),currentThread:Thread-1 at com.stu.lockthread.SynchronizedLockException.testMethod(SynchronizedLockException.java:17) at com.stu.lockthread.MyThread04.run(SynchronizedLockException.java:33) java.lang.ArithmeticException: / by zero at com.stu.lockthread.SynchronizedLockException.testMethod(SynchronizedLockException.java:17) at com.stu.lockthread.MyThread04.run(SynchronizedLockException.java:33)
关于Synchronized关键字
一 原子性(互斥性):实现多线程的同步机制,使得锁内代码的运行必需先获得对应的锁,运行完后自动释放对应的锁。
二 内存可见性:在同一锁情况下,synchronized锁内代码保证变量的可见性。
三 可重入性:当一个线程获取一个对象的锁,再次请求该对象的锁时是可以再次获取该对象的锁的。
如果在synchronized锁内发生异常,锁会被释放。
总结:
(1)synchronized方法 与 synchronized(this) 代码块 锁定的都是当前对象,不同的只是同步代码的范围
(2)synchronized (非this对象x) 将对象x本身作为“对象监视器”:
a、多个线程同时执行 synchronized(x) 代码块,呈现同步效果。
b、当其他线程同时执行对象x里面的 synchronized方法时,呈现同步效果。
c、当其他线程同时执行对象x里面的 synchronized(this)方法时,呈现同步效果。
(3)静态synchronized方法 与 synchronized(calss)代码块 锁定的都是Class锁。Class 锁与 对象锁 不是同一个锁,两者同时使用情况可能呈异步效果。
(4)尽量不使用 synchronized(string),是因为string的实际锁为string的常量池对象,多个值相同的string对象可能持有同一个锁。