线程安全
假如 Java 程序中有多个线程在同时运行, 而这些线程可能会同时运行一部分的代码. 如果说该 Java 程序每次运行的结果和单线程的运行结果是一样的, 并且其他的变量值也都是和预期的结果是一样的, 那么就可以说线程是安全的.
解析什么是线程安全: 卖电影票案例
假如有一个电影院上映《葫芦娃大战奥特曼》, 售票 100 张(1-100 号), 分三种情况卖票:
情况 1
该电影院开设一个售票窗口, 一个窗口卖一百张票, 没有问题. 就如同单线程程序不会出现安全问题一样.
情况 2
该电影院开设 n(n>1)个售票窗口, 每个售票窗口售出指定号码的票, 也不会出现问题. 就如同多线程程序, 没有访问共享数据, 不会产生问题.
情况 3
该电影院开设 n(n>1)个售票窗口, 每个售票窗口出售的票都是没有规定的(如: 所有的窗口都可以出售 1 号票), 这就会出现问题了, 假如三个窗口同时在卖同一张票, 或有的票已经售出, 还有窗口还在出售. 就如同多线程程序, 访问了共享数据, 会产生线程安全问题.
卖 100 张电影票 Java 程序实现: 出现情况 3 类似情况
- public class MovieTicket01 implements Runnable {
- /**
- * 电影票数量
- */
- private static int ticketNumber = 100;
- /**
- * 在实现类中重写 Runnable 接口的 run 方法, 并设置此线程要执行的任务
- */
- @Override
- public void run() {
- // 设置此线程要执行的任务
- while (ticketNumber> 0) {
- // 提高程序安全的概率, 让程序睡眠 10 毫秒
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 电影票出售
- System.out.println("售票窗口 (" + Thread.currentThread().getName() + ") 正在出售:" + MovieTicket01.ticketNumber + "号电影票");
- ticketNumber --;
- }
- }
- }
- // 测试
- public class Demo01MovieTicket {
- public static void main(String[] args) {
- // 创建一个 Runnable 接口的实现类对象.
- MovieTicket01 movieTicket = new MovieTicket01();
- // 创建 Thread 类对象, 构造方法中传递 Runnable 接口的实现类对象(三个窗口).
- Thread window0 = new Thread(movieTicket);
- Thread window1 = new Thread(movieTicket);
- Thread window2 = new Thread(movieTicket);
- // 设置一下窗口名字, 方便输出确认
- window0.setName("window0");
- window1.setName("window1");
- window2.setName("window2");
- // 调用 Threads 类中的 start 方法, 开启新的线程执行 run 方法
- window0.start();
- window1.start();
- window2.start();
- }
- }
控制台部分输出:
售票窗口 (window0) 正在出售: 100 号电影票
售票窗口 (window2) 正在出售: 99 号电影票
售票窗口 (window1) 正在出售: 100 号电影票
售票窗口 (window0) 正在出售: 97 号电影票
售票窗口 (window2) 正在出售: 97 号电影票
售票窗口 (window1) 正在出售: 97 号电影票
售票窗口 (window1) 正在出售: 94 号电影票
售票窗口 (window2) 正在出售: 94 号电影票
.
.
.
.
.
.
售票窗口 (window0) 正在出售: 7 号电影票
售票窗口 (window2) 正在出售: 4 号电影票
售票窗口 (window0) 正在出售: 4 号电影票
售票窗口 (window1) 正在出售: 2 号电影票
售票窗口 (window1) 正在出售: 1 号电影票
售票窗口 (window2) 正在出售: 0 号电影票
售票窗口 (window0) 正在出售:-1 号电影票
可以看到, 三个窗口 (线程) 同时出售不指定号数的票(访问共享数据), 出现了卖票重复, 和出售了不存在的票号数(0,-1)
Java 程序中为什么会出现这种情况
在 CPU 线程的调度分类中, Java 使用的是抢占式调度.
我们开启了三个线程, 3 个线程一起在抢夺 CPU 的执行权, 谁能抢到谁就可以被执行.
从输出结果可以知道, 刚开始抢夺 CPU 执行权的时候, 线程 0(window0 窗口)先抢到, 再到线程 1(window1 窗口)抢到, 最后线程 2(window2 窗口)才抢到.
那么为什么 100 号票已经在 0 号窗口出售了, 在 1 号窗口还会出售呢? 其实很简单, 线程 0 先抢到 CPU 执行权, 于是有了执行权后, 他就开始嚣张了, 作为第一个它通过 while 判断, 很自豪的拿着 ticketNumber = 100 进入 while 里面开始执行.
可线程 0 是万万没有想到, 这时候的线程 1, 在拿到执行权后, 在线程 0 刚刚实现 print 语句还没开始 ticketNumber -- 的时候, 线程 1 以 ticketNumber = 100 跑进了 while 里面.
线程 2 很遗憾, 在线程 0 执行了 ticketNumber -- 了才急匆匆的进入 while 里面, 不过它也不甘落后, 于是拼命追赶. 终于, 后来居上, 在线程 1 还没开始 print 的时候, 他就开始 print 了. 于是便出现了控制台的前三条输出的情况.
售票窗口 (window0) 正在出售: 100 号电影票
售票窗口 (window2) 正在出售: 99 号电影票
售票窗口 (window1) 正在出售: 100 号电影票
window0,window1,window2 分别对应线程 0, 线程 1, 线程 2
以此类推, 直到最后程序执行完毕.
解决情况 3 的共享数据问题
通过线程的同步, 来解决共享数据问题. 有三种方式, 分别是同步代码块, 同步方法, 锁机制.
同步代码块
- public class MovieTicket02 implements Runnable {
- /**
- * 电影票数量
- */
- private static int ticketNumber = 100;
- /**
- * 创建锁对象
- */
- Object object = new Object();
- /**
- * 在实现类中重写 Runnable 接口的 run 方法, 并设置此线程要执行的任务
- */
- @Override
- public void run() {
- // 设置此线程要执行的任务
- synchronized (object) {
- // 把访问了共享数据的代码放到同步代码中
- while (ticketNumber> 0) {
- // 提高程序安全的概率, 让程序睡眠 10 毫秒
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 电影票出售
- System.out.println("售票窗口 (" + Thread.currentThread().getName() + ") 正在出售:" + MovieTicket02.ticketNumber + "号电影票");
- ticketNumber --;
- }
- }
- }
- }
- // 进行测试
- public class Demo02MovieTicket {
- public static void main(String[] args) {
- // 创建一个 Runnable 接口的实现类对象.
- MovieTicket02 movieTicket = new MovieTicket02();
- // 创建 Thread 类对象, 构造方法中传递 Runnable 接口的实现类对象(三个窗口).
- Thread window0 = new Thread(movieTicket);
- Thread window1 = new Thread(movieTicket);
- Thread window2 = new Thread(movieTicket);
- // 设置一下窗口名字, 方便输出确认
- window0.setName("window0");
- window1.setName("window1");
- window2.setName("window2");
- // 调用 Threads 类中的 start 方法, 开启新的线程执行 run 方法
- window0.start();
- window1.start();
- window2.start();
- }
- }
控制台输出:
售票窗口 (window0) 正在出售: 100 号电影票
售票窗口 (window0) 正在出售: 99 号电影票
售票窗口 (window0) 正在出售: 98 号电影票
售票窗口 (window0) 正在出售: 97 号电影票
售票窗口 (window0) 正在出售: 96 号电影票
.
.
.
.
.
.
售票窗口 (window0) 正在出售: 5 号电影票
售票窗口 (window0) 正在出售: 4 号电影票
售票窗口 (window0) 正在出售: 3 号电影票
售票窗口 (window0) 正在出售: 2 号电影票
售票窗口 (window0) 正在出售: 1 号电影票
这时候, 控制台不再出售不存在的电影号数以及重复的电影号数了.
通过代码块中的锁对象, 可以使用任意的对象. 但是必须保证多个线程使用的锁对象是同一. 锁对象作用: 把同步代码块锁住, 只让一个线程在同步代码块中执行.
总结: 同步中的线程, 没有执行完毕, 不会释放锁, 同步外的线程, 没有锁, 进不去同步.
同步方法
- public class MovieTicket03 implements Runnable {
- /**
- * 电影票数量
- */
- private static int ticketNumber = 100;
- /**
- * 创建锁对象
- */
- Object object = new Object();
- /**
- * 在实现类中重写 Runnable 接口的 run 方法, 并设置此线程要执行的任务
- */
- @Override
- public void run() {
- // 设置此线程要执行的任务
- ticket();
- }
- public synchronized void ticket() {
- // 把访问了共享数据的代码放到同步代码中
- while (ticketNumber> 0) {
- // 提高程序安全的概率, 让程序睡眠 10 毫秒
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 电影票出售
- System.out.println("售票窗口 (" + Thread.currentThread().getName() + ") 正在出售:" + MovieTicket03.ticketNumber + "号电影票");
- ticketNumber --;
- }
- }
- }
测试与同步代码块一样.
锁机制(Lock 锁)
在 Java 中, Lock 锁机制又称为同步锁, 加锁 public void lock(), 释放同步锁 public void unlock().
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- public class MovieTicket05 implements Runnable {
- /**
- * 电影票数量
- */
- private static int ticketNumber = 100;
- Lock reentrantLock = new ReentrantLock();
- /**
- * 在实现类中重写 Runnable 接口的 run 方法, 并设置此线程要执行的任务
- */
- @Override
- public void run() {
- // 设置此线程要执行的任务
- while (ticketNumber> 0) {
- reentrantLock.lock();
- // 提高程序安全的概率, 让程序睡眠 10 毫秒
- try {
- Thread.sleep(10);
- // 电影票出售
- System.out.println("售票窗口 (" + Thread.currentThread().getName() + ") 正在出售:" + MovieTicket05.ticketNumber + "号电影票");
- ticketNumber --;
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- reentrantLock.unlock();
- }
- }
- }
- }
- // 测试
- public class Demo05MovieTicket {
- public static void main(String[] args) {
- // 创建一个 Runnable 接口的实现类对象.
- MovieTicket05 movieTicket = new MovieTicket05();
- // 创建 Thread 类对象, 构造方法中传递 Runnable 接口的实现类对象(三个窗口).
- Thread window0 = new Thread(movieTicket);
- Thread window1 = new Thread(movieTicket);
- Thread window2 = new Thread(movieTicket);
- // 设置一下窗口名字, 方便输出确认
- window0.setName("window0");
- window1.setName("window1");
- window2.setName("window2");
- // 调用 Threads 类中的 start 方法, 开启新的线程执行 run 方法
- window0.start();
- window1.start();
- window2.start();
- }
- }
控制台部分输出:
售票窗口 (window0) 正在出售: 100 号电影票
售票窗口 (window0) 正在出售: 99 号电影票
售票窗口 (window0) 正在出售: 98 号电影票
售票窗口 (window0) 正在出售: 97 号电影票
售票窗口 (window0) 正在出售: 96 号电影票
.
.
.
.
.
.
售票窗口 (window1) 正在出售: 7 号电影票
售票窗口 (window1) 正在出售: 6 号电影票
售票窗口 (window1) 正在出售: 5 号电影票
售票窗口 (window1) 正在出售: 4 号电影票
售票窗口 (window1) 正在出售: 3 号电影票
售票窗口 (window2) 正在出售: 2 号电影票
售票窗口 (window1) 正在出售: 1 号电影票
与前两种方式不同, 前两种方式, 只有线程 0 能够进入同步机制执行代码, Lock 锁机制, 三个线程都可以进行执行, 通过 Lock 锁机制来解决共享数据问题.
Java 多线程安全问题就到这里了, 如果有什么不足, 错误的地方, 希望大佬们指正.
来源: https://www.cnblogs.com/liyihua/p/12210893.html