根据前面讲的Java内存模型,已经接触不少synchronized,而且它非常强大,能解决大部分的并发问题,今天我们一起来学习它吧。 以下是本文包含的知识点: 1.Java的线程安全 2.synchronized的用法 3.synchronized的实现原理 一、Java的线程安全 我们这里讨论的线程安全,限定于多个线程之间存在共享数据访问的这个前提下。如果一段代码根本不会和其它线程共享数据,那么也不存在线程安全的问题。 那我们应该如何实现线程安全呢? 互斥同步是一种常见的并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段。 在Java中最基本的互斥同步手段就是synchronized关键字。还有一种是重入锁(ReentrantLock),后面会讲到。 二、synchronized的用法 可以参考之前的文章Thread类的使用,线程同步一节有讲到 1.作用于代码块上,只同步这一段代码: synchronize(this){//this指锁定当前对象 num++; System. out.println(name + ', 你是第' + num + '个使用timer的线程' );} 2.放在方法声明中,表明整个方法为同步方法: public synchronized void add(String name) {//还可以修饰static方法 num++; System. out.println(name + ', 你是第' + num + '个使用timer的线程' );} 有几点需要注意的地方: 1.当一个线程正在访问一个对象的synchronized方法时,其它线程不能访问该对象的其它synchronized方法。因为该对象的锁还未被释放,其它线程拿不到。 2.当一个线程正在访问一个对象的synchronized方法时,其它线程可以访问该对象的其它非synchronized方法。因为非synchronized方法,不需要锁。 3.当一个线程正在访问一个对象的synchronized方法时,其它线程可以访问其它对象的synchronized方法或非synchronized方法。因为锁的对象不一样。 另外,每个类也有自己的锁,被synchronized修饰的static方法,类锁与对象的锁也不会发生互斥。看下面代码: public synchronized void test1{//对象锁 //}public synchronized static void test2{//类锁 //} 当多线程同时访问test1和test2时,可以并发访问,不会发生互斥。因为锁的对象不一样,一个是类,一个是对象。 三、synchronized的实现原理 synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型参数来指明需要锁定和解锁的对象。如果在程序中明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,那根据synchronized修饰的是实例方法还是静态方法,去取对应的对象实例或Class对象来作为锁对象。 看下面这段代码: package com.yuwl.thread.demo;public class TestSynchronized { public void test1{ } public void test2{ synchronized( this){ } } public synchronized void test3{ } } 对其反编译的字节码为: 从反编译的字节码可以看出,加了synchronized的代码块多了两个指令。 根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果已经拿到对象的锁,则把计数器加1,相应的在执行monitorexit指令时会将计数器减1,当计算器为0时,锁就被释放了。如果获取对象失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放。对于synchronized方法,执行中的线程会识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记,如果有会自动获取对象的锁,调用方法,最后释放锁。 synchronized还有一个重要特性——可重入性,不会自己把自己锁死。解释下,比喻一个线程执行同步方法1,而同步方法1又要调用同步方法2,这种情况是可重入的。 synchronized对异常的处理,如果同步块有异常发生,线程会自动释放锁。 参考 《深入Java 虚拟机》 |
|