Java多线程

多线程学习

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

  • 进程和线程
    进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
    和多线程相比,多进程的缺点在于:

    1. 创建进程比创建线程开销大,尤其是在Windows系统上;
    2. 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

  • 线程基础
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

线程状态:

  1. New:新创建的线程,尚未执行;
  2. Runnable:运行中的线程,正在执行run()方法的Java代码;
  3. Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  4. Waiting:运行中的线程,因为某些操作在等待中;
  5. TimedWaiting:运行中的线程,因为执行sleep()方法正在计时等待;
  6. Terminated:线程已终止,因为run()方法执行完毕。

thread.start();//启动新线程
(start()方法会在内部自动调用实例的run()方法)

Thread.sleep();//可以把当前线程暂停一段时间

Thread.setPriority(int n) // 1~10, 默认值5;
设置线程优先级,优先级越高被调度更频繁,但未必会先执行

thread.join(); // 等待线程结束
(通过对另一个线程对象调用join()方法可以等待其执行结束;可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;对已经运行结束的线程调用join()方法会立刻返回。)

thread.interrupt();//中断线程
interrupt()方法仅仅向线程发出了“中断请求”,线程响应的方式:线程的while循环会检测isInterrupted()

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
volatile关键字的目的是告诉虚拟机:
1.每次访问变量时,总是获取主内存的最新值;
2.每次修改变量后,立刻回写到主内存。

thread.setDaemon(true);
守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等)。(这是因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。)

Thread.currentThread()获取当前线程;

ThreadLocal
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal实例通常总是以静态字段初始化;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal要用try … finally结构,并在finally中清除。

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
try {
    threadLocalUser.set(user);
    ...
} finally {
    threadLocalUser.remove();
}

(也可以通过AutoCloseable接口配合try (resource) {…}结构,让编译器自动为我们关闭)

public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}
try (var ctx = new UserContext("Bob")) {
    // 可任意调用UserContext.currentUser():
    String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
  • 线程同步
    多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
    同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
    注意加锁对象必须是同一个实例;
    对JVM定义的单个原子操作不需要同步。
    (原子操作是指不能被中断的一个或一系列操作。
    保证一段代码的原子性就是通过加锁和解锁实现的。)
    如何使用synchronized
    1.找出修改共享变量的线程代码块;
    2.选择一个共享实例作为锁;
    3.synchronized(lockObject) { // 获取锁

    } // 释放锁

JVM规范定义了几种原子操作:
1.基本类型(long和double除外)赋值,例如:int n = m;不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
2.引用类型赋值,例如:List list = anotherList。

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe);
没有特殊说明时,一个类默认是非线程安全的。
用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;

public synchronized void add(int n) { // 锁住this
    count += n;
} // 解锁

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。

public synchronized static void test(int n) {
    ...
}

VM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
Java的synchronized锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。
(死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。)

  • 锁LOCK
  1. synchronized(可重入锁)

在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
必须在已获得的锁对象上调用notify()或notifyAll()方法;(notify()随机唤醒一个,notifyAll()唤醒全部)
已唤醒的线程还需要重新获得锁后才能继续执行。

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

  1. ReentrantLock(可重入锁)

java.util.concurrent.locks包提供的ReentrantLock可以替代synchronized进行同步;
ReentrantLock获取锁更安全;
必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();//加锁
        try {
            count += n;
        } finally {
            lock.unlock(); //释放
        }
    }
}

可以使用tryLock()尝试获取锁。

if (lock.tryLock(1, TimeUnit.SECONDS)) {
//尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false
    try {
        ...
    } finally {
        lock.unlock();
    }
}

Condition对象必须从Lock对象获取;

private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();  

await()会释放当前锁,进入等待状态;
signal()会唤醒某个等待线程;
signalAll()会唤醒所有等待线程;
唤醒线程从await()返回后需要重新获得锁。

 if (condition.await(1, TimeUnit.SECOND)) {
    // 被其他线程唤醒
} else {
    // 指定时间内没有被其他线程唤醒
}
  1. ReadWriteLock

使用ReadWriteLock可以提高读取效率:

  • ReadWriteLock只允许一个线程写入;
  • ReadWriteLock允许多个线程在没有写入时同时读取;
  • ReadWriteLock适合读多写少的场景。
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
  1. StampedLock(不可重入锁)

Java 8引入StampedLock,它提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;

private final StampedLock stampedLock = new StampedLock();                                                                public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        double currentX = x;
        double currentY = y;
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
  • Concurrent(并发)集合(线程安全)

使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码

interfacenon-thread-safethread-safe
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque

使用这些并发集合与使用非线程安全的集合类完全相同。

Map<String, String> map = new ConcurrentHashMap<>();

