6.1多线程并发经典案例
目录介绍
- 01.先来看一个案例
- 1.1 卖电影票案例1
- 1.2 卖电影票案例2
- 1.3 卖电影票案例3
- 02.线程安全问题探讨
- 2.1 思考一下电影票案例
- 2.2 为何出现脏数据
- 2.3 线程模型数据私有化
- 2.4 解决多线程安全思路
- 2.5 如何解决多线程安全
- 03.线程安全解决实践
- 3.1 synchronized解决同步问题
- 3.2 ReentrantLock解决同步问题
- 3.3 CAS乐观锁解决同步问题
- 3.4 volatile关键字来保证可见性
- 04.处理多线程并发使用
- 4.1 synchronized使用场景
- 4.2 ReentrantLock使用场景
- 4.3 CAS锁使用场景
- 4.4 volatile关键字使用场景
01.先来看一个案例
1.1 卖电影票案例1
- 继承Thread类的方式卖电影票案例,代码如下所示
public class ThreadDemo { public static void main(String[] args) { /** * 需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。 */ // 创建3个线程对象 SellTicktes t1 = new SellTicktes() ; SellTicktes t2 = new SellTicktes() ; SellTicktes t3 = new SellTicktes() ; // 设置名称 t1.setName("窗口1") ; t2.setName("窗口2") ; t3.setName("窗口3") ; // 启动线程 t1.start() ; t2.start() ; t3.start() ; } } public class SellTicktes extends Thread { private static int num = 100 ; @Override public void run() { /** * 定义总票数 * * 如果我们把票数定义成了局部变量,那么表示的意思是每一个窗口出售了各自的100张票; 而我们的需求是: 总共有100张票 * 而这100张票要被3个窗口出售; 因此我们就不能把票数定义成局部变量,只能定义成成员变量 */ // 模拟售票 while(num != 0) { if( num > 0 ) { System.out.println(Thread.currentThread().getName() + " 正在出售 " + (num--) + " 张票"); } } } }
1.2 卖电影票案例2
- 实现Runnable接口的方式卖电影票,代码如下所示,假设有三个窗口
public class SellTicektesDemo { public static void main(String[] args) { // 创建SellTicektes对象 SellTicektes st = new SellTicektes() ; // 创建Thread对象 Thread t1 = new Thread(st , "窗口1") ; Thread t2 = new Thread(st , "窗口2") ; Thread t3 = new Thread(st , "窗口3") ; // 启动线程 t1.start() ; t2.start() ; t3.start() ; } } public class SellTicektes implements Runnable { private static int num = 100 ; @Override public void run() { while(true) { if(num > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + (num--) + "张票"); } } } }
1.3 卖电影票案例3
- 讲解过电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况。
- 所以,在出售一张票以后,需要一点时间的延迟。改实现接口方式的卖票程序,每次卖票延迟100毫秒
public class ThreadDemo { public static void main(String[] args) { // 创建3个线程对象 SellTicktes t1 = new SellTicktes() ; SellTicktes t2 = new SellTicktes() ; SellTicktes t3 = new SellTicktes() ; // 设置名称 t1.setName("窗口1") ; t2.setName("窗口2") ; t3.setName("窗口3") ; // 启动线程 t1.start() ; t2.start() ; t3.start() ; } } public class SellTicktes extends Thread { private static int num = 100 ; @Override public void run() { // 模拟售票 while(num != 0) { if( num > 0 ) { try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售" + (num--) + "张票"); } } } }
02.线程安全问题探讨
2.1 思考一下电影票案例
2.2 为何出现脏数据
- 首先想为什么出现问题?
- 是否是多线程环境,是否有共享数据,是否有多条语句操作共享数据
2.3 线程模型数据私有化
2.4 解决多线程安全思路
- 如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境。怎么实现呢?把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
2.5 如何解决多线程安全
- 保证原子性
- 锁和同步:lock锁 或者 synchronized同步
- CAS
- 保证可见性
- volatile:比如单利对象用volatile修饰
- 保证有序性
- synchronized和锁保证顺序性
03.线程安全解决实践
3.1 synchronized解决同步问题
- 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能
- 可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。
synchronized(对象){ 需要同步的代码; }
- 同步代码块优势和劣势
- 同步的好处: 同步的出现解决了多线程的安全问题。
- 同步的弊端: 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
3.2 ReentrantLock解决同步问题
- ReentrantLock,一个可重入的互斥锁
- 它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
- 针对上述方法,具体的解决方式如下:
public class SellTicktes implements Runnable { // 当前拥有的票数 private int num = 100; ReentrantLock lock = new ReentrantLock(); public void run() { while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { } lock.lock(); // 输出卖票信息 if (num > 0) { System.out.println(Thread.currentThread().getName() + ".....sale...." + num--); } lock.unlock(); } } }
3.3 CAS乐观锁解决同步问题
- 基础类型变量自增(i++)是一种常被误以为是原子操作而实际不是的操作。
- Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。
public static class SellTicktes extends Thread { private static AtomicInteger integer = new AtomicInteger(100); @Override public void run() { // 模拟售票 while (integer.get() != 0) { if (integer.get() > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //CAS乐观锁解决同步问题 System.out.println(Thread.currentThread().getName() + "正在出售" + integer.getAndDecrement() + "张票"); } } } }
3.4 volatile关键字来保证可见性
- Java提供了volatile关键字来保证可见性。
- 当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。
public static class SellTicktes4 extends Thread { private static volatile int num = 100; @Override public void run() { // 模拟售票 while (num != 0) { if (num > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售 " + (num--) + " 张票"); } } } }