Không thể tạo một nhóm luồng được lưu trữ với giới hạn kích thước?


127

Dường như không thể tạo nhóm luồng được lưu trong bộ nhớ cache với giới hạn số lượng luồng mà nó có thể tạo.

Đây là cách Executors.newCachedThreadPool được triển khai trong thư viện Java tiêu chuẩn:

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Vì vậy, sử dụng mẫu đó để tiếp tục tạo nhóm luồng được lưu trong bộ nhớ cache có kích thước cố định:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

Bây giờ nếu bạn sử dụng điều này và gửi 3 nhiệm vụ, mọi thứ sẽ ổn. Đệ trình bất kỳ nhiệm vụ hơn nữa sẽ dẫn đến ngoại lệ thực hiện bị từ chối.

Đang thử điều này:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

Sẽ dẫn đến tất cả các chủ đề thực hiện tuần tự. Tức là, nhóm luồng sẽ không bao giờ tạo nhiều hơn một luồng để xử lý các tác vụ của bạn.

Đây là một lỗi trong phương thức thực thi của ThreadPoolExecutor? Hoặc có thể đây là cố ý? Hoặc có một số cách khác?

Chỉnh sửa: Tôi muốn một cái gì đó chính xác như nhóm luồng được lưu trữ (nó tạo ra các luồng theo yêu cầu và sau đó giết chúng sau một khoảng thời gian chờ) nhưng với giới hạn về số lượng luồng mà nó có thể tạo và khả năng tiếp tục xếp hàng các tác vụ bổ sung sau khi có đạt giới hạn chủ đề của nó. Theo phản hồi của sjlee, điều này là không thể. Nhìn vào phương thức exec () của ThreadPoolExecutor thì thực sự không thể. Tôi sẽ cần phải phân lớp ThreadPoolExecutor và ghi đè thực thi () giống như SwingWorker, nhưng những gì SwingWorker thực hiện trong thực thi của nó () là một bản hack hoàn chỉnh.


1
Câu hỏi của bạn là gì? Không phải ví dụ mã thứ 2 của bạn là câu trả lời cho tiêu đề của bạn?
rsp

4
Tôi muốn một nhóm luồng sẽ thêm các luồng theo yêu cầu khi số lượng tác vụ tăng lên, nhưng sẽ không bao giờ thêm nhiều hơn số lượng luồng tối đa. CacheedThreadPool đã làm điều này, ngoại trừ nó sẽ thêm số lượng chủ đề không giới hạn và không dừng lại ở một số kích thước được xác định trước. Kích thước tôi xác định trong các ví dụ là 3. Ví dụ thứ hai thêm 1 luồng, nhưng không thêm hai luồng nữa khi các tác vụ mới đến trong khi các tác vụ khác chưa hoàn thành.
Matt Crinklaw-Vogt

Kiểm tra cái này, nó giải quyết nó, debuggingisfun.blogspot.com/2012/05/ từ
ethan

Câu trả lời:


235

ThreadPoolExecutor có một số hành vi chính sau đây và các vấn đề của bạn có thể được giải thích bằng các hành vi này.

Khi nhiệm vụ được gửi,

  1. Nếu nhóm luồng chưa đạt kích thước lõi, nó sẽ tạo ra các luồng mới.
  2. Nếu kích thước lõi đã đạt được và không có chủ đề nhàn rỗi, nó sẽ xếp hàng các tác vụ.
  3. Nếu kích thước lõi đã đạt được, không có luồng nào nhàn rỗi và hàng đợi trở nên đầy đủ, nó sẽ tạo ra các luồng mới (cho đến khi đạt kích thước tối đa).
  4. Nếu kích thước tối đa đã đạt được, không có luồng nào nhàn rỗi và hàng đợi trở nên đầy đủ, chính sách từ chối sẽ được áp dụng.

