目录
四、线程的同步
4.1 线程同步介绍
-
场景案例
例如电影院有三台自动售票机售卖100张电影票,不考虑实际情况,票号从100号依次减少,售票过程包括出票和自减两个步骤,先自减再出票,当其中一台机器A自减结束未完成出票的操作,另一台机器B执行了自减操作,这样导致两台机器均售出了相同的票(重票、漏票),再者,剩余最后一张票时,三台机器同时执行自减操作,这样三台售卖机可能卖出0号票、-1号票的情况(错票)。所以,当一台售票机执行售票操作(自减和出票),不允许其他机器同时执行售票操作(即线程同步),三台售票机相当于三条线程,而100张电影票则是共享资源,且实际情况下,出票和自减均是一瞬间的事情,所以从感官上是三台机器同时操作。
-
原因分析
当多条语句在操作线程共享数据时,一个线程对多条语句只执行其中一部分,还没有执行完成,另一个线程参与进来,导致共享数据错误,即发生线程安全问题。
-
解决方案
对于多条操作共享数据的语句,只允许其中一个线程都执行完,在执行过程中,不允许其他的线程参与执行,哪怕正在执行中的线程处于堵塞状态,也不允许其他线程去执行。
-
线程同步
当线程共享了相同的资源,在某些时刻同时读写该资源时,可能会产生冲突,从而导致线程的安全问题,因此一次只能允许其中一条线程来访问共享资源,即线程同步。
4.2 线程同步实现
4.2.1 场景介绍
-
需求描述
准备100张票作为共享资源,三台自动售票机进行售票,即启动三个线程去消费100张电影票。
-
继承Thread方式
class Window1 extends Thread {
private static Integer ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
Window1 w1 = new Window1();
Window1 w2 = new Window1();
Window1 w3 = new Window1();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
-
实现Runnable接口
class Window2 implements Runnable {
private Integer ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
-
错误现象
4.2.2 Synchronized使用
-
同步代码块
// 格式
synchronized(同步监视器){
// 需要被同步的业务代码程序
}
1.操作共享数据的程序代码,即是需要被同步的代码,不能包含代码多了,也不能包含代码少了;
2.共享数据,即多个线程可以共同操作的变量,例如100张电影票;
3.同步监视器,俗称锁,任何一个对象都可以充当锁,潜在的要求就是多个线程对象必须共用一把锁,所以推荐继承Thread类的的子类,使用子类的对象(类名.class),实现Runnable接口的子类,使用当前子类对象本身(this)。
-
继承Thread方式
private static Integer ticket = 100;
while (true) {
synchronized (Window3.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
-
实现Runnable接口
private Integer ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
-
同步方法
// synchronized还可以放在方法声明中,表示整个方法为同步方法。
private static synchronized void show();
private synchronized void show();
// 在方法的修饰词中加上synchronized关键字
1.静态方法,同步方法的同步监视器是类名.class;
2.非静态方法,同步方法的同步监视器是this
-
继承Thread方式
private static synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
-
实现Runnable接口
private synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
注意,这里一个使用的是静态的同步方法,一个使用的非静态的同步方法。
总结
优点:同步的方式,解决了线程的安全问题 缺点:操作同步代码,只能一个线程参与,其他线程等待,相当于是单线程操作,效率低
同步锁机制: 在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。