《深入理解java虚拟机》笔记:线程安全与锁优化

半个读书笔记,没什么技术含量
线程安全定义:

这本书里就这个概念的定义和《java并发编程实战》里的定义大体上是一样的,应该是引用了同一个大佬的说法,我这里直接把上一篇文章的话复制过来:

当多个线程访问某个类时,不管运行时如何调度,或者线程如何交替执行,在主调代码中不需要额外同步操作的情况下,这个类都能表现出正确的行为,即可称该类是线程安全的。

java语言中5类强度的线程安全,从强到弱:
1、不可变:即《java并发编程实战》中说的不可变对象,只要不可变对象被安全地构造出来(没有发生this逃逸),那么一定就是线程安全的。
不可变对象特征:共享数据为基本类型时,用final来进行修饰
共享数据为可变对象时,需要保证对象行为不会对其状态产生任何影响(这里还是举例上一篇文章中的String类,像substring这种方法都是new一个对象返回去)
2、绝对线程安全:基本就等同于文章一开始提到的线程安全的概念。
值得一提的是,Java API中标注自己是线程安全的类,绝大多数都不是绝对线程安全的类。举个例子,Vector类是线程安全的,它的方法都加了同步,但是在下面这段代码中,却是线程不安全的:
public class ThreadVectorTest {
	
	public static Vector<Integer> v = new Vector<>(10);

	public static void main(String[] args) throws InterruptedException {
		
		for(int i = 0 ; i < 10 ; i++) {
			v.add(i);
		}
		
		Thread t1 = new Thread() {
			@Override
			public void run() {
				for(int i = 0 ; i < 10 ; i++) {
					v.remove(i);
				}
			}
		};
		
		Thread t2 = new Thread() {
			@Override
			public void run() {
				for(int i = 0 ; i < 10 ; i++) {
					v.get(i);
				}
			}
		};
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("finish");
	}

}

运行结果如下:

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 5
	at java.util.Vector.get(Vector.java:748)

抛了数组越界的异常,两个线程同时对同一个Vector做操作,如果这个线程已经把元素给移除了,而获取元素的循环刚好在移除的那个位置,那么就会报数组越界的异常了。这里需要对移除和获取元素的操作作额外的同步,然而绝对线程安全的定义是对这种类不需要任何同步操作了,所以像这些标注为线程安全的类,大多数都不是绝对线程安全。

3、相对线程安全:通常意义上的线程安全,像Vector这种基本都归到这一类中。调用对象内部的操作时不需要额外的同步,然而对特定顺序的连续调用,则需要额外的同步手段。
4、线程兼容:通常意义上的线程不安全,可以通过适当的同步手段来保证线程安全。
5、线程对立:同步手段没什么用,不管怎么同步都是线程不安全。比如Thread类的suspend()和resume()方法,一个中断,一个恢复,容易死锁。这俩方法已经都被jdk给废弃掉了。
线程安全实现方法:
1、互斥同步:
sychronized关键字,编译后在同步块前后分别形成monitorenter,monitorexit两个字节码指令。这两个字节码指令都需要一个reference类型的参数(即锁对象)。
获得锁,对锁的计数器+1,monitorexit对锁的计数器-1,计数器为0时,锁被释放。
具有可重入性,是重量级操作,消耗大(线程阻塞,唤醒这些操作需要在用户态和内核态之间来回切换),针对这个有自旋锁等优化。
除了synchronized,还可以通过ReentrantLock类来实现,通过lock()和unlock()方法,配合try/finally块来操作。Lock是API层的锁,synchronized是原生关键字。
ReentrantLock相比synchronized增加了几个功能:
①可等待中断:等待线程可以选择放弃等待,先去处理其他事情。针对线程长时间持有锁的情况。synchronized是没有等待中断的。
②公平锁:按照申请锁的时间顺序来依次获得锁。默认是非公平锁,可以在ReentrantLock构造方法中设置成公平锁,synchronized则就是非公平锁。
③绑定多条件:synchronized实现类似功能需要添加多个锁,每个锁一个条件。而ReentrantLock则可以一个线程同时绑定多个条件(condition对象)
早期synchronized效率不行,jdk1.6之后针对做了优化,现在效率已经不输于ReentrantLock了。
互斥同步属于悲观锁思想
2、非阻塞同步:乐观锁思想,我先更新,如果旧值为期待值则直接更新,如果不是,则进行冲突重试。
CAS操作即为该思想,3个参数,内存位置V,旧的预期值A,新值B。当V符合旧预期值A的时候,则用B更新,否则不更新。
一个例子,AtomicInteger类中自增的操作就是CAS思想。count++不是原子性操作,所以需要有自增操作时,为了线程安全,可以使用AtomicInteger类。
CAS也是有自己的问题的,比如ABA问题。这个问题的大意就是,我在更新值的时候发现旧值符合预期,但是不代表这个值没有被修改过,比如我A值修改成B,再改回A。JUC包下提供了AtomicStampedReference类,通过数据版本号来进行标记。不过多数情况下ABA问题不影响并发的正确性。
3、无同步方案:有些代码天生就是线程安全的:可重入代码(书里举了一个例子,比如递归调用自己),以及线程本地存储(ThreadLocal类)
锁优化:
1、自旋锁与自适应自旋:有些同步块不会用很长时间执行,让线程不断切换阻塞和运行状态比较浪费资源,所以有了自旋锁。
如果线程在等待锁,不会马上进入阻塞状态,可能先执行一个忙循环(自旋),如果自旋一定次数仍然未能获得锁,则进入阻塞状态。
自适应自旋:根据前一个线程在同一个锁上的自旋时间以及锁拥有者的状态来决定本次自旋时间。比如上次自旋时间很短就获得锁了,那么本次自旋时间则会适当延长。
2、锁消除:即时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
3、锁粗化:如果虚拟机探测到有一串零碎操作都为同一个对象加锁,则会把加锁范围扩展(粗化)到整个操作序列的外部,以减少互斥开销。
比如StringBuffer类,每次append的时候都会加锁,那如果多次append操作的话,虚拟机就会进行锁粗化的优化,将加锁范围扩大,不然来回加锁解锁,开销很大。
4、轻量级锁:在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生性能消耗。
在对象头中存储有锁的标志位,初始为01(未锁定)。
在代码进入同步块时,将对象头的部分信息(hashcode,GC年龄这些)拷贝存储到线程的栈帧中的锁记录空间,然后尝试更新对象头为指向锁记录的指针,如果更新成功,则线程获取对象锁,锁标志位更新为00,即轻量级加锁。
如果存在锁竞争操作,则清凉所膨胀为重量级锁,锁标志位更新为10。
解锁过程,即为将锁记录空间里存储的对象头信息和被更新成为锁记录指针的对象头进行交换,交换成功则释放锁,交换不成功说明有其他线程也申请获得锁了,需要唤醒其他线程。
加锁,解锁过程均通过CAS进行操作。
5、偏向锁:在无竞争的情况下消除整个同步。
偏向锁会偏向于第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要进行同步。锁标志位为01,如果发生竞争,则退回至未加锁状态,或轻量级锁状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值