Java基础(32)——多线程相关知识详解及示例分析四(线程间的互斥)
版权声明
- 本文原创作者:清风不渡
- 博客地址:https://blog.csdn.net/WXKKang
一、线程间的互斥
1、为什么需要实现线程间的互斥
我们知道,线程会有多个并发运行的情况,那么,当多个并发的线程需要使用共享数据时,我们就必须需要考虑使用共享数据的线程状态与行为,否则就不能保证共享数据的一致性,也就不能保证程序的正确性了
(1)共享数据
那么,什么是共享数据呢?很简单,就是当这个数据需要被多个线程使用的时候,这些数据就是共享数据
如下图所示,有Object1、Object2、Object3三个对象,线程A访问的是Object1、Object2这两个对象,而线程B访问的是Object2、Object3这两个对象,那么,Object2即为共享对象,Object1、Object3则不是共享对象
(2)如果线程对共享数据只有读操作,没有写操作(更改操作)
既然访问共享数据的线程都是只有读操作,那么这段共享数据就不会改变,那么这些线程之间就不会产生并发冲突,即不会产生并发互斥,因为所有的线程每次读取出来的共享数据的值都是相同的,这与我们所希望读取的数据相符合
(3)如果线程对共享数据既有读操作,也有写操作(更改操作)
当多个线程之间有共享数据时,如果有多个线程需要读写共享数据,那么线程之间就可能会产生并发冲突,导致严重的逻辑错误
由于多个线程之间没有做任何控制,这两个线程的调度顺序是随机的,它们的执行顺序是不可预测的。由于对共享对象有写操作来修改对象状态,导致同一线程读取共享对象的状态可能前后不一致
2、示例
对于上面所说的线程对共享数据既有读操作,也有写操作(更改操作),我们通过一个示例来更好的学习它,现在我们模拟一个售票场景,比如现在有20张机票,需要在三个窗口同时售卖,那么我们该如何设计这个小程序呢?
(1)无共享数据
SaleTicket类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
int num = 20; //票数,在每个对象里都会维护一份数据
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
测试类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//创建SaleTicket实体类线程对象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
执行结果如下:
由执行结果我们可以发现,20张机票被共被售卖了60次,每一个窗口都售卖了20张票,这是为什么呢?
因为num是非静态成员变量,所以在每个对象中都会维护一份成员变量,这样三个线程总共会有三份对象,因此总和是60张
解决方案:把num票数共享出来给三个线程对象使用,方法就是使用static修饰变量num,代码如下:
(2)有共享数据
SaleTicket类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
static int num = 20; //票数,在每个对象里都会维护一份数据
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
测试类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//创建SaleTicket实体类线程对象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
执行结果如下:
由上面的执行结果我们可以发现,虽然总体来说比没有设置共享数据的时候误差小了,但还是有问题的,那是什么问题呢?就是我们之前所说过的线程安全问题,一张票被售卖了多次!!!
因为多个线程并发执行时是无序的,并不能保证售卖票的时候不被打断,这样就可能发生同一张票被重复售卖,那么我们怎么解决这个问题呢?这就要使用到之前所讲过的Java提供的互斥机制了
3、互斥实现
多个线程并发访问共享变量、共享资源(以下统称共享数据),容易产生线程安全问题。为了解决该问题:在某一时刻,共享数据只能被一个线程访问,该线程访问结束后其他线程才可以对其进行访问。锁(Lock)就是这种思路以保障线程安全的线程同步机制。一个线程在访问共享数据时获得锁,在访问结束后释放锁以便其他线程可以访问该共享数据
(1)线程同步机制
在Java中我们有两种方式实现同步:同步代码块、同步函数,都需要使用synchronized来定义
A、同步代码块
同步代码块的格式,需要显示指定锁对象。在进入代码块时需要获得锁对象,如果没有得到锁对象则等待;在退出代码块时自动释放锁对象,语法格式如下:
synchronized(锁对象){
需要被同步的代码…
}
注意:同步代码块中的锁对象可以是任意对象
B、同步函数
在普通函数的返回值前面加上synchronized关键字,这种方式的锁对象就是这个函数所在的对象。在进入函数时需要获得锁对象,如果没有得到锁对象则等待;在退出函数时自动释放锁对象,语法格式如下:
synchronized 函数返回值 函数名([参数列表]){
需要被同步的代码…
}
注意:
1、同步函数中的锁对象是this
2、如果该方法是静态的同步函数那么锁对象是类的字节码文件对象
C、注意事项
1、任意的一个对象都可以做为锁对象
2、在同步代码块中调用了sleep方法并不释放锁对象
3、只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率
4、多个线程操作共享数据的锁对象必须是同一个锁,否则加锁无效。不理解这个,就无法理解生产者,消费者示例中需要在两处同步代码中加同一个锁
(2)详细实现
多线程互斥程序设计的重点在于确定锁对象和互斥区域,其具体步骤如下:
A、确定锁对象
互斥产生的原因,是由于多个线程对同一共享数据进行修改。为了保证某个线程的修改过程不被打断,我们通过设置互斥区域,在互斥区域中操作时将锁定这个共享数据,其它线程都不能使用。只有这个线程修改完成后,释放了锁对象,其它线程才有机会执行。因此,多个线程一定需要共享锁对象。 下 面例子中使用一个独立的Object对象作为锁对象,并将它定义为static,这样将被所有线程实例共享
B、确定互斥区域
通常,互斥区域越小,性能就越好,下面例子中使用互斥代码块实现
在SaleTicket类中封装了数据和相应的操作,只需要在互斥代码块中包括写共享数据的代码即可
C、示例代码
像上面示例中的问题如果需要使用互斥机制来解决的话该怎么办呢?代码如下:
SaleTicket类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
private static int num = 20; //票数,在每个对象里都会维护一份数据
private static Object lock = new Object();
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
synchronized (lock) {
//同步代码块
while(true){ //lock是锁对象
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
}
测试类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//创建SaleTicket实体类线程对象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
这样我们就实现了通过线程互斥机制来保证线程并发时数据的安全性,如果效果不明显可将票数调大一点即可