Java代码质量改进之:同步对象的选择

举报
码农星球陆敏技 发表于 2019/03/19 13:54:30 2019/03/19
【摘要】 在Java中,让线程同步的一种方式是使用synchronized关键字,它可以被用来修饰一段代码块,如下: synchronized(被锁的同步对象) { // 代码块:业务代码 } 当synchronized被用来修饰代码块的时候表示,如果有多个线程正在执行这段代码块,那么需要等到其中一个线程执行完毕,第二个线程才会再执行它。

       在Java中,让线程同步的一种方式是使用synchronized关键字,它可以被用来修饰一段代码块,如下: 

     synchronized(被锁的同步对象) {

          // 代码块:业务代码

     }

       当synchronized被用来修饰代码块的时候表示,如果有多个线程正在执行这段代码块,那么需要等到其中一个线程执行完毕,第二个线程才会再执行它。但是!如果被锁的同步对象没有被正确选择的话,上面的结论是不正确的哦。

       到底什么样的对象能够成为一个锁对象(也叫同步对象)?我们在选择同步对象的时候,应当始终注意以下几点:

第一点,需要锁定的对象在多个线程中是可见的、同一个对象

       “可见的”这是显而易见的,如果对象不可见,就不能被锁定。“同一个对象”,这理解起来也很好理解,如果锁定的不是同一个对象,那又如何来同步两个对象呢?可是,不见得我们在这上面不会犯错误。为了阐述本建议,我们先模拟一个必须使用到锁的场景:火车站卖火车票。一列火车一共有100张票,一共有3个窗口在同时卖票,代码如下:

package com.zuikc.thread;

public class SynchronizedSample01 {

public static void main(String[] args) {

// 创建

TicketWindow window1 = new TicketWindow("售票窗口1");

TicketWindow window2 = new TicketWindow("售票窗口2");

TicketWindow window3 = new TicketWindow("售票窗口3");

window1.start();

window2.start();

window3.start();

}

}

class TicketWindow extends Thread {

// 共100个座位

static int ticket = 100;

public TicketWindow(String name) {

super(name);

}

@Override

public void run() {

// 模拟卖票

while (ticket > 0) {

System.out.println(this.getName() + "卖出了座位号:" + ticket);

ticket--;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

       可是,运行之后,我们发现我们的火车票有些座位被卖了多次,比如:

图片2.png

       只要多运行几次,我们就会看到不同的结果。但是几乎每次都会有被座位号被卖多次的现象发生。

       有同学可能会说,简单:加synchronized锁定同步对象,于是我们修改代码:

class TicketWindow extends Thread {

// 共100个座位

static int ticket = 100;

// 定义被锁的同步对象

Object obj = new Object();

public TicketWindow(String name) {

super(name);

}

@Override

public void run() {

// 想要同步的代码块

synchronized (obj) {

// 模拟卖票

while (ticket > 0) {

System.out.println(this.getName() + "卖出了座位号:" + ticket);

ticket--;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

       运行之后,我们发现结果没有任何的改变。为什么呐?

       因为在3个线程中,我们锁定的不是同一个对象。

       我们看到,被锁的是一个实例变量,如下:

       Object obj = new Object();

       而存在三个线程,就意味着生成了3个obj,每个线程锁定的是这3个不同的obj对象,所以,同步代码块等于没有被同步。

       那应该怎么做呢?最简单的方法是,我们可以把实例变量改成成员变量,即静态变量,如下:

       Static Object obj = new Object();

       然后,再运行售票代码,就发现可以解决这个问题了。不信,试试看。


第二个注意事项:非静态方法中,静态变量不应作为同步对象

       上面刚说完,要修正第一点中的示例,需要将obj变成static。这似乎和本注意事项有矛盾。实际上,第一点中的示例代码仅出于演示的目的,在编写多线程代码时,我们可以遵循这样的一个原则:类型的静态方法应当保证线程安全,非静态方法不需实现线程安全。而如果将syncObject变成static,就相当于让非静态方法具备线程安全性,这带来的一个问题是,如果应用程序中该类型存在多个实例,在遇到这个锁的时候,都会产生同步,而这可能不是我们原先所愿意看到的。


第三点:值类型(基本数据类型)对象不能作为同步对象

       实际上,这样的代码也不会通过编译。

       值类型在传递另一个线程的时候,会创建一个副本,这相当于每个线程锁定的也是两个对象。故,值类型对象不能作为同步对象。这一点实际也可以归结到第一点中。


第四点,锁定字符串是完全没有必要,而且相当危险的

       这整个过程看上去和值类型正好相反。字符串在虚拟机中会被暂存到内存里,如果有两个变量被分配了相同内容的字符串,那么这两个引用会被指向同一块内存。所以,如果有两个地方同时使用了synchronized (“abc”),那么它们实际锁定的是同一个对象,导致整个应用程序被阻滞。


第五点:降低同步对象的可见性

       同步对象一般来说,不应该是一个public变量,我们应该始终考虑降低同步对象的可见性,将我们的同步对象藏起来,只开放给自己或自己的子类就够了(需要开放给子类的情况其实也不多见)。

       感谢关注“码农星球”。本文版权属于“码农星球”。我们提供咨询和培训服务,关于本文有任何困惑,请关注并联系我们。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。