文章目录
- 一、介绍
- 二、原理
- 三、同步锁机制
- (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)来解决。
根据案例简述:
窗口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;}}}
}
🍺运行结果(部分)
上一节已经分析了出现重票和错票的原因。
现在需要将它变成线程安全的状态。
首先要确定“操作共享数据”的代码。
谁是共享数据呢?显然是ticket
。
操作共享数据的代码是?如下蓝色部分:
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,并且将它放到三个线程里面了。如下:
既然只造了一个对象s,那么成员变量也就只有一个,所以这里的obj是唯一的。如下:
🌱代码
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;}}}}
}
🍺输出结果(部分)
现在来执行一下:
可以看到没有错票的问题了,但是有一个现象,全都是“窗口1”在售票。
这是巧合吗?
线程1释放锁之后,其他线程到底有没有进来呢?
是CPU的原因,为了呈现出别的线程是不是真的能抢,我们在这里让线程睡一下,如下:
记得处理一下异常:
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:是唯一的么? 是唯一的!//...}}}
}
再次执行代码:
可以看到别的窗口也在卖票了,说明代码没有问题。
没有重票、错票问题出现了。
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
的代码包裹起来,如下:
有三个窗口(线程)来卖票:
然后有一个同步监视器,就好比一个“绿灯”:
假设现在线程1抢到了监视器,它就会将这个“绿灯”变为“红灯”,把门锁住,别的线程就进不去了。如下:
在线程1操作的过程中,有一个sleep方法,如下:
即使sleep()
了,也不会释放这个锁。
就好比你上厕所将门锁住了,然后你在里面睡着了,这个锁是不会自己开的。
线程1在里面操作,即使阻塞了也没有关系,因为还锁着的,这时候其他线程都进不去。如下:
当线程1执行结束,出了synchronized
的大括号,就会将“同步监视器”给释放掉。如下:
线程1释放之后,其他线程就可以进去了。当然,有可能下一刻还是线程1抢到。
看一下顺序:
3、案例1之this的使用
刚才这个地方写的是obj
,其实写任何一个类的对象都可以。
为了体现这个“任意”类的对象都可以,我们来写一个类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{}
🍺输出结果(部分)
由输出结果可以看到,是可行的。
🗳️上面咱们都是自己造对象,那有没有一个现成的对象拿来直接使用呢?
是有的,最好用的就是自己this
啦(不一定能用),此时是非静态方法run
,调用它的就是对象,所以这里this
表示对象,如下:
那么this
是唯一吗?
这需要看this是谁,this表示当前对象(实现Runnable接口的实现类的对象),千万不要将this
理解为是“线程”。
Thread.currentThread()
才能获取到线程,三个线程不一样。如下:
this
表示调用run()
方法的对象,这个方法的对象就是SaleTicket
类的对象,SaleTicket
类的对象现在就造了一个s
,如下:
所以在这个案例中,this
就是s。s就只有一个,所以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;}}}}
}
🍺输出结果(部分)
所以,this是最方便使用的。
注意这个对象一定要是唯一的。
4、案例1的补充
刚才我们还提到一个问题,就是在写synchronized
的时候,大括号能不能将while
也包进去。
就是这样:
那么这样执行的时候安全吗?
稍微分析一下就知道不可行,当一个线程获得锁进入了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;}}}}
}
🍺输出结果(部分)
由输出结果可以看到,只有窗口1在卖票。这明显不是我们想要看到的,也不是多线程问题了。
所以,这种情况不要写了:
正确写法:
只有对临界资源进行访问修改之前才上锁,要不然可能导致线程长时间占用资源,造成其他线程死锁。
(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;}}}
}
🍺运行结果(部分)
此时的运行结果如下,可以看到有重票和错票的问题。
还是使用“同步代码块”的方式,注意需要用synchronized
包裹住什么代码,“同步监视器”是不是唯一的,若不唯一仍然不安全。
ticket
是共享数据,我们应该要将操作共享数据的代码使用synchronized
包裹起来,自然也就是这样一段代码需要包裹:
将上面的蓝色部分选中之后,按快捷键Ctrl+Alt+T
,选择环绕方式为synchronized
,如下:
然后就可以直接将选中的代码包裹起来了,如下:
然后在小括号里面,需要写一个同步监视器,而且是必须要写的。
由上一个案例的实现方式来说,这个地方最方便的就是写一个this
了。
🎲但是写this
靠谱吗?
现在这个this表示的是谁呢?
在当前代码中,this
表示的是调用run()
方法的对象,也就是当前类Window
的对象。
那么Window的对象有几个呢?哎呀,一看有三个嘞。
画个图吧:
所以,现在的this是不靠谱的。
就好比哪个线程过来调用这个run()
方法,谁就是this
。
现在这个this
表示的就是w1,w2,w3。也就是有三个同步监视器。
每个线程一个,这显然就不是我们想看到的情况。
这是我们的“理想状态”,举个例子:
但是现在变成了这样:
好了,分析结束,是不可行的,输出结果当然也是不理想的。
看一下:
🌱代码
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;}}}}
}
🍺输出结果(部分)
可以看到,还是有重票的问题。
所以,小括号里面写this
不可行!
2、案例2之static修饰
既然上面分析了this不可行,那么现在我们来造一个对象。
比如:
现在需要来看一下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;}}}}
}
🍺输出(部分)
可以看到,出现了重票、错票。
明显obj不唯一,此时它是一个实例变量,实例变量造三个对象,所以这里还是有三个,不唯一。
🗳️问:为什么这里的Object不唯一?造了三个对象但是这是Window造的而不是Object造的啊?
答:这是因为,每个Window对象都new了一个obj的实例,名字一样,但是地址不一样!
obj是Window的实例变量,造一个Window就会造一个obj。
这里的Object对象在类里面,在main方法中创建类的对象的时候,三个对象都会创建一个Object对象。
🍰那我们现在加一个static
呢?
如下:
现在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;}}}}
}
🍺输出(部分)
显然,现在的线程是安全的了。
3、案例2之类.class
上面用static
解决了问题,那么有没有更好的选择呢?
第一个案例中,我们用this
比较方便,因为本来就存在一个对象,它是唯一的。
现在这种继承Thread的方式,不能使用this了,那么有没有一种现成的,又是唯一的结构可以选择呢?
其实是有的,只不过现在有点超纲(后面讲到反射
的时候再来解释这个事情会好一点,现在就记一下)。
在小括号里面写上:Window.class
。
如下:
小括号里面应该是一个对象,现在这个东西是一个对象么?
这个其实也是一个对象,以后讲反射的时候,会说到这样一个结构Class clz=Window.class;
,声明的是Window.class
,它就是一个值,是Class
的一个值。如下:
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;}}}}
}
🍺输出(部分)
可以看到线程是安全的。
🍻补充:
这样来看,在上一个案例方式一实现Runnable接口中,不仅可以使用this
,也可以使用SaleTicket.class
,因为它在内存中也只加载一次,也是唯一的。
如下:
(3)注意要点
【同步代码块】
🗳️格式:
synchronized(同步锁){ //“同步锁”又叫“同步监视器”,放的是一个对象,不是放基本数据类型需要被同步操作的代码
}
☕注意
使用“同步代码块
”的时候,必须要显示指明一个“同步监视器”,或者叫“锁”,这个锁在指定的时候,会选择一个比较方便的对象去使用。
- 在实现
Runnable
接口的方式中,同步监视器可以考虑使用:this
。 - 在继承
Thread
类的方式中,同步监视器要慎用this(不是不能使用,关键要看它是不是唯一的,若是唯一的就可以使用),可以考虑使用:当前类.class
。