Java并发编程(四)
成员变量与静态变量是否线程安全
- 如果他们没有共享,则线程安全
- 如果他们共享了,根据他们的状态是否能够改变,有分为两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码处于临界区,则需要考虑线程的安全,如下代码
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
{
method2();
method3();
}
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
当多个线程同时执行时,如下模拟
static final int THREAD_NUMBER = 5;
static final int LOOP_NUMBER = 2000;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
- 情况一:size和add的数量不匹配。
- 分析如下:
- 加入线程1首先进入list.add(“1”),把1加入0的位置,此时刚准备要加入的时候CPU时间片用完了,也就是说list数组里面还是没有东西,线程2分配到了CPU时间片,这时候线程而也执行了添加操作, 此时的线程2还是加入到list的0的位置,因为线程1还没有加进入。此时线程2把size设置为了1.
- 线程1的时间片到了,开始执行add,由于CPU是在刚准备加入的时候停止的,此时线程1要加入的位置还是index = 0,那么线程1执行add操作加入到index = 0的位置,然后把size设置为2,实际上list里面才只有1个数据。
- 情况二:
- 在上述情况的基础上,由于两个线程都添加了元素,所以当两个线程都执行remove()方法时,会发生如下错误,只有一个数据,但却remove()两次
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at com.wjl.code01.ThreadUnsafe.method3(Test09.java:39)
at com.wjl.code01.ThreadUnsafe.method1(Test09.java:29)
at com.wjl.code01.Test09.lambda$main$0(Test09.java:16)
at java.lang.Thread.run(Thread.java:750)
- 将list修改为局部变量,对方法进行了改进,就不会出现线程的安全问题了
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
上述代码分析 list 是局部变量,每个线程调用时会创建其不同实例,没有共享 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象,method3 的参数分析与 method2 相同
局部变量是否线程安全
由于此时子类继承了ThreadSafe,导致list也被共享了,作为参数传递到子类的方法中可以被不同的线程调用,所以线程不安全
常见的线程安全类
- 常见线程安全类String,Integer,StringBuffer,Random,Vector,Hashtable,java.util.concurrent 包下的类
- 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为,他们的每个方法是原子的,但是注意他们多个方法的组合不是原子的
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
线程安全类的方法组合
Hashtable table = new Hashtable();
if( table.get("key") == null) {
table.put("key", value);
}
- 上述多个线程调用是否安全? 不安全,因为线程1和线程2有机会同时进入if条件中,此时就有一个线程的值会被另一个线程put的值覆盖
不可变线程的安全性
- String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。但是String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?从String的源码我们可以看到,到最后是**new String(value, beginIndex, subLen);**来生成了新的对象,所以操作的不是同一个对象,所以也就不存在线程不安全一说了。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
线程安全实例分析
- 普通成员变量
public class MyServlet extends HttpServlet {
Map<String,Object> map = new HashMap<>();
String S1 = "...";
final String S2 = "...";
Date D1 = new Date();
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
}
}
- 方法中的成员变量
public class MyServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
private int count = 0;
public void update() {
count++;
}
}
黑马程序员多线程课程:JUC并发编程全套教程
|