⭐写在前面⭐
Java 多线程
内容回顾
Java 多线程介绍及线程创建
Java 多线程七大状态
Java 多线程方法详解
今天我们进行 Java synchronized关键字实现线程同步 的学习,感谢你的阅读,内容若有不当之处,希望大家多多指正,一起进步!!!
♨️如果觉得博主文章还不错,可以三连支持⭐一下哦
问题描述:模拟三个窗口卖火车票的问题,假定一共有100张票。
因为继承
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;
}
}
}
}
因为实现
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 ,也就出现了票数 超卖 的现象,也有可能出现两个线程同时卖了一张票的情况,这就是出现了线程的安全问题。
当多个线程同时共享一份数据的时候,就极易容易出现
线程安全问题
,如上述卖票过程中,出现了重票,错票。
☘️举个栗子来形象的描述线程的安全问题。
比如我们在商场试衣间试衣服,进试衣间前是不是得先看一下试衣间有没有上锁呢?如果上锁了就表明有人正在使用试衣间,得等人使用完试衣间后才能进去,进去后的第一件事也是上锁,如果不上锁就不能保证其他人不进入试衣间,上完锁后,其他人就必须等待使用完后才能进去,上锁的过程其实就类比于保证线程安全的过程。
同步机制解决线程安全问题需要我们用到
synchronized
关键字。
同步代码块语法如下:
使用同步代码块的方式解决多线程卖票的安全问题(实现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类的形式)。
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
关键字来修饰,语法如下:
将卖票逻辑封装成一个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方法的代码,其他线程必须阻塞,等待释放锁才能进去。
将卖票逻辑封装成一个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
方法,其他线程必须阻塞,等待释放锁才能进去。