1.线程安全问题及解决.
1.1线程安全问题介绍.
- 当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
- 举例:
1.2线程安全问题案例演示-卖票.
- 案例:火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。
- 分析,采用哪种线程实现方式?
- 分析一:多个窗口(线程)共同卖100张票,也就是多个线程做同样的一件事情:卖这100张票。线程和线程之间数据是共享的,采用实现Runnable接口方式实现线程。
- 分析二:采用继承Thread类方式也可以实现,需要在线程类里面把票的总量定义成静态的全局变量:static int piao = 100.
- 通过实现Runnable接口方式实现该案例:
- 运行结果如下:
- 从结果上可以发现,出现了一票多卖的情况,这是多条线程在同一时间访问同一资源的原因造成的【资源访问冲突】,也就是线程安全问题。
1.3同步机制解决线程安全问题.
- 线程同步,即:排队。
- 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。如下图所示:
- 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
- 为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(blocked:封锁的/闭塞的/阻塞/堵塞)。
1.4同步机制解决线程安全问题的原理.
- 同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。
- 因为Java对象在堆中的数据分为分为对象头、实例变量、空白的填充。而对象头中包含:
- Mark Word:记录了和当前对象有关的GC、锁标记等信息。
- 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。
- 数组长度(只有数组对象才有)。
- 哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用“同步锁”对象。
1.5同步代码块和同步方法.
1.5.1同步代码块.
- 同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
- 语法如下:
- 同步锁:里的参数是同步监视器,俗称“同步锁”。同步锁必须一个唯一的对象(可以是常量,但不能是静态的常量),多条线程必须公用一把锁。如何判断对象是唯一的?比如可以在Runnable匿名实现类里面定义一个 Object obj = new Object();对象,然后 obj对象就可以作为同步锁。对象obj是唯一的前提条件是,obj对象所在的类只被实例化了一次,如果实例化多次,那么 obj对象就不是唯一的了。如果为了方便,不想再重新定义一个唯一的对象,可以采用 this对象作为同步锁,this对象代表调用当前方法的对象。
- 被同步的代码:需要被同步的代码指的是,操作共享数据的代码;
- 共享数据是:多个线程共同操作的变量。
- 通过同步代码块解决卖票案例问题:
- 注1:while循环不能被锁。分析:如果while循环也被锁了,那么第一个拿到锁的线程就会不停的卖票,直到把票卖完,这样100张票就会被第一个抢到锁的线程卖完(代码验证)。
- 注2:这里的同步锁 this代表调用当前 run方法的对象,也就是 runnable对象,该对象只被创建了一个,是唯一的,满足同步锁的要求。
1.5.2同步方法.
- 同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
- 被 synchronized 锁住的方法,同步锁是 this对象,this对象代表调用当前方法的对象。
- 语法如下:
- 通过同步方法方式解决卖票案例问题:
测试