Java并发 第四讲 共享模型之管程 2变量的线程安全分析

1. 变量的线程安全分析

成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全

public static void test1() {
int i = 10;
i++;
}

i++虽然不是原子操作,但它是局部变量,不同的方法调用会在不同的栈帧里创建,不存在共享的问题,

局部变量的引用稍有不同

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);
    }
}

有可能出现 

因为 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量。

将 list 修改为局部变量就没问题了。

常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。但注意它们多个方法的组合不是原子的,如

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    table.put("key", value);
}

可能会出现

几个例子

class MyServlet extends HttpServlet {
    // 是否安全?
    Map<String, Object> map = new HashMap<>();
    // 是否安全?
    String S1 = "...";
    // 是否安全?
    final String S2 = "...";
    // 是否安全?
    Date D1 = new Date();
    // 是否安全?
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    }
}

 否,是,是,否,否,最后一个因为加了final只是地址不变,但地址指向的东西有可能变。

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++;
    }
}

均为否,MyServlet只有一份,成员变量UserServiceImpl也就只有一份,count就会被共享带来线程安全问题,对count应该做线程安全的保护。

@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));
    }
}

否,Spring不额外设置scope的话均为单例,不是线程安全的,可设置为环绕通知变成局部变量解决。设置为多例不解决根本问题,开始和结束可能不是同一个线程计算出来无意义。


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) {
// ...
        }
    }
}

第三个是,因为没有可被修改的成员变量,而Connection又是局部变量所以没问题。

第二个是,UserDaoImpl();没有可被修改的成员变量

第一个是 是,只有一个私有的成员变量,别的线程不可修改。

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();
    }
}

这个不是,Connection作为成员变量因为myservelet只有一份会被共享,所以存在线程安全问题。

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 =null;

    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("", "", "");
// ...
        conn.close();
    }
}

UserDaoImpl成了局部变量,每个线程来会创建新的,就本例来说没啥问题,但不推荐这么写;


public abstract class Test {
    public void bar() {
        // 是否安全
        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();
    }
}

sdf虽然是局部变量,但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();
        }
    }

例题卖票

package cn.itcast.testcopy;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow ticketWindow = new TicketWindow(500);
        List<Thread> threadList = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int amount = ticketWindow.sell(randomme());
                try {
                    Thread.sleep(randomme());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        // 买出去的票求和
        log.debug("selled count:{}", amountList.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());

    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int randomme() {
        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 (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

只有500张卖出去了503张。线程不安全 

// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
sellCount.add(count);
});
list.add(t);

sell方法不是线程安全需要处理,add因为sellCount是vector实现线程安全,list只有主线程调用所以用ArrayList实现就行,sell和add虽然是线程安全的组合但和前面的

还不一样,putget组合不安全是因为对同一对象操作,而 sell和add是不同对象,解决方案对TicketWindow的sell加synchronized。

对比转账

@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) {

        if (this.money >= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
            
        }
    }
}

这次只在transfer加锁就不行了,因为涉及到两个账户,加锁相当于 synchronized(this) {},一个方法是对Account类加锁,但性能较低后续可改进。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值