Java同步与synchronized锁基础

一、线程同步与锁

 同步和锁的概念,主要是为了解决多线程共享资源的问题。当多个线程共享资源进行竞争访问,导致运行结果与期望不符时,通常就需要利用线程同步和锁的机制来消除这种多线程的竞争访问导致的差异性。示例:

public class ThreadCompetitionTest {

	static int count=0;
        public static void main(String[] args) throws InterruptedException {
        long start=System.currentTimeMillis();
        Thread t = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <5000000 ; i++) {
                    count++;
                }
                System.out.println("自定义线程:计算完成...,耗时"+(System.currentTimeMillis()-start));
            }
        };
        t.start();
        for (int i = 0; i <5000000 ; i++) {
            count++;
        }
        System.out.println("主线程:计算完成....,耗时"+(System.currentTimeMillis()-start));
        t.join();
        System.out.println("count:"+count);
    }

}

 次示例代码的其中一次运行结果如下(每次运行的结果几乎都不相同)

 

主线程:计算完成....,耗时11
自定义线程:计算完成...,耗时13
count:9973996

 次示例代码的运行结果并不是固定的一千万,而是每次都不一样。出现该现象的原因主要是这两个线程同时对共享资源count变量进行了修改,并且count++并不是一个原子操作。每次自增实际上是分为3个步骤:

 

  1. 获取count变量的当前值

  2. 将当前值加1

  3. 将加1后的值存储到count变量中

因为线程是并行执行的,因此这就可能出现问题。例如假设count变量当前值是0,主线程和自定义线程同时获取到这个值,主线程先完成自增的操作,将count变量的值设置为1。自定义线程随后完成自增的操作,因为自定义线程也是在0的基础上加1,然后将值赋值给count变量,最终导致实际上进行了两次自增操作,但实际上确只加了1。

 

我们可以通过同步代码块(synchronized block)和来解决上述代码中出现的问题,解决的办法是将主线程和自定义线程中的count++修改成:

synchronized (ThreadCompetitionTest.class) {
       count++;
}

当然这只是多种同步锁解决方案的其中一种,这里我们先以 synchronized来解决。这样修改之后不论运行多少次,其结果都是像期望的那样是一千万。

 

二、竞态条件与临界区

多线程并不是造成上述问题的关键,关键在于多个线程访问了相同的资源(即共享资源),并且只有当这些线程中的一个或多个对这些共享资源做了写操作才可能导致出现问题,只要资源没有发生变化,多个线程读取相同的资源还是安全的。

当多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件导致竞态条件发生的代码区称作临界区。例如上面示例代码中的count++操作,就是一个临界区,它会产生竞态条件。

 

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。因此,有时候我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被进行操作导致修改,从而消除竞态条件。

 

三、线程安全

允许被多个线程同时执行的代码称作线程安全的代码。也就是说,线程安全的代码对多个线程的访问顺序不敏感,不论多个线程以何种顺序进行访问其结果都不会发生改变,故线程安全的代码不包含竞态条件

 

一般情况下,局部基础类型变量和不会逃逸(通俗地讲,指不会被其他线程引用到)出当前线程的局部引用类型对象都是线程安全的。对于成员变量如果两个线程可以同时更新同一个对象的同一个成员,那这个代码就非线程安全的。对于非局部引用变量,就算其内部被操作的是一个线程安全的对象实例引用,但是该非局部引用变量本身还是可能是非线程安全的,因为,其内部被操作的那个线程安全的对象引用可能会被改变而重新指向另一块内存地址。

 

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

  1. 如果一个资源的创建,使用,销毁都在同一个线程内完成,
  2. 且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。

即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了只在各自线程上下文中使用的数据库连接,相对数据库连接来说,每个连接自身确实是线程安全的,但它们所连接到的同一个数据库实例也许就不是线程安全,例如:如果这两个线程都先查询某条记录X是否存在,如果不存在就插入X记录,如果恰好两个线程同时都查询到不存在X记录,那么将导致这两个线程分别插入了一条相同的记录。

 

类似的问题可能还会发生在文件操作或其他共享资源的操作上,因此,判断线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

 

四、Synchronized锁使用

synchronized关键字最主要有以下四种使用方式:

  1. 实例方法同步
  2. 实例方法同步块
  3. 静态方法同步
  4. 静态方法同步块

在Java中,每一个对象实例有且仅有一个锁与之相关联,所以任何一个对象实例都可以作为一个锁。当线程要执行同步方法或同步代码块的时候,必须首先获得相应的锁才能进入执行,否则将转为阻塞等待状态,这种阻塞等待除了获得了相应的锁,不能通过中断等方式唤醒从而终止继续阻塞等待。在多个线程同时准备获得同一把锁的时候,最多只能有一个线程能够成功获得这把锁(也称对象监视器),执行完毕退出同步方法或同步代码块之后,释放相应的锁。

所以,同步锁是掌握在当前运行线程手里的。另外,静态方法同步/块对应的锁,实际上是Class对象的一个特定实例,而实例同步方法/块的锁,是相应的特定的对象实例。例如,实例同步方法和使用this作为锁的同步代码块,其锁就是当前调用执行该方法的对象实例。

后记 

在查阅synchronized相关资料的时候,发现很多资料有提到“synchronized是不能被继承的” ,当我第一次看到这个结论的时候,编写了测试代码进行验证,但是却总是无法证实该观点,于是继续查证,最后在CSND找到如下解释:

synchronized不能被继承真相 写道
其实真相是这样的,“synchronized不能被继承”,这句话有2种不同意思,一种是比较正常的、很容易让人想到的意思;另一种是“不正常的”或者说是“java中文界”广泛认同了的意思。
楼主是第一种意思,其他人是第二种意思。所以,会出现该贴的尴尬讨论。
第一种理解方式:父类中有个synchronized方法,子类继承了父类,但子类没覆写该方法。通过子类实例来使用该方法时,按“synchronized不能被继承”,意思就为:该子类的该方法就变成了非synchronized方法。
第二种理解方式:synchronized并不属于方法定义的一部分,不能被继承。子类覆写了该方法,如果在覆写时不明确写上synchronized,那这个方法就不是synchronized。换句话说,虽然继承了,但是没把synchronized继承下来,也就意味着“synchronized不能被继承”。

我觉得“synchronized不能被继承”这句话,没把意思表述清楚。产生这种情况的原因,我推测是这样的:某前辈详细解释了以上2种意思,最后总结的时候,使用了“synchronized不能被继承”这句不太合适的话。某无知后辈转述前辈意思的时候,就直接用了“synchronized不能被继承”。结果一传十,十传百,这句话就传开了,也让初学者产生了迷惑。

在英文世界里,没有“synchronized不能被继承”的讨论。这也说明了点问题。

其实楼主的问题,可以记住这么句话:synchronized方法,一定要显示标明,它是不能隐式标明的。

中文里“一句话多种意思”的问题,真的是给学术界增加了不少麻烦!

原来我的理解是第一种方式,而这句话真正要说的是,当子类覆写了父类的同步方法时,synchronized必须显示的指定才会是同步方法,而不能通过从父类继承从而也成为同步方法,如果子类没有覆写父类的同步方法,通过子类实例调用该方法,其实使用的还是父类的方法,因此该方法仍然是synchronized。

猜你喜欢

转载自pzh9527.iteye.com/blog/2385106