Trong ví dụ đầu tiên, lưu ý rằng SynousQueue có kích thước cơ bản là 0. Do đó, thời điểm bạn đạt kích thước tối đa (3), chính sách từ chối sẽ bắt đầu (# 4).

Trong ví dụ thứ hai, hàng đợi lựa chọn là LinkedBlockingQueue có kích thước không giới hạn. Do đó, bạn bị mắc kẹt với hành vi # 2.

Bạn không thể thực sự tin tưởng nhiều với loại được lưu trong bộ nhớ cache hoặc loại cố định, vì hành vi của chúng gần như hoàn toàn được xác định.

Nếu bạn muốn có một nhóm luồng động và giới hạn, bạn cần sử dụng kích thước lõi dương và kích thước tối đa kết hợp với hàng đợi có kích thước hữu hạn. Ví dụ,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

Phụ lục : đây là một câu trả lời khá cũ và có vẻ như JDK đã thay đổi hành vi của mình khi có kích thước lõi là 0. Kể từ JDK 1.6, nếu kích thước lõi là 0 và nhóm không có bất kỳ luồng nào, ThreadPoolExecutor sẽ thêm luồng để thực hiện nhiệm vụ đó. Do đó, kích thước lõi của 0 là một ngoại lệ cho quy tắc trên. Cảm ơn Steve cho mang đến sự chú ý của tôi.


4
Bạn phải viết một vài từ về phương pháp allowCoreThreadTimeOutđể làm cho câu trả lời này hoàn hảo. Xem câu trả lời của @ user1046052
hsestupin 18/03/13

1
Câu trả lời chính xác! Chỉ cần một điểm để thêm: Các chính sách từ chối khác cũng đáng được đề cập. Xem câu trả lời của @brianegge
Jeff

1
Không nên hành vi 2 nói 'Nếu kích thước maxThread đã đạt được và không có chủ đề nhàn rỗi, nó sẽ xếp hàng các tác vụ.' ?
Zoltán

1
Bạn có thể giải thích về kích thước của hàng đợi ngụ ý gì không? Có nghĩa là chỉ có 20 nhiệm vụ có thể được xếp hàng trước khi chúng bị từ chối?
Zoltán

1
@ Zoltán Tôi đã viết điều này cách đây một thời gian, vì vậy có khả năng một số hành vi có thể đã thay đổi kể từ đó (tôi đã không theo dõi các hoạt động mới nhất quá chặt chẽ), nhưng giả sử những hành vi này không thay đổi, # 2 là đúng như đã nêu, và đó là có lẽ là điểm quan trọng nhất (và hơi ngạc nhiên) của điều này. Khi đạt được kích thước lõi, TPE ưu tiên xếp hàng hơn là tạo chủ đề mới. Kích thước hàng đợi đúng nghĩa là kích thước của hàng đợi được chuyển đến TPE. Nếu hàng đợi đầy nhưng không đạt kích thước tối đa, nó sẽ tạo ra một luồng mới (không từ chối các tác vụ). Xem # 3. Mong rằng sẽ giúp.
sjlee

60

Trừ khi tôi đã bỏ lỡ điều gì đó, giải pháp cho câu hỏi ban đầu rất đơn giản. Đoạn mã sau thực hiện hành vi mong muốn như được mô tả bởi người đăng ban đầu. Nó sẽ sinh ra tối đa 5 luồng để hoạt động trên hàng đợi không giới hạn và các luồng không hoạt động sẽ chấm dứt sau 60 giây.

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);

1
Bạn nói đúng. Phương pháp đó đã được thêm vào trong jdk 1.6, vì vậy không nhiều người biết về nó. Ngoài ra, bạn không thể có kích thước nhóm lõi "tối thiểu", điều này thật đáng tiếc.
jtahlborn

4
Điều lo lắng duy nhất của tôi là (từ các tài liệu JDK 8): "Khi một tác vụ mới được gửi trong thực thi phương thức (Runnable) và ít hơn các luồng corePoolSize đang chạy, một luồng mới được tạo để xử lý yêu cầu, ngay cả khi nhân viên khác chủ đề không hoạt động. "
veegee

Khá chắc chắn rằng điều này không thực sự hoạt động. Lần trước tôi đã thực hiện các thao tác trên thực tế chỉ thực hiện công việc của bạn trong một luồng ngay cả khi bạn sinh ra 5. Một lần nữa, sau vài năm, nhưng khi tôi bắt đầu triển khai ThreadPoolExecutor, nó chỉ được gửi đến các luồng mới khi hàng đợi của bạn đã đầy. Sử dụng một hàng đợi không giới hạn khiến điều này không bao giờ xảy ra. Bạn có thể kiểm tra bằng cách gửi công việc và đăng nhập tên chủ đề sau đó ngủ. Mỗi runnable sẽ kết thúc in cùng tên / không được chạy trên bất kỳ chủ đề khác.
Matt Crinklaw-Vogt

2
Điều này không hoạt động, Matt. Bạn đặt kích thước lõi thành 0, đó là lý do tại sao bạn chỉ có 1 luồng. Mẹo ở đây là đặt kích thước lõi thành kích thước tối đa.
T-Gergely

1
@vegee đã đúng - Điều này thực sự không hoạt động tốt - ThreadPoolExecutor sẽ chỉ sử dụng lại các luồng khi ở trên corePoolSize. Vì vậy, khi corePoolSize bằng với maxPoolSize, bạn sẽ chỉ được hưởng lợi từ bộ đệm ẩn luồng khi nhóm của bạn đầy (Vì vậy, nếu bạn có ý định sử dụng cái này nhưng thường ở dưới kích thước nhóm tối đa của mình, bạn cũng có thể giảm thời gian chờ của luồng xuống mức thấp giá trị và lưu ý rằng không có bộ nhớ đệm - luôn là các luồng mới)
Chris Riddell

7

Có vấn đề tương tự. Vì không có câu trả lời nào khác đặt tất cả các vấn đề lại với nhau, tôi thêm tôi:

Bây giờ nó được viết rõ ràng trong tài liệu : Nếu bạn sử dụng hàng đợi không chặn ( LinkedBlockingQueue) cài đặt chủ đề tối đa không có hiệu lực, chỉ có các chủ đề cốt lõi được sử dụng.

vì thế:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

Chấp hành viên này có:

  1. Không có khái niệm về các chủ đề tối đa vì chúng tôi đang sử dụng một hàng đợi không giới hạn. Đây là một điều tốt vì hàng đợi như vậy có thể khiến người thi hành tạo ra số lượng lớn các luồng không cốt lõi, bổ sung nếu tuân theo chính sách thông thường của nó.

  2. Một hàng đợi có kích thước tối đa Integer.MAX_VALUE. Submit()sẽ ném RejectedExecutionExceptionnếu vượt quá số lượng nhiệm vụ đang chờ xử lý Integer.MAX_VALUE. Không chắc chắn chúng ta sẽ hết bộ nhớ trước hoặc điều này sẽ xảy ra.

  3. Có 4 chủ đề cốt lõi có thể. Chủ đề lõi nhàn rỗi tự động thoát nếu không hoạt động trong 5 giây. Vì vậy, có, đúng theo chủ đề nhu cầu. Có thể thay đổi số liệu bằng setThreads()phương pháp.

  4. Đảm bảo số lượng chủ đề cốt lõi tối thiểu không bao giờ ít hơn một, nếu không submit()sẽ từ chối mọi tác vụ. Vì các luồng lõi cần phải> = các luồng tối đa, nên phương thức cũng setThreads()đặt các luồng tối đa, mặc dù cài đặt luồng tối đa là vô ích đối với hàng đợi không giới hạn.


Tôi nghĩ bạn cũng cần đặt 'allowCoreThreadTimeOut' thành 'true', nếu không, một khi các chủ đề được tạo, bạn sẽ giữ chúng mãi mãi: gist.github.com/ericdcobb/46b817b384f5ca9d5f5d
eric

Rất tiếc, tôi chỉ bỏ lỡ điều đó, xin lỗi, câu trả lời của bạn là hoàn hảo sau đó!
eric

6

Trong ví dụ đầu tiên của bạn, các tác vụ tiếp theo bị từ chối vì đây AbortPolicylà mặc định RejectedExecutionHandler. ThreadPoolExecutor chứa các chính sách sau mà bạn có thể thay đổi thông qua setRejectedExecutionHandlerphương thức:

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

Có vẻ như bạn muốn nhóm chủ đề được lưu trong bộ nhớ cache với CallerRunPolicy.


5

Không có câu trả lời nào ở đây khắc phục được sự cố của tôi, điều phải làm với việc tạo một số lượng kết nối HTTP hạn chế bằng ứng dụng khách HTTP của Apache (phiên bản 3.x). Vì tôi mất vài giờ để tìm ra một thiết lập tốt, tôi sẽ chia sẻ:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

Điều này tạo ra một ThreadPoolExecutorbắt đầu bằng năm và giữ tối đa mười luồng chạy đồng thời bằng cách sử dụng CallerRunsPolicyđể thực thi.


Vấn đề với giải pháp này là nếu bạn tăng số lượng hoặc nhà sản xuất, bạn sẽ tăng số lượng chủ đề chạy các chủ đề nền. Trong nhiều trường hợp đó không phải là điều bạn muốn.
Xám

3

Theo Javadoc cho ThreadPoolExecutor:

Nếu có nhiều hơn corePoolSize nhưng ít hơn các luồng tối đaPoolSize đang chạy, một luồng mới sẽ chỉ được tạo nếu hàng đợi đầy . Bằng cách đặt corePoolSize và MaximumPoolSize giống nhau, bạn tạo một nhóm luồng có kích thước cố định.

(Nhấn mạnh của tôi.)

Câu trả lời của jitter là những gì bạn muốn, mặc dù tôi trả lời câu hỏi khác của bạn. :)


2

có thêm một lựa chọn Thay vì sử dụng SynousQueue mới, bạn cũng có thể sử dụng bất kỳ hàng đợi nào khác, nhưng bạn phải đảm bảo kích thước của nó là 1, do đó sẽ buộc dịch vụ thực thi tạo luồng mới.


Tôi nghĩ bạn có nghĩa là kích thước 0 (theo mặc định), do đó sẽ không có nhiệm vụ được xếp hàng và thực sự buộc dịch vụ thực thi phải tạo luồng mới mỗi lần.
Leonmax

2

Không có vẻ như bất kỳ câu trả lời nào thực sự trả lời câu hỏi - thực tế tôi không thể thấy cách làm này - ngay cả khi bạn phân lớp từ PooledExecutorService vì nhiều phương thức / thuộc tính là riêng tư, ví dụ như làm cho add IfUnderMaximumPoolSize được bảo vệ làm như sau:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

Gần nhất tôi nhận được là thế này - nhưng ngay cả đó không phải là một giải pháp rất tốt

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

ps không kiểm tra ở trên


2

Đây là một giải pháp khác. Tôi nghĩ giải pháp này hoạt động như bạn muốn (mặc dù không tự hào về giải pháp này):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);

2

Đây là những gì bạn muốn (ít nhất tôi đoán vậy). Đối với một lời giải thích kiểm tra Jonathan Feinberg câu trả lời

Executors.newFixedThreadPool(int n)

Tạo nhóm luồng sử dụng lại một số luồng cố định hoạt động ngoài hàng đợi không giới hạn được chia sẻ. Tại bất kỳ thời điểm nào, hầu hết các luồng nThreads sẽ là các tác vụ xử lý đang hoạt động. Nếu các tác vụ bổ sung được gửi khi tất cả các luồng đang hoạt động, chúng sẽ đợi trong hàng đợi cho đến khi một luồng có sẵn. Nếu bất kỳ luồng nào kết thúc do lỗi trong khi thực hiện trước khi tắt máy, một luồng mới sẽ thay thế nó nếu cần để thực hiện các tác vụ tiếp theo. Các chủ đề trong nhóm sẽ tồn tại cho đến khi nó được tắt một cách rõ ràng.


4
Chắc chắn, tôi có thể sử dụng một nhóm luồng cố định nhưng điều đó sẽ để lại n luồng xung quanh mãi mãi hoặc cho đến khi tôi gọi tắt máy. Tôi muốn một cái gì đó chính xác như nhóm luồng được lưu trữ (nó tạo ra các luồng theo yêu cầu và sau đó giết chúng sau một khoảng thời gian chờ) nhưng với giới hạn về số lượng luồng mà nó có thể tạo.
Matt Crinklaw-Vogt

0
  1. Bạn có thể sử dụng ThreadPoolExecutortheo đề xuất của @sjlee

    Bạn có thể kiểm soát kích thước của hồ bơi một cách linh hoạt. Hãy xem câu hỏi này để biết thêm chi tiết:

    Hồ bơi chủ đề năng động

    HOẶC LÀ

  2. Bạn có thể sử dụng API newWorkStealsPool , đã được giới thiệu với java 8.

    public static ExecutorService newWorkStealingPool()

    Tạo nhóm luồng đánh cắp công việc bằng cách sử dụng tất cả các bộ xử lý có sẵn làm mức độ song song đích của nó.

Theo mặc định, mức độ song song được đặt thành số lõi CPU trong máy chủ của bạn. Nếu bạn có máy chủ CPU 4 lõi, kích thước nhóm luồng sẽ là 4. API này trả về ForkJoinPoolloại ExecutorService và cho phép công việc đánh cắp các luồng nhàn rỗi bằng cách đánh cắp các tác vụ từ các luồng bận trong ForkJoinPool.


0

Vấn đề đã được tóm tắt như sau:

Tôi muốn một cái gì đó chính xác như nhóm luồng được lưu trong bộ nhớ cache (nó tạo ra các luồng theo yêu cầu và sau đó giết chúng sau khi hết thời gian) nhưng với giới hạn về số lượng luồng mà nó có thể tạo và khả năng tiếp tục xếp hàng các tác vụ bổ sung sau khi nó đạt được giới hạn chủ đề.

Trước khi chỉ vào giải pháp tôi sẽ giải thích tại sao các giải pháp sau không hoạt động:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

Điều này sẽ không xếp hàng bất kỳ tác vụ nào khi đạt đến giới hạn 3 vì SynousQueue, theo định nghĩa, không thể chứa bất kỳ yếu tố nào.

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

Điều này sẽ không tạo nhiều hơn một luồng vì ThreadPoolExecutor chỉ tạo các luồng vượt quá corePoolSize nếu hàng đợi đầy. Nhưng LinkedBlockingQueue không bao giờ đầy đủ.

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

Điều này sẽ không sử dụng lại các luồng cho đến khi đạt được corePoolSize vì ThreadPoolExecutor tăng số lượng luồng cho đến khi đạt được corePoolSize ngay cả khi các luồng hiện tại không hoạt động. Nếu bạn có thể sống với nhược điểm này thì đây là giải pháp dễ nhất cho vấn đề. Nó cũng là giải pháp được mô tả trong "Java đồng thời trong thực tiễn" (chú thích trên trang 17).

Giải pháp hoàn chỉnh duy nhất cho vấn đề được mô tả dường như là giải pháp liên quan đến việc ghi đè offerphương thức của hàng đợi và viết RejectedExecutionHandlernhư được giải thích trong câu trả lời cho câu hỏi này: Làm cách nào để ThreadPoolExecutor tăng chủ đề lên tối đa trước khi xếp hàng?


0

Điều này hoạt động cho Java8 + (và khác, hiện tại ..)

     Executor executor = new ThreadPoolExecutor(3, 3, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()){{allowCoreThreadTimeOut(true);}};

trong đó 3 là giới hạn số lượng chủ đề và 5 là thời gian chờ cho các chủ đề nhàn rỗi.

Nếu bạn muốn kiểm tra xem nó có hoạt động không , đây là mã để thực hiện công việc:

public static void main(String[] args) throws InterruptedException {
    final int DESIRED_NUMBER_OF_THREADS=3; // limit of number of Threads for the task at a time
    final int DESIRED_THREAD_IDLE_DEATH_TIMEOUT=5; //any idle Thread ends if it remains idle for X seconds

    System.out.println( java.lang.Thread.activeCount() + " threads");
    Executor executor = new ThreadPoolExecutor(DESIRED_NUMBER_OF_THREADS, DESIRED_NUMBER_OF_THREADS, DESIRED_THREAD_IDLE_DEATH_TIMEOUT, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()) {{allowCoreThreadTimeOut(true);}};

    System.out.println(java.lang.Thread.activeCount() + " threads");

    for (int i = 0; i < 5; i++) {
        final int fi = i;
        executor.execute(() -> waitsout("starting hard thread computation " + fi, "hard thread computation done " + fi,2000));
    }
    System.out.println("If this is UP, it works");

    while (true) {
        System.out.println(
                java.lang.Thread.activeCount() + " threads");
        Thread.sleep(700);
    }

}

static void waitsout(String pre, String post, int timeout) {
    try {
        System.out.println(pre);
        Thread.sleep(timeout);
        System.out.println(post);
    } catch (Exception e) {
    }
}

đầu ra của mã ở trên đối với tôi là

1 threads
1 threads
If this is UP, it works
starting hard thread computation 0
4 threads
starting hard thread computation 2
starting hard thread computation 1
4 threads
4 threads
hard thread computation done 2
hard thread computation done 0
hard thread computation done 1
starting hard thread computation 3
starting hard thread computation 4
4 threads
4 threads
4 threads
hard thread computation done 3
hard thread computation done 4
4 threads
4 threads
4 threads
4 threads
3 threads
3 threads
3 threads
1 threads
1 threads
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.