[JAVA] 12. Java中的并行Concurrency

启用线程

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等。

构建线程安全的代码:

  1. 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处理,处理后返回内存,在这个过程中,一些变动被复写了,这种操作称为“非原子操作”

线程安全策略

  1. confinement不共享:即对每个线程中都创建一个记录进度的对象,在线程完成之后,汇总进度。

  2. immutability不变:即使用final对象来进行调用,这里只能进行读取。

  3. synchronization同步
    加锁:让线程等待资源

    死锁:两个线程相互等待

  4. atomic objects

  5. partitioning

    利用Collections中支持的partition操作来将资源分给不同的线程。

通过上述5种角度分别来解决并发的问题:

  1. 通过将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();
    }
}
  1. 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++;
        }
    }
}
  1. 用于解决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来实现。

  1. 原子对象
    简言之,就是将一个临界变量声明为一个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>();

猜你喜欢

转载自www.cnblogs.com/modai/p/12799497.html
今日推荐