线程安全的概念:
当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或者方法)就是线程安全的。
synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”
线程不安全的时候:
1 package com.java.day01; 2 3 public class MyThread extends Thread{ 4 5 private int count=5; 6 7 public void run(){ 8 count--; 9 System.out.println(this.currentThread().getName()+" "+"count="+count); 10 } 11 12 13 public static void main(String[] args) { 14 MyThread myThread = new MyThread(); 15 16 Thread t1 = new Thread(myThread,"t1"); 17 Thread t2 = new Thread(myThread,"t2"); 18 Thread t3 = new Thread(myThread,"t3"); 19 Thread t4 = new Thread(myThread,"t4"); 20 Thread t5 = new Thread(myThread,"t5"); 21 22 t1.start(); 23 t2.start(); 24 t3.start(); 25 t4.start(); 26 t5.start(); 27 28 } 29 30 31 }
运行结果:
1 t2 count=3 2 t1 count=3 3 t3 count=2 4 t4 count=0 5 t5 count=0
线程安全:
1 package com.java.day01; 2 3 public class MyThread extends Thread{ 4 5 private int count=5; 6 7 //synchronized加锁 8 public synchronized void run(){ 9 10 count--; 11 System.out.println(this.currentThread().getName()+" "+"count="+count); 12 } 13 14 15 public static void main(String[] args) { 16 MyThread myThread = new MyThread(); 17 18 Thread t1 = new Thread(myThread,"t1"); 19 Thread t2 = new Thread(myThread,"t2"); 20 Thread t3 = new Thread(myThread,"t3"); 21 Thread t4 = new Thread(myThread,"t4"); 22 Thread t5 = new Thread(myThread,"t5"); 23 24 t1.start(); 25 t2.start(); 26 t3.start(); 27 t4.start(); 28 t5.start(); 29 30 } 31 32 33 }
运行结果:
1 t1 count=4 2 t4 count=3 3 t3 count=2 4 t2 count=1 5 t5 count=0
总结:
当多个线程访问myThread的run方法时,以排队的方式进行处理(排队顺序按照cpu分配的先后顺序而定),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码的具体内容:拿不到锁,这个线程会不断的尝试获得这把锁,知道拿到为止,而且是多个线程同时去竞争这把锁(也就是会有锁竞争的问题)。
多个线程多个锁:多个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的具体内容
代码一:
1 package com.java.day01; 2 3 public class MyThread02 { 4 public static int num=0; 5 6 public synchronized void setNum(String tag){ 7 if(tag.equals("a")){ 8 9 num=100; 10 System.out.println("set num over!"); 11 12 }else if(tag.equals("b")){ 13 14 num=200; 15 System.out.println("set num over!"); 16 17 } 18 19 System.out.println("tag="+tag+" num="+num); 20 } 21 22 23 public static void main(String[] args) { 24 final MyThread02 m1 = new MyThread02(); 25 final MyThread02 m2 = new MyThread02(); 26 27 Thread t1 = new Thread(new Runnable() { 28 public void run() { 29 m1.setNum("a"); 30 } 31 }); 32 33 Thread t2 = new Thread(new Runnable() { 34 public void run() { 35 m2.setNum("b"); 36 } 37 }); 38 39 t1.start(); 40 t2.start(); 41 42 43 } 44 45 46 }
执行结果:
1 set num over! 2 set num over! 3 tag=a num=200 4 tag=b num=200
执行结果与预期不同,并没有实现线程安全
结果分析:num为共享变量,但是两个对象获得的是两个各自对象的锁,所以两个方法可以说是同时进行,并没有被锁住
代码二 给方法加入static:
1 package com.java.day01; 2 3 public class MyThread02 { 4 public static int num=0; 5 6 public static synchronized void setNum(String tag){ 7 if(tag.equals("a")){ 8 9 num=100; 10 System.out.println("tag a set num over!"); 11 12 }else if(tag.equals("b")){ 13 14 num=200; 15 System.out.println("tag b set num over!"); 16 17 } 18 19 System.out.println("tag="+tag+" num="+num); 20 } 21 22 23 public static void main(String[] args) { 24 final MyThread02 m1 = new MyThread02(); 25 final MyThread02 m2 = new MyThread02(); 26 27 Thread t1 = new Thread(new Runnable() { 28 public void run() { 29 m1.setNum("a"); 30 } 31 }); 32 33 Thread t2 = new Thread(new Runnable() { 34 public void run() { 35 m2.setNum("b"); 36 } 37 }); 38 39 t1.start(); 40 t2.start(); 41 42 43 } 44 45 46 }
运行结果:
1 tag a set num over! 2 tag=a num=100 3 tag b set num over! 4 tag=b num=200
总结:
关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁,所以实例中的那哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。
有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表锁定class类,类级别的锁(独占class类)
对象锁的同步和异步
同步:synchronized
同步的概念就是共享,我们要牢牢记住“共享”这两个字,如果不是共享资源,就没有必要进行同步。
异步:asynchronized
异步的概念就是独立,相互之间不受任何制约。就好像我们学习http的时候,在页面发起的ajax请求,我们还可以继续浏览或操作页面内容,二者之间没有任何关系。
同步的目的就是为了线程安全,其实对于线程来说,需要满足两个特性:
原子性(同步)
可见性
代码一:
1 package com.java.day01; 2 3 public class MyThread03 { 4 public synchronized void method1(){ 5 System.out.println(Thread.currentThread().getName()); 6 try { 7 Thread.sleep(4000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 13 public void method2(){ 14 System.out.println(Thread.currentThread().getName()); 15 } 16 17 public static void main(String[] args) { 18 final MyThread03 myThread = new MyThread03(); 19 20 Thread t1= new Thread(new Runnable() { 21 public void run() { 22 myThread.method1(); 23 } 24 },"t1"); 25 26 Thread t2 = new Thread(new Runnable() { 27 public void run() { 28 myThread.method2(); 29 } 30 },"t2"); 31 32 t1.start(); 33 t2.start(); 34 35 } 36 37 }
运行结果:
可以看出,在method1方法没有执行完的之后,method2方法已经执行完
代码2,给method2方法加上关键字synchronized:
1 package com.java.day01; 2 3 public class MyThread03 { 4 public synchronized void method1(){ 5 System.out.println(Thread.currentThread().getName()); 6 try { 7 Thread.sleep(4000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 13 public synchronized void method2(){ 14 System.out.println(Thread.currentThread().getName()); 15 } 16 17 public static void main(String[] args) { 18 final MyThread03 myThread = new MyThread03(); 19 20 Thread t1= new Thread(new Runnable() { 21 public void run() { 22 myThread.method1(); 23 } 24 },"t1"); 25 26 Thread t2 = new Thread(new Runnable() { 27 public void run() { 28 myThread.method2(); 29 } 30 },"t2"); 31 32 t1.start(); 33 t2.start(); 34 35 } 36 37 }
运行结果:
图1可以看出正在运行method1,method2并没有被运行,图2可以看出method1运行完之后method2也开始运行且运行完。
上述代码1的method1方法是同步的,method2方法是异步的
代码2的method1和method2方法都是同步的
A线程先持有object对象的Lock锁,B线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步
A线程先持有object对象的Lock锁,B线程可以以异步的方式调用对象中的非synchronized修饰的方法
脏读:
对于对象的同步和异步的方法,我们在设计自己的程序的时候,一定要考虑问题的整体,不然就会出现数据不一致的错误,很经典的错误就是脏读(dirty read)
代码1:
1 package com.java.day01; 2 3 public class DirtyRead { 4 private String name="source"; 5 private int age=0; 6 7 public synchronized void setValue(String name,int age){ 8 this.name=name; 9 10 try { 11 Thread.sleep(4000); 12 } catch (InterruptedException e) { 13 // TODO Auto-generated catch block 14 e.printStackTrace(); 15 } 16 17 this.age=age; 18 19 System.out.println("setValue最终的结果:"+"name:"+name+" "+"age:"+age); 20 } 21 22 public void getValue(){ 23 24 System.out.println("getValue方法最终得到的结果: "+"name:"+name+" "+"age:"+age); 25 } 26 27 28 public static void main(String[] args) { 29 final DirtyRead dr = new DirtyRead(); 30 Thread t1 = new Thread(new Runnable() { 31 public void run() { 32 dr.setValue("test1", 25); 33 } 34 }); 35 36 Thread t2 = new Thread(new Runnable() { 37 public void run() { 38 dr.getValue(); 39 } 40 }); 41 42 43 t1.start(); 44 t2.start(); 45 46 } 47 }
运行结果:
1 getValue方法最终得到的结果: name:test1 age:0 2 setValue最终的结果:name:test1 age:25
得到错误的数据
代码2,给getValue方法加锁:
1 package com.java.day01; 2 3 public class DirtyRead { 4 private String name="source"; 5 private int age=0; 6 7 public synchronized void setValue(String name,int age){ 8 this.name=name; 9 10 try { 11 Thread.sleep(4000); 12 } catch (InterruptedException e) { 13 // TODO Auto-generated catch block 14 e.printStackTrace(); 15 } 16 17 this.age=age; 18 19 System.out.println("setValue最终的结果:"+"name:"+name+" "+"age:"+age); 20 } 21 22 public synchronized void getValue(){ 23 24 System.out.println("getValue方法最终得到的结果: "+"name:"+name+" "+"age:"+age); 25 } 26 27 28 public static void main(String[] args) { 29 final DirtyRead dr = new DirtyRead(); 30 Thread t1 = new Thread(new Runnable() { 31 public void run() { 32 dr.setValue("test1", 25); 33 } 34 }); 35 36 Thread t2 = new Thread(new Runnable() { 37 public void run() { 38 dr.getValue(); 39 } 40 }); 41 42 43 t1.start(); 44 t2.start(); 45 46 } 47 }
运行结果:
1 setValue最终的结果:name:test1 age:25 2 getValue方法最终得到的结果: name:test1 age:25
上述结果是setValue方法执行完之后,在执行getValue方法。
总结:
在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue方法同时加锁synchronized同步关键字,保证业务(service)的原子性,不然会出现业务错误(也从侧面保证业务的一致性)(如果在赋值的时候进行取值,则可能取到错误的数据)
(ACID A:原子性 C:一致性 I:隔离性 D:永久性)
synchronized其他概念
synchronized锁重入:
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个此线程得到了一个对象的锁之后,再次请求此对象时是可以再次得到该对象的锁。
代码1:
1 package com.java.day01; 2 3 /** 4 * 5 * @author syousetu 6 *锁的重入 7 * 8 */ 9 public class SyncDubb01 { 10 public synchronized void method01() { 11 System.out.println("method01..."); 12 this.method02(); 13 } 14 15 public synchronized void method02() { 16 System.out.println("method02..."); 17 this.method03(); 18 } 19 20 public synchronized void method03() { 21 System.out.println("method03..."); 22 } 23 24 public static void main(String[] args) { 25 final SyncDubb01 s = new SyncDubb01(); 26 27 Thread t1 = new Thread(new Runnable() { 28 public void run() { 29 s.method01(); 30 } 31 }); 32 33 t1.start(); 34 35 } 36 37 }
运行结果:
1 method01... 2 method02... 3 method03...
代码2:有继承关系的synchronized的应用
1 package com.java.day01; 2 3 /** 4 * 5 * @author syousetu 6 * 7 *有继承关系的时候的synchronized的应用 8 */ 9 public class SyncDubb02 { 10 11 static class Main { 12 public int i = 10; 13 14 public synchronized void operationSup() { 15 try { 16 17 i--; 18 System.out.println("Main print i=" + i); 19 20 Thread.sleep(1000); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 } 25 26 } 27 28 static class Sub extends Main { 29 public synchronized void operationSub() { 30 while (i > 0) { 31 32 try { 33 i--; 34 System.out.println("Sub print i=" + i); 35 Thread.sleep(1000); 36 //调用父类的方法 37 this.operationSup(); 38 } catch (InterruptedException e) { 39 e.printStackTrace(); 40 } 41 42 43 } 44 45 } 46 47 } 48 49 50 public static void main(String[] args) { 51 Thread t1 = new Thread(new Runnable() { 52 public void run() { 53 Sub sub = new Sub(); 54 sub.operationSub(); 55 } 56 }); 57 58 t1.start(); 59 60 } 61 62 63 64 65 }
运行结果:
1 Sub print i=9 2 Main print i=8 3 Sub print i=7 4 Main print i=6 5 Sub print i=5 6 Main print i=4 7 Sub print i=3 8 Main print i=2 9 Sub print i=1 10 Main print i=0
synchronized 出现异常,锁自动释放:
说明:对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对你的应用程序逻辑业务产生严重的错误,比如,你现在在执行一个队列任务,很多对象都去等待第一个对象正确执行完毕再去释放锁,但是第一个对象由于异常的出现,导致业务逻辑没有正常执行完毕,就释放了锁,那么可想而知后续的对象执行的都是错误的逻辑。所以这一点一定要引起注意,在编写代码的时候一定要考虑周全。
1 package com.java.day01; 2 3 public class SyncException { 4 private int i=0; 5 6 public synchronized void opteration(){ 7 while(true){ 8 9 try { 10 i++; 11 Thread.sleep(1000); 12 System.out.println(Thread.currentThread().getName()+" i:"+i); 13 14 if(i==10){ 15 Integer.parseInt("a"); 16 } 17 } catch ( Exception e) {//interruptedException 18 //捕获到异常输出,然后继续往下执行 19 //出现异常后处理的方法: 20 //1.将出现异常的数据记录在日志(出现异常的数据和后续的操作没有关联关系)或者continue 21 //2.抛出运行异常或者打断异常 22 System.out.println("出现异常 :i="+i); 23 e.printStackTrace(); 24 // throw new RuntimeException(); 25 continue;//跳过这次继续 26 } 27 28 } 29 } 30 31 public static void main(String[] args) { 32 final SyncException s = new SyncException(); 33 Thread t1= new Thread(new Runnable() { 34 public void run() { 35 s.opteration(); 36 37 } 38 },"t1"); 39 40 t1.start(); 41 42 43 } 44 45 46 47 48 }
运行结果:
1 t1 i:1 2 t1 i:2 3 t1 i:3 4 t1 i:4 5 t1 i:5 6 t1 i:6 7 t1 i:7 8 t1 i:8 9 t1 i:9 10 t1 i:10 11 出现异常 :i=10 12 java.lang.NumberFormatException: For input string: "a" 13 at java.lang.NumberFormatException.forInputString(Unknown Source) 14 at java.lang.Integer.parseInt(Unknown Source) 15 at java.lang.Integer.parseInt(Unknown Source) 16 at com.java.day01.SyncException.opteration(SyncException.java:15) 17 at com.java.day01.SyncException$1.run(SyncException.java:35) 18 at java.lang.Thread.run(Unknown Source) 19 t1 i:11 20 t1 i:12 21 t1 i:13
storm:做分布式计算
使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长世间的任务,那么B线程就必须等待比较长的时间去执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,也就是通常所说的减小锁的粒度。(demo1)
synchronized可以使用任意的object进行加锁(demo2)
另外特别注意一个问题,就是不要使用String的常量加锁,会出现死循环问题。(demo3)
代码1 采用字符串常量加锁:
1 package com.java.day01; 2 3 public class StringLock { 4 5 public void method() { 6 synchronized ("字符串常量") { 7 8 while (true) { 9 10 try { 11 System.out.println(Thread.currentThread().getName() + "线程开始"); 12 Thread.sleep(200); 13 System.out.println(Thread.currentThread().getName() + "线程结束"); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 } 19 20 } 21 22 public static void main(String[] args) { 23 final StringLock sl = new StringLock(); 24 Thread t1 = new Thread(new Runnable() { 25 public void run() { 26 sl.method(); 27 } 28 }, "t1"); 29 30 Thread t2 = new Thread(new Runnable() { 31 public void run() { 32 sl.method(); 33 } 34 }, "t2"); 35 36 t1.start(); 37 t2.start(); 38 39 } 40 41 }
运行结果:
1 t1线程开始 2 t1线程结束 3 t1线程开始 4 t1线程结束 5 t1线程开始
线程2获取不到锁
代码2 采用new String("字符串常量的方式加锁"):
1 package com.java.day01; 2 3 public class StringLock { 4 5 public void method() { 6 //new String("字符串常量"); 7 //"字符串常量" 8 synchronized (new String("字符串常量")) { 9 10 while (true) { 11 12 try { 13 System.out.println(Thread.currentThread().getName() + "线程开始"); 14 Thread.sleep(200); 15 System.out.println(Thread.currentThread().getName() + "线程结束"); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 } 20 } 21 22 } 23 24 public static void main(String[] args) { 25 final StringLock sl = new StringLock(); 26 Thread t1 = new Thread(new Runnable() { 27 public void run() { 28 sl.method(); 29 } 30 }, "t1"); 31 32 Thread t2 = new Thread(new Runnable() { 33 public void run() { 34 sl.method(); 35 } 36 }, "t2"); 37 38 t1.start(); 39 t2.start(); 40 41 } 42 43 }
运行结果:
1 t2线程开始 2 t1线程开始 3 t2线程结束 4 t1线程结束 5 t1线程开始 6 t2线程开始
锁对象的改变问题,当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。(demo4)(用字符串常量进行加锁,在所里面修改字符串常量的值,预期锁本身应该发生了改变,但是,实验结果并没有得到验证;对于第二个实验,将对象的属性进行改变,锁仍然不改变)
死锁问题(demo5)