目录
常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
线程安全类也会线程不安全
如:Vector
public synchronized int size() { // 返回大小
return elementCount;
}
public synchronized E remove(int index) { // 移除操作
......
}
① 如上展示了 Vector 两个基本操作,获取大小、移除指定位置元素
② 方法都加了锁,保证了内部一系列操作的原子性;但方法组合在一起为非原子性
Vector 线程不安全情况举例
@Slf4j(topic = "c.UnsafeVector")
public class UnsafeVector {
private static List<Integer> vector = new Vector();
static { // 加入 1 个元素
vector.add(0);
}
public static void main(String[] args) {
log.debug("开始测试前,vector 容量为 {}", vector.size());
for(int i = 0; i < 2; i++){ // 启动线程数量大于 vector 容量
new Thread(() -> {
if(vector.size() != 0){
try {
Thread.sleep(1000); // 睡一秒,以防当前线程刚释放锁又马上获得锁
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("移除 {}", vector.remove(0));
return;
}
log.debug("容量为 0 无法执行移除操作");
}, "t" + i).start();
}
}
}

① 唯一元素,在线程 t1 被移除
② 线程 t0 本应输出 "容量为 0 无法执行移除操作" 但是抛出数组越界异常
当两个原子性方法组合到一起,就不是原子性的了
产生原因
省略了上下文切换说明
不可变类线程安全性
Integer、String 等为什么线程安全?
① 类用 final 修饰,代表不可继承,也就无法重写它们原本的方法
② 大部分属性用 final 修饰,代表不可变;即使没有 final 修饰,没有任何可以修改属性的方法;都足以说明:它们的属性是不可变的。
总结:想要设计出一个线程安全的类,只要它的属性都不可变即可(或者根本没有属性)
String 中 replace 方法如何解释?
假如与原来相比没有变化,则直接返回原字符串;假如发生了变化,则新建一个字符串返回
并没有改变原本字符串的属性
String 等类什么时候会出现线程安全问题?如何解决?
不可变类的引用是可以改变,这种时候就会出现线程安全问题
解决:用 final 使其引用不可变;读写操作时加锁
自定义不可变类
public class MyInteger {
private final int value; // 不可变属性
public MyInteger(){ // 无参构造方法,自动赋初值
this.value = 0;
}
public MyInteger(int value){ // 有参构造方法,通过传进来的参数赋初值
this.value = value;
}
public MyInteger(MyInteger myInteger){ // 有参构造方法,通过传进来的参数赋初值
this.value = myInteger.value;
}
public int getValue(){ // 获取属性值的方法
return this.value;
}
public MyInteger replace(int newValue){ // 一个替换方法
if(newValue != this.value)
return new MyInteger(newValue);
return this;
}
}
@Slf4j(topic = "c.Test9")
class Test9{
public static void main(String[] args) {
MyInteger myInteger1 = new MyInteger();
log.debug("myInteger1 属性值为 {}", myInteger1);
MyInteger myInteger2 = myInteger1.replace(1);
log.debug("myInteger1 == myInteger2 ? {}", myInteger1 == myInteger2);
}
}

解决线程安全问题
卖票
@Slf4j(topic = "c.SellTicket")
public class SellTicket {
private final static int BUYER_NUMS = 1000; // 用户数量
public static void main(String[] args) throws InterruptedException {
Seller seller = new Seller(2000);
List<Thread> buyers = new ArrayList(BUYER_NUMS);
for(int i = 0; i < BUYER_NUMS; i++){
Thread thread = new Thread(() -> {
new Buyer().buyTickets(seller);
}, "t" + i);
buyers.add(thread);
}
for(Thread thread : buyers){
thread.start();
}
for(Thread thread : buyers){
thread.join();
}
List<Integer> sellNums = seller.soldTickets();
int sum = 0;
for(Integer sellNum : sellNums){
sum += sellNum;
}
log.debug("卖出票:{},剩余:{}", sum, seller.restTicketNums());
}
}
@Slf4j(topic = "c.Seller")
class Seller{
private int ticketNums; // 票数
private List<Integer> sellNums; // 售票记录
public Seller(int ticketNums){
this.ticketNums = ticketNums;
this.sellNums = new Vector();
}
public void sell(int count){ // 卖票
if(this.ticketNums >= count){
ticketNums -= count;
this.sellNums.add(count);
return;
}
log.debug("票数不足~");
}
public int restTicketNums(){ // 剩余票数
return this.ticketNums;
}
public List<Integer> soldTickets(){ // 卖票记录
return sellNums;
}
}
class Buyer{
private int needTicketNums = new Random().nextInt(10); // 用户需要的票数
public void buyTickets(Seller seller){ // 买票
seller.sell(needTicketNums);
}
}

buyers 数组实际只被 main 线程使用,并没有共享
对象 seller 被多个 buyer 对象共享,临界区如下
public void sell(int count){ // 卖票 if(this.ticketNums >= count){ ticketNums -= count; // 减掉卖出票数 this.sellNums.add(count); // 添加卖票记录 return; } log.debug("票数不足~"); }① 对票数 ticketNums 与 售票记录 sellNums 都存在读写操作
② 但是,售票记录为 Vector 对象 ,已经有对象锁保证它单个操作的原子性(线程安全)
③ 票数 ticketNums 需要被保护;它只存在 seller 对象中,所以只需要加一把对象锁
public synchronized void sell(int count)
public synchronized int restTicketNums() // 剩余票数 public synchronized List<Integer> soldTickets() // 卖票记录由于此实例中,在其他线程都结束时才调用这两个方法,所以可以不加锁;但其实加锁更好
转账
@Slf4j(topic = "c.TransferMoney")
public class TransferMoney {
private static final int TRANSFER_NUMS = 1000;
public static void main(String[] args) throws InterruptedException {
Account account1 = new Account(100); // 金额初始值都为 100
Account account2 = new Account(100); // 金额初始值都为 100
Random random = new Random();
Thread t1 = new Thread(() -> {
for(int i = 0; i < TRANSFER_NUMS; i++){ // 都循环转账 1000 次
account2.transfer(account1, random.nextInt(5));
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int i = 0; i < TRANSFER_NUMS; i++){ // 都循环转账 1000 次
account1.transfer(account2, random.nextInt(5));
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("两个账号金额总和:{}", account1.getMoney() + account2.getMoney());
}
}
@Slf4j(topic = "c.Account")
class Account{
private int money;
public Account(int money){
this.money = money;
}
public int getMoney() { // 获取金额
return this.money;
}
public void setMoney(int money) { // 设置新金额
this.money = money;
}
public void transfer(Account other, int num){ // 转账
if(this.money >= num){
this.setMoney(this.money - num); // 自己账户减钱
other.setMoney(other.getMoney() + num); // 对方账户加钱
return;
}
log.debug("余额不足~");
}
}

很明显 account1 与 account2 被线程 t1、t2 共享;临界区如下
public void transfer(Account other, int num){ // 转账 if(this.money >= num){ this.setMoney(this.money - num); // 自己账户减钱 other.setMoney(other.getMoney() + num); // 对方账户加钱 return; } log.debug("余额不足~"); }① 假如直接加对象锁,那么 account1 与 account2 相当于两把锁
② 当 t1 获取 account1 锁时,不妨碍 t2 获取 account2 锁;
当对象不同时,但是锁需要一样时;那就加类锁
synchronized (Account.class){ // 转账 if(this.money >= num){ this.setMoney(this.money - num); // 自己账户减钱 other.setMoney(other.getMoney() + num); // 对方账户加钱 return; } log.debug("余额不足~"); }
本文深入探讨了Java中的线程安全问题,分析了Vector虽然方法内部同步但组合使用时仍可能出现线程不安全的情况。还讨论了不可变类如Integer和String的线程安全性,指出其不可变性确保了线程安全。此外,通过卖票和转账的示例,阐述了解决线程安全问题的方法,包括使用对象锁和类锁。最后,提出了自定义不可变类的设计原则。




10万+

被折叠的 条评论
为什么被折叠?



