线程安全性与synchronized
线程安全:多线程访问某个类时,这个类始终都能表现出正确的行为,这个类就是线程安全的。
简单的说,就是多线程执行的结果与单线程执行的结果始终一致,不会因为多线程的执行时序不同而出现不同的结果
以下是一个线程不安全的程序:
当这段代码在单线程中执行时,会得出正确的答案,而在多线程环境中,则出现了执行结果完全靠运气,结果依赖于线程之间的执行时序,显然违背了线程安全的定义:多线程访问某个类时,这个类始终都能表现出正确的行为。
为什么会出现这样的结果?
虽然count++看上去很像是一个操作,实际在执行的时候是三个独立操作:
-
读取count的值。
-
将值+1。
-
写入count。
当上面这一个“读取-修改-写入”的操作没有使用同步机制保证这三步操作是一个不可分割原子操作,就会出现不同线程的这三步操作交替执行。
只需要加上一些线程协调机制便可将这三步操作作为一个原子操作,比如同步锁:
以上现象有一个术语来形容,叫竞态条件,即多线程访问同一资源时,如果对资源访问顺序敏感,执行结果依赖于线程的执行顺序,则为竞态条件。
Synchronized是java内置的锁,这是一种互斥锁,只能由一个线程进入被锁保护的代码,因此这个锁保护的代码块以原子方式执行,可以提供对象锁/类锁/安全发布全局变量的功能,
Synchronized的几种用法:
1.****对象锁:
2.****类锁:
使用锁时需要注意的地方:
- 当需要锁来协调线程对某个变量的访问时,所有访问这个变量的位置需要用同一个锁。
2.使用锁时要清楚代码块的代码是否需要执行很长时间,比如网络和IO操作,如果长时间持有锁,会造成线程竞争,等待的线程有两种等待策略:
a.忙锁:自旋等待锁释放,适合代码块执行时间很短的情况。
b.闲锁:将等待的线程挂起,锁释放后在合适的时机上下文切换,有一定内存同步代价。
jvm会对synchronized代码块进行不同程度的锁膨胀,偏向锁、轻量级锁、重量级锁,这个在后面的jvm系列博文会总结到。
当代码块执行的任务需要很长时间时尽量不要加锁。
同步代码块应尽可能短小,不需要同步的代码尽量移出,代码越短小,执行时间越短,线程竞争就越少,性能和吞吐量会更好,简单说就是快进快出。
一些优化synchronized互斥锁性能的方法:
-
分段锁。
-
读写锁。
线程封闭
1. ad-hoc封闭:维护线程封闭性的职责完全由程序来承担。
例如代码中不做任何同步处理,只是把线程不安全的程序做成单线程程序,只有一个线程来执行了,自然就不会存在什么竞态条件、资源竞争,不推荐使用,建议用ThreadLocal。
在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行"读取—修改—写入"的操作。这种情况下相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile的可见性还确保其他的线程能看到最新的值。
- 栈封闭
简单说就是不用全局变量,用局部变量,方法内部声明的局部变量作用域是封闭在单个线程里的,不对其它线程共享,自然就不会出现竞态条件。
为什么栈封闭的局部变量能保证线程安全呢?后面更新的JVM系列会说明运行过程中的栈帧结构。
3. ThreadLocal
是一种更规范的维护线程封闭性的方式,使变量和持有变量的线程关联起来,每个变量都有一份自己的变量,相互隔离防止共享。
例如实现线程与用户信息的绑定:
不可变对象,不能修改,只读共享
不可变的成员变量不需要额外同步,天生线程安全,如果该成员变量是一个对象,则所有属性都需要声明为final保证不可变性。
如图static保证了jvm安全发布该变量,发布后对该类创建的所有线程都是可见的,final保证了发布后为只读,不可写。
当多线程程序存在线程安全问题时,选择解决方法的优先级应当如下:
1.能否做成无状态的不变对象。无状态是最安全的。
2.能否线程封闭。
3.采用何种同步技术。