Double lock checking trong những ngôn ngữ lập trình song song
Java
77
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

116 bài viết.
943 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
148 14
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 gần 2 năm trước
148 14
White
118 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
118 15
White
95 10
(Ảnh) Mở đầu Chắc nhiều bạn đã nghe đến khái niệm oauth. Về cơ bản thì oauth là một phương thức chứng thực, mà nhờ đó một web service hay một ap...
huydx viết 3 năm trước
95 10
Bài viết liên quan
White
0 0
Trong bài viết này, một số hình ảnh hoặc nọi dung có thể bị thiếu do quá trình chế bản. Vui lòng xem nội dung ở blog gốc sau: (Link) (Link), chúng...
programmerit viết gần 3 năm trước
0 0
Male avatar
9 5
Facade Design Patern Facade Patern thuộc vào họ mô hình cấu trúc (structural patern). Facade patern phát biểu rằng : "just provide a unified an...
DuongVanTien viết gần 2 năm trước
9 5
{{like_count}}

kipalog

{{ comment_count }}

bình luận

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


White
{{userFollowed ? 'Following' : 'Follow'}}
116 bài viết.
943 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á!