什么是线程安全
我碰到别人问的线程安全 为什么不是多线程安全 ,这个当时问蒙我了。在多线程的环境下 才有现成安全问题的。多线程安全是指在多线程并发执行的情况下,程序能够正确地完成预期的操作而不会出现数据不一致或者程序崩溃的情况,就是说多线程下的结果和单线程下的结果应该是一样的,而且不会产生死锁。
线程安全主要体现在这么几个方面:
原子性,有序性,可见性
原子性就是指的是整个操作是不可变的,可以这样理解 一个任务只在一个线程中进行。而线程的来回切换是导致原子性问题的根源。 我们知道java中的线程控制是由系统层面控制的,程序计数器会存储线程的上下文环境,以便来回切换使用。还有虚拟机栈会存储方法的局部变量等数据。要保证一个线程执行一个任务就要加锁机制。synchronized或者lock
有序性 在jvm层面 源码执行的过程中会优化执行顺序的。
可见性 一个对象在多个线程中应该值是一样的。
volatile 保证了有序性和可见性 。
如何保证线程安全
就是保证线程的上面几个方面是完整的;
原子性的话 在juc就是java.util.concurrent 这个类下面有很多安全的类,比如说atomicInteger,就是在不加锁的情况下通过cas保证了线程安全问题。但是存在ABA问题。
还有一种就是加锁的情况,保证了只有一个线程在处理当前事务。加锁的情况有两种,一种是synchronized 一种是lock。这是两种不同的实现方式。
synchronized和lock的差别
我通过两种很常见的生产消费者模式来告诉这两种的差别。
public class Producer {
private ArrayList<Apple> arrayList = new ArrayList<>();
public void procude() {
synchronized (arrayList) {
while (arrayList.size() > 100) {
try {
arrayList.wait();
} catch (InterruptedException e) {
System.out.println("暂停生产");
throw new RuntimeException(e);
}
}
arrayList.add(new Apple());
System.out.println(arrayList.size());
arrayList.notifyAll();
}
}
public void custom() {
synchronized (arrayList) {
while (arrayList.size() <= 0) {
try {
arrayList.wait();
} catch (InterruptedException e) {
System.out.println("暂停消费");
throw new RuntimeException(e);
}
}
arrayList.remove(arrayList.size() - 1);
System.out.println(arrayList.size());
arrayList.notifyAll();
}
}
public static void main(String[] args) {
Producer example = new Producer();
// 创建生产者线程并启动
Thread producerThread = new Thread(() -> {
while (true) {
example.procude();
}
});
producerThread.start();
for (int i = 0; i < 4; i++) {
Thread consumerThread = new Thread(() -> {
while (true) {
example.custom();
}
});
consumerThread.start();
}
}
}
public class Producer {
private ArrayList<Apple> arrayList = new ArrayList<>();
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public void procude() {
lock.lock();
try {
while (arrayList.size() > 100) {
try {
notFull.await();
} catch (InterruptedException e) {
System.out.println("暂停生产");
throw new RuntimeException(e);
}
}
arrayList.add(new Apple());
System.out.println(arrayList.size());
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public void custom() {
lock.lock();
try {
while (arrayList.size() <= 0) {
try {
notEmpty.await();
} catch (InterruptedException e) {
System.out.println("暂停消费");
throw new RuntimeException(e);
}
}
arrayList.remove(arrayList.size() - 1);
System.out.println(arrayList.size());
notFull.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Producer example = new Producer();
// 创建生产者线程并启动
Thread producerThread = new Thread(() -> {
while (true) {
example.procude();
}
});
producerThread.start();
for (int i = 0; i < 4; i++) {
Thread consumerThread = new Thread(() -> {
while (true) {
example.custom();
}
});
consumerThread.start();
}
}
}
两者实现的功能是一样的,都是加锁操作。
1.lock是显式的加锁,有lock 和unlock操作。syn是隐式的加锁,通过底层的monitor对象监视器来加锁上锁。
2.synchronized的锁升级,会从无锁状态-》偏向锁-》轻量锁-》重量锁这样一个锁升级状态。
lock没有这种的锁升级的状态
3.synchronized是一个关键字,lock是一个接口,有很多实现类。
4.灵活性:相对于synchronized关键字,Lock接口提供了更多的灵活性和扩展性。例如,可以选择公平锁或非公平锁、可重入性、尝试非阻塞地获取锁、可定时的获取锁等功能。同时,Lock还支持条件变量的机制,可以更灵活地进行线程间的等待和唤醒操作。
volatile
volatile 是 Java 中的一个关键字,用于声明变量是易变的(volatile variable)。当一个变量被声明为 volatile 时,表示它的值可能会被多个线程同时修改,因此每次访问该变量时都会从主内存中读取最新的值,并且在写入该变量时也会立即将新值刷新到主内存中,而不是仅存在于线程的本地缓存中。
什么是死锁,如何避免
我们想象一下有两个线程a,b 两个资源 x,y。当线程a获得x想去获取y,现成b得到y想去获取x。这就构成了死锁。死锁的条件就是
1.不可剥夺,2.首尾相应 3.互斥体检 4.请求与保持
避免的话就是破坏这几个条件之一
比如所有的资源要有顺序获取,一段时间获取不到就释放锁,死锁检测(可以在jdk中使用jstack来获取)
附代码
public class DeadlockExample {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and resource 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1 and resource 2");
}
}
});
thread1.start();
thread2.start();
}
}
附上jstack的信息 deadlock就是死锁,现成Thread 1和Thread2相互锁着
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f7ef4003828 (object 0x00000000ec461950, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f7ef4006168 (object 0x00000000ec461960, a java.lang.Object),
which is held by "Thread-1"