• 0

  • 481

Java多线程归纳总结

黑猫

我不是黑客

2星期前

Java多线程

线程和进程

线程(Thread)是程序运行的执行单元,依托于进程存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的 切换更加节省资源、更加轻量化,因而也被称为轻量级的进程。
进程(Processes)是程序的一次动态执行,是系统进行资源分配和调度的基本单位,是操作系统运行的基础,通常每一个进程都拥有自己独立的内存空间 和系统资源。简单来说,进程可以被当做是一个正在运行的程序。

进程和线程的区别

  • 进程间是独立的,不能共享内存空间和上下文,而线程可以;
  • 进程是程序的一次执行,线程是进程中执行的一段程序片段;
  • 线程占用的资源比进程少。

Java线程的使用

线程的基本状态有:new, runnable, blocked, waiting, time_waiting, terminated
而进程的基本状态有:new, ready, running, blocked, terminated

Java线程的创建

Java有两种创建方式

  1. 直接继承Thread类,重写run方法
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello World");
    }
}
复制代码
  1. 实现Runnable接口,实现run方法
public class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello World");
    }
}
复制代码

当然也可以使用匿名内部类的写法或者lambda表达式的写法

启动线程

调用线程实例的start方法

public class Main() {
    public static void main(String[] args) {
        // 第一种
        Thread myThread = new MyThread();
        myThread.start();
        // 第二种
        new Thread(new Runnable() {
            @Override public void run() {
                //TODO 
            }
        }).start();
    }
}
复制代码

获取线程的结果

上面的几种方式无法获取线程的结果,但是我们可以通过实现Callable接口来实现。

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("Callable");
        return "success";
    }
}
复制代码

使用

public class Main() {
    public static void main(String[] args){
        MyCallable myCallable =  new MyCallable();
        FutureTask<String> result = new FutureTask<String>(myCallable);
        new Thread(result).start();
        System.out.println(result.get());
    }
}
复制代码

线程的高级用法

线程等待

wait()来自Object

public class Main() {
    public static void main(String[] args){
      System.out.println(LocalDateTime.now());
      Object lock = new Object();
      Thread thread = new Thread(() -> {
          synchronized (lock){
              try {
                  // 1 秒钟之后自动唤醒
                  lock.wait(1000);
                  System.out.println(LocalDateTime.now());
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      });
      thread.start();
    }
}
复制代码

线程唤醒

notify()notifyAll()来自Object

线程休眠

sleep() 这个方法是属于线程对象的

等待线程执行完成

thread.join()

设置线程优先级

thread.setPriority(10)

线程中断

thread.interrupt()

交出CPU使用权

yield()

线程池的使用

使用线程池的好处:

  • 可重复使用已有线程,避免对象创建、消亡和过度切换的性能开销。
  • 避免创建大量同类线程所导致的资源过度竞争和内存溢出的问题。
  • 支持更多功能,比如延迟任务线程池(newScheduledThreadPool)和缓存线程池(newCachedThreadPool)等。

ThreadPoolExecutor

ThreadPoolExecutor的构造函数

public ThreadPoolExecutor(
//核心线程数,除非allowCoreThreadTimeOut被设置为true,否则它闲着也不会死
int corePoolSize, 
//最大线程数,活动线程数量超过它,后续任务就会排队                   
int maximumPoolSize, 
//超时时长,作用于非核心线程(allowCoreThreadTimeOut被设置为true时也会同时作用于核心线程),闲置超时便被回收           
long keepAliveTime,                          
//枚举类型,设置keepAliveTime的单位,有TimeUnit.MILLISECONDS(ms)、TimeUnit. SECONDS(s)等
TimeUnit unit,
//缓冲任务队列,线程池的execute方法会将Runnable对象存储起来
BlockingQueue<Runnable> workQueue,
//线程工厂接口,只有一个new Thread(Runnable r)方法,可为线程池创建新线程
ThreadFactory threadFactory
//线程池任务队列超过最大值之后的拒绝策略
//包含4种:ThreadPoolExecutor.DiscardPolicy(),ThreadPoolExecutor.DiscardOldestPolicy(),ThreadPoolExecutor.AbortPolicy(),ThreadPoolExecutor.CallerRunsPolicy()
RejectedExecutionHandler handler)
复制代码

线程池执行任务的过程:

  1. 当 currentSize < corePoolSize 的时候,直接启动新的核心线程并执行任务
  2. 当 currentSize >= corePoolSize 的时候,如果 workQueue 还没有满,将任务添加到 workQueue 中。
  3. 如果 workQueue 已经满了且 currentSize < maximumPoolSize 则新建非核心线程执行任务
  4. currentSize > maximumPoolSize 且 workQueue 已满,则执行拒绝策略。 例子:
public class ThreadPoolExecutorTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
                new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        threadPool.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
class MyThreadFactory implements ThreadFactory {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        String threadName = "MyThread" + count.addAndGet(1);
        t.setName(threadName);
        return t;
    }
}
复制代码

同样的线程池也可以返回结果

public class Main {
    public static void main(String[] args){
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
        // execute 使用
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, Java.");
            }
        });
        // submit 使用
        Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("Hello, 老王.");
                return "Success";
            }
        });
        System.out.println(future.get());
    }
}
复制代码

