Java多线程--同步机制解决线程安全问题方式一:同步代码块

文章目录

  • 一、介绍
  • 二、原理
  • 三、同步锁机制
    • (1)synchronized的锁是什么
    • (2)同步操作的思考顺序
    • (3)代码演示
  • 四、同步代码块
    • (1)同步代码块--案例1
      • 1、案例1
      • 2、分析同步原理
      • 3、案例1之this的使用
      • 4、案例1的补充
    • (2)同步代码块--案例2
      • 1、案例2之this的问题
      • 2、案例2之static修饰
      • 3、案例2之类.class
    • (3)注意要点

一、介绍

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制
(synchronized)来解决。
image.png

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。

也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

二、原理

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁

Java对象在中的数据分为对象头、实例变量、空白的填充

对象头中包含:

  • Mark Word:记录了和当前对象有关的GC、锁标记等信息。
  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。
  • 数组长度(只有数组对象才有)。

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程“释放”了锁对象,其他线程才能重新获得/占用“同步锁”对象。

第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

三、同步锁机制

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。

防止这种冲突的方法就是当资源被一个任务使用时,在其上加

(1)synchronized的锁是什么

同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。

对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为this或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:

  • 静态方法:当前类的Class对象(类名.class
  • 非静态方法:this

(2)同步操作的思考顺序

<1> 如何找问题,即代码是否存在线程安全?(非常重要)

(1)明确哪些代码是多线程运行的代码

(2)明确多个线程是否有共享数据

(3)明确多线程运行代码中是否有多条语句操作共享数据

<2> 如何解决呢?(非常重要)

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

即所有操作共享数据的这些语句都要放在同步范围中

<3> 切记

范围太小:不能解决安全问题

范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。

(3)代码演示

示例一:静态方法加锁

package com.atguigu.safe;class TicketSaleThread extends Thread{private static int ticket = 100;public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票while (ticket > 0) {saleOneTicket();}}public synchronized static void saleOneTicket(){//锁对象是TicketSaleThread类的Class对象,而一个类的Class对象在内存中肯定只有一个if(ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);ticket--;}}
}
public class SaleTicketDemo3 {public static void main(String[] args) {TicketSaleThread t1 = new TicketSaleThread();TicketSaleThread t2 = new TicketSaleThread();TicketSaleThread t3 = new TicketSaleThread();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}

示例二:非静态方法加锁

package com.atguigu.safe;public class SaleTicketDemo4 {public static void main(String[] args) {TicketSaleRunnable tr = new TicketSaleRunnable();Thread t1 = new Thread(tr, "窗口一");Thread t2 = new Thread(tr, "窗口二");Thread t3 = new Thread(tr, "窗口三");t1.start();t2.start();t3.start();}
}class TicketSaleRunnable implements Runnable {private int ticket = 100;public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票while (ticket > 0) {saleOneTicket();}}public synchronized void saleOneTicket() {//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以if (ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);ticket--;}}
}

示例三:同步代码块

package com.atguigu.safe;public class SaleTicketDemo5 {public static void main(String[] args) {//2、创建资源对象Ticket ticket = new Ticket();//3、启动多个线程操作资源类的对象Thread t1 = new Thread("窗口一") {public void run() {//不能给run()直接加锁,因为t1,t2,t3的三个run方法分别属于三个Thread类对象,// run方法是非静态方法,那么锁对象默认选this,那么锁对象根本不是同一个while (true) {synchronized (ticket) {ticket.sale();}}}};Thread t2 = new Thread("窗口二") {public void run() {while (true) {synchronized (ticket) {ticket.sale();}}}};Thread t3 = new Thread(new Runnable() {public void run() {while (true) {synchronized (ticket) {ticket.sale();}}}}, "窗口三");t1.start();t2.start();t3.start();}
}//1、编写资源类
class Ticket {private int ticket = 1000;public void sale() {//也可以直接给这个方法加锁,锁对象是this,这里就是Ticket对象if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);ticket--;} else {throw new RuntimeException("没有票了");}}public int getTicket() {return ticket;}
}

四、同步代码块

(1)同步代码块–案例1

同步代码块synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问

🗳️格式:

synchronized(同步锁){	//“同步锁”又叫“同步监视器”,放的是一个对象,不是放基本数据类型需要被同步操作的代码
}

🚗说明

  • 需要被同步的代码,即为操作共享数据的代码。(在卖票的场景当中,共享数据就是ticket,凡是操作ticket的代码都叫做“需要被同步的代码”)
  • 共享数据:即多个线程都需要操作的数据。比如:ticket
  • 需要被同步的代码(操作共享数据的代码),在被synchronized包裹以后(这些代码就是一个整体),就使得一个线程在操作这些代码的过程中,其它线程必须等待。直到这个线程将这段代码操作完之后,别的线程才能进去操作。
  • 同步监视器,俗称哪个线程获取了锁,哪个线程就能执行需要被同步的代码。(其他线程没有获得锁,其他线程就要等待)
  • 同步监视器可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。(是哪个类的对象没有要求,但必须是同一个)

1、案例1

方式一实现Runnable接口的代码如下:

package yuyi02.notsafe;/*** ClassName: WindowTest* Package: yuyi02.notsafe* Description:*      使用实现Runnable接口的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/1/27 0027 19:28*/
public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s=new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100;@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}
}

🍺运行结果(部分)

image.png

上一节已经分析了出现重票和错票的原因。

现在需要将它变成线程安全的状态。

首先要确定“操作共享数据”的代码。

谁是共享数据呢?显然是ticket

操作共享数据的代码是?如下蓝色部分:

image.png

while需不需要包裹住呢?待会儿再看。

先包裹住if-else,如下:

class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){synchronized (){if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

在小括号里面需要填写一个对象,是哪个类的对象没有要求,但是要求是唯一的。

现在好像没有什么对象,那就造一个吗?

造一个什么类的对象呢?什么都可以,那就造一个Object类的对象吧!然后将obj放入小括号里面。如下:

lass SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据Object obj=new Object();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){synchronized (obj){if(ticket>0){   //如果票数大于0就可以售票//...}else{break;}}}}
}

从语法这样没有问题,但是为了保证安全性,这个obj对象必须是唯一的。

什么叫唯一?就是在整个线程操作过程当中,有几个obj的对象。

可以看到,整个操作过程中,只造了一个SaleTicket的对象s,并且将它放到三个线程里面了。如下:

image.png

既然只造了一个对象s,那么成员变量也就只有一个,所以这里的obj是唯一的。如下:

image.png


🌱代码

public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s=new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据Object obj=new Object();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){synchronized (obj){ //obj:是唯一的么? 是唯一的!if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出结果(部分)

现在来执行一下:

image.png

可以看到没有错票的问题了,但是有一个现象,全都是“窗口1”在售票。

这是巧合吗?

image.png

线程1释放锁之后,其他线程到底有没有进来呢?

是CPU的原因,为了呈现出别的线程是不是真的能抢,我们在这里让线程睡一下,如下:

image.png

记得处理一下异常:

class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据Object obj=new Object();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj){ //obj:是唯一的么? 是唯一的!//...}}}
}

再次执行代码:

image.png

可以看到别的窗口也在卖票了,说明代码没有问题

没有重票、错票问题出现了。


2、分析同步原理

来看一下这个同步原理。

🌱代码

package yuyi02.runnablesafe;/*** ClassName: WindowTest* Package: yuyi02.notsafe* Description:*      使用实现Runnable接口的方式,实现卖票。-->存在线程安全问题*      使用同步代码块解决上述卖票中的线程安全问题。* @Author 雨翼轻尘* @Create 2024/1/27 0027 19:28*/
public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s=new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据Object obj=new Object();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj){ //obj:是唯一的么? 是唯一的!if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍰分析

synchronized将操作ticket的代码包裹起来,如下:

image.png

有三个窗口(线程)来卖票:

image.png

然后有一个同步监视器,就好比一个“绿灯”:

image.png

假设现在线程1抢到了监视器,它就会将这个“绿灯”变为“红灯”,把门锁住,别的线程就进不去了。如下:

image.png

在线程1操作的过程中,有一个sleep方法,如下:

image.png

即使sleep()了,也不会释放这个锁。

就好比你上厕所将门锁住了,然后你在里面睡着了,这个锁是不会自己开的。

线程1在里面操作,即使阻塞了也没有关系,因为还锁着的,这时候其他线程都进不去。如下:

image.png

当线程1执行结束,出了synchronized的大括号,就会将“同步监视器”给释放掉。如下:

image.png

线程1释放之后,其他线程就可以进去了。当然,有可能下一刻还是线程1抢到。

看一下顺序:

image.png


3、案例1之this的使用

刚才这个地方写的是obj,其实写任何一个类的对象都可以。

image.png

为了体现这个“任意”类的对象都可以,我们来写一个类Dog,如下:

class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据//Object obj=new Object();Dog dog=new Dog();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}synchronized (dog){ //dog:是唯一的么? 是唯一的!if(ticket>0){   //如果票数大于0就可以售票//...}else{break;}}}}
}class Dog{}

🌱代码

package yuyi02.runnablesafe;/*** ClassName: WindowTest* Package: yuyi02.notsafe* Description:*      使用实现Runnable接口的方式,实现卖票。-->存在线程安全问题*      使用同步代码块解决上述卖票中的线程安全问题。* @Author 雨翼轻尘* @Create 2024/1/27 0027 19:28*/
public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s=new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据//Object obj=new Object();Dog dog=new Dog();@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}synchronized (dog){ //dog:是唯一的么? 是唯一的!if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}class Dog{}

🍺输出结果(部分)

image.png

由输出结果可以看到,是可行的。

🗳️上面咱们都是自己造对象,那有没有一个现成的对象拿来直接使用呢?

是有的,最好用的就是自己this啦(不一定能用),此时是非静态方法run,调用它的就是对象,所以这里this表示对象,如下:

image.png

那么this是唯一吗?

这需要看this是谁,this表示当前对象(实现Runnable接口的实现类的对象),千万不要this理解为是“线程”。

Thread.currentThread()才能获取到线程,三个线程不一样。如下:

image.png

this表示调用run()方法的对象,这个方法的对象就是SaleTicket类的对象,SaleTicket类的对象现在就造了一个s,如下:

image.png

所以在这个案例中,this就是ss就只有一个,所以this是唯一的


🌱代码

package yuyi02.runnablesafe;/*** ClassName: WindowTest* Package: yuyi02.notsafe* Description:*      使用实现Runnable接口的方式,实现卖票。-->存在线程安全问题*      使用同步代码块解决上述卖票中的线程安全问题。* @Author 雨翼轻尘* @Create 2024/1/27 0027 19:28*/
public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s=new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable{   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket=100; //ticket是共享数据@Overridepublic void run() { //2.实现接口中的抽象方法run()方法while (true){try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}synchronized (this){ //this:是唯一的么? 是唯一的,就是案例中的对象sif(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出结果(部分)

image.png

所以,this是最方便使用的。

注意这个对象一定要是唯一的。


4、案例1的补充

刚才我们还提到一个问题,就是在写synchronized的时候,大括号能不能将while也包进去。

就是这样:

image.png

那么这样执行的时候安全吗?

稍微分析一下就知道不可行,当一个线程获得锁进入了while,其他线程就不能进来了。

当while执行结束,另一个线程才能进入while。

所以,只有当票卖完的时候,这个线程才能出while,另一个线程才能进来。不可行。

🌱代码

public class WindowTest {public static void main(String[] args) {//3.创建当前实现类的对象SaleTicket s = new SaleTicket();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);//给三个线程起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable {   //卖票    1.创建一个实现Runnable接口的类(实现类)int ticket = 100; //ticket是共享数据@Overridepublic void run() { //2.实现接口中的抽象方法run()方法synchronized (this) { while (true) {try {Thread.sleep(5);    //让线程1睡5ms} catch (InterruptedException e) {e.printStackTrace();}if (ticket > 0) {   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;} else {break;}}}}
}

🍺输出结果(部分)

image.png

由输出结果可以看到,只有窗口1在卖票。这明显不是我们想要看到的,也不是多线程问题了。

所以,这种情况不要写了:

image.png

正确写法:

image.png

只有对临界资源进行访问修改之前才上锁,要不然可能导致线程长时间占用资源,造成其他线程死锁。

(2)同步代码块–案例2

1、案例2之this的问题

方式二继承Thread类的代码如下:

package yuyi02.runnablesafe;/*** ClassName: WindowTest2* Package: yuyi02.notsafe* Description:*      使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/1/27 0027 22:26*/
public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}
}

🍺运行结果(部分)

此时的运行结果如下,可以看到有重票和错票的问题。

image.png

还是使用“同步代码块”的方式,注意需要用synchronized包裹住什么代码,“同步监视器”是不是唯一的,若不唯一仍然不安全。

ticket共享数据,我们应该要将操作共享数据的代码使用synchronized包裹起来,自然也就是这样一段代码需要包裹:

image.png

将上面的蓝色部分选中之后,按快捷键Ctrl+Alt+T,选择环绕方式为synchronized,如下:

image.png

然后就可以直接将选中的代码包裹起来了,如下:

image.png

然后在小括号里面,需要写一个同步监视器,而且是必须要写的。

由上一个案例的实现方式来说,这个地方最方便的就是写一个this了。


🎲但是写this靠谱吗?

image.png

现在这个this表示的是谁呢?

在当前代码中,this表示的是调用run()方法的对象,也就是当前类Window的对象。

那么Window的对象有几个呢?哎呀,一看有三个嘞。

画个图吧:

image.png

所以,现在的this是不靠谱的。

就好比哪个线程过来调用这个run()方法,谁就是this

现在这个this表示的就是w1,w2,w3。也就是有三个同步监视器。

每个线程一个,这显然就不是我们想看到的情况。

这是我们的“理想状态”,举个例子:

image.png

但是现在变成了这样:

image.png

好了,分析结束,是不可行的,输出结果当然也是不理想的。

看一下:

🌱代码

package yuyi02.runnablesafe;/*** ClassName: WindowTest2* Package: yuyi02.notsafe* Description:*      使用继承Thread类的方式,实现卖票*      使用同步代码块的方式解决上述卖票中的线程安全问题。* @Author 雨翼轻尘* @Create 2024/1/27 0027 22:26*/
public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){synchronized (this) {	//this:此时表示w1,w2,w3,不能保证锁的唯一性if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出结果(部分)

image.png

可以看到,还是有重票的问题。

所以,小括号里面写this不可行!

2、案例2之static修饰

既然上面分析了this不可行,那么现在我们来造一个对象。

比如:

image.png

现在需要来看一下obj是不是唯一的。

🌱代码

public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;Object obj=new Object();//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){synchronized (obj) {if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出(部分)

image.png

可以看到,出现了重票、错票。

明显obj不唯一,此时它是一个实例变量,实例变量造三个对象,所以这里还是有三个,不唯一。

🗳️问:为什么这里的Object不唯一?造了三个对象但是这是Window造的而不是Object造的啊?

答:这是因为,每个Window对象都new了一个obj的实例,名字一样,但是地址不一样!

obj是Window的实例变量,造一个Window就会造一个obj。

这里的Object对象在类里面,在main方法中创建类的对象的时候,三个对象都会创建一个Object对象。

🍰那我们现在加一个static呢?

如下:

image.png

现在obj表示全局唯一的,不管有几个窗口,都只有这样一个实例。

🌱代码

public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;static Object obj=new Object();//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){synchronized (obj) {	//obj:使用static修饰以后,就能保证其唯一性if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出(部分)

image.png

显然,现在的线程是安全的了。

3、案例2之类.class

上面用static解决了问题,那么有没有更好的选择呢?

第一个案例中,我们用this比较方便,因为本来就存在一个对象,它是唯一的。

现在这种继承Thread的方式,不能使用this了,那么有没有一种现成的,又是唯一的结构可以选择呢?

其实是有的,只不过现在有点超纲(后面讲到反射的时候再来解释这个事情会好一点,现在就记一下)。

在小括号里面写上:Window.class

如下:

image.png

小括号里面应该是一个对象,现在这个东西是一个对象么?

这个其实也是一个对象,以后讲反射的时候,会说到这样一个结构Class clz=Window.class;,声明的是Window.class,它就是一个值,是Class的一个值。如下:

image.png

Window.class它表示加载到内存中的任何一个类,这个类本身就充当了左边的类(Class)的对象(clz)。

这个类(Class)不是关键字class,注意C大写的,这是一个类,名字叫做Class。它的对象就是具体的某一个具体的类。

后面反射再说,现在就只需要知道Window.class是一个对象,就相当于是我们加载到内存中的这个类,这个类只加载一次,所以是唯一的。

🌱代码

package yuyi02.runnablesafe;/*** ClassName: WindowTest2* Package: yuyi02.notsafe* Description:*      使用继承Thread类的方式,实现卖票*      使用同步代码块的方式解决上述卖票中的线程安全问题。* @Author 雨翼轻尘* @Create 2024/1/27 0027 22:26*/
public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;//static Object obj=new Object();//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){synchronized (Window.class){    //结构:Class clz=Window.class; 类只加载一次,所以是唯一的if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}}
}

🍺输出(部分)

image.png

可以看到线程是安全的。


🍻补充:

这样来看,在上一个案例方式一实现Runnable接口中,不仅可以使用this,也可以使用SaleTicket.class,因为它在内存中也只加载一次,也是唯一的。

如下:

image.png

(3)注意要点

【同步代码块】

🗳️格式:

synchronized(同步锁){	//“同步锁”又叫“同步监视器”,放的是一个对象,不是放基本数据类型需要被同步操作的代码
}

☕注意

使用“同步代码块”的时候,必须要显示指明一个“同步监视器”,或者叫“”,这个锁在指定的时候,会选择一个比较方便的对象去使用。

  • 在实现Runnable接口的方式中,同步监视器可以考虑使用:this
  • 在继承Thread类的方式中,同步监视器要慎用this(不是不能使用,关键要看它是不是唯一的,若是唯一的就可以使用),可以考虑使用:当前类.class

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/438358.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

堆和堆排序【数据结构】

目录 一、堆1. 堆的存储定义2. 初始化堆3. 销毁堆4. 堆的插入向上调整算法 5. 堆的删除向下调整算法 6. 获取堆顶数据7. 获取堆的数据个数8. 堆的判空 二、Gif演示三、 堆排序1. 堆排序(1) 建大堆(2) 排序 2.Topk问题 四、完整代码1.堆的代码Heap.cHeap.htest.c 2. 堆排序的代码…

Ubuntu本地部署Nextcloud并结合内网穿透实现远程访问搭建个人云盘

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 摘要1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访…

PCB的层叠结构介绍

1. PCB 单层板 剖去我们不要的&#xff0c;然后放入电阻 但是铜皮非常脆弱&#xff0c;很容易断&#xff0c;所以我们放在一块木板上 我们把铜皮放在基板上 显而易见基板肯定是绝缘的 此时单层板就诞生了 此时问题就诞生了&#xff0c;铜皮过电流是有电的&#xff0c;很容易触…

监听项目中指定属性数据,点击或模块显示时

当项目中&#xff0c;需要获取某个页面上、某个标签上、有指定自定义属性时&#xff0c;需要在点击该元素时进行公共逻辑处理&#xff0c;或该元素在显示的时候进行逻辑处理&#xff0c;这时可以定义一个公共的方法&#xff0c;在每个页面引用&#xff0c;并写入数据即可 &…

算法沉淀——二分查找(leetcode真题剖析)

算法沉淀——二分查找 01.二分查找02.在排序数组中查找元素的第一个和最后一个位置03.搜索插入位置04.x 的平方根05.山脉数组的峰顶索引06.寻找峰值07.寻找旋转排序数组中的最小值08.LCR 173. 点名 二分查找&#xff08;Binary Search&#xff09;是一种在有序数组中查找特定元…

prism 10 for Mac v10.1.1.270激活版 医学绘图分析软件

GraphPad Prism 10 for Mac是一款专为科研工作者和数据分析师设计的绘图和数据可视化软件。以下是该软件的一些主要功能&#xff1a; 软件下载&#xff1a;prism 10 for Mac v10.1.1.270激活版 数据整理和导入&#xff1a;GraphPad Prism 10支持从多种数据源导入数据&#xff0…

重写Sylar基于协程的服务器(0、搭建开发环境以及项目框架 || 下载编译简化版Sylar)

重写Sylar基于协程的服务器&#xff08;0、搭建开发环境以及项目框架 || 下载编译简化版Sylar&#xff09; 重写Sylar基于协程的服务器系列&#xff1a; 重写Sylar基于协程的服务器&#xff08;0、搭建开发环境以及项目框架 || 下载编译简化版Sylar&#xff09; 前言 sylar是…

Unity 解释器模式(实例详解)

文章目录 示例1&#xff1a;基础解释器结构示例2&#xff1a;小于表达式&#xff08;LessThanExpression&#xff09;示例3&#xff1a;逻辑或表达式&#xff08;OrExpression&#xff09;示例4&#xff1a;逻辑非表达式&#xff08;NotExpression&#xff09;示例5&#xff1a…

​ PaddleHub 首页图像 - 文字识别chinese_ocr_db_crnn_server​

PaddleHub 便捷地获取PaddlePaddle生态下的预训练模型&#xff0c;完成模型的管理和一键预测。配合使用Fine-tune API&#xff0c;可以基于大规模预训练模型快速完成迁移学习&#xff0c;让预训练模型能更好地服务于用户特定场景的应用 零基础快速开始WindowsLinuxMac Paddle…

小项目:使用MQTT上传温湿度到Onenet服务器

前言 我们之前分别编写了 DHT11、ESP8266 和 MQTT 的代码&#xff0c;现在我们将它们仨整合在一起&#xff0c;来做一个温湿度检测小项目。这个项目可以实时地将 DHT11 传感器获取到的温湿度数据上传到 OneNET 平台。通过登录 OneNET&#xff0c;我们随时随地可以查看温湿度数…

IndexedDB入门

https://www.cnblogs.com/zhangzuwei/p/16574791.html 注意 1.删除表&#xff0c;创建表只能在数据库版本升级里面进行。 2.keyPath: key 要和表字段对应&#xff0c;而且格式要一样&#xff0c;不然不运行不报错。 3.使用 autoIncrement: true 代替 keyPath: key&#xff…

MongoDB安装以及卸载

查询id&#xff1a; docker ps [rootlocalhost ~]# docker stop c7a8c4ac9346 c7a8c4ac9346 [rootlocalhost ~]# docker rm c7a8c4ac9346 c7a8c4ac9346 [rootlocalhost ~]# docker rmi mongo sudo docker pull mongo:4.4 sudo docker images 卸载旧的 sudo docker stop mong…