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.rhkb.cn/news/247526.html

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

相关文章

【Sql Server】新手一分钟看懂在已有表基础上增加字段和说明

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对…

单片机14-17

目录 LCD1602 LCD1602液晶显示屏 直流电机驱动&#xff08;PWM&#xff09; LED呼吸灯 直流电机调速 AD/DA&#xff08;SPI通信&#xff09; AD模数转换 DA数模转换 红外遥控&#xff08;外部中断&#xff09; 红外遥控 红外遥控电机调速 LCD1602 LCD1602液晶显示屏 …

使用dockers-compose搭建开源监控和可视化工具

简介 Prometheus 和 Grafana 是两个常用的开源监控和可视化工具。 Prometheus 是一个用于存储和查询时间序列数据的系统。它提供了用于监控和报警的数据收集、存储、查询和图形化展示能力。Prometheus 使用拉模型&#xff08;pull model&#xff09;&#xff0c;通过 HTTP 协议…

10s 内得到一个干净、开箱即用的 Linux 系统

安装 使用官方脚本安装我的服务器不行 官方脚本 mkdir instantbox && cd $_ bash <(curl -sSL https://raw.githubusercontent.com/instantbox/instantbox/master/init.sh) 下面是我的完整安装过程 mkdir /opt/instantbox cd /opt/instantbox 1.脚本文件 (这个没…

13.前端--CSS-盒子模型

1.盒子模型的组成 CSS 盒子模型本质上是一个盒子&#xff0c;封装周围的 HTML 元素&#xff0c;它包括&#xff1a;边框、外边距、内边距、和 实际内容 2.边框&#xff08;border&#xff09; 2.1 边框的使用 1、border设置元素的边框。边框有三部分组成:边框宽度(粗细) 边框…

【Git】项目管理笔记

文章目录 本地电脑初始化docker报错.gitignoregit loggit resetgit statusgit ls-filesgit rm -r -f --cached拉取仓库文件更新本地的项目报错处理! [rejected] master -> master (fetch first)gitgitee.com: Permission denied (publickey).error: remote origin already e…

在linux、window环境搭建kafka环境

一、搭建环境前置准备 下载kafka的官网 http://kafka.apache.org/downloads根据自己的需求选择版本,安装包不区分linux和windows环境,这一个安装包均可部署。 源代码包含kafka的代码文件,使用scala编写的。 二、linux环境 1. 上传安装包 我下载的版本是kafka_2.12-3.6.1…

盒子模型的内容总结

知识引入 1.认识盒子模型 在浏览网站时我们会发现内容都是按照区域划分的。这使得网页很工整、美观。在页面中&#xff0c;每一块区域分别承载不同的内容&#xff0c;使得网页的内容虽然零散&#xff0c;但是在版式排列上依然清晰有条理。如图1 图1 *承载内容的区域称为盒子…

幻兽帕鲁越玩越卡,内存溢出问题如何解决?

近期幻兽帕鲁游戏大火&#xff0c;在联机组队快乐游玩的同时&#xff0c;玩家们也发现了一些小问题。由于游戏有随机掉落材料的设定&#xff0c;服务器在加载掉落物的过程中很容易会出现掉帧、卡顿的情况。某些玩家甚至在游戏1&#xff5e;2时后就出现服务器崩溃的情况&#xf…

MarkDown快速入门-以Obsidian编辑器为例

直接上图&#xff0c;左右对应。 首先是基础语法。 # 标题&#xff0c;几个就代表几级标题&#xff1b;* 单个是序号&#xff0c;两个在一起就是斜体&#xff1b;- [ ] 代表任务&#xff0c;注意其中的空格&#xff1b; 然后是表格按钮代码 | 使用中竖线代表表格&#xff0c…

操作系统-调度的概念,层次(低中高级调度和挂起状态与七模型)和进程调度的过程,时机,切换,方式(临界区,进程调度的时机,方式,切换与过程)

文章目录 调度的概念&#xff0c;层次总览调度的基本概念调度的三个层次-高级调度调度的三个层次-低级调度调度的三个层次-中级调度补充&#xff1a;挂起状态与七状态模型三层调度的联系&#xff0c;对比小结 进程调度的过程&#xff0c;时机&#xff0c;切换&#xff0c;方式总…

源聚达科技:开一家抖音店铺怎么做最好

在数字化浪潮的推动下&#xff0c;抖音不仅是年轻人展示才华的舞台&#xff0c;也成为商家争夺流量的新阵地。开一家抖音店铺&#xff0c;看似简单&#xff0c;实则需要精心策划和周到运营。 首要任务是确立店铺定位。正如古人云“磨刀不误砍柴工”&#xff0c;明确目标受众和主…

k8s 进阶实战笔记 | Scheduler 调度策略总结

文章目录 Scheduler 调度策略总结调度原理和过程调度策略nodeSelect亲和性和反亲和性NodeAffinify亲和验证PodAffinity 亲和验证PodAntiAffinity 反亲和验证污点与容忍跳过 Scheduler 调度策略 调度策略场景总结 Scheduler 调度策略总结 调度原理和过程 Scheduler 一直监听着…

【C++】C++入门基础讲解(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 导读 接着上一篇的内容继续学习&#xff0c;今天我们需要重点学习引用。 1. 引用 在C中&#xff0c;引用是一种特殊的变量&#xff…

免费电视TV盒子软件,好用的免费电视盒子软件大全,免费电视盒子APP大全,2024最新整理

1、TVbox下载地址、影视接口、配置教程 下载地址 TVbox TVbox可用接口地址合集 注&#xff1a;接口均来源于互联网收集分享&#xff01;所有接口都是经过测试的&#xff0c;如果出现加载失败等情况&#xff0c;可能是因为接口针对的盒子有兼容问题&#xff0c;可以多试试几…

Linux多线程服务端编程:使用muduo C++网络库 学习笔记 第十一章 反思C++面向对象与虚函数(上)

C的面向对象语言设施相比其他现代语言可算得上“简陋”&#xff0c;而且与语言的其他部分&#xff08;better C、数据抽象、泛型&#xff09;融合度较差&#xff08;见电子工业出版社出版的《C Primer&#xff08;第4版&#xff09;&#xff08;评注版&#xff09;》第15章&…

语义分割 | 基于 VGG16 预训练网络和 Segnet 架构实现迁移学习

Hi&#xff0c;大家好&#xff0c;我是源于花海。本文主要使用数据标注工具 Labelme 对猫&#xff08;cat&#xff09;和狗&#xff08;dog&#xff09;这两种训练样本进行标注&#xff0c;使用预训练模型 VGG16 作为卷积基&#xff0c;并在其之上添加了全连接层。基于标注样本…

uni-app 接口封装,token过期,自动获取最新的token

一、文件路径截图 2、新建一个文件app.js let hosthttp://172.16.192.40:8083/jeecg-boot/ //本地接口 let myApi {login: ${host}wx/wxUser/login, //登录 } module.exports myApi 3、新建一个文件request.js import myApi from /utils/app.js; export const r…

MySQL知识点总结(二)——explain执行计划、SQL优化

MySQL知识点总结&#xff08;二&#xff09;——explain执行计划、SQL优化 explain执行计划typepossible_keyskeysextra SQL优化SQL优化的流程SQL优化技巧范围查询优化排序优化分组查询优化distinct优化分页查询优化join关联查询优化排序分页 关联查询分组 关联查询 排序in与…

力扣hot100 实现Trie(前缀树) 字典树 一题双解

Problem: 208. 实现 Trie (前缀树) 文章目录 思路复杂度&#x1f49d; TrieNode版&#x1f49d; 二维数组版 思路 &#x1f469;‍&#x1f3eb; 宫水三叶 复杂度 &#x1f49d; TrieNode版 public class Trie {class TrieNode{boolean end;//标记是否有以当前节点为结尾的字…