【记录】《java并发编程实战》读书笔记:线程安全,对象状态,对象发布逃逸,线程封闭,不可变对象,安全发布等(前三章)

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

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

对象状态

一般来讲,对象里包含的域就可以指代对象状态了。
没有任何域,也不包含任何对其他类中域的引用的对象,称为无状态对象
无状态对象一定是线程安全的

原子性操作

一个线程,要么全部执行完,要么干脆不执行的操作,则为原子性操作

synchronized与volatile

volatile部分在JVM的笔记中有提到,就不再写了。
https://blog.csdn.net/qq_34785454/article/details/88722479
volatile不保证原子性,所以当变量的改变不依赖当前变量值的时候(比如a = a * 2这种),才可以使用volatile来保证可见性。

synchronized部分不来自于本书,其他地方记的,也一并放到这里吧:

Synchronnized 两种用法:
1、对象锁,包括方法锁(默认锁对象为this,当前对象实例)和同步代码块锁(自己制定锁对象)。
2、类锁,包括修饰静态方法,以及指定锁对象为Class对象。

synchronized性质:
可重入,不可中断
可重入:同一线程的外层函数获得锁后,内层函数可以直接再次获取该锁
好处:避免死锁,提升封装性
粒度:线程而非调用

不可中断:如果锁被其他线程获得,该线程只能无限等待或阻塞下去,直到锁被其他线程释放掉
相比之下,Lock类具有可中断的特性:有权中断获得锁的线程运行,或有权放弃退出等待

synchronized深层基于monitor指令:monitorenter加锁,monitorexit解锁,两个命令并不一定是一一对应,exit命令数量可能比enter命令多,因为释放锁的时机不一定,可能是方法执行完毕,也可能是方法抛出了异常。
两个monitor命令分别令对象锁计数+1-1
一个monitor的lock锁只能被一个线程在同一时间相关联

反编译命令:javap -verbose 类名.class

可重入原理:加锁次数计数器
JVM负责跟踪对象被加锁的次数
每一次加锁,计数都会+1,当前任务离开,计数递减,直到为0,则会完全释放锁

synchronized可见性原理:java内存模型(JMM)
加锁后直接从主内存读取数据,释放锁之前要把修改的数据全部写回主内存中

synchronized缺陷:
效率低:锁释放情况少(任务结束或抛出异常),没有超时时间,不可中断
灵活性低:反例读写锁,读时不加锁,写时加锁,灵活性更高
无法知道是否成功获取锁

方法抛异常后,会释放锁:
Lock锁不会释放,synchronized会释放锁

当操作执行时间较长时(比如计算量很大,或者IO部分),这部分不可以加锁,否则影响效率。
在不影响同步的情况下,加锁的部分应该尽可能地小

实际情况中,应该尽可能优先使用线程安全的类(比如juc包下的一些,以及Atomic系列)

对象发布

对象在当前作用域外的代码中使用:
1、将一个对象的引用保存到其他代码可以访问的地方,比如:

public List<Person> pList = new ArrayList<Person>();

直接搞公有域,其他代码就能访问到了

2、在某一个非私有的方法中返回该引用,比如get方法
3、引用传送到其他类方法中,比如作为其他类方法的参数这种

在对象构造完成前便发布该对象,则为逸出,会破坏线程安全性,要避免。
内部类发布使得this引用逃逸,要避免,比如:

/**
 * this引用逃逸
 * @author wmx
 *
 */
public class ThisAway {
	
	private int i;
	private String s;
	
	public ThisAway() {
		s = "dddd";
		new InnerClass();
		i = 5;
	}
	
	private class InnerClass{
		InnerClass(){
			System.out.println("s="+ThisAway.this.s+",i="+ThisAway.this.i);
		}
	}

	public static void main(String[] args) {
		new ThisAway();
	}

}

运行代码,输出:

s=dddd,i=0

在ThisAway类的构造方法没执行完时,已经在内部类可以获得当前的this引用了,而此时变量i还没有被赋值,所以输出还是默认的0,这种情况不能出现。
解决方案:私有构造方法+静态工厂方法获得对象实例

线程封闭

将对象封闭在单个线程中,实现线程安全的最简单的方法之一。
比如JDBC的connection,便是隐式地将一个connection对象封闭在一个线程中,直到与数据库交互执行完毕。

线程封闭的三个方法:
1、Ad-hoc线程封闭:维护线程封闭性完全由程序承担,非常脆弱,不建议用。
2、栈封闭:应该是指方法栈封闭,说白了就是方法局部变量。这部分是线程私有,人手一份,随便折腾,不存在安全问题,只要不泄露出去。
3、使用ThreadLocal,我打算单独写一篇这个类的源码分析(挖坑)。不过这个类也不能滥用,会降低代码可用性,增加类之间的隐含的耦合性

不可变对象

对象状态不可变,同步等问题也就不存在了。
不可变对象只有一种状态,由构造方法控制,创建后即不可修改。
不可变对象一定是线程安全的
满足以下条件的对象为不可变对象:
1、对象创建后其状态不能修改
2、对象的所有域均为final类型(单拿出final域来说的话,如果引用的对象是可变的,那么访问引用对象状态的时候还是需要同步的。不过不可变对象是三个条件综合起来才满足的)
3、对象被正确创建(即创建期间没有this逃逸)

比如说,String对象即为不可变对象,本身String为final类,不能被继承重写,对String类的操作也都是返回一个新的对象,原有的状态是不被修改的,比如substring方法:

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

这种不可变对象即保证了线程安全。

安全发布

安全发布常用模式:
1、静态初始化函数中初始化一个对象引用(可能是指静态工厂方法)
2、对象引用保存到volatile类型或者AtomicReference对象中
3、对象引用保存到被正确构造的对象的final域中
4、对象引用保存到被锁保护的域中

对象发布需求:
不可变对象可通过任意机制发布
事实不可变对象(技术上可变,实际不可变,这个我理解的可能就是业务上约定的不可变,比如出生日期这种,Date类型本身可以变,但是实际上这种数据是不变的)必须通过安全方式发布
可变对象必须通过安全方式发布,并且必须是线程安全或是被某个锁所保护起来

所以根据以上的几条来看,是不是可以说明安全发布不等于线程安全?个人理解好像是这样的,不太确定

个人观感

这部分看下来,感觉实际工作中真的有按照这些个规则去写代码吗,好像是没有的,挺麻烦的这些。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值