Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

    • 多线程

      • 基本概念
      • 线程的创建和使用
      • 线程的生命周期
      • 线程的同步
        • 问题的提出
        • 问题的原因
        • 解决办法
        • Synchronized关键字
        • 分析同步原理
        • 关于同步方法的总结
        • 同步范围
        • 释放锁的操作
        • 不会释放锁的操作
        • 懒汉式线程安全问题
        • 死锁
        • Lock(锁)
        • synchronized 与Lock 的对比
        • 练习
      • 线程的通信
      • Callable和线程池
    • Java常用类

    • 枚举类与注解

    • Java集合

    • 数据结构与算法

    • 泛型

    • IO流

    • 网络编程

    • 反射

    • 函数式编程

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java核心基础
  • 多线程
SaulJWu
2020-12-20

线程的同步

# 问题的提出

  • 多个线程执行的不确定性引起执行结果的不稳定
  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

比如:当你和媳妇去取钱时,2个线程都是取2000元,但是账户里面只有3000,这时如果多线程安全问题不解决,就会出现2个人都能取钱成功。

程序执行是需要时间的,虽然这个时间极短,当一个线程判断为true时,还未执行到扣钱的时候,另一个线程同事也达到了判断条件,并且判断true时,那么2个线程都能取钱,这就是线程安全问题。

image-20201220014906253

包括之前的3个窗口卖票的场景,也会出现线程安全问题。

class Window2 implements Runnable {

    private int ticket = 100;

    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            } else {
//                System.out.println(Thread.currentThread().getName() + "抢不到票,卖光了!");
                break;
            }
        }
    }


}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();java
        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
窗口2:卖票,票号为:100
窗口1:卖票,票号为:100
窗口3:卖票,票号为:100
窗口3:卖票,票号为:97
窗口2:卖票,票号为:97
窗口1:卖票,票号为:97
窗口2:卖票,票号为:94
窗口1:卖票,票号为:94
窗口3:卖票,票号为:94
窗口2:卖票,票号为:91
窗口3:卖票,票号为:91
窗口1:卖票,票号为:91
窗口3:卖票,票号为:88
窗口1:卖票,票号为:88
窗口2:卖票,票号为:88
窗口3:卖票,票号为:85
窗口1:卖票,票号为:85
窗口2:卖票,票号为:85
窗口3:卖票,票号为:82
窗口2:卖票,票号为:82
窗口1:卖票,票号为:82
窗口2:卖票,票号为:79
窗口3:卖票,票号为:79
窗口1:卖票,票号为:79
窗口1:卖票,票号为:76
窗口2:卖票,票号为:76
窗口3:卖票,票号为:76
窗口1:卖票,票号为:73
窗口2:卖票,票号为:73
窗口3:卖票,票号为:73
窗口2:卖票,票号为:70
窗口3:卖票,票号为:70
窗口1:卖票,票号为:70
窗口1:卖票,票号为:67
窗口3:卖票,票号为:67
窗口2:卖票,票号为:67
窗口3:卖票,票号为:64
窗口2:卖票,票号为:64
窗口1:卖票,票号为:64
窗口3:卖票,票号为:61
窗口1:卖票,票号为:61
窗口2:卖票,票号为:61
窗口2:卖票,票号为:58
窗口1:卖票,票号为:58
窗口3:卖票,票号为:58
窗口2:卖票,票号为:55
窗口1:卖票,票号为:55
窗口3:卖票,票号为:55
窗口2:卖票,票号为:52
窗口3:卖票,票号为:52
窗口1:卖票,票号为:52
窗口2:卖票,票号为:49
窗口1:卖票,票号为:49
窗口3:卖票,票号为:49
窗口3:卖票,票号为:46
窗口2:卖票,票号为:46
窗口1:卖票,票号为:46
窗口1:卖票,票号为:43
窗口2:卖票,票号为:43
窗口3:卖票,票号为:43
窗口3:卖票,票号为:40
窗口2:卖票,票号为:40
窗口1:卖票,票号为:40
窗口1:卖票,票号为:37
窗口2:卖票,票号为:37
窗口3:卖票,票号为:37
窗口2:卖票,票号为:34
窗口3:卖票,票号为:34
窗口1:卖票,票号为:34
窗口3:卖票,票号为:31
窗口2:卖票,票号为:31
窗口1:卖票,票号为:31
窗口2:卖票,票号为:28
窗口1:卖票,票号为:28
窗口3:卖票,票号为:28
窗口1:卖票,票号为:25
窗口2:卖票,票号为:25
窗口3:卖票,票号为:25
窗口3:卖票,票号为:22
窗口2:卖票,票号为:22
窗口1:卖票,票号为:22
窗口1:卖票,票号为:19
窗口3:卖票,票号为:19
窗口2:卖票,票号为:19
窗口2:卖票,票号为:16
窗口3:卖票,票号为:16
窗口1:卖票,票号为:16
窗口1:卖票,票号为:13
窗口2:卖票,票号为:13
窗口3:卖票,票号为:13
窗口1:卖票,票号为:10
窗口3:卖票,票号为:10
窗口2:卖票,票号为:10
窗口3:卖票,票号为:7
窗口2:卖票,票号为:7
窗口1:卖票,票号为:7
窗口2:卖票,票号为:4
窗口3:卖票,票号为:4
窗口1:卖票,票号为:4
窗口3:卖票,票号为:1
窗口1:卖票,票号为:1
窗口2:卖票,票号为:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