关闭线程池 shutdown()等所有任务执行完后终止 shutdownNow()立即终止

Executors

  • FixedThreadPool(n):创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。(一堆人排队上公厕)
  • CachedThreadPool():短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。(一堆人去一家很大的咖啡馆喝咖啡)
  • SingleThreadExecutor():创建一个单线程线程池。(公厕里只有一个坑位)
  • ScheduledThreadPool(n):创建一个数量固定的线程池,支持执行定时性或周期性任务。(唯一一个有延迟执行和周期重复执行的线程池)
  • SingleThreadScheduledExecutor():此线程池就是单线程的 newScheduledThreadPool。
  • WorkStealingPool(n):Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

ThreadLocal

ThreadLocal 用于解决多线程间的数据隔离问题,为每一个线程创建一个单独的变量副本。

ThreadLocal 使用

ThreadLocal 常用方法有 set(T)、get()、remove() 等,具体使用请参考以下代码。

public class Main{
    public static void main(String[] args){
      ThreadLocal threadLocal = new ThreadLocal();
      // 存值
      threadLocal.set(Arrays.asList("老王", "Java 面试题"));
      // 取值
      List list = (List) threadLocal.get();
      System.out.println(list.size());
      System.out.println(threadLocal.get());
      //删除值
      threadLocal.remove();
      System.out.println(threadLocal.get());
    }
}
复制代码

通过 ThreadLocal 实现数据间信息共享

public class Main() {
    public static void main(String[] args){
      ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
      inheritableThreadLocal.set("老王");
      new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
    }
}
复制代码

原理和内存溢出问题

ThreadLocal 内存溢出与它底层的存储有关,ThreadLocal 底层使用 ThreadLocalMap 进行存储。
源码:

// ThreadLocal.set
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// ThreadLocalMap.set
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}        
复制代码

一个 Thread 中只有一个 ThreadLocalMap,每个 ThreadLocalMap 中存有多个 ThreadLocal,ThreadLocal 引用关系如下:
ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟 不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。
解决办法:只需要在使用完 ThreadLocal 之后,调用 remove() 方法。

线程安全

线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。
非线程安全的例子:

public class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> addNumber());
        Thread thread2 = new Thread(() -> addNumber());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}
复制代码

解决办法:

  • 数据不共享,单线程可见,比如 ThreadLocal 就是单线程可见的;
  • 使用线程安全类,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全类(后面文章会专门介绍);
  • 使用同步代码或者锁。

锁的种类

  1. 乐观锁和悲观锁
    • 悲观锁
      悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形 式。悲观地认为,不加锁的并发操作一定会出问题。
    • 乐观锁
      乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据 是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
  2. 公平锁和非公平锁
    • 公平锁
      公平锁是指多个线程按照申请锁的顺序来获取锁。
    • 非公平锁
      非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
  3. 独占锁和共享锁
    • 独占锁
      独占锁是指任何时候都只有一个线程能执行资源操作。
    • 共享锁
      共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个 线程进行写操作,允许多个线程读操作。
  4. 可重入锁
    可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。
  5. 自旋锁
    自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。

CAS 和 ABA 问题

CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,但 CAS 也不是没有任何副作用,比如著名的 ABA 问题就是 CAS 引起的。

ABA 问题描述

老王去银行取钱,余额有 200 元,老王取 100 元,但因为程序的问题,启动了两个线程,线程一和线程二进行比对扣款,线程一获取原本有 200 元, 扣除 100 元,余额等于 100 元,此时阿里给老王转账 100 元,于是启动了线程三抢先在线程二之前执行了转账操作,把 100 元又变成了 200 元, 而此时线程二对比自己事先拿到的 200 元和此时经过改动的 200 元值一样,就进行了减法操作,把余额又变成了 100 元。这显然不是我们要的正确结果, 我们想要的结果是余额减少了 100 元,又增加了 100 元,余额还是 200 元,而此时余额变成了 100 元,显然有悖常理,这就是著名的 ABA 的问题。 执行流程如下:

  • 线程一:取款,获取原值 200 元,与 200 元比对成功,减去 100 元,修改结果为 100 元。
  • 线程二:取款,获取原值 200 元,阻塞等待修改。
  • 线程三:转账,获取原值 100 元,与 100 元比对成功,加上 100 元,修改结果为 200 元。
  • 线程二:取款,恢复执行,原值为 200 元,与 200 元对比成功,减去 100 元,修改结果为 100 元。
ABA 问题解决

常见解决 ABA 问题的方案加版本号,来区分值是否有变动。以老王取钱的例子为例,如果加上版本号,执行流程如下。

  • 线程一:取款,获取原值 200_V1,与 200_V1 比对成功,减去 100 元,修改结果为 100_V2。
  • 线程二:取款,获取原值 200_V1 阻塞等待修改。
  • 线程三:转账,获取原值 100_V2,与 100_V2 对比成功,加 100 元,修改结果为 200_V3。
  • 线程二:取款,恢复执行,原值 200_V1 与现值 200_V3 对比不相等,退出修改。
Java 中的实现
String name = "老王";
String newName = "Java";
AtomicStampedReference<String> as = new AtomicStampedReference<String>(name, 1);
System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());
as.compareAndSet(name, newName, as.getStamp(), as.getStamp() + 1);
System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());
复制代码

synchronized

synchronized 是 Java 提供的同步机制,是一种悲观锁和非公平锁。

使用

synchronized 可以用来修饰代码块或者方法

// 修饰代码块
synchronized (this) {
    // do something
}
// 修饰方法
synchronized void method() {
    // do something
}
复制代码
实现原理

JVM(Java 虚拟机)是采用 monitorentermonitorexit 两个指令来实现同步的,monitorenter 指令相当于加锁,monitorexit 相当于释放锁。 而 monitorentermonitorexit 就是基于 Monitor 实现的。

synchronized 是如何实现锁升级的

在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为 其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋 循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

ReentrantLock

ReentrantLock(再入锁)是 Java 5 提供的锁实现,它的功能和 synchronized 基本相同。再入锁通过调用 lock() 方法来获取锁,通过调用 unlock() 来释放锁。 ReentrantLock 提供公平锁和非公平锁,通过给构造函数传入 true 或者 false 来生成。

使用
public class LockTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // ReentrantLock 使用
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}
复制代码

ReentrantLock 还可以无阻塞获取锁,使用 tryLock() 或者 tryLock(long timeout, TimeUnit unit) 用于尝试在一段时间内获取锁。

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -> {
    try {
        reentrantLock.lock();
        Thread.sleep(2 * 1000);

    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -> {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock());
        Thread.sleep(2 * 1000);
        System.out.println(reentrantLock.tryLock());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();    
复制代码
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

信息安全

481

相关文章推荐

未登录头像

暂无评论