启用线程
public static void show() {
System.out.println(Thread.currentThread().getName());
for (var i : IntStream.range(0, 10).toArray()) {
var thread = new Thread(() -> System.out.println("download.." + Thread.currentThread().getName()));
thread.start();
}
}
与此同时还有对线程的暂停sleep,中止interrupt,等待其他线程join等。
构建线程安全的代码:
- Race Condition
两个线程抢占公共变量。
ThreadDemo 开4个线程下载资源,观察得到的最后的总字节数。
public class ThreadDemo {
public static void show() {
System.out.println(Thread.currentThread().getName());
var status = new DownloadStatus();
var threads = new ArrayList<Thread>();
for (var i : IntStream.range(0, 10).toArray()) {
var thread = new Thread(new DownloadingThread(status));
thread.start();
// try {
// thread.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
threads.add(thread);
}
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(status.getTotalBytes());
System.out.println();
}
}
DownloadStatus 保存下载状态
public class DownloadStatus {
private int totalBytes;
public int getTotalBytes() {
return totalBytes;
}
public void increaseTotalBytes(){
totalBytes++;
}
}
DownloadingThread 下载线程,下载10000个byte
import java.util.stream.IntStream;
public class DownloadingThread implements Runnable {
private DownloadStatus status;
public DownloadingThread(DownloadStatus status) {
this.status = status;
}
@Override
public void run() {
System.out.println("download.." + Thread.currentThread().getName());
IntStream.range(0, 10_000).forEach(
(i) -> status.increaseTotalBytes()
);
}
}
发现得到的并不是预期的40_000,这就是race condition的锅,在totalBytes更新的时候,首先需要读取到内存,之后内存到CPU处理,处理后返回内存,在这个过程中,一些变动被复写了,这种操作称为“非原子操作”
线程安全策略
-
confinement不共享:即对每个线程中都创建一个记录进度的对象,在线程完成之后,汇总进度。
-
immutability不变:即使用final对象来进行调用,这里只能进行读取。
-
synchronization同步
加锁:让线程等待资源
死锁:两个线程相互等待
-
atomic objects
-
partitioning
利用Collections中支持的partition操作来将资源分给不同的线程。
通过上述5种角度分别来解决并发的问题:
- 通过将status对象创建放在每个线程内,
ThreadDemo.java
public class ThreadDemo {
public static void show() {
System.out.println(Thread.currentThread().getName());
// var status = new DownloadStatus();
var threads = new ArrayList<Thread>();
var status = new ArrayList<DownloadStatus>();
for (var i : IntStream.range(0, 10).toArray()) {
var statusOfSingleTask = new DownloadStatus();
status.add(statusOfSingleTask);
var thread = new Thread(new DownloadingThread(statusOfSingleTask));
thread.start();
threads.add(thread);
}
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
int totalByte = status.stream()
.map(DownloadStatus::getTotalBytes)
.reduce(Integer::sum).orElse(0);
System.out.println(totalByte);
System.out.println();
}
}
- locks锁
2.1 修改DownloadStatus,其他保持原样
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DownloadStatus {
private int totalBytes;
private Lock lock = new ReentrantLock();
public int getTotalBytes() {
return totalBytes;
}
public void increaseTotalBytes() {
lock.lock();
try {
totalBytes++;
} finally {
lock.unlock();
}
}
}
2.2 synchronized关键字
有三种使用方法:
1. 将操作临界资源的实例方法声明为同步方法,这时默认锁为实例
2. 将操作临界资源的静态方法声明为同步方法,这时默认锁为类
3. 采用synchronized代码块,通过synchronized(obj){操作临界资源},这时锁为obj
当一个类内有多个需要加锁的操作时,最好使用第三种,并赋予不同的obj作为锁,这样可以提升效率,并避免异常的出现。
public class DownloadStatus {
private int totalBytes;
private Object totalBytesLock = new Object();
public int getTotalBytes() {
return totalBytes;
}
public void increaseTotalBytes() {
synchronized (totalBytesLock) {
totalBytes++;
}
}
}
- 用于解决visibility problem的volatile关键字
当一个内存中的对象存入经由两个线程存入CPU的cache中时,虽然一个更改了之前内存中的对象,但是另一个并不知道这个变动,依然使用自己的cache。
volatile关键字声明的字段会告诉JVM,这个字段是不稳定的unstable,读取的时候不能看cache,要去内存里读,这样另一个线程就可以监控到内存中的变化。
这时,如果想要在一个线程任务结束时,由另一个线程发出结束通知,可以将代码更改为:
DownloadStatus.java
public class DownloadStatus {
private int totalBytes;
private Object totalBytesLock = new Object();
private volatile boolean isDone=false; //新增
public int getTotalBytes() {
return totalBytes;
}
public void increaseTotalBytes() {
synchronized (totalBytesLock) {
totalBytes++;
}
}
public boolean isDone() {
return isDone;
}
public void Done() {
isDone = true;
}
}
DownloadingThread.java
public class DownloadingThread implements Runnable {
private DownloadStatus status;
public DownloadingThread(DownloadStatus status) {
this.status = status;
}
@Override
public void run() {
System.out.println("download.." + Thread.currentThread().getName());
IntStream.range(0, 10_000).forEach(
(i) -> status.increaseTotalBytes()
);
status.Done();//新增
}
public DownloadStatus getStatus() {
return status;
}
}
ThreadDemo.java
public class ThreadDemo {
public static void show() {
var status = new DownloadStatus();
Thread thread1 = new Thread(new DownloadingThread(status));
var thread2 = new Thread(() -> {
while (true) if (status.isDone()) break;
System.out.println("下载完成。");
});
thread1.start();
thread2.start();
}
}
由于我们不想第二个线程一直问询,希望isDone变量变更时,再做操作,这时可以通过wait和notify来实现。
- 原子对象
简言之,就是将一个临界变量声明为一个Atomic对象,并通过该对象,进行变量的增减,读取等操作。
import java.util.concurrent.atomic.AtomicInteger;
public class DownloadStatus {
private AtomicInteger totalBytes = new AtomicInteger();
public void increaseTotalBytes() {
totalBytes.incrementAndGet();
}
public int getTotalBytes() {
return totalBytes.get();
}
}
synchronized collections
对于一个collection对象进行多线程操作,通过调用synchronized*进行该对象的创建。
public class ThreadDemo {
public static void show() {
var integers = Collections.synchronizedList(new ArrayList<Integer>());
var thread1 = new Thread(() -> {
integers.addAll(List.of(1, 2, 3));
});
var thread2 = new Thread(() -> {
integers.addAll(List.of(4, 5, 6));
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(integers);
}
}
concurent collection
允许在不阻塞线程的情况下,几个线程同时修改map,多用于 插入数据,少量读取的场景
其他代码和上面一致,只是变量声明处变更了。
var integers = new ConcurrentSkipListSet<Integer>();