这里不但会出现重复票的事情,还有可能是负数。

sleep只是为了模拟线程的阻塞状态,目的是提高安全隐患爆发的概率,提升出现0或者1出现的概率。

不能因为概率小就不会出现,不去解决,这是错误的,在程序中,就算是0.01%也要去解决。

否则在现实业务场景中,产生问题造成的经济损失无法估量。可能你不理解这句话的意思,打个夸张的比喻,当坐飞机,每一万次航班出现一次故障,那么它的后果就非常恐怖了。

# 问题的原因

那么一定会出现线程安全问题吗?

也不一定,但是只要操作同一个共享数据时,就可能会发生,比如火车站抢票、明星演唱会抢票、电商网站秒杀活动等等。这些都会存在安全隐患。

就拿上面卖票的案例,票数应该是递减才对,不会出现重票或错票。是的在理想状态下是这样,但是如果是极端状态,别的线程也一起进去操作数据时,就会产生线程安全问题。

理想的状态

image-20201220021135568

极端状态

image-20201220021158731

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

# 解决办法

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

那么怎么实现呢?就是加锁。

# Synchronized关键字

Java对于多线程的安全问题提供了专业的解决方式:同步机制。

Synchronized有两种方式来实现同步机制:

  • 同步代码块
  • 同步方法

同步代码块:

synchronized (同步监视器){
	// 需要被同步的代码;
}
1
2
3
  • 需要被同步的代码:是指操作共享数据的代码,即为需要被同步的代码。
    • 共享数据:多个线程共同操作的变量。比如:本案例中的ticket变量。
  • 同步监视器:俗称:锁。任何一个类的对象,都可以充当锁。
    • 要求:多个线程必须要公用同一把锁。

同步方法

synchronized还可以放在方法声明中,表示整个方法为同步方法

例如:

public synchronized void show (String name){
	....
}
1
2
3

在同步方法中,它的同步监视器其实是this

# 分析同步原理

image-20201220023815785

t1、t2、t3、看谁先抢到同步锁,抢到了同步锁的线程,进入代码块内,操作数据,其他线程等待,等操作完毕,线程将同步锁释放,3个线程又重新开始抢同步锁,一直循环……

在Java中,通过同步机制,来解决了线程的安全问题。但是操作同步代码时,只能有一个线程参与,其他线程等待。相当于一个单线程的过程,效率变低了。

使用同步代码块解决案例线程安全问题

class Window implements Runnable {

    //共享数据
    private int ticket = 100;

    //监视器/锁
   private static final Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
//                System.out.println(Thread.currentThread().getName() + "抢不到票,卖光了!");
                    break;
                }
            }
        }
    }


}

public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

输出结果是递减了,虽然很多次都是窗口1,但是只是概率问题,全部被t1抢到了同步锁,有时还是能有其他线程能同步抢到了。

使用同步方法解决实现Runnable案例线程安全问题

其实这个案例不太适合使用同步方法,因为while也是在操作共享数据时的必要条件。如果使用同步方法,`会导致一个窗口一直售票,其他窗口都拿不到锁。所以还需要定义另一个方法。

class Window2 implements Runnable {

    //共享数据
    private int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
            this.show();
        }
    }

    private synchronized void show() {
        if (ticket > 0) {
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }


}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

使用同步方法处理继承Thread类的方式中的线程安全问题

class Window4 extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
            show();
        }
    }

    private static synchronized void show() {//同步监视器为Window4.class
//        private  synchronized void show() 同步监视器为t1,t2,t3,此种解决方式是错误的
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 关于同步方法的总结

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

  • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
  • 同步方法的锁:
    • 静态方法(类名.class),当前类本身
    • 非静态方法(this),当前对象
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class

注意

  • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全

  • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

    • 需要谨慎的原因是,如果是继承方式实现多线程,是多个对象,this是指当前对象,肯定不是同一把锁,所以需要用类名.class

    • 类名.class,类也是对象。而且也是唯一的对象,因为类只会加载一次。那么就可以成为共用的一把锁。

    • Class clazz = Window.class;
      
      1

# 同步范围

1、如何找问题,即代码是否存在线程安全?(非常重要)

  • 明确哪些代码是多线程运行的代码
  • 明确多个线程是否有共享数据
  • 明确多线程运行代码中是否有多条语句操作共享数据

2、如何解决呢?(非常重要)

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

即所有操作共享数据的这些语句都要放在同步范围中。

3、切记

范围太小:没锁住所有有安全问题的代码

范围太大:没发挥多线程的功能。

# 释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

# 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
    • 应尽量避免使用suspend()和resume()来控制线程8.4 线程的同步

# 懒汉式线程安全问题

使用同步机制,将单例模式中的懒汉式改写为线程安全的。

回顾懒汉式实现步骤:

1、私有化构造器

2、声明当前类的对象

3、提供公共的静态方法返回类的对象

线程安全有问题的写法:

public class Bank{
    //1,私有化构造器
    private Bank() {
        
    }
	
    //2,声明当前类的对象
    private static Bank instance = null;
	
    //3,提供公共的静态方法返回类的对象
    public static Bank getInstance() {
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同步方法解决方案

public class Bank {
    //1,私有化构造器
    private Bank() {

    }
	
    //2,声明当前类的对象
    private static Bank instance = null;
	
    //3,提供公共的静态方法返回类的对象
    public static synchronized Bank getInstance() {
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同步代码块解决方案

public class Bank {
    //1,私有化构造器
    private Bank() {

    }
	
    //2,声明当前类的对象
    private static Bank instance = null;
	
    //3,提供公共的静态方法返回类的对象
    public static Bank getInstance() {
        synchronized (Bank.class) {
            if (instance == null) {
                instance = new Bank();
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这种方式效率稍差,为什么?

当对象未创建时,可以用synchronized来确保只有一个对象被创建,不会产生线程安全问题。

但是对象如果已经被创建后,其实可以直接返回对象了,return不是操作共享数据的,当对象已经创建时,所有线程还是要排队一个个地拿到同步锁,才能获取对象,所以效率稍差。

# 死锁

线程的死锁

产生原因:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

导致问题:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,程序无法继续。

解决方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

演示线程的死锁问题

public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    //提高占用时间,提高死锁概率
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println("s1 = " + s1);
                        System.out.println("s2 = " + s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");
                    //提高占用时间,提高死锁概率
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println("s1 = " + s1);
                        System.out.println("s2 = " + s2);
                    }
                }
            }
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

如果不调用线程的sleep,这个死锁概率很低,都能正常输出,但是现在同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,程序无法继续。

我们使用同步锁时,要避免死锁。

演示死锁问题

package com.saul.deadlock;

class A {
   public synchronized void foo(B b) {
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 进入了A实例的foo方法"); // ①
      try {
         Thread.sleep(200);
      } catch (InterruptedException ex) {
         ex.printStackTrace();
      }
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 企图调用B实例的last方法"); // ③
      b.last();
   }

   public synchronized void last() {
      System.out.println("进入了A类的last方法内部");
   }
}

class B {
   public synchronized void bar(A a) {
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 进入了B实例的bar方法"); // ②
      try {
         Thread.sleep(200);
      } catch (InterruptedException ex) {
         ex.printStackTrace();
      }
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 企图调用A实例的last方法"); // ④
      a.last();
   }

   public synchronized void last() {
      System.out.println("进入了B类的last方法内部");
   }
}

public class DeadLock implements Runnable {
   A a = new A();
   B b = new B();

   public void init() {
      Thread.currentThread().setName("主线程");
      // 调用a对象的foo方法
      a.foo(b);
      System.out.println("进入了主线程之后");
   }

   public void run() {java
      Thread.currentThread().setName("副线程");
      // 调用b对象的bar方法
      b.bar(a);
      System.out.println("进入了副线程之后");
   }

   public static void main(String[] args) {
      DeadLock dl = new DeadLock();
      new Thread(dl).start();
      dl.init();
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

输出结果:

当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 主线程 企图调用B实例的last方法
当前线程名: 副线程 企图调用A实例的last方法
1
2
3
4

程序到这里就无法继续了

我们先来看主线程:

当main方法作为主线程启动后,主线线程调用init方法,然后调用调用a对象的foo方法,foo是一个同步方法,里面又调用b对象的last方法,last方法也是一个同步方法,只有执行完last方法才算执行完。

那么主线程要用到的锁有:

  • 1、A.class
  • 2、B.class

现在来看副线程:

main方法启动后,创建一个DeadLock对象,启动线程对象的strat方法,自动调用run方法,里面是调用b对象的bar方法,bar方法是一个同步方法,里面又调用a对象的last方法,last方法也是一个同步方法,只有执行完last方法才算执行完。

那么分线程要用到的锁有:

  • 1、B.class
  • 2、A.class

那么跟上面的案例很类似,是现在同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。所以程序无法继续了。

这些案例调用Thread.sleep方法只是提高死锁的概率,就算没这个概率,程序还是存在安全隐患,所以我们写程序要避免死锁。

# Lock(锁)

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • **java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。**锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

实现步骤

1、实例化ReentrantLock 对象

2、调用锁定方法lock()

3、把代码块放入try{}当中

4、调用解锁方法unlock().

class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码;
        }finally{
            lock.unlock();  
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

这样说比较抽象,解析来用案例来说明:

就以之前卖票的案例来:

/**
 * 解决线程安全问题的方式三:Lock锁 -- JDK5.0新增
 *
 * @author Saul.J.Wu
 * @date 2020/12/20
 */
class Window implements Runnable {

    private int ticket = 100;
    //实例化`ReentrantLock` 对象
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //调用`ReentrantLock` .`lock`锁定方法
            lock.lock();
            try {
                if (ticket > 0) {
                    Thread.sleep(1);
                    //提高安全隐患概率
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //调用解锁方法`unlock`.
                lock.unlock();
            }
        }
    }
}


public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

这样就解决了线程安全问题,不会出现重票或者错票。

# synchronized 与Lock 的对比

  • synchronized 与Lock都是用来解决线程安全问题

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。

  • Lock只有代码块锁,synchronized有代码块锁和方法锁

  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

在开发当中,对于解决线程安全问题,优先使用顺序:Lock >>>同步代码块(已经进入了方法体,分配了相应资源)>>>>同步方法(在方法体之外)

虽然很多源码都是用synchronized ,那是因为那些源码是很久以前写的。

# 练习

银行有一个账户。

有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

问题:该程序是否有安全问题,如果有,如何解决?

【提示】

1,明确哪些代码是多线程运行代码,须写入run()方法

2,明确什么是共享数据。

3,明确多线程运行代码中哪些语句是操作共享数据的。

【分析】

1、是否是多线程问题?是,两个储户

2、是否有共享数据?有,账户(或账户余额)

3、是否有线程安全问题?有

4、需要考虑解决线程安全问题?同步机制:有三种方式。

  • Lock关键字
  • synchronized 同步代码块
  • synchronized 同步方法

未解决线程安全问题

class Account {

    private double balacne;

    public Account(double balacne) {
        this.balacne = balacne;
    }

    /**
     * 存钱
     *
     * @param amt 要存的金额
     */
    public void deposit(double amt) {
        double temp = balacne;
        balacne += amt;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "存入" + amt + ",余额:" + temp + "=>" + balacne);
    }
}

class Customer extends Thread {

    private Account account;

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account account = new Account(0);
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  • 输出结果:
甲存入1000.0,余额:0.0=>2000.0
乙存入1000.0,余额:1000.0=>2000.0
甲存入1000.0,余额:2000.0=>4000.0
乙存入1000.0,余额:3000.0=>4000.0
乙存入1000.0,余额:5000.0=>6000.0
甲存入1000.0,余额:4000.0=>6000.0
1
2
3
4
5
6

未解决线程安全问题,明显存在问题。

这里我只使用synchronized 同步方法就解决了

因为此时只有一个账户,直接使用synchronized 同步方法就解决了。直接在操作共享数据的方法上加上关键字synchronized。

public synchronized void deposit(double amt) {
    double temp = balacne;
    balacne += amt;
    try {
        Thread.sleep(1000java);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "存入" + amt + ",余额:" + temp + "=>" + balacne);
}
1
2
3
4
5
6
7
8
9
10

输出结果:

甲存入1000.0,余额:0.0=>1000.0
甲存入1000.0,余额:1000.0=>2000.0
甲存入1000.0,余额:2000.0=>3000.0
乙存入1000.0,余额:3000.0=>4000.0
乙存入1000.0,余额:4000.0=>5000.0
乙存入1000.0,余额:5000.0=>6000.0
1
2
3
4
5
6

这样就保证线程安全问题了。

帮我改善此页面 (opens new window)
#synchronized关键字#同步锁#死锁#lock关键字
上次更新: 2020/12/23, 03:51:52
线程的生命周期
线程的通信

← 线程的生命周期 线程的通信→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式