成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了 ,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
1 局部变量分析
测试代码:
@Slf4j(topic = "test4")
public class test4 {
public static void test() {
int i = 10;
i++;
}
}
字节码(先编译代码,再反编译:javap -v test4.class
)
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 18: 0
line 19: 3
line 20: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
每个线程调用test()方法时,局部变量i会在每个线程的栈帧内存中创建多份,因此不存在共享。
2 成员变量与局部变量对比分析
ThreadUnsafe
类有成员变量list
和三个方法,method2方法是向list中添加元素,method3是从list中移除元素。
当两个线程共同作用同一个list对象时,会出现以下情况:线程1添加元素没有写进内存时被线程2中断,线程2添加元素并写进内存,之后线程1再写进内存,两次添加相当于只添加了一次,之后再移除两次,就会发生错误。
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
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+1)).start();
}
}
}
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); }
}
报错IndexOutOfBoundsException
:
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 Chapter4.ThreadUnsafe.method3(TestThreadSafe.java:39)
at Chapter4.ThreadUnsafe.method1(TestThreadSafe.java:34)
at Chapter4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:23)
at java.lang.Thread.run(Thread.java:748)
分析:
- 无论哪个线程中的method2引用的都是同一个对象中的list成员变量
- method3与method2分析相同
当把list成成员变量改成局部变量后:
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe{
public 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相同
- method2和method3都是私有方法,操作list的方法只有这两个。
3 局部变量暴露引用
方法访问修饰符带来的思考,如果把method2和method3的方法修改为public会不会代理线程安全问题?
- 情况1:有其它线程调用method2和method3
- 情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法,即
class ThreadSafe{
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) { list.add("1"); }
public void method3(ArrayList<String> list) { list.remove(0); }
}
将method2和method3的权限修饰符改为public,此时仍线程安全。因为,在调用method2和method3是,操作的list并不是在method1内的list。
添加子类ThreadSafeSubClass
继承ThreadSafe
,并重写method3方法,在其内部创建线程来操作list。
class ThreadSafe{
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) { list.add("1"); }
public void method3(ArrayList<String> list) { list.remove(0); }
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
此时会出现线程安全问题,因为method1创建list对象操作list,调用method3时会创建另一个线程操作这个list,多个线程共享list,将出现线程安全问题。
从例子可以看出private和final提供【安全】的意义所在。
private修饰的方法不能被重写。
final防止子类覆盖方法。
4 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
5 线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if (table.get("key") == null) {
table.put("key", "value");
}
可能的执行顺序如下:
理想情况下,可能只有一个线程进行了 put;但也有可能一个线程put完,又被另一个线程put覆盖。覆盖前面的情况,丢失了更新内容。
线程安全类中每个方法都能保证原子性,但是组合不能保证原子性。如果想让组合保证原子性,需要在组合的外面加一个保护。
6 不可变线程安全性
String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变(只能读,不能改),因此它们的方法都是线程安全的
但,String 有replace, substring 等方法可以改变值,那么这些方法又是如何保证线程安全的呢?
查看String的源码,其中的substring方法创建了一个新的字符串,并不是对原字符串的修改。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
7 实例分析
例1:
Servlet运行在Tomcat环境下,只有一个实例,会被tomcat的多个线程共享,因此其中的成员变量都会存在共享问题。
- Map map:不是线程安全的,非线程安全类,HashTable是线程安全的。
- String S1:线程安全,因为它是不可变类。
- final String S2:同样线程安全。
- Date D1:不是线程安全的,非线程安全类。
- final Date D2:Date的对象D2的引用值不能变,但日期里的属性(年月日)可以改变,不是线程安全的。对象的引用不可变不代表对象的状态不可变。
例2:
从底向上分析:
- DAO没有成员变量,即使多个线程访问,也没有要修改的状态,所以是线程安全的。
- Connection是方法内的局部变量,即使有多线程,其他线程访问不到(每个线程都会单独创建一个Connection),也是线程安全的。
- Service里面包含私有的成员变量,但没有其他的地方可以修改它,它是不可变的,所以它是线程安全的。
例3:
此时Connection为DAO的局部变量,Servelet只有一份,所以Service只有一份,所以DAO只有一份,所以DAO肯定会被多个线程共享,所以其中的变量会被多个线程共享。所以,不是线程安全的。
比如,线程1刚刚创建Connection,还没用呢,线程2就拿着Connection使用,使用后close,那么机会出问题了。
例4:
与例4相比,Dao相同,但Service不同。在每次调用Service时,会重新创建一个新的Dao,没有将Dao当成Service的成员变量。
Dao为Service内方法的局部变量,每个新线程都会创建一个新的Dao,新的Dao会创建一个新的Connection,所以不会出现线程安全问题。但是这种方式不太好。
例5:
虽然SimpleDateFormat是剧本变量,但是它可通过抽象方法foo暴露给外面。它的子类可能做了一些不恰当的方法。
其中foo的行为是不确定的,可能导致不安全的发生,被称之为外星方法。
不想暴露给外面的方法或属性就设置为final
。String
的实现就是这样做的。
8 习题
卖票
@Slf4j(topic = "ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);
// 卖出票数统计
List<Integer> amountList = new Vector<>();
// 所有线程的结合
List<Thread> threadList = new ArrayList<>();
// 卖票
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
// 买票
int amount = window.sell(randomAmount());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}
// 等待线程运行结束
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出票数和剩余票数
log.debug("余票:{}", window.getCount());
log.debug("卖出票数:{}", amountList.stream().mapToInt(i->i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机1-5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
/**
* 售票窗口
*/
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public int sell(int amount) {
if (count >= amount) {
count -= amount;
return amount;
} else {
return 0;
}
}
}
多运行几次可能会产生错误:出售的票数 + 剩余票数 != 最初票数
找出临界区,临界区:对共享资源的多线程读写操作
卖票window里的count是一个共享变量
int amount = window.sell(randomAmount());
这也是一个共享变量,但他是线程安全的。
amountList.add(amount);
所以只要保证这个卖票这个动作涉及的共享变量是线程安全的即可,添加synchronized
public synchronized int sell(int amount) {
if (count >= amount) {
count -= amount;
return amount;
} else {
return 0;
}
}