Java并发编程系列(二)-synchronized同步锁

2018-11-28 08:52:41来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

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 由浅入深GUI编程实战练习(一)

下一篇:7.4 (java学习笔记)网络编程之TCP