java.util.Collections工具类还提供了一个旧的线程安全集合转换器,可以这么用:(这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用

Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
  • Atomic(原子操作的封装类)

使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:
原子操作实现了无锁的线程安全;
适用于计数器,累加器等。

Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。

以AtomicInteger为例,它提供的主要操作有:

  1. 增加值并返回新值:int addAndGet(int delta)
  2. 加1后返回新值:int incrementAndGet()
  3. 获取当前值:int get()
  4. 用CAS方式设置:int compareAndSet(int expect, int update)

CAS编写incrementAndGet(),它大概长这样:

public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();
        next = prev + 1;
    } while ( ! var.compareAndSet(prev, next));
    return next;
}
CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。
  • 线程池

JDK提供了ExecutorService实现了线程池功能:
线程池内部维护一组线程,可以高效执行大量小任务;
Executors提供了静态方法创建不同类型的ExecutorService;
必须调用shutdown()关闭ExecutorService;

ExecutorService只是接口,Java标准库提供的几个常用实现类有

  • FixedThreadPool:线程数固定的线程池;
ExecutorService executor = Executors.newFixedThreadPool(3);
  • CachedThreadPool:线程数根据任务动态调整的线程池;
ExecutorService executor =Executors.newCachedThreadPool();
//线程池的大小限制在4~10个之间动态调整
ExecutorService es = new ThreadPoolExecutor(4, `10,
        60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
  • SingleThreadExecutor:仅单线程执行的线程池
  • ScheduledThreadPool:定期反复执行
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);                                                             
// 2秒后开始执行定时任务,每3秒执行(不管上一个任务是否结束):
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);                                                            
// 2秒后开始执行定时任务,以3秒为间隔执行(上一个任务执行完毕后隔3s):
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer。

CountDownLatch

public void Main(){
	ExecutorService es = Executors.newFixedThreadPool(threadNum);
	CountDownLatch latch = new CountDownLatch(threadNum);
	for (int i = 0 ; i < threadNum ; i++) {
		AuditConfirmationThread thread = new AuditConfirmationThread(latch,....);
		es.execute(thread);
	}
	try{
	   // 等线程全部完成,再继续执行
	   latch.await();
	   // 关闭线程池
	   es.shutdown();
	} catch (InterruptedException e){
	   throw new RuntimeException(e);
	}
}
public class AuditConfirmationThread implements Runnable {
	public AuditConfirmationThread(CountDownLatch threadCountDown) {
	        this.threadCountDown = threadCountDown;
	}
	@Override
	    public void run() {
	        // 先执行处理逻辑后,自减1
	        threadCountDown.countDown();
	    }
}
  • Future(任务返回结果对象)

     Runnable接口无返回值,Callable接口有返回值。
     executor.execute()执行线程无返回值;executor.submit()执行线程有返回值;
    
ExecutorService executor = Executors.newFixedThreadPool(4); 
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞        

一个Future接口表示一个未来可能会返回的结果,它定义的方法有:

  • get():获取结果(可能会等待)
  • get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;
  • cancel(boolean mayInterruptIfRunning):取消当前任务;
  • isDone():判断任务是否已完成。

CompletableFuture

java8引入。
CompletableFuture的命名规则:
xxx():表示该方法将继续在已有的线程中执行;
xxxAsync():表示将异步在线程池中执行。

public static void main(String[] args) throws Exception {
        // 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
        // 如果执行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 如果执行异常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(200);
    }

CompletableFuture可以指定异步处理流程:

  • thenAccept()处理正常结果;
  • exceptional()处理异常结果;
  • thenApplyAsync()用于串行化另一个CompletableFuture;
  • anyOf()和allOf()用于并行化多个CompletableFuture。
    (anyOf()可以实现“任意个CompletableFuture只要一个成功”,allOf()可以实现“所有CompletableFuture都必须成功)
public class Main {
    public static void main(String[] args) throws Exception {
        // 两个CompletableFuture执行异步查询:
        CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://finance.sina.com.cn/code/");
        });
        CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://money.163.com/code/");
        });

        // 用anyOf合并为一个新的CompletableFuture:
        CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
    }
}
  • Fork/Join

java7引入。
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
使用Fork/Join模式可以进行并行计算以提高效率。

class SumTask extends RecursiveTask<Long> {
    protected Long compute() {
        // “分裂”子任务:
        SumTask subtask1 = new SumTask(...);
        SumTask subtask2 = new SumTask(...);
        // invokeAll会并行运行两个子任务:
        invokeAll(subtask1, subtask2);
        // 获得子任务的结果:
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        // 汇总结果:
        return subresult1 + subresult2;
    }
}
Java标准库提供的java.util.Arrays.parallelSort(array)可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值