聊聊項目中的MVC分層構架中的一些線程安全問題

 

變量的線程安全分析

 

 

 

 

成員變量和靜態變量是否線程安全?

  如果它們沒有共享,則線程安全
  如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
 

 

  如果它們沒有共享,則線程安全
  如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
     如果只有讀操作,則線程安全
     如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全

局部變量是否線程安全?

  局部變量是線程安全的
  但局部變量引用的對象則未必
     如果該對象沒有逃離方法的作用訪問,它是線程安全的
     如果該對象逃離方法的作用範圍,需要考慮線程安全

局部變量線程安全分析

局部變量如果引用的對象沒有逃離整個方法的作用範圍,哪它就是線程安全的,每個線程調用 test1() 方法時局部變量 i,會在每個線程的棧幀內存中被創建多份,因此不存在共享,因此不存在線程安全問題

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

javac 後使用javap -v  查看字節碼

  public static void test1();
    descriptor: ()V
    flags: (0x0009) 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 11: 0
        line 12: 3
        line 13: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I

由此我們可以看出這裏的i++,轉化成字節點是incr,他是原子的,不像靜態的變量中的i++操作

 

 

 

成員變量線程安全問題分析

成員變量在多線程環境下訪問是存在共享 ,因此會引起線程安全問題

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserService();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{
    // 成員變量在多線程環境中線程不安全
    List<String> list = new ArrayList<>();

    private void addUser(){
        list.add("test");
    }

    private void deleteUser(){
        list.remove(0);
    }

    public void method(int times){
        for (int i = 0; i < times; i++){
            addUser();
            deleteUser();
        }
    }
}

運行結果異常:

內存分析:

修改爲局部變量:

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserService();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{


    private void addUser(List<String> list){
        list.add("test");
    }

    private void deleteUser(List<String> list){
        list.remove(0);
    }

    public void method(int times){
        List<String> list = new ArrayList<>();
        for (int i = 0; i < times; i++){
            addUser(list);
            deleteUser(list);
        }
    }
}

運行結果正常

內存分析:list是局部變量,每個線程調用時會生成不同的線程棧,每個線程棧創建不同的實例,沒有共享,因此不存在線程安全問題

 

 

局部變量-暴露引用:

情況一:如果將以上方法 addUser、deleteUser修改爲public意味着可以在外部調用,這時候如果是線程1調用addUser,線程2調用deleteUser,他們是傳的list參數是在不同的線程棧的,因此也是安全的

情況二:

public class ThreadSafeTest {

    public static void main(String[] args){
        UserService userService = new UserServiceSubClass();
        for (int i = 0; i < 2; i++){
            new Thread(()->{
                userService.method(200);
            }, "Thread" + (i+1)).start();
        }
    }
}


class UserService{


    private void addUser(List<String> list){
        list.add("test");
    }

    public void deleteUser(List<String> list){
        list.remove(0);
    }

    public void method(int times){
        List<String> list = new ArrayList<>();
        for (int i = 0; i < times; i++){
            addUser(list);
            deleteUser(list);
        }
    }
}

class UserServiceSubClass extends UserService{

    public void deleteUser(List<String> list){
        new Thread(()->{
            list.remove(0);
        }).start();
    }
}

因此,private在一定程序上保護方法的線程安全問題,限制子類重寫父類方法無效,如果徹底不想讓子類重寫還可以直接加一個final修飾

再論關鍵字private、 final 安全意義所在

 

不想被子類重寫的方法請記用final修飾

 

 

不想給外面訪問的方法請記用private修飾

常見線程安全類

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

分析:

這時想要線程安全,就要在這兩個操作之外使用synchronized包裹

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

常見不可變類:

String、Integer 等都是不可變類,因爲其內部的成員變量不可以改變,因此它們的方法都是線程安全的,每次都是在重新操作後返回一個新的對象

 

 

實例分析

示例一

// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {

    Map<String, Object> map = new HashMap<>();  // 不安全
    String s1 = "....";                         // 安全,String是不可變類
    final  String s2 = "....";                  // 安全,String是不可變類
    Date d1 = new Date();                       // 不安全
    final  Date d2 = new Date();                // 不安全, final只是表示d2不能變,但是Date實例中的屬性是可以改變的,因此在多線程環境下不安全

    public void doGet(HttpServletRequest req, HttpServletResponse resp) {

    }
}

示例二:

// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 不是線程安全的,servlet只有一份,因爲userService是成員變量所以也只有一份

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    // 記錄調用數次
    private int count = 0;
    @Override
    public void update() {
        // ....
        count ++;
    }
}

示例三:

 

在Spring中默認每個對象都是單例的,意味着是被多個線程共享的,因此也裏面的成員變量也是補共享的,所在before、after方法對成員變量的修改會存在線程安全問題,在這裏可以使用around環繞通知,使用把開始時間,結束時間定義成局部變量。如果將LogAspect定義成多例bean,也是不行的,有可能進入before時是一個實例,進入after時是另一個實例,計算出的時間有問題。

示例四:

// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();        // 雖然userDao是成員變量,但是由於UserDao沒有可更改的成員變量(無狀態),所以也是線程安全的

    @Override
    public void update() {

    }
}

interface UserDao{

    void update();
}

// Dao沒有成員變量,它們是線程安全的
class UserDaoImpl implements UserDao{

    @Override
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
        try(Connection conn = DriverManager.getConnection("localhost", "admin", "123456")){

        }catch (Exception e){

        }
    }
}

因此MVC模式中的這種貧血模型架構是線程安全的

 

示例五:

// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();        // 雖然userDao是成員變量,但是由於UserDao沒有可更改的成員變量(無狀態),所以也是線程安全的

    @Override
    public void update() {

    }
}

interface UserDao{

    void update() throws SQLException;
}

// useDao會被多個線程共享,它的成員變量conn也會被多個線程共享,因此會發生線程安全
class UserDaoImpl implements UserDao{
    private Connection conn = null;
    @Override
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        // Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
        conn = DriverManager.getConnection("localhost", "admin", "123456");
        conn.close();
    }
}

 

示例六:

// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();        // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        userService.update();
    }
}

interface UserService {
    void update();
}

class UserServiceImpl implements UserService {


    @Override
    public void update() {
        try {
            // UserDao是局部變量,每個線程棧中存在一份userDao,哪它內部的conn成員變量也是各存在一份,所以不會有線程安全問題
            UserDao userDao = new UserDaoImpl();
            userDao.update();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

interface UserDao{

    void update() throws SQLException;
}


class UserDaoImpl implements UserDao{
    private Connection conn = null;
    @Override
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        // Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
        conn = DriverManager.getConnection("localhost", "admin", "123456");
        conn.close();
    }
}

每一個請求,userService中的userDao會創建一份實例,它不存在線程安全問題,但是性能有一些隱患

 

示例七:

public class  ThroughVariableTest{
    public static void main(String[] args){
        new DateFormatUtil().getDate();
    }
}
abstract class BaseDateFormat {

    public void getDate(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
        parse(simpleDateFormat);
    }

    // 這時子類的形爲不確定,可能導致不安全的發生,被稱之爲外星方法
    abstract void parse(SimpleDateFormat simpleDateFormat);

}
class DateFormatUtil extends BaseDateFormat{

    @Override
    void parse(SimpleDateFormat simpleDateFormat) {
        String datastr = "1990-05-25 00:00:00";
        for (int i = 0; i < 20; i++){
            new Thread(()->{
                try {
                    simpleDateFormat.parse(datastr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

這時發生了多個線程(包括main線程)訪問同一個simpleDateFormat對象 simpleDateForma對象本身是線程不安全的,所以整個代碼造成線程不安全問題,

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章