线程安全的理解与面临的问题
个人理解线程安全是对在多线程访问同一个资源下该资源的最终结果一定是开发者想要的,不会出现错误或者遗漏的情况。
所以在开发中多线程可能会存在的问题
数据不一致
数据不一致的问题通常出现在程序运行、程序运行完毕结果为不是开发者想要的且出现了错误的结果。原因为多线程在同一个资源进行修改、访问操作,当一个线程修改了数据没有同步给其他线程或者一个线程获取到的是错误的数据那么数据不一致就会发生。
竞态条件
在程序编写过程中,如果多线程同时对一个共享资源进行修改处理,那么资源的结果会依赖于多线程的执行顺序。
死锁
形成死锁的条件通常由资源互斥、循环等待、不可剥夺、请求保持原因构成。在多线程场景下多个线程同时等待互相持有的资源释放而造成死锁。
活锁
通常发生在多线程互相谦让各自持有的资源,简单理解就是给你用我不抢所造成的假死现象。
如何解决线程安全带来的问题?
对症下药
我们知道了线程安全可能会带来以上的问题,那我们就可以对症下药了。
- 互斥访问:使用锁机制(如synchronized关键字或Lock接口),确保同一时间只有一个线程可以访问共享资源。锁机制可以防止多个线程同时修改数据,从而避免竞态条件和数据不一致。
- 原子操作:使用原子类(如AtomicInteger、AtomicBoolean等)或volatile关键字,确保对共享变量的操作是原子性的,不会被中断。
- 线程封闭:将共享数据限制在单个线程中,例如使用ThreadLocal类,使每个线程都有自己的数据副本,从而避免线程间的数据竞争。
- 不可变性:使用不可变对象,即对象创建后不可修改。不可变对象是线程安全的,因为它们不会被多个线程同时修改。
- 同步容器:使用Java提供的线程安全的容器类,如Vector、Hashtable、ConcurrentHashMap等,它们内部实现了线程安全的操作。
- 并发工具类:使用Java并发包中提供的工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以协调多个线程的执行顺序,避免死锁和活锁的发生。
多线程中synchronized的理解
认识synchronized
synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。
使用synchronized
常见使用方法为:
1、[public|default|private|protected] synchronized [static] type method();
2、同步代码块
private final Object OBJECT =new Object();
public void sync(){
synchorniezd(OBJECT){
.....
}
}
分析与深入synchronized
首先很多朋友都听过sync是锁可以保证啥啥啥的,但它并非锁。它只是从作用上看起来想那么回事,为了论证让我们从原理上来分析吧。
import java.util.concurrent.TimeUnit;
/**
* 一个对象 用sync限制资源访问
*/
public class Mutex {
private final static Object MUTEX = new Object();
public void getResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 该类为测试sync在底层实现上的原理以及分析
*/
public class SynchronizedTest {
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for (int s = 0; s < 5; s++) {
//模拟五个线程对mutex 资源的访问
new Thread(mutex::getResource).start();
}
}
}
接下来,我们使用jconsole 来分析一下
可以发现只有“Thread-0” 进入到了TIME_WAITING,其他线程Thread-1、2、3、4均处于BLOCKED。再使用jstack,可以看出跟jconsole无差别。
让我们进行更进一步分析。使用 javap命令对Mutex进行反编译。
CMD执行: javap -c Mutex.class
Compiled from "Mutex.java"
public class Mutex {
public Mutex();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void getResource();
Code:
0: getstatic #2 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUni
t;
9: ldc2_w #4 // long 10l
12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23
18: astore_2
19: aload_2
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1
24: monitorexit
25: goto 33
28: astore_3
29: aload_1
30: monitorexit
31: aload_3
32: athrow
33: return
Exception table:
from to target type
6 15 18 Class java/lang/InterruptedException
6 25 28 any
28 31 28 any
static {};
Code:
0: new #9 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field MUTEX:Ljava/lang/Object;
10: return
}
反编译后查询关键字:monitorenter、 monitorexit 解析,因为没有找到synchronized相关信息。
monitorenter
通常情况下monitorenter、 monitorexit 成对出现,当一个线程访问在被monitorenter包裹的代码会出现以下情况。
解释:
- 如果monitor的计数为0,那么代表该monitor还未被其他线程获取,当一个线程得到该信息后立马将monitor的计数+1,至此该线程作为monitor的持有者,可以访问包裹在其中的代码
- 如果一个已经持有monitor的线程再次进行重入,那么monitor的计数再次累加。
- 如果monitor已经被其他线程获取了,则其他未持有的线程会进行阻塞等待monitor的计数变成0,然后尝试获取持有权。
monitorexit
当一个线程在执行完成包裹内的代码时,会调用monitorexit释放持有权,前提时拿到过。释放过程就是将monitor的计数进行-1,当该线程对这个monitor的计数为0时(因为有重入情况),表示不再需要该monitor锁住的资源,退出竞争。与此同时,其他进入阻塞等待的线程,开始竞争该monitor。
反思:synchronized带来的缺点,作为Lock锁的部分由来原因
- synchronized不能对为空对象使用
- 作用域太大,对资源的访问限制较强硬。不能超时释放等原因。
- 不同的monitor试图对相同方法加锁,常见于多线程内创建不同的monitor,又想达到一个加锁状态。
- 多个monitor交叉容易造成死锁:死锁条件中的资源互斥。
延伸:类锁、对象锁是个啥?
不知道大家在工作中有没有听过对象锁、类锁的这种叫法?在专业的描述上面叫做thisMonitor、classMonitor。为啥会有这个说法以及区别是什么?我们从实际的代码中去看看。
This Monitor
import java.util.concurrent.TimeUnit;
/**
* 对象锁测试
*/
public class ThisMonitor {
public synchronized void method1() {
System.out.println("方法1 运行");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
synchronized (this) {
System.out.println("方法2 运行");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1).start();
new Thread(thisMonitor::method2).start();
}
}
老规矩,打开jstack 查询线程
可以看到这两个线程一个time waiting、一个blocked 说明两个线程中的synchronized对同一个资源进行了处理。虽然ThisMonitor的两个方法编写的不一样但在执行时是对一个对象进行管理。
Class Monitor
import java.util.concurrent.TimeUnit;
/**
* 对象锁测试
*/
public class ClassMonitor {
public synchronized static void method1() {
System.out.println("方法1 运行");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void method2() {
System.out.println("方法2 运行");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(ClassMonitor::method1).start();
new Thread(ClassMonitor::method2).start();
}
}
stack分析后跟this monitor的表现差不多,唯一的区别在于对类的方法访问方式。