多线程(三)线程同步

线程同步

前面说到的线程都是独立、且异步执行,而多线程编程的特点就是多个线程能够读写相同的变量或数据结构 。所以,编写多线程程序时,必须注意每个线程是否干扰了其他线程的工作。
下面首先给出一个多个线程在使用相同资源时会出现问题的例子。
示例1:

public class Site implements Runnable {
    private int count = 10;   //  剩余票数
    private int num = 0;      //  记录买到第几张
    @Override
    public void run() {
        while (count > 0) {      //没有余票时,跳出循环
            //第一步,修改数据
            num++;
            count--;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //第二步,显示信息
            System.out.println(Thread.currentThread().getName() +
                    "抢到第" + num + "张票,剩余" + count + "张票!");
        }
    }
}

public class TestSite {
    public static void main(String[] args) {
        Site site=new Site();
        Thread person1 = new Thread(site, "唐甜");
        Thread person2 = new Thread(site, "抢票软件");
        Thread person3 = new Thread(site, "黄牛");
        person1.start();
        person2.start();
        person3.start();
    }
}

以上代码的运行结果如下:
运行结果
可以看到,最终显示结果出现了以下问题:

  1. 不是从第一张票开始
  2. 存在多人抢到同一张票的情况
  3. 有些票号没有被抢到

这是由于多个线程并行工作操作同一共享资源时,带来的数据不安全问题。这是由于多个线程并行工作操作同一共享资源时,带来的数据不安全问题。
当多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这就称为线程同步(synchronized)。

线程同步的实现

在 Java 程序中最简单实现同步的方法就是上锁。为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。
假想有一个房间,任一时刻只允许有一个人进入,就需要给这个房间上锁。房间只有一把钥匙,若没有钥匙就不能进入该房间。进入房间后锁门,其他人不能进入。退出房间后锁门,下一个人得到钥匙才能进入。
给共享变量上锁就使得 Java 线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,就可以知道没有其他线程能够访问该对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线程开锁。一旦锁被打开,这些睡眠进程就会被唤醒并移到准备就绪队列中。
采用线程同步来控制线程的执行方法有两种,即同步方法和同步代码块。
Java使用synchronized关键字来获得锁。

1.同步方法

在方法声明中加入synchronized关键字,声明这是一个同步方法。
修改上面Site类的代码如下:
示例2

public class Site implements Runnable {
    private int count = 10;		//记录剩余票数
    private int num = 0;		//记录买到第几张票
    private boolean flag = false;	//记录是否售完

    public void run() {
        while (!flag) {
            sale();
        }
    }

	//同步方法:售票
    public synchronized void sale() {
        if (count <= 0) {
            flag = true;
            return;
        }
        //第一步,修改数据
        num++;
        count--;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //第二部,显示信息
        System.out.println(Thread.currentThread().getName() + "抢到第"
                + num + "张票,剩余" + count + "张票.");
    }
}

此为使用synchronized声明了sale()为同步方法
运行结果如下:
在这里插入图片描述
上述方法为非静态同步方法,即synchronized修饰的是类的实例方法,锁定的当前对象,如果是类的其他对象就没有这个约束了,而当synchronized修饰的方法是静态方法时,即

public static synchronized void sale(){//略}

此时锁定的是当前类,可以控制类的所有实例的访问。
同步方法的缺陷:如果将一根运行时间比较长的方法声明为synchronized将会影响效率。例如,将实例1中的run()方法声明为synchronized,由于在线程的整个声明周期内它一直在运行,因此就有可能导致run()方法会执行很长时间,那么其他的线程就需要一直等到run()方法结束了才能执行。

2.同步代码块

代码块即使用{}括起来的一段代码,使用synchronized关键字修饰的代码块,称为同步代码块。其语法如下:

synchronized(syncObject){
	//需要同步的代码块
}

示例3

public class Site implements Runnable {

    private int count = 10;   //  剩余票数
    private int num = 0;      //  记录买到第几张
    public void run() {
        while (count > 0) {      //没有余票时,跳出循环
            synchronized (this) {   //同步代码块
            	//第一步,修改数据
            	num++;
            	count--;
           	 	try {
                	Thread.sleep(500);
            	} catch (InterruptedException e) {
                	e.printStackTrace();
            	}
            	//第二步,显示信息
            	System.out.println(Thread.currentThread().getName() +
                    	"抢到第" + num + "张票,剩余" + count + "张票!");
            }
        }
    }
}

以上代码将修改数据和显示信息的操作进行了同步,实现效果与使用同步方法相同。同步代码块可以更精准的限制访问区域。
以下几点需注意:

1. 当多个并发线程访问同一对象的同步代码块时,同一时刻只能有一个线程得到执行,其他线程必须等待当前线程执行完毕之后才能执行该代码块。
2. 当一个线程访问对象的一个同步代码块时,其他线程对对象中所有其他同步代码块的访问将被阻塞。即该线程获得这个对象的锁,其他线程对该对象所有同步代码块的访问都被暂时阻塞。
3. 当一个线程访问一个对象的同步代码块时,其他线程仍可以访问该对象的非同步代码块。

小结:

本章开始,才真正意义上接触到线程编程,举例说明了多线程编程有可能会出现的数据不安全情况,并简单介绍了解决办法——线程同步synchronized。下一章将介绍多线程编程中,应对控制访问一组(大量)资源时,常用的一种方法,信号量计数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值