1. 多线程快速入门
1.1 进程与线程
-
什么是进程?
CPU从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程。
一个程序如果被CPU多次读取到内存中,则变成多个独立的进程。
-
什么是线程?
线程是程序执行的最小单位,在一个进程中可以有多个不同的线程同时执行。
-
为什么在进程中还需要线程呢?
例如,一个文本编辑器进程,在编辑器中,需要同时做很多事情:监听用户按下的键盘事件、将文本渲染到屏幕上,将文本内容持久化到硬盘,这三件事就是三个线程。线程是最小的并行单位。
-
为什么需要使用多线程?
采用多线程的形式执行代码,目的就是为了提高程序的效率。
比如:一个项目只有一个程序员开发,需要开发的模块需求有会员模块、支付模块、订单模块等,该程序员要按顺序依次将各个模块完成。而当有三个程序员同时完成不同的模块,那么就可以大大提高开发效率了。
-
串行与并行的区别
串行也就是单线程执行,代码执行效率非常低,代码从上到下执行。
并行就是多个线程一起执行,效率比较高。
-
多线程的应用场景有哪些?
- 客户端(/移动App)开发
- 异步发送短信/邮件
- 将执行比较耗时的代码改用多线程异步执行
- 异步写入日志 日志框架底层
- 多线程下载
-
同步与异步的区别
同步:代码从头到尾执行
异步:单独分支执行,相互之间没有任何影响
1.2 继承Thread类创建线程
public class ThreadTest01 extends Thread { /** * 线程执行的代码在run方法 */ @Override public void run() { //获取当前线程名称 System.out.print(Thread.currentThread().getName()); System.out.println("子线程执行..."); } public static void main(String[] args) { //获取当前线程名称 System.out.println(Thread.currentThread().getName()); //启动线程 调用start方法而不是run方法 //调用start()线程不是立即被CPU调度执行。 new ThreadTest01().start(); new ThreadTest01().start(); } }
1.3 实现Runnable接口创建线程
public class ThreadTest02 implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "子线程执行..."); } public static void main(String[] args) { //启动线程 new Thread(new ThreadTest02()).start(); //使用匿名内部类的形式创建线程 new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "子线程执行..."); } }).start(); //使用Lambda创建多线程 new Thread(() -> System.out.println(Thread.currentThread().getName() + "子线程执行...")).start(); } }
1.4 使用Callable和Future创建线程
Callable和Future线程可以获取到返回结果,抛出异常,底层基于LockSupport
从Java1.5开始,Java提供了Callable接口,该接口是Runnable接口的增强版,Callable提供了一个call()方法,可以看作是线程的执行体,但call()方法比run()方法更强大。
假设有三个连续的代码块(代码块1,2,3),本属于单线程(线程1)执行是从头到尾依次执行,此时要求代码2使用Callable模式(线程2),也就是使用异步执行且带返回结果。线程2就会是一个单独的线程执行:线程1在执行完代码1执行到代码2的时候,会单独创建一个线程,执行代码2,线程1需要拿到代码2整个执行的返回结果,在拿到以后线程1继续执行。
-
call()方法可以有返回值
-
all()方法可以声明抛出异常
public class ThreadTest03 implements Callable<Integer> { /** * 当前线程需要执行的代码 返回结果 * * @return * @throws Exception */ @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"子线程开始执行..."); try { Thread.sleep(3000); }catch (Exception e){ } System.out.println(Thread.currentThread().getName()+"返回1"); return 1; } }public class ThreadTest04 { public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadTest03 threadCallable = new ThreadTest03(); FutureTask<Integer> futureTask = new FutureTask<>(threadCallable); new Thread(futureTask).start(); //调用get方法时 主线程阻塞 子线程执行完毕 再唤醒主线程 Integer result = futureTask.get(); System.out.println(Thread.currentThread().getName()+" "+result); } }
1.5 使用线程池创建线程
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始执行子线程..."); } }); }
JUC并发中会详细说明
1.6 @Async异步注解创建线程
项目中会使用Spring的@Async注解和线程池来实现多线程
在方法上添加@Async注解,当调用此方法时,就会创建新的线程来异步执行此方法。若没有添加异步注解,顺序执行程序,调用到该方法时,如果该方法有sleep,会一直等到该方法执行完毕才会继续执行。
因此,一般将比较耗时的代码添加@Async注解。
1.7 线程同步/线程安全性问题
线程如何实现同步?(如何保证线程安全性问题)
核心思想:上锁。当多个线程共享同一个全局变量时,将可能会发生线程安全的代码上锁,最终只能有一个线程能够获取到锁,保证只有拿到锁的线程才可以执行该代码,没有拿到锁的线程不可以执行,需要经历锁的升级过程,如果一直没有获取到锁,则会一直阻塞等待。
如果线程A获取锁,但是线程A一直不释放锁,线程B就一直获取不到锁,会一直阻塞等待。
- 使用synchronized锁
- 使用Lock锁(属于JUC并发包)。底层基于aqs+cas实现
- 使用Threadlocal
- 原子类CAS非阻塞式
2. synchronized锁
2.1 概述
什么是线程安全问题?
当多个线程共享同一个全局变量,做写的操作时,可能会受到其他线程的干扰,就会发生线程安全问题。
public class ThreadCount implements Runnable { private int count = 100; @Override public void run() { while (true){ if (count > 1) { try { //运行状态->休眠状态——CPU的执行权让给其他线程 Thread.sleep(30); } catch (Exception e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + ":" + count); }else{ break; } } } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); //开启线程 new Thread(threadCount).start(); new Thread(threadCount).start(); } }
在这个程序中,两个线程很大概率会同时对count进行操作。
上synchronized锁:那么代码的哪一块需要上锁?——可能发生线程安全性问题的代码需要上锁
如果将synchronized锁加在run方法上,那么就会变成单线程,因为两个线程有非公平锁的特性,即谁拿到锁/抢到锁,谁就可以执行run方法,谁抢不到,谁就会一直阻塞等待。又因为run方法有死循环,不会释放锁,另一个线程就会一直阻塞等待
public class ThreadCount implements Runnable { private int count = 100; @Override public synchronized void run() { ... } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); //开启线程 new Thread(threadCount).start(); new Thread(threadCount).start(); } }
因此在加锁的时候并不是一次将整块代码都上锁,可能会使线程变为单线程,而且加锁后,可能会影响程序的执行效率,因为执行该代码前要竞争锁的资源。
正确加锁:
public class ThreadCount implements Runnable { private int count = 100; @Override public void run() { while (true){ if (count > 1) { ... synchronized (this) { count--; System.out.println(Thread.currentThread().getName() + ":" + count); } }else{ break; } } } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); //开启线程 new Thread(threadCount).start(); //线程0 new Thread(threadCount).start(); //线程0 } }
线程0、线程1同时获取this锁,假设线程0获取到this锁,意味着线程1没有获取到锁,则会阻塞等待。等线程0执行完count--,释放锁之后,就会唤醒线程1重新竞争锁资源。
synchronized获取锁和释放锁底层已经由虚拟机实现,会自动获取锁、释放锁并唤醒其他阻塞线程竞争锁资源。
2.2 synchronized锁的基本用法
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
synchronized(对象锁){ 需要保证线程安全的代码 }对象锁需要保证是同一个对象
比如:
ThreadCount threadCount1 = new ThreadCount(); ThreadCount threadCount2 = new ThreadCount(); //开启线程 new Thread(threadCount1).start(); new Thread(threadCount2).start();两个线程并不是同一个对象锁,这时也会出现线程安全问题
@Override public void run() { while (true){ cal(); } } public void cal(){ if (count > 1) { try { //运行状态->休眠状态——CPU的执行权让给其他线程 Thread.sleep(30); } catch (Exception e) { e.printStackTrace(); } synchronized (this) { count--; System.out.println(Thread.currentThread().getName() + ":" + count); } } } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); //开启线程 new Thread(threadCount).start(); new Thread(threadCount).start(); } -
修饰实例方法,作用与当前实例加锁,进入同步代码前要获得当前实例的锁
@Override public void run() { while (true) { if (count > 1) { try { //运行状态->休眠状态——CPU的执行权让给其他线程 Thread.sleep(30); } catch (Exception e) { e.printStackTrace(); } cal(); } else { break; } } } public synchronized void cal() { count--; System.out.println(Thread.currentThread().getName() + ":" + count); }将synchronized加在实例方法上,则默认使用的是this锁
-
修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得当前类对象的锁
2.3 synchronized死锁问题
我们如果在使用synchronized 需要注意 synchronized锁嵌套的问题,避免死锁的问题发生。
案例:
public class DeadlockThread implements Runnable { private int count = 1; private String lock = "lock"; @Override public void run() { while (true) { count++; if (count % 2 == 0) { // 线程1需要获取lock锁 再获取a方法this锁 // 线程2需要获取this锁 再获取b方法lock锁 synchronized (lock) { a(); } } else { synchronized (this) { b(); } } } } public synchronized void a() { System.out.println(Thread.currentThread().getName() + ",a方法..."); } public void b() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + ",b方法..."); } } public static void main(String[] args) { DeadlockThread deadlockThread = new DeadlockThread(); Thread thread1 = new Thread(deadlockThread); Thread thread2 = new Thread(deadlockThread); thread1.start(); thread2.start(); } }
线程1先获取自定义对象的lock锁,进入a方法需要获取this锁
线程2先获取this锁,进入b方法需要获取自定义对象的lock锁
当两个线程同时执行,开始线程1和线程2分别拿到了lock锁和this锁,之后两个线程都需要对方已经持有的锁,最终出现死锁问题。
如何排查synchronized死锁问题
使用synchronized 死锁诊断工具:JDK安装目录jdkjdk8binjconsole.exe


3. 线程之间通讯
等待/通知机制
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:
- notify() :通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
- notifyAll():通知所有等待在该对象的线程
- wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。
注意:wait,notify和notifyAll要与synchronized一起使用
wait/notify的简单用法
public class Thread03 extends Thread { @Override public void run() { try { synchronized (this) { System.out.println(Thread.currentThread().getName() + ">>当前线程阻塞,同时释放锁!<<"); this.wait(); } System.out.println(">>run()<<"); } catch (InterruptedException e) { } } public static void main(String[] args) { Thread03 thread = new Thread03(); thread.start(); try { Thread.sleep(3000); //3s后唤醒子线程 } catch (Exception e) { } synchronized (thread) { // 唤醒正在阻塞的线程 thread.notify(); } } }
多线程通讯实现生产者与消费者
看以下案例:
package com.mark.sunchronized; /** * @author Mark * @version 1.0 * @className Thread * @date 2022/11/6 18:41 */ public class Thread04 { /** * 共享对象Res */ class Res { /** * 姓名 */ private String userName; /** * 性别 */ private char sex; } /** * 输入线程 */ class InputThread extends Thread { private Res res; public InputThread(Res res) { this.res = res; } @Override public void run() { int count = 0; while (true) { if (count == 0) { res.userName = "张三"; res.sex = '男'; } else { res.userName = "李四"; res.sex = '女'; } count = (count + 1) % 2; } } } /** * 输出线程 */ class OutPutThread extends Thread { private Res res; public OutPutThread(Res res) { this.res = res; } @Override public void run() { while (true) { System.out.println(res.userName + "," + res.sex); } } } public static void main(String[] args) { new Thread04().print(); } private void print() { //全局对象 Res res = new Res(); //输入线程 InputThread inputThread = new InputThread(res); //输出线程 OutPutThread outPutThread = new OutPutThread(res); inputThread.start(); outPutThread.start(); } }
可以发现,输入输出线程公用Res对象,该程序存在线程安全问题。
修改:加synchronized锁
/** * 输入线程 */ class InputThread extends Thread { private Res res; public InputThread(Res res) { this.res = res; } @Override public void run() { int count = 0; while (true) { synchronized (res) { if (count == 0) { res.userName = "张三"; res.sex = '男'; } else { res.userName = "李四"; res.sex = '女'; } } count = (count + 1) % 2; } } } /** * 输出线程 */ class OutPutThread extends Thread { private Res res; public OutPutThread(Res res) { this.res = res; } @Override public void run() { while (true) { synchronized (res) { System.out.println(res.userName + "," + res.sex); } } } }
那么如何实现交替进行输出,而不是一直在一段时间里输出相同的姓名性别?
在Res中添加一个flag标记,输入线程为false,输出线程为true
/** * 输入线程 */ class InputThread extends Thread { private Res res; public InputThread(Res res) { this.res = res; } @Override public void run() { int count = 0; while (true) { synchronized (res) { if (res.flag) { try { res.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (count == 0) { res.userName = "张三"; res.sex = '男'; } else { res.userName = "李四"; res.sex = '女'; } res.flag = true; //唤醒输出线程 res.notify(); } count = (count + 1) % 2; } } } /** * 输出线程 */ class OutPutThread extends Thread { private Res res; public OutPutThread(Res res) { this.res = res; } @Override public void run() { while (true) { synchronized (res) { //如果 res.flag = false 则输出的线程主动释放锁 也就是让输出线程进入WAITING状态,阻塞输出线程 if (!res.flag) { try { res.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(res.userName + "," + res.sex); //输出完毕,改变状态 res.flag = false; res.notify(); } } } } }
4. 多线程核心API
4.1 Join的底层原理
public static void main(String[] args){ Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1"); Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t2"); Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t3"); t1.start(); t2.start(); t3.start(); }
执行上述代码发现,三个进程并不是按start的先后顺序启动。那么如何实现三个线程按期望的顺序去执行呢?
public static void main(String[] args) { Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1"); Thread t2 = new Thread(() -> { try { //t1执行完才执行t2 t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",线程执行"); }, "t2"); Thread t3 = new Thread(() -> { try { //t2执行完才执行t3 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",线程执行"); }, "t3"); t1.start(); t2.start(); t3.start(); }
Join底层原理是基于wait封装的,唤醒的代码在jvm Hotspot 源码中。jvm在关闭线程之前会检测线阻塞在t1线程对象上的线程,然后执行notfyAll(),这样t2就被唤醒了。
4.2 多线程的七种执行状态
- 初始化状态
- 就绪状态
- 运行状态
- 死亡状态
- 阻塞状态
- 等待状态
- 超时等待

start():调用start()方法会使得该线程开始执行,正确启动线程的方式。、wait():调用wait()方法,进入等待状态,释放资源,让出CPU。需要在同步快中调用。sleep():调用sleep()方法,进入超时等待,不释放资源,让出CPUstop():调用sleep()方法,线程停止,线程不安全,不释放锁导致死锁,过时。join():调用sleep()方法,线程是同步,它可以使得线程之间的并行执行变为串行执行。yield():暂停当前正在执行的线程对象,并执行其他线程,让出CPU资源可能立刻获得资源执行。yield()的目的是让相同优先级的线程之间能适当的轮转执行notify():在锁池随机唤醒一个线程。需要在同步快中调用。notifyAll():唤醒锁池里所有的线程。需要在同步快中调用。
使用sleep方法避免cpu空转 防止cpu占用100%
sleep(long millis) 线程睡眠 millis 毫秒
sleep(long millis, int nanos) 线程睡眠 millis 毫秒 + nanos 纳秒
public static void main(String[] args) { new Thread(() -> { while (true) { try { //线程每隔30ms休眠一次 Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
wait/join和sleep之间的区别
sleep(long)方法在睡眠时不释放对象锁
Wait(long)方法在等待的过程中释放对象锁
join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁底层是基于wait封装的
4.3 守护线程与用户线程
java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置属性,默认为用户线程。
- 守护线程依赖于用户线程,用户线程退出了,守护线程就会退出,典型的守护线程如垃圾回收线程。
- 用户线程是独立存在的,不会因为其他用户线程退出而退出。
4.4 安全停止线程
-
调用stop方法(不推荐)
stop:中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议用。
destroy: JDK未实现该方法。
-
Interrupt
Interrupt 打断正在运行或者正在阻塞的线程。
-
如果目标线程在调用Object class的wait()、wait(long)或wait(long, int)、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。
public class Thread02 extends Thread { @Override public void run() { while (true) { try { System.out.println("1"); Thread.sleep(1000000); System.out.println("2"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread02 thread02 = new Thread02(); thread02.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("中断..."); thread02.interrupt(); } } -
如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。
-
-
标志位
在代码逻辑中,增加一个判断,用来控制线程执行的中止。
private volatile boolean isFlag = true; @Override public void run() { while (isFlag) { } } public static void main(String[] args) { Thread07 thread07 = new Thread07(); thread07.start(); // thread07.isFlag = false; }
4.5 多线程优先级
-
在java语言中,每个线程都有一个优先级,当线程调控器有机会选择新的线程时,线程的优先级越高越有可能先被选择执行,线程的优先级可以设置1-10,数字越大代表优先级越高
注意:Oracle为Linux提供的java虚拟机中,线程的优先级将被忽略,即所有线程具有相同的优先级。
所以,不要过度依赖优先级。
-
线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一个线程的默认优先级是5,即Thread.NORM_PRIORTY
-
如果cpu非常繁忙时,优先级越高的线程获得更多的时间片,但是cpu空闲时,设置优先级几乎没有任何作用。
public static void main(String[] args) { Thread t1 = new Thread(() -> { int count = 0; for (; ; ) { System.out.println(Thread.currentThread().getName() + "," + count++); } }, "t1线程:"); Thread t2 = new Thread(() -> { int count = 0; for (; ; ) { System.out.println(Thread.currentThread().getName() + "," + count++); } }, "t2线程:"); t1.setPriority(Thread.MIN_PRIORITY); t1.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); }
5. Lock锁的使用
在jdk1.5后新增的ReentrantLock类同样可达到锁的效果,且在使用上比synchronized更加灵活。
相关API:
- 使用ReentrantLock实现同步
- lock()方法:上锁
- unlock()方法:释放锁
- 使用Condition实现等待/通知,类似于 wait()和notify()及notifyAll()
- Lock锁底层基于AQS实现,需要自己封装实现自旋锁。
Synchronized属于JDK关键字,底层通过C++JVM虚拟机底层实现
Lock锁底层基于AQS实现,变为重量级锁
Synchronized底层原理:锁的升级过程。推荐使用Synchronized锁
使用Lock锁过程中要注意获取锁、释放锁
5.1 ReentrantLock用法
使用synchronized获取锁和释放锁全部由虚拟机来完成
而使用Lock锁需要手动获取锁和释放锁,需要开发者自己定义
public class Thread04 { /** * 定义锁 */ private Lock lock = new ReentrantLock(); public static void main(String[] args) { Thread04 thread04 = new Thread04(); thread04.print1(); try { Thread.sleep(500); System.out.println("开始执行线程2抢锁"); } catch (InterruptedException e) { e.printStackTrace(); } thread04.print2(); } private void print1() { new Thread((() -> { //获取锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "获取锁成功"); }), "t1").start(); } public void print2() { new Thread((() -> { System.out.println("1"); lock.lock(); System.out.println(Thread.currentThread().getName() + "获取锁成功"); }), "t2").start(); } } /* t1获取锁成功 开始执行线程2抢锁 1 */
上述程序中,t1未释放锁,则t2无法获取锁,阻塞。
因此在获取锁后要释放锁。
private void print1() { new Thread((() -> { try { //获取锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "获取锁成功"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }), "t1").start(); }
5.2 Condition用法
Condition接口提供了与Object阻塞(wait())与唤醒(notify()或notifyAll())相似的功能,只不过Condition接口提供了更为丰富的功能,如:限定等待时长等
public class Thread05 { private Lock lock = new ReentrantLock(); /** * 定义 */ private Condition condition = lock.newCondition(); public static void main(String[] args) { Thread05 thread05 = new Thread05(); thread05.cal(); try { Thread.sleep(3000); } catch (Exception e) { } //释放锁 thread05.signal(); } public void signal() { try { //获取锁 lock.lock(); //唤醒线程 condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void cal() { //唤醒线程 new Thread(() -> { try { lock.lock(); System.out.println("1"); //释放锁,变为阻塞状态 condition.await(); System.out.println("2"); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } }).start(); } }
6.多线程综合案例实战
6.1 线程安全性问题分析
分析线程安全性问题需要站在下面几个维度考虑:
-
字节码角度
JVM已经把底层封装得很好,很难了解底层,因此需要从字节码汇编指令分析线程安全性问题
-
上下文切换
单核CPU上的多线程,并不是真正意义上的多线程,而是线程切换实现多线程
-
JMM java内存模型
public class Run extends Thread{ private static int sum = 0; @Override public void run() { sum(); } public void sum(){ for (int i = 0 ; i <10000; i++){ sum ++; } } public static void main(String[] args) throws InterruptedException { Run run1 = new Run(); Run run2 = new Run(); run1.start(); run2.start(); run1.join(); run2.join(); System.out.println(sum); } }
不考虑线程安全问题,上述代码应当输出20000,然而,输出的却比20000小。
通过反编译来查看过程:
- target中找到Run.class文件
- 打开Terminal,将Run.class所在目录拖到Terminal
- 输入命令:
javap -p -v Run.class
分析:
共享变量值 sum=0
假设现CPU执行到t1线程,t1线程执行完++但是还没有保存sum,就切换到t2线程执行,t2线程将静态变量sum=0改成sum=1,CPU又切换到t1线程,使用之前的sum++ 得到的sum=1赋值给共享变量sum,导致最终结果为sum1,然而现在sum++实际上已经执行了两次,最终结果却为1。
6.2 Callable和FutureTask原理分析
public interface MarkCallable<V> { /** * 当前线程执行完毕返回的结果 * @return * @throws Exception */ V call(); }
public class MarkFutureTask<V> implements Runnable { private MarkCallable<V> markCallable; private Object lock = new Object(); private V result; public MarkFutureTask(MarkCallable<V> markCallable) { this.markCallable = markCallable; } @Override public void run() { //线程需要执行代码 result = markCallable.call(); //如果子线程执行完毕,唤醒主线程,可以拿到返回结果 synchronized (lock) { lock.notify(); } } public V get() { //获取子线程异步执行完毕后的返回结果 //主线程阻塞 synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return result; } }
public class MarkCallableImpl implements MarkCallable<Integer>{ @Override public Integer call(){ try { System.out.println(Thread.currentThread().getName()+",子线程执行"); Thread.sleep(3000); }catch (Exception e){ } //耗时代码执行完毕,返回1 return 1; } }
public static void main(String[] args) { MarkCallableImpl markCallable = new MarkCallableImpl(); MarkFutureTask<Integer> markFutureTask = new MarkFutureTask<Integer>(markCallable); new Thread(markFutureTask).start(); Integer result = markFutureTask.get(); System.out.println(result); }
使用LockSupport实现:
LockSupport:不需要实现synchronized即可实现wait和notify相似的操作
public class MarkFutureTask<V> implements Runnable { private MarkCallable<V> markCallable; private Object lock = new Object(); private V result; private Thread currentThread; public MarkFutureTask(MarkCallable<V> markCallable) { this.markCallable = markCallable; } @Override public void run() { //线程需要执行代码 result = markCallable.call(); if (currentThread != null) { LockSupport.unpark(currentThread); } } 00 public V get() { //获取子线程异步执行完毕后的返回结果 //主线程阻塞 currentThread = Thread.currentThread(); LockSupport.park(); return result; } }
7. ConcurrentHashMap
7.1 HashTable与HashMap的区别
- 在多线程情况下,同时对一个共享HashMap使用put方法做写操作,底层会共享一个table数组,发生线程安全问题,在多线程操作中,需要使用synchronized关键字。而HashTable线程是安全的,在每个公共方法上都使用了synchronized。
- HashMap是允许key和value为null的,key为null的hash值为0,存在index=0的位置,而HashTable不允许key和value为空
- HashMap需要重新计算hash值作为hashCode,而HashTable直接使用对象的hashCode
- HashMap继承了AbstractMap类,而HashTable继承了Didtionary类
7.2 Hashtable集合的缺陷
- 使用传统的Hashtable保证线程问题,是采用synchronized锁将整个Hashtable中的数组锁住,在多线程中只允许一个线程访问put或get,效率非常低,但是能够保证线程安全问题。当多个线程对Hashtable在get或put时,会发生this锁的竞争,多个线程竞争锁,最终只会有一个线程获取到this锁,获取不到的阻塞等待,最终只能单线程get/put。所以在多线程并不推荐使用Hashtable,因为其效率非常低。
7.3 ConcurrentHashMap1.7实现原理
数据结构实现:数组+Segments分段锁+HashEntry链表实现
锁的实现:Lock锁+CAS乐观锁+UNSAFE类
扩容实现:支持多个Segment同时扩容
原理就是将大的Hashtable拆分成n多个小的Hashtable集合,默认16个。——分段锁
分段锁的核心思想是减少多个线程对锁的竞争:不会再访问到同一个Hashtable(每个小的HashTable都有一个独立锁,多个线程访问大的Hashtable,会先根据key计算存放具体小的Hashtable的位置,然后进行操作)
ConcurrentHashMap get()方法没有锁的竞争,而Hashtable get()方法有锁的竞争
而在JDK1.8取消了分段锁。
在多线程情况下访问ConcurrentHashMap1.7版本进行操作,如果多个线程操作的key最终计算落地到不同的小的Hashtable集合中,就可以实现多线程同时操作Hashtable而不会发生锁的竞争。但是如果多个线程操作的key最终计算落地到同一个小的Hashtable集合中就会发生锁的竞争。
(实际在ConcurrentHashMap中,并不是叫HashTable,而是叫Segments和Segment)
7.4 ConcurrentHashMap的使用
使用方法与HashMap一样
7.5 手写ConcurrentHashMap
- 提前创建固定数组容量大小的小的Hashtable集合
- 通过构造函数初始化Hashtable数组
public class MarkConcuurentHashMap<K, V> { /** * 创建一个存放小的HashTable集合 */ private Hashtable<K, V>[] hashTables; public MarkConcuurentHashMap() { //默认情况下 初始化16个小的HashTable hashTables = new Hashtable[16]; for (int i = 0; i < hashTables.length; i++) { hashTables[i] = new Hashtable<>(); } } public void put(K k, V v) { //先计算key存放到哪个具体小的HashTable集合中 int hashTableIndex = k.hashCode() % hashTables.length; //将key存入到具体小的HashTable集合中 hashTables[hashTableIndex].put(k, v); } public void get(K k) { //先计算key存放到了哪个具体小的HashTable集合中 int hashTableIndex = k.hashCode() % hashTables.length; //根据key从具体小的HashTable集合中get hashTables[hashTableIndex].get(k); } }
7.6 分段锁设计概念
ConcurrentHashMap底层采用分段锁设计,将一个大的HashTable线程安全的集合拆封成n多个小的HashTable集合,默认初始化16个小的HashTable集合。如果多个线程最终根据key计算出的index值落地到不同的小的HashTable集合,不会发生锁的竞争,同时支持多个线程访问ConcurrentHashMap进行写的操作,效率非常高。
ConcurrentHashMap会计算两次index值:
- 第一次计算index的值,计算key具体存放到哪个小的HashTable
- 第二次计算index的值,计算key存放到具体小的HashTable对应具体数组index的哪个位置(HashTable底层也是通过数组+链表实现的)