一、方法上的synchronized
1、实例方法:
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
2、静态方法:
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
3、不加synchronized的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
二、变量的线程安全
1、成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
-
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1();
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 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
2、局部变量是否线程安全?
- 局部变量是线程安全的
- 局部变量引用的对象则未必
-
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
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 = 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).start();
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
分析:
无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
method3 与 method2 分析相同
将 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 相同
3、常见线程安全类
- 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();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
注意ImmutableValue
实例的值是如何在构造函数中传递的。还要注意没有 setter 方法。一旦ImmutableValue
实例被创建,你不能改变它的值。它是不可变的。但是,您可以使用该getValue()
方法阅读它。
如果想增加一个增加的方法呢?
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public Immutable add(int v){
return new Immutable(this.value + v);
}
}
注意该add()
方法如何返回一个ImmutableValue
带有添加操作结果的新实例,而不是将值添加到自身。
4、引用不是线程安全的!
重要的是要记住,即使一个对象是不可变的,因此是线程安全的,对该对象的引用也可能不是线程安全的。看这个例子:
public class Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
}
本Calculator
类持有一个参考ImmutableValue
实例。请注意如何通过setValue()
和add()
方法更改该引用。因此,即使Calculator
类在内部使用不可变对象,它本身也不是不可变的,因此不是线程安全的。换句话说:ImmutableValue
该类是线程安全的,但它的使用 不是。在尝试通过不变性实现线程安全时,需要牢记这一点。
为了使Calculator
您可能已经声明的类线程安全的getValue()
, setValue()
和add()
方法synchronized
。那本来就成功了。