1.7 写方法和读方法的加锁选择问题
下面代码中:存在一个 Account 对象(属性有名称和余额),下面代码中存在的注释暂时忽略不计:set() 方法是同步的,getBalance() 方法不是同步的。
main方法启动的时候,先启动了个线程,设置账户名和余额,主线程会两次读取账户余额,可能看到的设置余额和读取余额是一致的,但是当 Account 类的 run() 方法中增加线程睡眠,以增大线程阻塞问题,就会出现设置账户名称之后,还没设置账户余额的时候,其他线程就读取到了账户余额,因此导致脏读的问题,在金融业务中,脏读很有必要避免的。
避免线程脏读问题就需要对读操作也进行加锁。
1 | /** |
1.8 synchronized 是重入锁
下面代码中:m1() 和 m2() 方法都加了 synchronized 锁,两个锁的对象都是this
,那么当一个线程已经运行 m1() 方法之时,在 m1() 方法中调用了 m2() 方法,且这个 m2() 方法也是加了锁的,是否可以调用这个 m2() 方法运行,答案是可以的,synchronized 会有计数器进行计数,表示重入了几次。
1 | /** |
在子类和父类之间,也是可以 synchronized 重入。
1 | /** |
1.9 synchronized遇到抛出异常的时候会释放锁
程序在执行过程中,如果出现异常,默认情况锁会被释放,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。代码中,当 count 子增到 5 的时候会因为算术计算异常导致锁被释放,那么当前线程只执行了一半操作,另一半未操作的资源会被其他线程抢去操作,造成线程同步问题。
1 | /** |
1.10 volatile 保证线程的可见性
1 | /** |
注意:volatile 并不能保证原子性
十个线程在自增的时候,当自增完了就会刷新到主内存,当其他线程要拿的时候会更新,但是拿完之后,刚要自增操作的时候,CPU 资源被其他线程抢占,因此别的线程都自增成很大的数值,而此线程再抢到 CPU 资源执行自增的时候,还是之前的小数值自增,因此下面代码中的每个线程自增一万,十个线程执行之后,最终的 count 应该是十万,当时结果很差强人意。
1 | /** |
使用带有原子性操作的对象,下面代码中的 AtomicInteger在自增时候就是原子操作,因此可以保证一致性问题。
1 | /** |
1.11 synchronized 优化(粗细锁)
下面代码中,由于 synchronized 锁的范围不同,会导致程序执行效率的不同,越精细的锁越高效。
1 | /** |
1.12 到底锁的是什么(锁定堆中的对象)
下面代码中,m() 方法是一个死循环,当第一个线程拿到锁的时候,按理来说,一定是不会释放锁的,因为这个线程就是个无限死循环执行代码,而当锁对象发生改变,会造成锁定要锁的对象被释放掉了。
1 | /** |