例6
//本类是线程安全的,因为userService中没有可变的属性. public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } //本类是线程安全的,因为userDao被定义成了局部变量. public class UserServiceImpl implements UserService { // 是否安全 Yes public void update() { UserDao userDao = new UserDaoImpl(); userDao.update(); } } //本类的分析过程同上 例5 public class UserDaoImpl implements UserDao { //是否安全 No private Connection conn = null;//不建议这样写,即使外部service中使用的局部变量 public void update() throws SQLException{ String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","","") //... conn.close(); } }
例7
public abstract class Test { public void bar() { // 是否安全 No //只含有局部变量的类不一定就是线程安全的,要看这个变量引用是否在子类/其他方法 以一个新的线程被用到 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); foo(sdf); } public abstract foo(SimpleDateFormat sdf); public static void main(String[] args) { new Test().bar(); } }
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
//当子类这样重写foo的方法时,就不是线程安全的 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(); } }
请比较 JDK 中 String 类的实现
String中的char数组被final private 修饰,并且没有提供修改字符数组的方法, 从而导致字符数组的不可变.
另外String类被final修饰,避免了子类继承,从而破坏其不可变的性质。
**扩展:**思考下面程序执行的结果
3.6 常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
public void test(){ Hashtable table = new Hashtable(); //以下关于table的操作是线程安全的 new Thread(()->{ table.put("key", "value1"); }).start(); new Thread(()->{ table.put("key", "value2"); }).start(); }
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
3.6.1 线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable(); // 创建 线程1,线程2,分别执行一下代码 if( table.get("key") == null) { table.put("key", value); }
线程1执行完get之后,锁释放。此时有可能线程2抢占到了锁,所以线程2也判断get==null成立,这时线程2可以进入下面的put逻辑。线程2执行完后,线程1继续执行做put操作,从而会出现值覆盖。此时两个线程table的操作就不是线程安全的。
3.6.2 不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
本质是当执行这些操作时,会创建新对象来保存值。
3.7 习题
3.7.1 卖票练习
测试下面代码是否存在线程安全问题,并尝试改正
/** * @author lxy * @version 1.0 * @Description 卖票案例 * @date 2022/6/8 22:21 */ @Slf4j(topic = "c.ExerciseSell") public class ExerciseSell { public static void main(String[] args) throws InterruptedException { //售票窗口,默认有10000张票 TicketWindow window = new TicketWindow(10000); //卖出的票数统计 List <Integer> amountList = new Vector <>(); //所有线程的集合 List<Thread> threadList = new ArrayList <>(); //创建2000个线程,模拟用户来买票 for (int i = 0; i < 2000; i++) { Thread thread = new Thread(() -> { //买票 int amount = window.sell(randomAmount()); amountList.add(amount); }); thread.start(); threadList.add(thread); } //等所有乘客买完票再进行统计 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 void setCount(int count) { this.count = count; } //售票 public int sell(int amount){ if(this.count >= amount){ this.count -= amount; return amount; }else{ return 0; } } }
另外,用下面的代码行不行,为什么?
List<Integer> sellCount = new ArrayList<>();//这里不可以这样写,因为sellCount需要被多个线程所操作,会有线程安全的问题,所以需要使用线程安全的集合.
测试脚本运行:
for /L %n in (1,1,10) do java -cp ".;E:\Soft\apache-maven-3.8.1\mvn_repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;E:\Soft\apache-maven-3.8.1\mvn_repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;E:\Soft\apache-maven-3.8.1\mvn_repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" com.rg.thread.ExerciseSell //解释: //window下执行多次 java文件的脚本(可用于测试观察文件多次执行的结果) //从1开始,每次递增1,循环到10 cp:类路径 "当前路径,所需jar包路径" 概括来说就是:下边的原子操作不依赖上边原子操作的结果的话,就不用考虑两个原子操作合在一起的安全性
分析并修改代码可能存在线程安全的部分
- 寻找临界区(多个线程对共享变量的读写操作的区域)
再次运行:
3.7.2 转账练习
测试下面代码是否存在线程安全问题,并尝试改正
/** * @author lxy * @version 1.0 * @Description 转账案例 * @date 2022/6/10 15:53 */ @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());//a向b转一个随机金额 } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { b.transfer(a, randomAmount());//b向a转一个随机金额 } }, "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); } } // 账户 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){ if(this.money >= amount){ this.setMoney(this.getMoney()- amount);//转账者金额减少 target.setMoney(target.getMoney()+amount);//收款者金额增加 } } }
运行结果:
分析:
修改: