Double lock checking trong những ngôn ngữ lập trình song song
Java
84
Scala
50
White

huydx viết ngày 30/09/2015

Nếu bạn đã từng lập trình với những ngôn ngữ có khả năng tiến hành song song sử dụng thread (hay là gần đây là fiber (ruby không tính nhé :trollface:)), chắc các bạn sẽ thấy việc "kiểm soát" mọi thứ là vô cùng khó khăn, đặc biệt là khi nhiều thread của bạn phải dùng chung một "tài nguyên", thì bạn phải sắp xếp sự sử dụng của chúng sao cho hợp lý.
Mình có một ví dụ như sau, bạn có một class là User, class này sẽ access vào database để lấy ra rất nhiều thông tin. Tuy nhiên những thông tin này chỉ cần lấy ra 1 lần, và sẽ không thay đổi trong suốt quá trình sử dụng, ví dụ như thông tin về số lượng tiền lớn nhất User có thể mang được chẳng hạn. Nếu mỗi lần sử dụng classUser bạn lại phải "chui" vào database và lấy ra tất cả mọi thứ thì sẽ tốn rất nhiều cost. Vậy bạn nghĩ ra cách nào để giảm bớt cost đó, chắc bạn đã nghĩ ra rồi nên mình nói luôn, đó chính là bạn sẽ cache thông tin đó lại! Đúng là cache phải không.

object User {
    lazy val cache = new ConcurrentHashMap[Information, Value]();
    def accessUserBy(key: Information): Value = {
        if (cache == null) {
            initialize(cache); // very very costly!!!
        } else {
            return cache.get(information)
        }
    }
}

Ồ!, rất đơn giản đúng không, khi nào mà cache chưa được khởi tạo thì bạn sẽ khởi tạo cache, còn không thì trả về thông tin nằm bên trong cache. Dễ như ăn kẹo rồi! (Chú ý ở đoạn code trên mình dùng Object chứ không phải Class, tức là đây là một Singleton class. Bạn nào chưa biết có thể tìm hiểu thêm ở đây.

Cơ mà bạn nghĩ xem nhé, nếu class User được sử dụng bời nhiều thread khác nhau thì sao nhỉ?

Thread1+             
       |             
       ++---> +-----+
Thread2 |     |     |
        +---> | User|
              |     |
Thread3 +---> +-----+
        +-------^    
        |            
Thread4 +            

Lúc đó sẽ có những trường hợp nào xảy ra nhỉ :

  • Trường hợp dễ nghĩ đến nhất là khi mà Thread1 chui vào trong block if (cache != null), tiến hành khởi tạo cache. Việc khởi tạo này tốn rất lâu, cứ cho là 10 phút đi, thì khi đó nếu 1 Thread2 gọi đến hàm accessUserBy, nó thấy rằng cache vẫn khác null, nên lại chui vào khởi tạo cache tiếp, vậy tức là cache của bạn được khởi tạo lặp đi lặp lại, giả sử cache tốn 200mb, mà ram của bạn chỉ có 300mb, thì việc khởi tạo 2 lần sẽ dẫn đến chương trình của bạn ngỏm củ tỏi chắc rồi. Nếu có không ngỏm thì với java hay là scala, việc memory overhead sẽ dẫn theo cả CPU overhead vì tác động của GC, nên không sớm thì muộn chương trình của bạn cũng ngỏm.
  • Một trường hợp khác là cache chưa được khởi tạo hoàn toàn xong, nhưng bạn xử lý để khi vừa chui vào đoạn code initialize thì cache sẽ được gán khác null, khi đó thread2 sẽ bị sử dụng phải một object mà không "hoàn chỉnh", sẽ dễ dẫn đến các bug khó tìm.

Vậy phải xử lý ra sao nhỉ, nếu bạn là một lập trình viên đã từng làm với multi threaded thì sẽ nghĩ rằng: quá dễ, thêm synchronized vào để lock là xong!

object User {
    lazy val cache = new ConcurrentHashMap[Information, Value]();
     def accessUserBy(key: Information): Value = synchornized {
        if (cache == null) {
            initialize(cache); // very very costly!!!
        } else {
            return cache.get(information)
        }
    }
}

Tuy nhiên bạn có biết rằng bản thân việc synchronized cũng là một xử lý rất "tốn kém" hay không? Khi hàm accessUserBy của bạn được gọi đi gọi lại rất nhiều trong code, việc đặt synchrronized ở đây có thể "down" hiệu năng hệ thống của bạn xuống hàng chục, thậm trí hàng trăm lần, vì mất đi sự tối ưu của multithread. Tuy nhiên xem kĩ thêm đoạn code ở trên một chút, bạn sẽ thấy rằng, liệu có cần synchronize "toàn bộ" hàm accessUserBy hay không? Chúng ta chỉ cần đồng bộ việc khởi tạo cache thôi chứ đúng không. Sau đó bạn sửa thành:

object User {
    lazy val cache = new ConcurrentHashMap[Information, Value]();
     def accessUserBy(key: Information): Value =  {
        if (cache == null) {
            synchronized {
                initialize(cache); // very very costly!!!
             }
        } else {
            return cache.get(information)
        }
    }
}

Mọi việc xem ra đã ổn thoả nhỉ!
Cơ mà bạn đã nghĩ đến trường hợp này chưa:

  • Thread1 chui vào block bên trong if (cache == null). Sau khi chui vào nhưng chưa kịp khởi tạo cache rồi nó bị interrupt bởi Thread2.
  • Thread2 thấy cache chưa được khởi tạo, và chưa bằng null, nói chui vào bên trong if (cache == null), và nó đợi lock từ Thread1
  • Thread1 khởi tạo cache và chui ra
  • Thread2 nhận được lock và khởi tạo cache lần thứ 2!!!

Như vậy chúng ta vẫn gặp vấn đề như cũ, vậy phải làm sao? Đến đây chúng ta sẽ có khái niệm Double-Check Locking :

object User {
    lazy val cache = new ConcurrentHashMap[Information, Value]();
     def accessUserBy(key: Information): Value =  {
        if (cache == null) {
            synchronized {
                if (cache == null) {
                    initialize(cache); // very very costly!!!
                }
             }
        } else {
            return cache.get(information)
        }
    }
}

Bạn có thể thấy đoạn code trên "an toàn tuyệt đối" , bởi sau khi nhận được lock thì bất kì thread nào cũng phải check lại xem cache đã được khởi tạo chưa, rồi mới tiến hành khởi tạo.
Việc "check" điều kiện 2 lần như trên gọi là Double-Check Locking.
Tuy nhiên mọi việc vẫn chưa dừng lại ở đây!!!!!!
Mình sẽ viết tiếp ở bài viết sau, mọi người đón đọc nhé :smile:. Bài viết sau sẽ nói về Java Memory Model.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

huydx

118 bài viết.
1045 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
164 15
Introduction (Link) là một cuộc thi ở Nhật, và cũng chỉ có riêng ở Nhật. Đây là một cuộc thi khá đặc trưng bởi sự thú vị của cách thi của nó, những...
huydx viết 2 năm trước
164 15
White
145 14
Một ngày đẹp trời, bạn quyết định viết một dịch vụ web dự định sẽ làm thay đổi cả thế giới. Dịch vụ của bạn sẽ kết nối tất cả các thiết bị di động ...
huydx viết 2 tháng trước
145 14
White
133 15
Happy programmer là gì nhỉ, chắc ai đọc xong title của bài post này cũng không hiểu ý mình định nói đến là gì :D. Đầu tiên với cá nhân mình thì hap...
huydx viết hơn 3 năm trước
133 15
Bài viết liên quan
White
10 0
Kí tự Regex cơ bản Về cơ bản thì các sử lý matching của scala.util.matching.Regex sẽ được "phó thác" (delegate) cho java Regex. Bạn có thể tạo một ...
huydx viết hơn 3 năm trước
10 0
White
7 1
Trong scala kí tự _ được dùng với khá nhiều mục đích .. không liên quan đến nhau. Tạm note lại cái đã khi nào có time sẽ quay lại viết cẩn thận sa...
huydx viết hơn 3 năm trước
7 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
118 bài viết.
1045 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!