导图
一、基础概念
1、在并发编程中为什么会有线程安全的问题?
首先由于Java的内存模型,线程并不是直接对共享变量进行操纵,而是拷贝了一个副本,到当前线程本身的栈内存当中;对副本修改完成后再刷新到主存当中;
以计数count为例:
线程A拿到共享变量count(初始值为1)后加1;线程B也要拿到这个共享变量并进行累加操作;那么就有可能出现,线程A将count拷贝到自己的工作内存空间后进行处理并加1,但此时还未将count刷新到主存,线程B从主存拿到的count依然为1,就会导致计数不准确,造成一定的脏数据。
2、线程安全问题的解决思路
线程安全问题只在多线程环境下才会出现;并且如果所有的线程都对数据仅有读操作,没有写操作的话,也不会有线程安全的问题;因此保证高并发场景下的线程安全,可以从以下四个维度考量;
数据单线程内可见
单线程总是线程安全的。通过限制单线程仅在单线程内部可见,可以避免被其他线程锁篡改;比如ThreadLocal局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程并无瓜葛;
只读对象
如果对象的属性是不可写入修改的,那么这个对象也是线程安全的;比如用final修饰的关键字。
线程安全类
在Java当中提供了很多线程安全的操作类;在类的内部实现了非常明确的安全机制;比如StringBuffer 内部的操作都是synchronized;或者Atomic相关的原子操作类等。
同步与锁机制
使用线程同步或者加锁机制;比如JUC并发包中的内容。
二、死锁
1、什么是死锁
死锁是指俩两个或者两个以上的线程在执行过程中,因争夺资源而造成的互相等待的情况;若无外力作用的情况,这些线程会一直相互等待无法继续执行下去。
2、死锁产生的条件
死锁的产生必须具备以下4个条件:
互斥条件
指的是线程对已经获得的资源具有排它属性;即该资源同时只由一个线程占用;如果此时其他线程想要获得该资源,就只能等待。
请求并保持条件
指一个线程已经获得至少一个资源后,又提出了新的资源请求;而新资源已被其他线程占有,所以当前线程会被阻塞;但阻塞的同时不会释放已经获得的资源。
不可剥夺条件
指线程获取到的资源在自己使用完成之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
循环等待条件
指在发生死锁时,必然存在一个线程——资源的环形链,例如T1等待T2占用的资源…Tn等待T0占用的资源。
一个死锁的示例:
public class ThreadTest {
private Object lockA = new Object();
private Object lockB = new Object();
public void run() {
new Thread(new TestRunnable()).start();
new Thread(new Test2Runnable()).start();
}
private class TestRunnable implements Runnable {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread1 get resource A");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 waiting resource B");
synchronized (lockB) {
System.out.println("Thread1 get resource B");
}
}
}
}
private class Test2Runnable implements Runnable {
@Override
public void run() {
synchronized (lockB) {
System.out.println("Thread2 get resource B");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2 waiting resource A");
synchronized (lockA) {
System.out.println("Thread2 get resource A");
}
}
}
}
}
这个例子当中,线程1拿到lockA之后还需要拿到lockB,而此时lockB被线程2所占用,并且线程B除了持有lockB还等待线程1释放lockA;因此形成死锁,这两个线程会永远等待下去。
3、如何避免死锁?
如果要避免死锁的发生,只需要将死锁的4个条件之一打破即可;但目前只有请求和保持以及循环等待这两个条件是可以被打破的。
造成死锁的原因和申请资源的顺序是有很大关系的;使用资源申请的有序性原则就可以避免死锁,例如上面死锁的例子,将线程1,2申请资源(锁)的顺序改为一样,每当一个线程获取到资源后,另外一个线程阻塞不会去获取另外的资源,通过这种方式可以避免死锁的发生:
public class ThreadTest {
private Object lockA = new Object();
private Object lockB = new Object();
public void run() {
new Thread(new TestRunnable()).start();
new Thread(new Test2Runnable()).start();
}
private class TestRunnable implements Runnable {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread1 get resource A");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 waiting resource B");
synchronized (lockB) {
System.out.println("Thread1 get resource B");
}
}
}
}
private class Test2Runnable implements Runnable {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread2 get resource A");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2 waiting resource B");
synchronized (lockB) {
System.out.println("Thread2 get resource B");
}
}
}
}
}