一、synchronized的使用
(一)、synchronized同步方法
1. “非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题。
2. 如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题。
3. synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。因此在多线程访问同一个对象时,哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁lock,其他线程只能呈等待状态。
但如果多个线程访问多个对象,则JVM会创建多个锁。
4. 调用关键字synchronized声明的方法一定是排队运行的。另外只有共享资源的读写访问才需要同步化,如果不是共享资源,就没有同步化的必要。
5. 脏读。当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized关键字的同步方法。
6. synchronized锁重入。使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时,是可以再次得到该对象的锁的。
7. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
8. 同步不可以继承。如果父类方法使用了关键字synchronized,那么子类方法不会继承synchronized,如果子类需要同步,还需要在子类方法中添加synchronized。
(二)、synchronized同步代码块
1. 同一个方法中,不在synchronized块中的就是异步执行,在synchronized块中的就是同步执行。
2. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这是因为synchronized使用的是“对象监视器”。
3. 和synchronized方法一样,synchronized(this)代码块也是锁定当前对象。
4. java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个任意对象大多数是实例变量及方法的参数,使用格式为:synchronized(非this对象x),此时将x对象本身作为“对象监视器”
锁非this对象的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this对象x)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以大大提高运行效率。
5. synchronized还可以应用在static静态方法上,如果这样写,就是对当前的*.java文件对应的class类进行加锁。
跟synchronized加到非static方法上的区别:synchronized加到static静态方法上,是给Class类上锁;而synchronized加到非static静态方法上,是给对象上锁。
6. 由于JVM具有String常量池缓存的功能,因此在大多数情况下,同步synchronized代码块都不使用String作为锁对象:synchronized(string),而改用其他的,比如new Object实例化一个Object对象,但它并不放入缓存中。
以上方法的具体代码例子可以参考《java多线程编程核心技术》
二、synchronized的实现原理
目前在Java中存在两种锁机制:synchronized和Lock
synchronized实现同步的基础:Java中每一个对象都可以作为锁,具体表现为以下3种方式:(Synchronized锁, 锁住的是对象。)
1. 对于普通同步方法,锁是当前实例对象
2. 对于静态同步方法,锁是当前类的class对象
3. 对于同步方法块,锁是synchronized括号里配置的对象
synchronized用的锁是存在java对象头里的。重量级锁、轻量级锁和偏向锁之间的转换:
Synchronized同步块和同步方法在实现上的区别:
在下面的例子中,使用了同步块和同步方法,通过使用javap工具查看生成的class文件信息,来分析synchronized关键字的实现细节。
public class Synchronized { public static void main(String[] args) { // 对Synchronized Class对象进行加锁 synchronized (Synchronized.class) { } // 静态同步方法,对Synchronized Class对象进行加锁 m(); } public static synchronized void m() { } }
在synchronized.class的同级目录下执行:javap -v synchronized.class,部分输出如下:
说明:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC //方法修饰符,表示public static
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class testnew/Synchronized
2: dup
3: monitorenter // monitorenter:监视器进入,获取锁
4: monitorexit // monitorexit: 监视器退出,释放锁
5: invokestatic #16 // Method m:()V
8: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //方法修饰符,表示public static synchronized
Code:
stack=0, locals=0, args_size=0
0: return
对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方法,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一个时刻只能有一个线程获取到由synchronized所保护对象的监视器。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。
可以看出,任意线程对Object(Object由synchronized保护)的方法,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列总的线程,使其重新尝试对监视器的获取。
相关阅读:java中的锁分类
参考:
《Java并发编程的艺术》
Java中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景 & AtomicReference