线程安全问题及解决
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作
,那么不会发生线程安全问题。但是多个线程中对资源有读和写
的操作。就容易出现线程安全问题。
举例:
同一个资源问题和线程安全问题
案例:
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。
例题: 开启三个窗口售票,总票数为100张。
分别使用两种线程创建方式实现:
理想状态:
极端状态:
实现Runnable接口方式:
package thread.demo03_threadsafe.notsafe;
//使用实现Runnable接口的方式,实现卖票
public class WindowTest {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
//正常运行后发现每个窗口都卖了100号票
}
}
class SaleTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
try {
Thread.sleep(10);//有了这一段运行后看结果竟然还出现-1号票,这就是线程的安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket);
ticket--;
}else {
break;
}
}
}
}
继承Thread方式:
package thread.demo03_threadsafe.notsafe;
//使用继承Thread类的方式,实现卖票。
public class WindowTest1 {
public static void main(String[] args) {
SaleTicket1 s1 = new SaleTicket1("窗口1");
SaleTicket1 s2 = new SaleTicket1("窗口2");
SaleTicket1 s3 = new SaleTicket1("窗口3");
s1.start();
s2.start();
s3.start();
//正常运行后还是有重票三个100,甚至错票-1
}
}
class SaleTicket1 extends Thread{
public SaleTicket1() {
}
public SaleTicket1(String name) {
super(name);
}
static int ticket = 100;//没有static直接300张票,每个窗口都卖1——100
//一个对象一份的是实例变量。
//所有对象一份的是静态变量。
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "票号为: " + ticket);
ticket--;
}else {
break;
}
}
}
}
以上两种线程创建方式运行后都出现了线程安全问题!!
怎么解决?看下面
线程的安全问题与线程的同步机制
1. 多线程卖票出现的问题:出现了重票和错票
2.什么原因导致的? 线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作。
3.如何解决? 必须保证一个线程a在操作ticket的过程中,其他线程必须等待,
直到线程a操作ticket结束以后,其它线程才可以进来继续操作ticket。(ticket是共享数据)
比如上厕所几个坑位,坑位就是共享数据,排队进去后,上完,后面的人才能进来,不能一次进去好几个人,这样才安全。
4.Java是如何解决线程的安全问题? 使用线程的同步机制。
方式1:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
> 需要被同步的代码,及为操作共享数据的代码
> 共享数据:即多个线程都需要操作的数据。比如:ticket
> 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
> 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
比如一个坑位,有个门锁,你进去把门锁上别人就进不来了,上完厕所把锁打开其他人才能进来。
> 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。
注意:在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。
*下面的同步方法解决安全问题下节课看*
方式2:同步方法
说明:
5.synchronized的好处:
使用 同步代码块 解决两种线程创建方式的线程安全问题
解决实现Runnable接口方式出现的线程安全问题:
package thread.demo03_threadsafe.runnablesafe;
//使用实现Runnable接口的方式,实现卖票。---> 存在线程安全问题
//使用同步代码块解决上述卖票中的线程安全问题。
public class WindowTest {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
//运行后正常,解决了线程安全问题,达到了预期结果。
}
}
class SaleTicket implements Runnable{
int ticket = 100;//ticket是共享数据
Object obj = new Object();
Dog dog = new Dog();
@Override
public void run() {
// synchronized(this){ //全部用synchronized包起来运行查看结果全是窗口一,一个线程进来一直循环完才出来,不合适
while (true){
try {
Thread.sleep(5); //试了两次发现运行结果全是同一个窗口卖完了,这段代码就让进来的睡一下,其他的窗口就能获取到了。
} catch (InterruptedException e) {
e.printStackTrace();
}
// synchronized(obj){ //要想保证安全性,对象必须是唯一的。 obj是唯一的?yes
// synchronized(dog){ //dog:是唯一的?yes
//this:最方便的。它是唯一的?yes,this是run方法的对象,也就是SaleTicket的对象,启动类中new了一下它,就是题目中的s,是唯一的。
synchronized(this){ //这个可以
// synchronized (SaleTicket.class){ 后面学的这个对象也是唯一的,而且靠谱,也能用。
if (ticket > 0){
try {
Thread.sleep(10);//有了这一段运行后看结果竟然出现-1号票,这就是线程的安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket);
ticket--;
}else {
break;
}
}
}
}
}
class Dog{
}
分析线程同步机制原理:
解决继承Thread方式出现的线程安全问题:
package thread.demo03_threadsafe.threadsafe;
//使用继承Thread类的方式,实现卖票。
//使用同步代码块的方式解决线程安全问题。
public class WindowTest {
public static void main(String[] args) {
SaleTicket1 s1 = new SaleTicket1("窗口1");
SaleTicket1 s2 = new SaleTicket1("窗口2");
SaleTicket1 s3 = new SaleTicket1("窗口3");
s1.start();
s2.start();
s3.start();
//运行后正常,解决了线程安全问题,达到了预期结果。
}
}
class SaleTicket1 extends Thread{
public SaleTicket1() {
}
public SaleTicket1(String name) {
super(name);
}
static int ticket = 100;//没有static直接300张票,每个窗口都卖1——100
//一个对象一份的是实例变量。
//所有对象一份的是静态变量。
static Object obj = new Object();//加了static就是全局唯一的。
@Override
public void run() {
while (true){
// synchronized (this) { //this不安全,this就是这个方法的对象,也就是SaleTicket1类的对象,被new了三次,不是唯一的,不安全。
// synchronized (obj) { //obj:使用static就能保证其唯一性,这个可以用,但是还有没有现成的而且还唯一的?
synchronized (SaleTicket1.class) { //这里应该放对象,看起来像类,这个比较超纲,后续学到反射讲解,这其实也是一个对象。
//结构:Class clz = SaleTicket1.class SaleTicket1.class本来就是Class的对象,是唯一的。
//大写Class就相当于一个类,它的对象就是具体的某个具体的类。
if (ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为: " + ticket);
ticket--;
}else {
break;
}
}
}
}
}