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安装目录\jdk\jdk8\bin\jconsole.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底层也是通过数组+链表实现的)