14.synchronized关键字
synchronized关键字
导言
本节将介绍多线程中一个比较重要的知识点synchronized的关键字。什么是synchronized的?synchronized的中文叫做同步,
它是一个关键字,可用来给对象和方法或者代码块加锁。当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码,
简而言之就是被synchronized修饰的代码,同一时刻最多只有一个线程执行这段代码。
那么 synchronized可以修饰哪些代码呢?
synchronized
代码块
第一个是代码块
被synchronized修饰的代码块称之为同步代码块,
小括号中的object是同步锁,也叫同步对象,它可以是任意对象,包括this。
synchronized修饰的方法
第二个是方法被synchronized修饰的方法称之为同步方法。
synchronized修饰的静态方法称之为静态同步方法
第三个是静态方法,被synchronized修饰的静态方法称之为静态同步方法,
无论是同步代码块还是同步方法或者是静态同步方法,它们的作用都是一样的。那就是写在大括号里面的代码同一时刻最多只有一个线程执行这段代码,
案例
下面我们来通过一个例子来感受一下同步的好处。首先自定义一个卖票的任务,在任务中定义一个int类型的quantity属性表示这个数量。接着在run方法中判断车票数量是否大于0,当车票数量大于0时出售车票,别忘了还将车票数量减一。
卖票的任务
package com.chenjie.executor.day14; /** * packageName com.chenjie.executor.day14 * * @author chenjie * @version JDK 8 * @className TicketTask (此处以class为例) * @date 2024/5/28 * @description TODO */ public class TicketTask implements Runnable { private Integer quantity=10; @Override public void run() { while (quantity>0){ //打印车票 System.out.println(Thread.currentThread().getName()+"---"+quantity+"号票"); quantity--; } } }
接下来执行这个卖票任务,首先将卖票任务创建出来,然后创建出三个线程。接着将卖票任务分别传递给这三个线程,最后启动这三个线程。
main
package com.chenjie.executor.day14; /** * packageName com.chenjie.executor.day09 * * @author chenjie * @version JDK 8 * @className Main1 (此处以class为例) * @date 2024/5/27 * @description TODO */ public class Main { /** * 创建三个线程来跑这段代码 * @param args */ public static void main(String[] args) { TicketTask task=new TicketTask(); Thread thread = new Thread(task,"thread"); Thread thread1 = new Thread(task,"thread1"); Thread thread2 = new Thread(task,"thread2"); thread.start(); thread1.start(); thread2.start(); } }
执行结果
从运行结果来看,程序出现了bug,卖出了三张10号车票,
thread---10号票 thread2---10号票 thread1---10号票 thread2---8号票 thread2---6号票 thread2---5号票 thread2---4号票 thread2---3号票 thread2---2号票 thread2---1号票 thread---9号票 thread1---7号票
同步代码块
下面我们将使用同步代码块来解决这个问题,改写之前的卖票任务,将卖票代码放入同步代码块中,这里需要注意的是我们需要区分哪些内容应该放入同步代码块中,哪些内容不应该放入同步代码块中。
例如我们不应该将整个while循环放入同步代码块中,因为这样意味着一个线程就可以卖完所有车票,就失去了多线程的意义,所以不要将整个while循环放入同步代码块中,
错误写法
package com.chenjie.executor.day14; /** * packageName com.chenjie.executor.day14 * * @author chenjie * @version JDK 8 * @className TicketTask (此处以class为例) * @date 2024/5/28 * @description TODO */ public class TicketTask implements Runnable { private Integer quantity=10; @Override public void run() { synchronized (this){ while (quantity>0){ //打印车票 System.out.println(Thread.currentThread().getName()+"---"+quantity+"号票"); quantity--; } } } }
打印结果
可以发现只有一个线程执行了方法
thread---10号票 thread---9号票 thread---8号票 thread---7号票 thread---6号票 thread---5号票 thread---4号票 thread---3号票 thread---2号票 thread---1号票
解决了重复十张票的问题,出现了超卖的问题
我们可以把打印车票和减少车票数量的代码放入同步代码块中,这是改写后的售票任务代码。
package com.chenjie.executor.day14; /** * packageName com.chenjie.executor.day14 * * @author chenjie * @version JDK 8 * @className TicketTask (此处以class为例) * @date 2024/5/28 * @description TODO */ public class TicketTask1 implements Runnable { private Integer quantity=10; @Override publicvoid run() { while (quantity>0){ synchronized (this){ //打印车票 System.out.println(Thread.currentThread().getName()+"---"+quantity+"号票"); quantity--; } } } }
结果
从运行结果来看,解决了重复车票的问题,
thread---10号票 thread---9号票 thread---8号票 thread---7号票 thread---6号票 thread---5号票 thread---4号票 thread---3号票 thread---2号票 thread---1号票 thread2---0号票 thread1----1号票
但是出现了新问题,有两张错票,0号票和-1号票,
我们明明都用了同步,他们又是怎么出现的?问题就处在同步代码块内部程序运行至while循环值当quantity等于1,也就是车票数量为一,三个线程同时判断quantity是否大于0时,此时条件成立,
于是三个线程顺利进入while循环体,接下来碰到同步代码块,
他们三个开始争夺同步锁,拿到手的线程进入同步代码块内部执行卖票代码,没有拿到锁的线程继续等待,但三个线程终究都会拿到手,所以卖票代码被执行了三遍,
quantity被递减了三次,于是就产生了0-1的情况,问题竟然已经找到,下面我们来解决它改写之前的任务demo,在同步代码块中再判断一遍quantity是否大于0,形成同步代码块内外双重判断。
正确写法
package com.chenjie.executor.day14; /** * packageName com.chenjie.executor.day14 * * @author chenjie * @version JDK 8 * @className TicketTask (此处以class为例) * @date 2024/5/28 * @description TODO */ public class TicketTask1 implements Runnable { private int quantity=10; @Override public void run() { while (quantity>0){ synchronized (this){ if(quantity>0){ //打印车票 System.out.println(Thread.currentThread().getName()+"---"+quantity+"号票"); quantity--; } } } } }
运行结果
从运行结果来看,错票问题得到解决。
thread---10号票 thread---9号票 thread---8号票 thread---7号票 thread---6号票 thread---5号票 thread---4号票 thread---3号票 thread---2号票 thread---1号票
同步方法
下面我们用同步方法再来改写一次任务代码,将同步代码块抽取出来,封装成一个同步方法。就像这样,
package com.chenjie.executor.day14; import java.security.PublicKey; /** * packageName com.chenjie.executor.day14 * * @author chenjie * @version JDK 8 * @className TicketTask (此处以class为例) * @date 2024/5/28 * @description TODO */ public class TicketTask2 implements Runnable { private int quantity=10; @Override public void run() { while (quantity>0){ execute(); } } public synchronized void execute(){ if(quantity>0){ //打印车票 System.out.println(Thread.currentThread().getName()+"---"+quantity+"号票"); quantity--; } } }
结果
下面我们来运行程序试试,从运行结果来看依然是没有问题的。
thread---10号票 thread---9号票 thread---8号票 thread---7号票 thread---6号票 thread---5号票 thread---4号票 thread---3号票 thread---2号票 thread---1号票
总结
最后我们来总结一下本节的内容。本节我们介绍了synchronized关键字,它可以用来给对象和方法或者大模块加锁。当他锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码,在实际开发中经常会用到同步。
本文来自博客园,作者:小陈子博客,转载请注明原文链接:https://www.cnblogs.com/cj8357475/p/16086028.html