Java并发编程系列(二)-synchronized同步锁
2018-11-28 08:52:41来源:博客园 阅读 ()
synchronized基本用法
从一个简单的例子入手
1 public class C1 { 2 private int count = 10; 3 4 public void foo() { 5 count--; 6 System.out.println(Thread.currentThread().getName() + " count=" + count); 7 } 8 9 public static void main(String[] args) { 10 C1 c1 = new C1(); 11 for (int i = 0; i < 5; i++) { 12 new Thread(c1::foo).start(); 13 } 14 } 15 }
类里有个count变量初始是10.foo方法执行一次,count减1,打印出当前线程名和count.
程序入口方法new出一个C1的实例,开启5个线程同时去执行foo方法.
运行结果:
运行两次结果不一样,第一次符合预期,每次减1.第二次出了状况,9没了,出现了两次8.
原因:Thread-0线程执行了count--,count变成了9,还没有打印结果.这时Thread-1线程也进来了,执行count--,count变成了8.然后Thread-0开始打印结果,这时count是8,输出的是8.然后Thread-1开始打印结果,这时count也是8,所以输出了两次8.
多运行几次,还会出现不同的结果.单线程执行没问题的代码,到了多线程环境下.执行结果就会变得不可预期.
synchronized关键字就是用来解决这种问题的.
1 public class C1 { 2 private int count = 10; 3 private Object o = new Object(); 4 5 public void foo1() { 6 synchronized (o) { 7 count--; 8 System.out.println(Thread.currentThread().getName() + " count=" + count); 9 } 10 } 11 12 public static void main(String[] args) { 13 C1 c1 = new C1(); 14 for (int i = 0; i < 5; i++) { 15 new Thread(c1::foo1).start(); 16 } 17 }
运行结果:
无论运行多少次,结果都是符合预期的.
将count--和打印结果放在synchronized的大括号里,表示这是一个原子操作,不能分割.运行到一半的时候,别的线程是进不来的,只能阻塞.等到这个线程执行完大括号的代码时,会释放锁.这时候别的线程才能执行这段代码.
像这种简单的场景每次想加锁时还要new一个对象,感觉有点多此一举啊.所以还有别的写法,可以锁定当前对象this.静态方法没有this对象,所以静态方法锁定的是类对象.下面几种方法,对于这个场景,效果是一样的.
1 public class C1 { 2 private int count = 10; 3 private Object o = new Object(); 4 5 public void foo() { 6 count--; 7 System.out.println(Thread.currentThread().getName() + " count=" + count); 8 } 9 10 public void foo1() { 11 synchronized (o) { 12 count--; 13 System.out.println(Thread.currentThread().getName() + " count=" + count); 14 } 15 } 16 17 //锁定this 18 public void foo2() { 19 synchronized (this) { 20 count--; 21 System.out.println(Thread.currentThread().getName() + " count=" + count); 22 } 23 } 24 25 //将synchronized加到方法声明上 同样锁定的是this 26 public synchronized void foo3() { 27 count--; 28 System.out.println(Thread.currentThread().getName() + " count=" + count); 29 } 30 31 private static int sCount = 10; 32 private static Object so = new Object(); 33 34 public static void bar() { 35 sCount--; 36 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 37 } 38 39 public static void bar1() { 40 synchronized (so) { 41 sCount--; 42 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 43 } 44 } 45 46 //静态方法锁定类对象 47 public static void bar2() { 48 synchronized (C1.class) { 49 sCount--; 50 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 51 } 52 } 53 54 //将synchronized加到静态方法的方法声明上 同样锁定的是类对象 55 public synchronized static void bar3() { 56 sCount--; 57 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 58 } 59 }
synchronized锁定的对象
1 public class C2 { 2 private int count = 10; 3 private int count1 = 10; 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 public synchronized void foo() { 7 System.out.println("foo() " + simpleDateFormat.format(new Date())); 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 count--; 14 System.out.println(Thread.currentThread().getName() + " count=" + count); 15 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 16 } 17 18 public synchronized void foo1() { 19 System.out.println("foo1() " + simpleDateFormat.format(new Date())); 20 try { 21 Thread.sleep(100); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 count1--; 26 System.out.println(Thread.currentThread().getName() + " count1=" + count1); 27 System.out.println("foo1() end " + simpleDateFormat.format(new Date())); 28 } 29 30 public static void main(String[] args) { 31 C2 c2 = new C2(); 32 new Thread(c2::foo).start(); 33 new Thread(c2::foo1).start(); 34 } 35 }
运行结果:
可以看到,两个方法修改两个变量,他们之间应该是没有影响的.但现在第二个方法要等到第一个方法执行完才执行.
出现这种情况是因为,synchronized虽然加到了各自的方法上,但最终锁是加到了this对象上.
synchronized同步锁可以这么来理解:当一个线程要执行synchronized大括号内的代码时,这会有一个看门的(synchronized)告诉线程,想要执行这段代码,你得去看看那边的那个对象(this)上有没有锁,没锁你才能执行.同时你得在那个对象上挂个锁,这样别的线程就进不来了.
这样,Thread-0进入foo方法时,就在this上挂了个锁.这时Thread-1来执行foo1方法,虽然和Thread-0执行的不是同一块代码甚至不是同一个方法.但是也被synchronized拦了下来,让他去看this对象上有没有锁.这时this上有锁.所以只能阻塞,等Thread-0执行完才能执行.
所以,修改两个变量想互不干涉.只能锁定两个对象.
1 public class C2 { 2 private int count = 10; 3 private int count1 = 10; 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 private Object o = new Object(); 7 private Object o1 = new Object(); 8 9 public void bar() { 10 synchronized (o) { 11 System.out.println("bar() " + simpleDateFormat.format(new Date())); 12 try { 13 Thread.sleep(100); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 count--; 18 System.out.println(Thread.currentThread().getName() + " count=" + count); 19 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 20 } 21 } 22 23 public void bar1() { 24 synchronized (o1) { 25 System.out.println("bar1() " + simpleDateFormat.format(new Date())); 26 try { 27 Thread.sleep(100); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 count1--; 32 System.out.println(Thread.currentThread().getName() + " count1=" + count1); 33 System.out.println("bar1() end " + simpleDateFormat.format(new Date())); 34 } 35 } 36 37 public static void main(String[] args) { 38 C2 c2 = new C2(); 39 new Thread(c2::bar).start(); 40 new Thread(c2::bar1).start(); 41 } 42 }
运行结果:
可以看到bar1没有等到bar执行完才进入方法.
synchronized锁定在堆内存
1 public class C3 { 2 private int count = 10; 3 private Object o = new Object(); 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 public void foo() { 7 synchronized(o){ 8 System.out.println("foo() " + simpleDateFormat.format(new Date())); 9 //此处高能 10 o=new Object(); 11 try { 12 Thread.sleep(100); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 count--; 17 System.out.println(Thread.currentThread().getName() + " count=" + count); 18 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 19 } 20 } 21 22 public void bar(){ 23 synchronized (o){ 24 System.out.println("bar() " + simpleDateFormat.format(new Date())); 25 //此处高能 26 o=new Object(); 27 try { 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 count--; 33 System.out.println(Thread.currentThread().getName() + " count=" + count); 34 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 35 } 36 } 37 38 public static void main(String[] args) { 39 C3 c3=new C3(); 40 new Thread(c3::foo).start(); 41 new Thread(c3::bar).start(); 42 } 43 }
运行结果:
上面两个方法同样的逻辑:记录开始和结束时间,count--,打印线程名和数量,synchronized锁定的都是o.
从运行结果看,哇完全是同时执行的嘛,根本就没有阻塞,根本就没有原子.
注意注释高能处o=new Object().就是上完锁之后,他把地址给换了,指向了一个没有锁的内存推.
所以重新理解下这个过程:
当一个线程要执行synchronized代码块的代码时,先去栈里找o这个对象的引用地址,然后根据地址去堆里找new Object()分配的内存,有锁的话阻塞等待,直到解锁.没锁的话就在这个区域的推内存上加一个锁.这样别的线程就不能执行这块代码了.但上面的例子里,线程把锁加上之后,又new了一个Object,在堆里新分配了一块内存,把o处的引用地址改成了这里.所以第二个线程通过o来找堆内存的时候,找到的是这个新分配的堆内存,自然是没有加锁的.这样就不会阻塞等待.直接去执行了
脏读问题
通过下面代码了解脏读是怎么产生的,当然脏读不一定就是问题,允不允许脏读要看业务.
1 public class C4 { 2 3 private int balance = 0; 4 5 public synchronized void addBalance(int money) { 6 try { 7 Thread.sleep(100); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 balance += money; 12 } 13 14 public void showBalance() { 15 System.out.println(balance); 16 } 17 18 public synchronized void showBalanceSync() { 19 System.out.println(balance); 20 } 21 22 public static void main(String[] args) { 23 C4 c4 = new C4(); 24 new Thread(() -> c4.addBalance(2)).start(); 25 new Thread(c4::showBalance).start(); 26 try { 27 Thread.sleep(100); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 new Thread(c4::showBalance).start(); 32 33 try { 34 Thread.sleep(1000); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 System.out.println("---分割线---"); 39 new Thread(() -> c4.addBalance(3)).start(); 40 new Thread(c4::showBalanceSync).start(); 41 } 42 }
运行结果:
先执行余额加2,马上去读,读书来是0.睡100毫秒再去读,读出来的才是2.因为写余额的方法加锁了,但是读的方法没加锁.所以写入余额还没执行完的时候去读,读的是以前的.
分割线下面的部分调用的是加同步锁的读余额,写入余额加3没执行完的时候,读方法时阻塞的.等写完后才开始读,所以读出来的是5.
synchronized重入
1 public class C5 { 2 3 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 4 5 public synchronized void foo(){ 6 System.out.println("foo() " + simpleDateFormat.format(new Date())); 7 try { 8 Thread.sleep(100); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 13 } 14 15 public synchronized void bar(){ 16 System.out.println("bar() " + simpleDateFormat.format(new Date())); 17 try { 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 //当前线程内执行foo() 23 foo(); 24 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 25 } 26 27 public static void main(String[] args) { 28 C5 c5=new C5(); 29 new Thread(c5::bar).start(); 30 } 31 }
运行结果:
类里有两个同步方法,都锁定的是当前对象this.执行bar的时候加了锁.在bar方法中调用了foo,而foo也是同步方法,也需要判断this上有没有锁.那这时foo方法会阻塞吗.从执行结果上看,并没有阻塞.因为bar和foo方法在同一个线程内执行,当线程执行foo方法时去内存堆里看有没有锁时,这时有锁,但这个锁是刚才自己挂上去的,所以就继续往下执行了.如果不这样做,方法就永远执行不了,foo方法需要bar释放锁才能执行,而bar方法没有执行完foo不会释放锁.所以可以理解成锁的使用权在当前线程.
为了说明这个问题,看下面例子
public class C5 { private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); public synchronized void foo(){ System.out.println("foo() " + simpleDateFormat.format(new Date())); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("foo() end " + simpleDateFormat.format(new Date())); } public synchronized void bar1(){ System.out.println("bar1() " + simpleDateFormat.format(new Date())); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //新开一个线程执行foo() new Thread(this::foo).start(); System.out.println("bar1() end " + simpleDateFormat.format(new Date())); } public static void main(String[] args) { C5 c5=new C5(); new Thread(c5::bar1).start(); } }
运行结果:
和上面唯一的区别就是bar1方法调用foo的时候,是开了一个线程调用的.如结果所示,执行foo的时候阻塞了,bar1执行完释放锁之后,foo才开始执行.
synchronized死锁
最简单的死锁情况如下例所示
1 public class C6 { 2 3 private Object o = new Object(); 4 private Object o1 = new Object(); 5 6 public void foo() { 7 System.out.println("foo()"); 8 synchronized (o) { 9 try { 10 Thread.sleep(100); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 System.out.println("foo() lock o"); 15 synchronized (o1){ 16 System.out.println("foo() lock o1"); 17 } 18 } 19 } 20 21 public void bar(){ 22 System.out.println("bar()"); 23 synchronized (o1){ 24 System.out.println("bar() lock o1"); 25 synchronized (o){ 26 System.out.println("bar() lock o"); 27 } 28 } 29 } 30 31 public static void main(String[] args){ 32 C6 c6=new C6(); 33 new Thread(c6::foo).start(); 34 new Thread(c6::bar).start(); 35 }
运行结果:
这个结果是永远也执行不完的.因为死锁了.o等着o1释放,o1等着o释放.天长地久海枯石烂.
synchronized锁会在抛出异常时被释放
1 public class C7 { 2 3 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 4 5 public synchronized void foo(){ 6 System.out.println("foo() "+simpleDateFormat.format(new Date())); 7 //处理逻辑1耗时100毫秒 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 foo1(); 14 //处理逻辑2耗时100毫秒 15 try { 16 Thread.sleep(100); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 } 21 22 private void foo1(){ 23 throw new RuntimeException("假设处理foo1逻辑的时候出了异常"); 24 } 25 26 public synchronized void bar(){ 27 System.out.println("bar() "+simpleDateFormat.format(new Date())); 28 } 29 30 public static void main(String[] args) { 31 C7 c7=new C7(); 32 new Thread(c7::foo).start(); 33 /** 34 * 正常foo执行需要200毫秒 200毫秒后释放锁 bar执行 35 * 现在foo执行到100毫秒时 foo1执行 foo1执行时抛出了异常 锁被释放 36 * 所以bar实际在foo执行100毫秒后执行 37 */ 38 new Thread(c7::bar).start(); 39 } 40 }
运行结果:
注意执行时间,相差101毫秒,远不到200毫秒.所以并发编程时一定要处理好异常,不然业务处理到一半抛出异常,释放锁后直接就被其他线程占用了.
源码下载:java并发示例.zip (p2包里的内容)
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 国外程序员整理的Java资源大全(全部是干货) 2020-06-12
- 2020年深圳中国平安各部门Java中级面试真题合集(附答案) 2020-06-11
- 2020年java就业前景 2020-06-11
- 04.Java基础语法 2020-06-11
- Java--反射(框架设计的灵魂)案例 2020-06-11
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash