多线程编程需要考虑最多的问题就是线程安全问题, 线程安全指的就是共享资源的同步问题. 所谓同步就是指多个线程在同一个时间段内只能由一个线程进行操作, 其他线程需要等待正在操作的线程完成操作之后才可以继续执行.
1 深入了解 Thread 类和 Runnable 接口
通过继承 Thread 类和实现 Runnable 接口都可以实现多线程的创建, 那么两者有哪些联系和区别呢?
01,Thread 类的定义
以下代码片段, 摘自 JDK 源码里 Thread 类的部分定义:
public class Thread implements Runnable {
通过以上 Thread 类的定义, 可以看出, Thread 类是 Runnable 接口的子类, 但在 Thread 类中并没有完全地实现 Runnable 接口中的 run 方法. Thread 类中的 run 方法调用的是 target 的 run 方法, 而 target 则是 Runnable 接口的子类, 所以通过继承 Thread 类实现多线程, 则该线程子类必须覆写 run 方法.
02, 通过实现 Runnable 接口创建多线程的 UML 图
通过以上的 UML 类图, 我们可以发现, Thread 类和 MyThread 类都实现了 Runnable 接口, 通过实现 Runnable 接口来创建线程类 MyThread, 其实就是把 MyThread 类放到了 Thread 类的 target 变量中, 线程启动后, 实际上是调用的 target 的 run 方法, 也就是 MyThread 类中的 run 方法, 所以说 MyThread 类必须覆写 run 方法. 这种操作模式是典型的代理设计模式.
通过以上 2 个部分的说明, 我们可以很明显地看出 Thread 类和 Runnable 接口的联系, 那就是 Thread 类实现了 Runnable 接口.
03, 通过继承 Thread 类实现的多线程能实现资源共享吗
答案是不能, 如果一个类继承 Thread 类的话, 它就不适合多个线程之间共享资源.
示例: 通过继承 Thread 类的方式实现模拟售票场景, 假设有 5 张电影票, 两个窗口同时卖这 5 张电影票.
public class SaleTicketTask extends Thread {
从以上的程序运行结果来看, 通过继承 Thread 类实现的多线程, 每一个窗口都卖了 5 张电影票, 没有达到 5 张电影票资源共享的目的.
04, 通过实现 Runnable 接口创建的多线程可以实现资源共享
如果一个类继承 Thread 类的话, 它就不适合多个线程之间共享资源, 但是一个类实现了 Runnable 接口的话, 就可以方便的实现资源共享.
示例: 通过实现 Runnable 接口的方式实现模拟售票场景, 假设有 5 张电影票, 两个窗口同时卖这 5 张电影票.
public class SaleTicketTask1 implements Runnable {
从以上的运行结果来看, 通过实现 Runnable 接口实现的多线程, 两个窗口共卖了 5 张电影票, 达到了 5 张电影票资源共享的目的.
实现 Runnable 接口相对于继承 Thread 类来说, 有以下 3 个优势:
适合多个相同程序代码的线程去处理同一资源的情况;
可以避免由于 Java 的单继承特性带来的局限;
代码能够被多个线程共享, 代码与数据可以独立;
因此, 在开发中建议通过实现 Runnable 接口来创建多线程.
2 演示线程安全问题
上面通过实现 Runnable 接口实现的线程, 达到了两个窗口卖出 5 张电影票的目的, 那么请大家思考, 以上的程序是线程安全的吗?
答案是以上的程序不是线程安全的, 可能出现重复售票的情况.
01, 演示共享资源的线程不安全问题
我们可以在上面的 SaleTicketTask1 类中的 run 方法中加入一段线程休眠的代码, 来放大这种线程不安全性, 再次观察程序的运行情况.
public class SaleTicketTask1 implements Runnable {
通过上面两次程序的运行结果很明显的可以看出, 两个线程窗口售票出现了共享资源 (5 张电影票) 不安全的情况, 第一次的程序卖出了 3 张重复票, 第二次的程序票已近卖完了, 依然又卖出了一张票号为 0 的票.
3 解决线程安全问题
在多线程编程中, 遇到多个线程要操作同一个资源的时候, 就有可能出现共享资源的同步问题. 要想解决这样的问题, 就必须使用同步.
所谓同步就是指多个线程在同一个时间段内只能由一个线程进行操作, 其他线程需要等待正在操作的线程完成操作之后才可以继续执行.
在 Java 编程中, 实现同步操作的关键字是 synchronized, 解决资源共享的同步操有两种方法: 一是使用同步代码块, 二是使用同步方法.
01, 使用同步代码块解决共享资源的线程不安全问题
代码块就是指用 "{}" 括起来的一段代码, 如果再代码块上加上 synchronized 关键字, 则此代码块就是同步代码块.
同步代码块的格式如下:
synchronized ( 同步对象 ){
在使用同步代码块时必须指定一个同步对象, 一般将当前对象 (this) 设置成同步对象.
示例 1: 使用同步代码块解决以上售票的同步问题
public class SaleTicketTask1 implements Runnable {
通过上面的结果可以看出, 加入同步代码块确实解决了共享资源电影票安全性的问题, 但是上面的示例代码, 在 run 方法开始的时候就加入了 synchronized 同步, 导致只有一个窗口在卖票, 因此以上的代码是不完美的.
示例 2: 使用同步代码块解决以上售票的同步问题
public class SaleTicketTask1 implements Runnable {
说明, 上面的程序通过同步代码块, 完成了共享资源的同步安全性问题.
02, 使用 **** 同步方法 **** 解决共享资源的线程不安全问题
同步方法顾名思义就是在方法的声明的时候加上 synchronized 关键字.
Java 中方法定义的完整格式如下:
访问权限{public|default|protected|private} [final] [static]
示例: 使用同步方法解决以上售票的同步问题
public class SaleTicketTask2 implements Runnable {
说明, 上面的程序通过同步方法, 也能完成共享资源的同步安全性问题.
以上内容对多线程的两种实现方式的联系和区别, 对多线程编程中共享资源同步问题的解决进行了说明, 希望大家可以熟练掌握.
来源: http://www.jianshu.com/p/3199d7b59d4c