文章目录

  • 线程和进程
    • 进程和线程的区别
  • Java线程的使用
    • Java线程的创建
    • 启动线程
    • 获取线程的结果
    • 线程的高级用法
      • 线程等待
      • 线程唤醒
      • 线程休眠
      • 等待线程执行完成
      • 设置线程优先级
      • 线程中断
      • 交出CPU使用权
  • 线程池的使用
    • ThreadPoolExecutor
    • Executors
  • ThreadLocal
    • ThreadLocal 使用
    • 原理和内存溢出问题
  • 线程安全
      • 锁的种类
      • CAS 和 ABA 问题
        • ABA 问题描述
        • ABA 问题解决
        • Java 中的实现
      • synchronized
        • 使用
        • 实现原理
        • synchronized 是如何实现锁升级的
      • ReentrantLock
        • 使用

线程和进程

线程(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{@Overridepublic void run() {System.out.println("Hello World");}
}
  1. 实现Runnable接口,实现run方法
public class MyThread implements Runnable{@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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);@Overridepublic 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() {@Overridepublic void run() {System.out.println("Hello, Java.");}});// submit 使用Future<String> future = threadPoolExecutor.submit(new Callable<String>() {@Overridepublic 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);elsecreateMap(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();

Java多线程归纳总结相关推荐

  1. Java多线程闲聊(四):阻塞队列与线程池原理

    Java多线程闲聊(四)-阻塞队列与线程池原理 前言 复用永远是人们永恒的主题,这能让我们更好地避免重复制造轮子. 说到多线程,果然还是绕不开线程池,那就来聊聊吧. 人们往往相信,世界是存在一些规律的 ...

  2. Java多线程闲聊(二):活锁和死锁

    Java多线程闲聊(二):活锁和死锁 这两个情况其实都是应该需要避免的情况,为了便于自己的回顾,我还是希望通过尽可能简单的表达来进行简要的归纳. 何谓死锁,就是正正紧紧按照Java的规范进行编程依然会 ...

  3. Java多线程系列---“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下: 01. Java多线程系列--"JUC锁"01之 框架 02. Java多线程系列--"JUC锁 ...

  4. java多线程与并发原理

    三.java多线程与并发原理 1.进程和线程的区别: 进程和线程的由来: (1)串行:初期的计算机只能串行执行任务,并且需要长时间等待用户输入: (2)批处理:预先将用户的指令集集中成清单,批量串行处 ...

  5. 【java多线程学习】多线程的基本概念

    今天开始系统的学习了java多线程有关的基础知识,大致先分为三个步骤:多线程的基本概念,多线程的两种使用方法(继承Thread类.实现Runable接口),线程的同步.这里先记录下下多线程的基本概念. ...

  6. Java多线程与并发相关 — 原理

    Java多线程与并发相关 - 原理 一 synchronized同步 1. 线程安全问题的主要诱因? 存在共享资源(也称临界资源); 存在多条线程共同操作这些共享数据; 2. 解决办法. 同一时刻有且 ...

  7. Java 多线程深入浅出

    首先关于Volatile修饰符: Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值.而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存.这样在任何时刻,两 ...

  8. Java 多线程的基本方式

    Java 多线程的基本方式 基础实现两种方式: 通过实现Callable 接口方式(可得到返回值):

  9. Java多线程读取本地照片为二进制流,并根据系统核数动态确定线程数

    Java多线程读取图片内容并返回 1. ExecutorService线程池 2. 效率截图 3. 源码 1. ExecutorService线程池 ExecutorService线程池,并可根据系统 ...

最新文章

  1. 判断密文加密类型hash-identifier
  2. ARZIO让AR应用制作变得更简单
  3. mdp文件-Chapter1-MINIM.mdp
  4. Visual Studio 智能提示功能消失解决办法
  5. ABAP modify screen:修改屏幕,实现隐藏、禁止输入字段
  6. 互联网汽车迎新成员 Alibaba YunOS Auto冠名2016世俱杯
  7. xftp实现本地与服务器的文件上传下载(windows)
  8. java 获取远程文件_java获取远程文件
  9. 如何从程序中改变音量?
  10. Exchange企业实战技巧(1)验证安装及配置产品密钥
  11. python爬虫实例100例-Python 练习实例1
  12. jenkins 基础配置安装(Ⅰ)
  13. 如何将MyEclipse开发的项目导入到Eclipse中运行
  14. 关于翁恺老师Java网课中细胞自动机的一点点想法
  15. JNI 调用 DLL
  16. table表格表头添加斜线
  17. 开发对接微信卡包会员卡_微信JS-SDK实现微信会员卡功能(给用户微信卡包里发送会员卡)...
  18. 【莫烦Python】Matplotlib Python 画图教程 plot in plot图中图
  19. 阿里云网络和安全配置实验(云计算)
  20. java SSM 班级同学录聚会报名网站-毕业设计源码介绍

热门文章

  1. mset redis_redis mset string 命令简介
  2. express设置html模板,node express使用HTML模板的方法示例
  3. html5跟html4有什么区别,Html5和Html4的区别
  4. 奈飞文化手册_《奈飞文化手册》速阅提炼分享4
  5. 系统吞吐量、TPS(也叫QPS)、用户并发量、性能测试概念和公式
  6. Linux压缩文件与解压文件(*.zip)
  7. Android开发笔记(六十九)JNI实战
  8. prim算法详解java_Prim算法(三)之 Java详解
  9. nginx 负载均衡 最初级版本
  10. python基础知识7——迭代器,生成器,装饰器