多线程知识点
-
多线程使用的问题
-
线程安全问题
多个线程操作共享变量产生的问题
-
线程性能问题
在共享方法上使用synchronized关键字可以解决线程安全问题,但是带来了严重的性能问题
-
解决方法
使用多线程时,一定要保证线程是安全的。解决线程安全问题的方法:
- 无状态 (没有共享变量)
- 使用final使该引用变量不可变 (如果该对象也引用了其他对象,那么无论是发布还是使用都需要加锁)
- 加锁 (内置锁,显式Lock锁)
- 使用JDK提供的线程安全类
- 原子性 (可以使用AtomicLong等类来实现原子操作)
- 容器 (ConcurrentHashMap等)
- ……
原子性与可见性
-
原子性
在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免线程安全问题。
- count++ ,先读取,后加一,再赋值。如果该操作是原子性的,那么就可以实现线程安全(原子性)
原子性就是执行某一个操作是不可分割的。
- 如Count++ 操作,它就不是一个原子性的操作,而是分成了三个步骤来实现这个操作的。
- JDK中有atomic包实现了原子操作
-
可见性
对于可见性,java提供了一个关键字:Volatile
- 可以任务,volatile是一种轻量级的同步机制
- volatile仅仅用来保证该变量对所有线程的
volatile仅仅用来保证该变量对有多线程的可见性,但不保证原子性。
- 保证该变量对所有线程的可见性
- 在多线程的环境下,当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
- 不保证原子性
- 修改变量实质上是在JVM中分了好几步,而在这几步内,它是不安全的
使用volatile修饰的变量保证了三点:
- 一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
- 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
- volatile可以防止重排序。如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
一般来说,volatile大多用于标志位上(判断操作),满足以下条件才应该使用volatile修饰变量:
- 修改变量时不依赖变量的当前值 (因为volatile是不保证原子性的)
- 该变量不会纳入到不变性条件中 (该变量是可变的)
- 在访问变量的时候不需要加锁 (加锁就没必要使用volatile这种轻量级同步机制了)
线程封闭
在多线程的环境下,只要我们不使用成员变量 (不共享数据),那么就不会出现线程安全的问题了。
在一个web应用中,有很多的servlet,但servlet并不加锁,所有的数据都是在方法上操作的,每个线程都拥有自己的变量,互不干扰。
在方法上操作,只要保证不再方法上发布对象 (每个变量的作用于仅仅停留在当前的方法上),那么线程就是安全的。
在线程封闭上还有另一种方法,就是使用ThreadLocal。
这个类的API可以保证每个线程自己独占一个变量。
不变性
前面描述的共享变量都是可变的,正是因为可变,才会出现线程安全问题。如果状态是不可变的,那么随便多少个线程访问都是没有问题的。
java提供了final修饰符,但final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的。
final HashMap<Person> hashMap = new HashMap<>();
就像这样,这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量所指向是不可变的,但是hashMap内部的数据是可变的,也就是说,可以add,remove等操作集合。
因此,仅仅只能够说明hashMap是一个不可变的对象引用
不可变的对象引用在使用的时候还是需要加锁的
- 或者把Person也设计出是一个线程安全的类
- 因为内部的状态是可变的,不加锁或者person不是线程安全类,操作都是有危险的。
要将对象设计成不可变对象,那么需要满足三个条件:
- 对象创建后状态不能修改
- 对象所有的域都是final修饰的
- 对象是正确创建的
线程安全性委托
很多时候需要实现线程安全不需要自己设计和加锁
可以使用JDK提供的**JUC对象(java.util.concurrent)**来完成线程安全设计
总结
正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。
无论是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,我们的程序才可以更加健壮!