1. 线程同步的方法
1.1 同步方法
就是使用synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
1.2 同步代码块
即由synchronized关键字修饰的代码块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
因为同步是一种高开销的操作,如果没有必要同步整个方法,那就使用同步代码块同步关键代码即可。
1.3 使用volatile关键字实现线程同步
使用volatile修饰变量就相当于告诉JVM,该值可能会被很多线程访问,每次访问时都是从内存中读取,而不是从缓存中读取,因此每个线程访问到的变量值都是一样的,这也就是所谓的可见性。这样就保证了同步。
但是volatile不会提供任何原子操作,因此volatile不能代替synchronized。也不能用来修饰final类型的变量。
1.4 使用ReentrantLock类实现线程同步
它相对于synchronized关键字有更强的灵活性,而且在竞争比较激烈时有更好的性能,但是需要在finally代码中手动释放锁。
1.5 使用ThreadLocal
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
2. Lock原理基石——CAS算法简述
比如i++的操作,第一个线程先读取i的值,执行计算后获得一个新值B(i从1变成2),然后CAS算法就试图将i的值从1改为2。这时如果检测到i的值未被其他线程更改(值还是1),则说明这段时间无竞争,CAS 算法将i的值变成2。
若其他线程修改了变量(比如i从1变成了3),那么CAS会检测到这个变化,就会放弃把i的值赋值为2。这时会获得新值3,并重新进行自己的计算。
CAS算法无法避免ABA问题,误认为没有竞争,可以使用版本号解决,即1A2B3A。
3. Synchronized的实现原理
Java中的每个对象都可以作为锁。
(1)普通同步方法,锁是当前实例对象。
(2)静态同步方法,锁是当前类的class对象。
(3)同步代码块,锁是括号中的对象。
3.1 Synchronized的使用
public class SynchronizedTest {
private static Object object = new Object();
public static void main(String[] args) throws Exception{
synchronized(object) {
}
}
public static synchronized void m() {}
}
通过javap -c命令查看程序对应的字节码。
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object
3: dup
4: astore_1
5: monitorenter //监视器进入,获取锁
6: aload_1
7: monitorexit //监视器退出,释放锁
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 9: 0
3.2 对于同步代码块
可以看到编译器会在同步代码块的前后,分别会为我们添加Monitorenter和Monitorexit指令。
关于Monitorenter和Monitorexit的官方解释如下:
//Each object is associated with a monitor. A monitor is locked if and only if it has an owner.
//The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
//If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one.
//The thread is then the owner of the monitor.
//If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
//If another thread already owns the monitor associated with objectref, the thread blocks
//until the monitor's entry count is zero, then tries again to gain ownership.
//The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
//The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner.
//Other threads that are blocking to enter the monitor are allowed to attempt to do so.
简单翻译总结概述如下:
每个对象有一个monitor。当monitor被占有时就会处于锁定状态,线程执行到Monitorenter指令时尝试获取monitor的所有权:
(1)如果monitor的进入数为0,那么该线程就会进入monitor,然后将进入数置为1,从而该线程即占有了monitor。
(2)如果该线程已经占有该monitor,重新进入会使monitor的进入数加1。
(3)并且执行monitorexit的线程肯定是monitor的持有者。当monitorexit指令执行时,monitor的进入数减1,如果进入数变为0,占有锁的线程就会退出monitor,不再是这个monitor的持有者。
(4)如果其他线程已经占有了monitor,那么这个线程就会进入阻塞状态,直到monitor的进入数为0,才会尝试去获取monitor的所有权。
通过上面的描述,我们知道Synchronized关键字的底层是通过一个monitor的对象来完成同步的功能,其实wait/notify等方法也是依赖于monitor对象,所以Java才规定只有在同步块/方法中才能调用wait/notify等方法。
3.3 对于同步方法
同步方法中依靠方法的ACC_SYNCHRONIZED标识符来实现。当同步方法被调用时,会检查方法的 ACC_SYNCHRONIZED标识符是否被设置,如果设置了,执行线程将先获取monitor后才能执行方法体,执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
综上,两种同步方式本质上都是对指定对象相关联的monitor的互斥性获取。
4 锁的四种状态
一般锁有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
偏向锁,轻量级锁,重量级锁分别解决三个问题,只有一个线程进入临界区,多个线程交替进入临界区,多线程同时进入临界区。
4.1 重量级锁
Synchronized同步是通过对象内部的monitor来实现的。JVM实现线程之间的切换成本比较高,这也是为什么Synchronized效率低的原因。因此,称之为重量级锁。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,引入了“轻量级锁”和“偏向锁”。
4.2 偏向锁
public class SynchronizedTest {
private static Object lock = new Object();
public static void main(String[] args) {
method1();
method2();
}
synchronized static void method1() {}
synchronized static void method2() {}
}
当某个线程访问method1时,会在SynchronizedTest.class对象的对象头和栈帧的锁记录中存储该线程ID,下次该线程在进入同步方法2时,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行加锁和解锁操作。
4.3 轻量级锁
轻量级锁并不是用来代替重量级锁的,它的本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
轻量级锁性能提升的依据是“大部分同步代码一般都处于无锁竞争状态”,这是一个经验数据。轻量级锁的轻就体现在,如果没有竞争,就会使用CAS操作完成锁的获取和释放,然后避免使用互斥锁的开销。但如果存在竞争。除了互斥的开销外,还要有额外的CAS操作。因此在有竞争的情况下,轻量级锁会比传统的重量级锁还要慢。这个时候轻量锁就不再有效,要膨胀为重量级锁,后面等待锁的线程要进入阻塞状态。
5 其他的一些优化
5.1 自旋锁
自旋不是锁的一种状态,只是轻锁膨胀成重锁后的一个优化动作。
重锁的互斥同步操作对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转移到内核态去执行,这些操作会降低并发性能,同时很多时候synchronized代码块中逻辑简单执行速度快,为了这段时间去挂起和恢复线程并不值得。
因此,当线程在获取轻量级锁执行CAS操作失败的时候,要通过自旋来获取重量级锁的。就是让等待锁的线程不要被阻塞,而是在Synchronized的边界做忙循环,这就是自旋。就是让后面请求的线程先“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。当尝试一定的次数后如果仍然没有成功获得锁,再进入阻塞状态。
(自旋锁在1.4就有只不过默认的是关闭的,jdk1.6是默认开启的)
5.2 适应性自旋锁
自旋本身也要一直占用处理器时间,如果锁被占用的时间很短,那么自旋等待的效果就会很好。反之自旋的线程只能白白的占用处理器自旋,而不会做任何有用的工作,反而会给性能带来浪费。所以自旋几次没有获取到锁之后才把线程挂起。
但是JDK1.6之后采用了一种更聪明的方式——适应性自旋,简单来说就是如果在同一个锁对象上,自旋锁刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也很有可能再次成功,进而他会将自旋等待的时间相对更长一些,比如100个循环。反之,如果在这个锁上通过自旋从来没有获得过,以后获取这个锁将可能省略了自旋过程,以避免浪费处理器时间。所以说适应性自旋是对自旋的一种优化。
5.3 锁粗化
原则上,将同步代码块的范围尽量缩小,使得需要同步的操作尽可能的少,如果存在锁竞争那等待的线程也能很快拿到锁。但是如果在没有竞争时,对一个对象进行一系列连续的加解锁,就会造成性能的浪费,比如:
StringBuffer sb = new StringBuffer();
sb.append(1);
sb.append(2);
sb.append(3);
这种情况下,虚拟机就会进行锁粗化,以上述代码为例,就是将锁范围扩展到第一个append之前到最后一个append之后。这样只需要一次加锁就可以了。
5.4 锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:
for (int i = 0; i < 100000000; i++) {
append("abc", "def");
}
public void append(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,就可以进行锁消除。
博主水平有限,如有问题请多指正。转载请注明出处为:http://blog.csdn.net/seu_calvin/article/details/68512707。