一、 线程安全分析
1. 成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
2. 局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3. 成员变量与局部变量的线程安全分析
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
(1)案例一,不安全的成员变量
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
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);
}
}
- 其中一种情况是,如果线程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.n4.ThreadUnsafe.method3(TestThreadSafe.java:33)
at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:24)
at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
(2)案例二,安全的局部变量
- private 或 final 可以防止修改,符合开闭原则中的【闭】
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
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 {
// final防止子类继承,重写该方法
public final void method1(int loopNumber) {
// 将 list 修改为局部变量
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
// 将list作为参数传递,private防止子类继承重写该方法
private void method2(ArrayList<String> list) {
list.add("1");
}
// 将list作为参数传递,private防止子类继承重写该方法
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
(3)案例三,方法访问修饰符暴露引用带来的线程安全问题
- 如果把 method2 和 method3 的方法修改为 public,并且出现以下情况, 就会有线程安全问题:
- 情况1:有其它线程调用 method2 和 method3
- 情况2:在 情况1 的基础上,添加一个子类,子类覆盖 method2 或 method3 方法,并且在子类方法中再开一个线程
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
// 将 list 修改为局部变量
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
// 将list作为参数传递,把 method2 和 method3 的方法修改为 public
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) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
Exception in thread "Thread-384" 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.n4.ThreadSafeSubClass.lambda$method3$0(TestThreadSafe.java:62)
at java.lang.Thread.run(Thread.java:748)
4. 常见线程安全的类
一、常见线程安全类
-
String
-
Integer
-
StringBuffer
-
Random
-
Vector
-
Hashtable
-
java.util.concurrent 包下的类
-
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
-
也可以理解为它们的每个方法是原子的。
注意:它们多个方法的组合不是原子的。
二、线程安全类方法的组合,不是线程安全的
Hashtable table = new Hashtable();
// 线程1,线程2,同时执行时,会有线程安全问题
if( table.get("key") == null) {
table.put("key", value);
}
- 虽然Hashtable的get、set方法都是线程安全的,但是get、put之间,可能有其他线程也在操作,并且操作的还是同一个共享变量table,这样会导致线程安全问题。
三、不可变类线程安全性
- String、Integer、StringBuffer都是不可变类,因为其内部的属性(状态)不可以改变,因此它们的方法都是线程安全的。
- String 虽然有 replace,substring 等方法【可以】改变值,其实是new了一个新对象,修改的是拷贝对象的值。
String 源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 截取字符串的方法
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 以当前字符串的value,再创建一个新的字符串,原有的字符串并没有改变
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 新创建的字符串会对原有的字符串进行拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
}
四、实例分析,代码是否线程安全
- 不安全,虽然D2的引用地址不能变,但是Date对象里的其他属性会改变,从而造成线程不安全。
// 成员变量
final Date D2 = new Date();
- 不安全,UserServiceImpl里的成员变量会被多线程共享。
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++;
}
}
- 不安全,在Spring中单例bean会被共享,类里面的成员变量也会被共享,可以加一个环绕通知将成员变量作为局部变量
// Spring AOP,例如:自定义一个统计时间的切面类
@Aspect
@Component
public class MyAspect {
// 成员变量
private long start = 0L;
// 前置通知,记录开始时间
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
// 后置通知,记录结束时间
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
}
- 安全,conn是局部变量,userService、userDao是私有变量,不会被重写
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 UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
- 不安全,conn是成员变量可以多线程共享
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 UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
- 安全,虽然conn是成员变量,但是userDao是局部变量,每次调用UserServiceImpl时,都会创建新对象,但不建议这样写
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
- 不安全,抽象方法foo会被重写, foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
// 抽象方法,会被重写, foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
// 局部变量sdf的引用会被暴露出去
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
// 例如:重写抽象方法,再创建线程修改共享变量,导致不安全
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
二、多线程习题
1. 卖票案例
- 不对sell()方法加锁时,会有线程安全问题
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟1000人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计,Vector是线程安全的集合
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) { // 2000个线程
Thread thread = new Thread(() -> {
// 这里会发生竞态条件
// 每个人随机买票数量
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}
// 等待所有线程执行完
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出的票数和剩余票数
// 统计卖出的票数和剩余票数
System.out.println("余票:" + window.getCount());
System.out.println("卖出的票数:" + amountList.stream().mapToInt(i-> i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}
// 售票窗口
class TicketWindow {
// 剩余票数
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 获取余票数量
public int getCount() {
return count;
}
// 售票
public int sell(int amount) {
// 临界区
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
测试脚本
- 在 Windows 下 使用 CMD 语法写测试脚本,进行批处理循环测试
参数 /L (该集表示以增量形式从开始到结束的一个数字序列。可以使用负的 Step)
---
格式:FOR /L %variable IN (start,step,end) DO command [command-parameters]
该集表示以增量形式从开始到结束的一个数字序列。可以使用负的 Step
从1开始,每次增加1,循环5次 Java 代码
for /L %n in (1,1,5) do java cn.itcast.n4.exercise.ExerciseSell
步骤
- 先编译 cn.itcast.n4.exercise.ExerciseSell 类,cn.itcast.n4.exercise.ExerciseSell 类会被放在target的classes目录下
- 在该目录下输入脚本命令
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>for /L %n in (1,1,5) do java cn.itcast.n4.exercise.ExerciseSell
- 以下是循环测试的结果
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1010
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1001
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
分析
- 临界区:多个线程对共享变量进行读写操作
// 模拟1000人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计,Vector是线程安全的集合
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) { // 2000个线程
Thread thread = new Thread(() -> {
// 每个人随机买票数量
// 此处是临界区,window是共享变量,sell方法有读写操作,所以需要对sell方法进行加锁
int amount = window.sell(random(5));
// 统计买票数
// 此处是临界区,amountList是共享变量,但是amountList是Vector集合本身是线程安全的,这里不用再处理
amountList.add(amount);
// 注意:window、amountList属于不同的共享变量,所以不用考虑这两行代码组合时的线程安全问题
});
// threadList是共享变量,但在这里只会被main线程使用,所以在这里不用考虑线程安全问题
threadList.add(thread);
thread.start();
}
- 综上分析,最终只需在sell方法加锁
// 售票,对修改共享变量进行加锁
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
2. 转账案例
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
分析
public void transfer(Account target, int amount) {
// 这里的对象锁不能是synchronized (this),因为该方法涉及到2个共享变量 this、target,
// 这2个共享变量都是Account类型,所以需要加Account.class对象锁
synchronized (Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}