1 synchronized 关键字的用法
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized关键字是解决并发问题常用解决方案。
1.1 同步普通方法
在上面的代码当中的add方法只有一个简单的count++操作,因为这个方法是使用synchronized修饰的因此每一个时刻只能有一个线程执行add方法,因此上面打印的结果是20000。如果add方法没有使用synchronized修饰的话,那么线程t1和线程t2就可以同时执行add方法,这可能会导致最终count的结果小于20000,因为count++操作不具备原子性。
上面的分析还是比较明确的,但是我们还需要知道的是synchronized修饰的add方法一个时刻只能有一个线程执行的意思是对于一个SyncDemo类的对象来说一个时刻只能有一个线程进入。比如现在有两个SyncDemo的对象s1和s2,一个时刻只能有一个线程进行s1的add方法,一个时刻只能有一个线程进入s2的add方法,但是同一个时刻可以有两个不同的线程执行s1和s2的add方法,也就说s1的add方法和s2的add是没有关系的,一个线程进入s1的add方法并不会阻止另外的线程进入s2的add方法,也就是说synchronized在修饰一个非静态方法的时候,“锁”住的只是一个实例对象,并不会“锁”住其它的对象。其实这也很容易理解,一个实例对象是一个独立的个体别的对象不会影响他,他也不会影响别的对象。
1.2 同步静态方法
上面的代码最终输出的结果也是20000,但是与前一个程序不同的是。这里的add方法用static修饰的,在这种情况下真正的只能有一个线程进入到add代码块,因为用static修饰的话是所有对象公共的,因此和前面的那种情况不同,不存在两个不同的线程同一时刻执行add方法。
你仔细想想如果能够让两个不同的线程执行add代码块,那么count++的执行就不是原子的了。那为什么没有用static修饰的代码为什么可以呢?因为当没有用static修饰时,每一个对象的count都是不同的,内存地址不一样,因此在这种情况下count++这个操作仍然是原子的!
1.3 同步类
下面提供了两种同步类的方法,锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。这里的两种用法是同步块的用法,这里表示只有获取到这个类锁才能进入这个代码块。
public void synchronizedMethod_1() {
synchronized (ThreadDemo.class) {
System.out.println("同步类");
}
}
public void synchronizedMethod_2() {
synchronized (this.getClass()) {
System.out.println("同步类");
}
}
1.4 同步this实例
这也是同步块的用法,表示锁住整个当前对象实例,只有获取到这个实例的锁才能进入这个方法。用法和同步普通方法锁一样,都是锁住整个当前实例。
public void synchronizedMethod() {
synchronized (this) {
System.out.println("同步this");
}
}
1.5 同步对象实例
这也是同步块的用法,和上面的锁住当前实例一样,这里表示锁住整个 LOCK 对象实例,只有获取到这个 LOCK 实例的锁才能进入这个方法。
public class ThreadDemo {
private static Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
System.out.println("同步对象实例");
}
}
}
另外,类锁与实例锁不相互阻塞,但相同的类锁,相同的当前实例锁,相同的对象锁会相互阻塞。
2 synchronized同步方法
1、方法内的局部变量为线程安全,实例变量非线程安全;
2、同一个对象中的同步方法;注意:在两个线程访问同一个对象中的同步方法时一定是线程安全的;
3、多个对象多个锁;注意:两个线程分别访问同一类的两个不同实例对象的相同名称的同步方法,效果是异步执行的(产生了两个锁);
4、synchronized与锁对象 :1、A线程现持有object对象的锁,B线程可以异步的方式调用object对象的非synchronized类型的方法。2、A线程现持有object对象的锁,B线程如果在这时调用object对象中的synchronized的类型的方法则需要等待,也就是同步;
5、synchronized锁重入:在使用synchronized的时候,当一个线程得到一个对象锁后,再次请求此对象时,是可以再次得到该对象的锁;
6、出现异常,锁自动释放;
7、对父亲的同步方法重写不具有同步性(缺少synchronized)
3 synchronized同步语句块
synchronized同步代码块的使用:当两个并发线程访问同一个对象中的 synchronized(this) 同步代码块的时。一段时间只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块;
当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该对象的非synchronized(this)同步代码块:
1、同步代码块同步执行,非同步代码块的异步执行;
2、同步代码块间的同步性:当一个线程访问object的一个synchronized(this) 同步代码块时,其他线程对用一个object中的所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个,synchronized(this) 锁定当前对象;
3、将任意对象作为对象监视器:多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this) 同步代码块时,调用的效果就是同步执行。这里的任意对象大多数是:实例变量、方法的参数等。格式:synchronized(非this对象)。
4、当多个线程同时执行synchronized(x){}同步代码块时呈现同步效果;同一个锁对象,同步调用;不同锁对象,异步执行;
5、当其他线程执行x对象中的synchronized同步方法时呈现同步效果,当其他线程执行x对象中的synchronized(this)代码块时呈现同步效果;
4 synchronized底层实现原理
public class SynchronizedTest {
public synchronized void doSth(){
System.out.println("Hello World");
}
public void doSth1(){
synchronized (SynchronizedTest.class){
System.out.println("Hello World");
}
}
}
反编译之后的代码:
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
对于同步方法,JVM采用ACC_SYNCHRONIZED
标记符来实现同步。 对于同步代码块,JVM采用monitorenter
、monitorexit
两个指令来实现同步。
方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED
标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁,这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。(为什么会有两个monitorexit呢?的原因在于此)
可以把执行monitorenter
指令理解为加锁,执行monitorexit
理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
5 synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。