线程安全问题就是某个代码在多线程环境下被调用引起的一些bug
原因 :1, 操作系统:针对多线程的随机调度,抢占式执行的过程。(这种过程是操作系统决定的,我们无法从其本身解决这个问题)
2,代码结构:多个线程同时修改同一个变量(也是由线程的随机调度抢占式执行引起的禁止变量修改也是一种解决方式但是这种方式在java中普适性不高)
3,修改操作不是原子性的:某些操作在执行过程中会被分为多个步骤,而这些步骤并非原子操作,也就是说它们不能保证在执行过程中不被其他线程所干扰或打断(就如同多个线程修改同一个变量)。
4,内存可见性:这种问题是由JVM本身引起的,在某些对代码进行优化的过程中情况下JVM错误的理解了我们的操作导优化与程序员实现的代码事与愿违,从而导致的问题。
5,指令重排序。
线程安全问题的解决方案有许多种下面我们开始逐一介绍:
1,对线程进行加锁 synchronized(){} 关键字
其中()为指定锁对象,{}中放的就是一个完整的代码。
进入到代码块中就会针对对象进行加锁此时如果其他加了同类锁的线程在执行加锁操作时就会停止运行称为锁竞争/锁冲突此时除非上一个加锁的对象解锁不然这个线程中的{}中内容就违法运行,出了代码块就会解锁(需要特别注意的是这里的锁对象必须是同一个才会产生锁竞争,如果两个线程同时加锁了但是不是相同的锁对象那么就不会发生锁竞争/锁冲突,也就意味着线程安全问题依旧存在,相当于没有进行任何操作)。
synchronized 的使用方法: 填写类对象 ,修饰方法,修饰静态方法。
修饰方法:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
使用这种方法就不会产生线程安全问题,count也会是正确答案。
修饰静态方法:
public class SynchronizedStaticExample {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedStaticExample example = new SynchronizedStaticExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
当然synchronized的使用也需要符合正确的规范不然就会产生“死锁”,死锁就是synchronized的不规范使用导致程序陷入一种 尴尬的处境。
死锁:死锁是一种情况,其中两个或多个线程彼此等待彼此释放所需的资源,从而导致所有线程都无法继续执行的情况。
死锁通常发生在多线程程序中以下几个条件可能会导致死锁发生:
-
互斥条件(Mutual Exclusion):至少有一个资源必须处于互斥状态,即一次只能被一个线程占用。这意味着当一个线程获取了资源后,其他线程必须等待该线程释放资源后才能获取。
-
持有和等待条件(Hold and Wait):线程持有至少一个资源并在等待获取另一个资源时,不释放已经持有的资源。这意味着在多个资源之间形成循环等待的关系。
-
不可剥夺条件(Non-preemption):资源只能由占有它的线程显式释放,其他线程不能强制从持有资源的线程手中夺取资源。
-
循环等待条件(Circular Wait):存在一个线程等待序列,其中每个线程都在等待前一个线程所持有的资源。这样就形成了一个循环等待的情况,导致死锁的发生。
其中1,3条件是锁的基本特性是无法改变的
当这四个条件同时满足时,就会发生死锁。例如,在一段代码中 ,线程A持有资源X并等待资源Y,而线程B持有资源Y并等待资源X,两个线程都无法继续执行下去,就会陷入死锁状态。
为了避免死锁,可以采取一些方法,如确保线程按照相同的顺序获取资源、使用超时机制释放资源、减少共享资源的数量等。还可以使用工具和技术来帮助识别和解决潜在的死锁问题。
内存可见性问题,是编译器优化问题,优化是一个综合性操作即是在javac编译阶段操作的事情,也是在java运行阶段做的事情。这种问题我们可以同过volatile关键字来解决问题。
在Java中,可以使用volatile关键字来解决内存可见性问题。当一个变量被声明为volatile时,该变量的值在一个线程中被修改后会立即对其他所有线程可见,确保所有线程都看到最新的值。
使用volatile可以避免线程之间的数据不一致性,特别是在多线程环境下共享变量的情况下。当一个线程对一个volatile变量的值进行修改时,会立即更新到主内存中,其他线程读取该变量时会从主内存中获取最新的值,而不是从本地线程缓存中获取。
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
while (!flag) {
// do something
}
System.out.println("Flag is now true");
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag set to true");
}
}