第十八讲 多线程——多线程安全问题的解决方案——同步代码块和同步函数
发布日期:2021-06-30 18:03:03 浏览次数:2 分类:技术文章

本文共 5236 字,大约阅读时间需要 17 分钟。

同步代码块

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码块被称作同步代码块,格式如下:

synchronized(对象) {    需要被同步的代码}

对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取CPU的执行权,也进不去,因为没有锁。火车上的卫生间就是一个经典的同步例子,对于简单的卖票程序,加入同步之后的代码为:

class SaleTicket implements Runnable{	private int tickets = 300;	Object obj = new Object();	public void run()	{		while (true)		{			synchronized (new Object())			{				if (tickets > 0)				{					try {Thread.sleep(10);} catch (InterruptedException e) {}//让线程到这里稍微停一下。					System.out.println(Thread.currentThread().getName() + "...." + tickets--);				}			}		}	}}class TicketDemo3 {	public static void main(String[] args) 	{		//线程任务对象。		SaleTicket t = new SaleTicket();		//创建四个线程。通过Thread类的对象。		Thread t1 = new Thread(t);		Thread t2 = new Thread(t);		Thread t3 = new Thread(t);		Thread t4 = new Thread(t);		t1.start();		t2.start();		t3.start();		t4.start();	}}

同步的前提

有可能出现这样一种情况,多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧!这时咋办呢?要想到肯定是同步出现了问题,我们只要遵守了同步的前提,就可以解决。同步的前提:

  1. 必须要有两个或者两个以上的线程;
  2. 必须是多个线程使用同一个锁。

必须保证同步中只有一个线程在运行。同步在目前的情况下保证了一次只能有一个线程在执行,其他线程进不来,这就是同步的锁机制。

同步的好处

解决了多线程的安全问题。

同步的弊端

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

同步函数

例,银行有一个金库,有两个储户,分别存300元,每次存100元,存3次。试问如下程序是否有安全问题,如果有,该如何解决?

class Bank{	private int sum;	public void add(int n)	{		sum = sum + n;		try {Thread.sleep(10);} catch (InterruptedException e) {}		System.out.println("sum = " + sum);	}}class Customer implements Runnable{	private Bank b = new Bank();	public void run()	{		for (int x = 0; x < 3; x++)		{			b.add(100);		}	}}class BankDemo {	public static void main(String[] args) 	{		//1,创建任务对象。		Customer c = new Customer();		Thread t1 = new Thread(c);		Thread t2 = new Thread(c);		t1.start();		t2.start();	}}

以上程序绝壁是有问题的,那我们又该如何找到问题?我们只要明确以下几点就能找到问题。

  • 明确哪些代码是多线程运行代码;
  • 明确共享数据;
  • 明确多线程运行代码中哪些语句是操作共享数据的。

同步代码块,咱已经很熟了,所以就没必要再用它了,不妨我们试试同步函数,同步函数其实就是在函数上加上了同步关键字进行修饰。

class Bank{	private int sum;	public synchronized void add(int n)	{		sum = sum + n;		try {Thread.sleep(10);} catch (InterruptedException e) {}		System.out.println("sum = " + sum);	}}class Customer implements Runnable{	private Bank b = new Bank();	public void run()	{		for (int x = 0; x < 3; x++)		{			b.add(100);		}	}}class BankDemo {	public static void main(String[] args) 	{		//1,创建任务对象。		Customer c = new Customer();		Thread t1 = new Thread(c);		Thread t2 = new Thread(c);		t1.start();		t2.start();	}}

这里我们思考一个问题,同步函数用的是哪一个锁呢? 答案是函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。

验证同步函数使用的锁是this

需求:验证同步函数使用的锁是this。

我们可以这样做,启动两个线程,一个线程负责执行同步代码块(使用明锁),另一个线程使用同步函数(使用this锁)。两个线程执行的任务都是一样的(例如都是卖票),如果它们没有使用相同的锁,说明它们没有同步,会出现数据错误。下面我们就以卖票程序来验证该需求。

class Ticket implements Runnable {    private int tick = 100;    Object obj = new Object();    //定义一个boolean标记。    boolean flag = true;    public void run() {        if(flag) {            while(true) {                synchronized(this) {                    if(tick > 0) {                        try { Thread.sleep(10); } catch(Exception e) {}                        System.out.println(Thread.currentThread().getName()+"...code:"+tick--);                    }                }            }        } else {            while(true)                 show();        }    }    public synchronized void show() { // this        if(tick > 0) {            try { Thread.sleep(10); } catch(Exception e) {}            System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);        }    }}class ThisLockDemo {    public static void main(String[] args) {        Ticket t = new Ticket();        Thread t1 = new Thread(t); // 创建一个线程        Thread t2 = new Thread(t); // 创建一个线程        t1.start();        // 主线程停10ms,此刻能运行的线程只有t1        try { Thread.sleep(10); } catch(Exception e) {}        t.flag = false;        // 主线程醒了之后,t1和t2线程同时运行        t2.start();    }}

验证static同步函数的锁是类名.class

现在我们思考如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不再是this,因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class。静态的同步方法,使用的锁是该方法所在类的字节码文件对象,即类名.class

class Ticket implements Runnable {    private static int tick = 100;    // Object obj = new Object();    boolean flag = true;    public void run() {        if(flag) {            while(true) {                synchronized(Ticket.class) {                    if(tick > 0) {                        try { Thread.sleep(10); } catch(Exception e) {}                        System.out.println(Thread.currentThread().getName()+"...code:"+tick--);                    }                }            }        } else {            while(true)                 show();        }    }    public static synchronized void show() {         if(tick > 0) {            try { Thread.sleep(10); } catch(Exception e) {}            System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);        }    }}class StaticMethodDemo {    public static void main(String[] args) {        Ticket t = new Ticket();        Thread t1 = new Thread(t); // 创建一个线程        Thread t2 = new Thread(t); // 创建一个线程        t1.start();        // 主线程停10ms,此刻能运行的线程只有t1        try { Thread.sleep(10); } catch(Exception e) {}        t.flag = false;        // 主线程醒了之后,t1和t2线程同时运行        t2.start();    }}

同步函数和同步代码块的区别

  • 同步代码块使用的是任意的对象作为锁;
  • 同步函数只能使用this作为锁。

同步函数和同步代码块的应用场景

如果说一个类中只需要一个锁,这时可以考虑同步函数,使用this,写法简单。但是,一个类中如果需要多个锁,还有多个类中使用同一个锁,这时只能使用同步代码块。建议使用同步代码块。

转载地址:https://liayun.blog.csdn.net/article/details/82670572 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:第十九讲 多线程——死锁
下一篇:第十七讲 多线程——多线程的安全问题

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月22日 09时35分03秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章