线程安全
- 概念:
当多个对象访问某一个类(对象或方法)时,这个类始终都表现出正确的行为,那么这个类(对象或方法)就是线程安全的。 - synchronized:
可以在任意对象及方法上加锁,而加锁的这段代码称为互斥区或临界区。
1 | public class MyThread extends Thread { |
- 总结:
当多个线程访问MyThread的run方法时,以排队的方式进行处理(按照CPU分配的先后顺序而定)。当一个线程执行synchronized修饰的方法里的代码时,首先尝试获得锁,如果拿到锁,执行方法体中的内容,拿不到锁时,就会不断尝试获取这把锁,直到拿到为止。这里存在多个线程的锁竞争问题。
多个线程多个锁
- 概念:
多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体中的类容。
1 | public class MultiThread{ |
- 总结:
关键字synchronized获取到的都是对象锁,所以示例代码中两个线程在执行synchronized修饰的方法时,获得的是两个不同的对象锁,他们互不影响。当在静态方法上加synchronized时,表示锁定.class类,即类级别的锁(独占.class类)。
对象锁的同步和异步
- 同步:synchronized
同步的概念就是共享,这里“共享”是关键点,如果不是共享的资源,就没必要同步。 - 异步:asynchronized
异步的概念就是独立,相互之间不受到任何制约。就好比我们学习http时,当页面发起的Ajax请求时,我们还可以浏览网页,二者之间没有任何关系。
同步的目的就是为了线程安全,对于线程安全来说,需要满足两个特性:
- 原子性
- 可见性
1 | public class MyObject { |
- 总结:
A线程持有对象锁时,B线程如果这个时候调用对象中的同步(synchronized)方法则需要等待,也就是同步。
A线程持有对象锁时,B线程如果这个时候调用对象中的非同步(非synchronized修饰)方法则不需要等待,也就是异步。脏读
对于对象的同步和异步方法,我们在设计自己的程序时候,一定要考虑问题的整体性,不然就会出现数据的不一致的错误,很经典的就是脏读。
1 | public class DirtyRead { |
- 总结:在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue方法同时加锁,保证业务的原子性,不然就会出现业务错误。
synchronized其他概念
- synchronized锁重入:
synchronized拥有锁重入的功能,当使用synchronized时,当一个线程得到一个对象的锁后,再次请求次对象时可以再次获得该对象的锁。
1 | public class SyncDubbo { |
- 出现异常时,锁自动释放:
对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对你的应用程序业务逻辑产生严重错误,比如你现在在执行一个队列任务,很多对象都去等待一个对象正确执行完毕并释放锁,但是第一个对象由于异常的出现,导致业务逻辑没有正确执行,就释放了锁,那么可想而知执行的都是错误的逻辑。所以这一点要引起注意,在编码时要考虑周全。synchronized代码块
使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况就可以使用synchronized代码块去优化代码执行时间,也就是缩小锁的粒度。 - synchronized可以使用任意的Object进行加锁。
1 | public class ObjectLock { |
- 不要使用String的常量加锁,会出现死循环。
1 | public class StringLock { |
- 锁对象改变问题:
当一个对象加锁的时候,要注意对象本身发生改变的时候,持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使对象的属性发生改变。
1 | public class ChangeLock { |
1 | public class ModifyLock { |
volatile关键字
- 概念:
volatile关键字的主要作用就是使变量在多个线程间可见。
1 | public class RunThread extends Thread{ |
- 总结:
在java中,每一个线程都有一块工作内存,其中存放着所有线程共享的主内存中的变量值得拷贝。当线程执行时,他在自己的工作内存中操作这些变量。为了存取一个共享变量,一个线程通常获取锁定并清除他的内存工作区,把这些共享变量从所有线程共享的内存区中正确的装入到他自己的工作内存区中,当线程解锁时保证该工作区中的变量的值写回到共享内存中。
一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、锁定(lock)、解锁(unlock)。
而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock)。每个操作都是原子的。
volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不是去线程工作内存区里去读取,从而实现多个线程间的变量可见,也就是满足线程安全的可见性。
volatile关键字的非原子性
- 概念:
volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上一个轻量级的synchronized,性能比synchronized强。但是需要注意:一般volatile用于只针对多个线程可见变量的操作,并不能替代synchronized的同步功能。
1 | public class VolatileNotAtom extends Thread { |
- 总结:
volatile关键字只具有可见性,没有原子性。要实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本省方法原子性,并不保证多次操作的原子性)。
1 | public class AtomicUse { |