?写在前面?
🧭Java 多线程 🎉 内容回顾 Java 多线程介绍及线程创建 Java 多线程七大状态 Java 多线程方法详解 📢今天我们进行 Java synchronized关键字实现线程同步 的学习,感谢你的阅读,内容若有不当之处,希望大家多多指正,一起进步💯!!! ??如果觉得博主文章还不错,可以👍三连支持?一下哦😀
??Java synchronized关键字实现线程同步
??多线程卖票问题
问题描述:模拟三个窗口卖火车票的问题,假定一共有100张票。
🍀继承Thread类的方式
因为继承Thread 类的方式创建三个线程需要我们创建三个对象,所以我们需要用static 关键字来修饰票的数量来保证三个线程同时卖100张票。
代码示例:
public class SellTickets {
public static void main(String[] args) {
Window window1 = new Window();
Window window2 = new Window();
Window window3 = new Window();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
}else {
System.out.println("票已售罄~~~");
break;
}
}
}
}
执行结果:
🍀实现Runuable接口的方式
因为实现Runnable 接口的方式创建三个线程我们只需要创建一个对象,然后交给代理类,此时,三个线程用的是同一个对象,也就是保证了三个线程同时卖100张票了,因此也就不需要用static 关键字来修饰票的数量了。
代码示例:
public class SellTickets {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
} else {
System.out.println("票已售罄~~~");
break;
}
}
}
}
执行结果:
🍀超卖现象解读
通过执行结果我们发现不管是用实现Thread 类的方式还是用实现Runnable 接口的方式创建线程,都出现了超卖的现象,显然在现实生活中这样的现象是不允许的,那么为什么会出现超卖的现象呢?
假如此时剩余票数为1,两个线程通过检查(1 > 0,有票)同时进入卖票的这块代码,一个线程对票数进行 -1 操作后票数变为 0 ,而此时另一个线程也需要对票数 -1 操作后变为 -1 ,也就出现了票数 超卖 的现象,也有可能出现两个线程同时卖了一张票的情况,这就是出现了线程的安全问题。
??线程安全问题
当多个线程同时共享一份数据的时候,就极易容易出现线程安全问题 ,如上述卖票过程中,出现了重票,错票。
- 🌻问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 🌻问题的解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以操作ticket。在这种情况下即使线程a出现了阻塞,也不能改变。 也就是通过加锁的方式来保证同一时刻内只能有同一个线程来操作。
??举个栗子来形象的描述线程的安全问题。
比如我们在商场试衣间试衣服,进试衣间前是不是得先看一下试衣间有没有上锁呢?如果上锁了就表明有人正在使用试衣间,得等人使用完试衣间后才能进去,进去后的第一件事也是上锁,如果不上锁就不能保证其他人不进入试衣间,上完锁后,其他人就必须等待使用完后才能进去,上锁的过程其实就类比于保证线程安全的过程。
??同步机制解决线程安全问题
同步机制解决线程安全问题需要我们用到synchronized 关键字。
🍀同步代码块
同步代码块语法如下:
🍁 说明
- 被同步的代码即为操作共享数据的代码。
- 共享数据: 多个线程共同操作的变量。如上述示例中的ticket。
- 同步监视器: 俗称: 锁 。任何一个类的对象都可以充当锁。( 要求多个线程必须要共用一把锁 )。
🍃同步代码块处理实现Runnable接口的线程安全问题
使用同步代码块的方式解决多线程卖票的安全问题(实现Thread类的形式)。
public class SellTickets {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
} else {
System.out.println("票已售罄~~~");
break;
}
}
}
}
}
执行结果:
🌴 代码解读: 在主方法中只new了一个对象(Window window = new Window(); )即三个线程同时使用同一把锁,即同一个对象obj ,保证了3个线程同一时刻只能有一个线程操作被同步的代码,其他线程必须阻塞,等待释放锁才能进去。
🍁 补充 因为实现Runnable 接口的多个线程,只需要new 一次对象,我们可以new一个新的对象作为同步监视器,也可以用当前对象作为同步监视器即传入this 。
🍃同步代码块处理继承Thread类的线程安全问题
使用同步代码块的方式解决多线程卖票的安全问题(继承Thread类的形式)。
public class SellTickets {
public static void main(String[] args) {
Window1 window1 = new Window1();
Window1 window2 = new Window1();
Window1 window3 = new Window1();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window1 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
} else {
System.out.println("票已售罄~~~");
break;
}
}
}
}
}
执行结果:
🌴 代码解读: 在主方法中创建3个线程new了三个对象,此时三个线程要想使用同一把锁,即要把对象obj 声明为static ,保证了3个线程同一时刻只能有一个线程操作被同步的代码,其他线程必须阻塞,等待释放锁才能进去。
🍁 补充 因为继承Thread类 的多个线程,需要new 多个对象,我们可以new一个新的static 对象作为同步监视器,也可以用类对象作为同步监视器,即传入当前类名.class 。
同步代码块解决线程安全问题的注意问题: 同步代码块不能包含多了,也不能包含少了,包含少了可能会解决不了线程安全问题,包含多了会影响执行效率,甚至还可能引发新的问题,比如上述的卖票问题中,如果把whlie语句也归结于同步代码块,就会导致只能有一个线程在卖票,其他线程完全没有机会。
🍀同步方法
同步方法:如果操作的数据代码完整的声明在一个方法中,我们不妨将此方法声明为同步的,仍然用synchronized 关键字来修饰,语法如下:
🍃同步方法处理实现Runnable接口的线程安全问题
将卖票逻辑封装成一个sell方法(实现Runnable接口的形式)。
public class SellTickets03 {
public static void main(String[] args) {
Window3 window = new Window3();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Window3 implements Runnable {
private int ticket = 100;
private Object obj = new Object();
private synchronized void sell() {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
}
}
@Override
public void run() {
while (true) {
if (ticket <= 0) {
System.out.println("票已售罄~~~");
return;
}
sell();
}
}
}
执行结果:
🌴 代码解读: 在主方法中只new了一个对象(Window3 window = new Window3(); ),将sell 方法用synchronized 关键字来修饰,此时的同步监视器为当前对象 this ,保证了3个线程同一时刻只能有一个线程进入sell方法的代码,其他线程必须阻塞,等待释放锁才能进去。
🍃同步方法处理继承Thread类的线程安全问题
将卖票逻辑封装成一个sell方法(继承了Thread类的形式)。
public class SellTickets04 {
public static void main(String[] args) {
Window4 window1 = new Window4();
Window4 window2 = new Window4();
Window4 window3 = new Window4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window4 extends Thread {
private static int ticket = 100;
private static synchronized void sell() {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票剩余:" + (--ticket));
}
}
@Override
public void run() {
while (true) {
if (ticket <= 0) {
System.out.println("票已售罄~~~");
return;
}
sell();
}
}
}
执行结果:
🌴 代码解读: 在主方法中创建3个线程new了三个对象,将sell方法用static 和synchronized 关键字来修饰,此时的同步监视器为当前类对象,即 Window4.class 保证了3个线程同一时刻只能有一个线程进入sell 方法,其他线程必须阻塞,等待释放锁才能进去。
|