本篇文章的目标
认识多线程
• 掌握多线程程序的编写
• 掌握多线程的状态
• 掌握什么是线程不安全及解决思路
• 掌握synchronized、volatile,关键字
1.多线程
1.1 线程概念
1) 什么是线程
⼀个线程就是⼀个"执⾏流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执⾏着多份代码.
2) 为什么要有线程
首先是因为 并发编程成为刚需
1.单核cpu的执行能力达到了瓶口,要想有更好的算力就需要多核cpu,然后并发编程可以更好的运用多核cpu资源
2.有些地方需要用到 IO,"IO"就是读和写的意思,为了让等待的IO能够去做一些事情,所以就需要并发编程.
其次!!!!
虽然进程也可以并发编程,但是我们还是选择线程
因为创建线程比创建进程快
销毁线程比创建进程快
调度线程比调度进程快
最后,线程虽然⽐进程轻量,但是⼈们还不满⾜,于是⼜有了"线程池"(ThreadPool)和"协程"
(这个我们后面再介绍)
3) 进程和线程的区别
进程是包含线程的,每个进程至少有一个线程存在,即主线程
进程和线程不共用一个内存,同一个进程内的线程之间共享同一个内存空间
进程是资源分配的最小单位,线程是系统调度的最小单位
一个进程挂了可能不会影响到其他进程,但是如果一个线程掉了,可能整个进程都会崩溃.
4) java线程和操作系统之间的线程的关系
线程是操作系统的概念,操作系统内核实现了线程这样的概念,并且对用户层提供了一些API供用户使用.(例如Linux的pthread)
java标准库中Thread类可以视为对操作系统提供的API进行了进一步的抽象和封装.
1.2 第一个多线程程序
感受一个普通程序和多线程程序的区别:
每个线程是一个独立的执行流;
多个线程并发执行;
public class timu3 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
System.out.println("11111");
}
});
Thread t2 = new Thread(()->{
while(true) {
System.out.println("22222");
}
});
t1.start();
t2.start();
}
}
使用jconsole进行线程的查看
1.3 创建线程
方法1:继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
方法2:实现Runable对象
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
Thread t = new Thread(new MyRunnable());
对比以上两种方法:
继承Thread类的可以用this直接表示
实现Runnable接⼝,this表⽰的是MyRunnable 的引⽤.需要使⽤
Thread.currentThread()
class MyThread extends Thread{
@Override
public void run(){
System.out.println("i");
System.out.println(this);
}
}
class MyRunable implements Runnable{
@Override
public void run(){
System.out.println("j");
System.out.println(Thread.currentThread());
}
}
public class Demo45 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyThread();
Thread t2 = new Thread(new MyRunable());
t1.start();
t2.start();
System.out.println(Thread.currentThread());
}
}
其他方法:
匿名内部类创建Thread子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
匿名内部类创建Runnable⼦类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
lambda表达式创建Runnable⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
});
1.4 多线程的优势---增加运行速度
可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
使⽤System.nanoTime() 可以记录当前系统的纳秒级时间戳.
• serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算.
public class ThreadAdvantage {
// 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使⽤并发⽅式
concurrency();
// 使⽤串⾏⽅式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利⽤⼀个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运⾏结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串⾏: %f 毫秒%n", ms);
}
}
并发:399.651856毫秒
串⾏:720.616911毫秒
2.Thread类和常见的方法
Thread类是JVM⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的Thread对象与之关
联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽Thread类的对象就是⽤来描述⼀个线程执⾏流的,JVM会将这些Thread对象组织起来,⽤于线程调度,线程管理.
2.1 Thread常见的构造方法
方法 说明
Thread() 创建线程对象
Thread(Runable runable) 使用Runable对象创建线程对象
Thread(String name) 创建线程对象.并命名
Thread(Runable runable,String name) 使用Runable创建线程对象,并命名
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread的⼏个常⻅属性
ID是线程的唯一标识,不同线程不会重复.
名称是
状态表示线程当前所处的一个情况,下面我们会进一步说明.
优先级高的线程理论上来说更容易被调度到.
关于后台线程,需要记住一点:jvm会在一个进程的所有非后台线程结束后.才会结束运行
关于是否存活,即简单的理解,为run方法是否结束运行结束了.
可能大家会有疑问,什么是前台线程??什么是后天线程
在多线程编程中,前台线程和后台线程是两种不同的线程类型。
前台线程是指在应用程序运行期间一直存在的线程。当所有的前台线程都结束时,应用程序才会退出。前台线程通常用于执行关键任务,如用户交互、计算和数据处理等。它们可以访问和操作应用程序的所有资源,并且可以阻止应用程序的退出。
后台线程是指在应用程序运行期间存在的一种辅助线程。当所有的前台线程结束时,后台线程会自动终止,不会阻止应用程序的退出。后台线程通常用于执行一些不需要持续存在的任务,如日志记录、定时任务和后台数据同步等。它们不能访问和操作应用程序的所有资源,只能访问一部分受限资源。
2.3 启动线程-start()
之前我们已经看到了如何通过覆写run⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。
*覆写run是提供给线程要做的事情的指令清单
*线程对象是可以认为是把王五叫过来,但是还没开始行动
*start是说行动开始的那个,start之后线程才是真正启动
调用start()才是真正在操作系统创建一个线程
2.4 中断一个线程
比如在某个场景下,李四⼀旦进到⼯作状态,他就会按照⾏动指南上的步骤去进⾏⼯作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如⽼板突然来电话了,说转账的对⽅是个骗⼦,需要赶紧停⽌转账,那张三该如何通知李四停⽌呢?这就涉及到我们的停⽌线程的⽅式了。
目前常见的有两种形式:
*通过共享的方式来沟通
*调用interrupt()方法来通知
示例1:用自定义的变量来定义标志位
需要给标志位加上volatile这个标志词(这个标准词在后续会介绍)
class Myrunable implements Runnable{
volatile boolean isQuit = false;
@Override
public void run(){
while(!isQuit){
System.out.println("我忙着工作呢"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("抱歉还好来得及");
}
}
public class Demo47 {
public static void main(String[] args) throws InterruptedException {
Myrunable targer = new Myrunable();
Thread t= new Thread(targer,"李四");
t.start();
System.out.println("让李四开始转账");
Thread.sleep(3000);
System.out.println("老板说那个是骗子,快停下");
targer.isQuit = true;
}
}
示例2:用Thread.interrupt()或者Thread.currentThread().interrupt自定义标志位
Thread内部包含了⼀个boolean类型的变量作为线程是否被中断的标记.
public class Demo48 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种⽅法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内⻤,终⽌交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了⼤事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
thread.interrupt();
}
}
thread收到通知的⽅式有两种:
1. 如果线程因为调⽤wait/join/sleep等⽅法⽽阻塞挂起,则以InterruptedException异常的形式通
知,清除中断标志◦ 当出现InterruptedException的时候,要不要结束线程取决于catch中代码的写法.可以选择忽略这个异常,也可以跳出循环结束线程.
2. 否则,只是内部的⼀个中断标志被设置,thread可以通过◦ Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志
这种⽅式通知收到的更及时,即使线程正在sleep也可以⻢上收到。
2.5 等待一个线程join()
有时候,我们需要一个线程等待另外一个线程结束后才可以执行这个线程.
例如:张三需要等李华结束了 才可以开始
public class Demo49 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable1 = new Runnable() {
@Override
public void run() {
int i = 0;
while(i<5){
System.out.println(Thread.currentThread().getName()+"在努力工作");
i++;
}
System.out.println(Thread.currentThread().getName()+"工作完毕");
}
};
Thread t1 = new Thread(runnable1,"李华");
Thread t2 = new Thread(runnable1,"张三");
t1.start();//线程1启动
t1.join();
t2.start();
t2.join();
System.out.println("张三工作结束了");
}
}
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等millis毫秒
public void join(long millis,int nanos) 同理,但可以更高精确
2.6 获取当前线程引用
这个方法我们已经很熟悉了,因为我们上面的代码已经使用过很多了
public static Thread cuttentThread (); 返回当前线程的引用对象
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7 休眠当前线程
也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
3.线程的状态
3.1 观察线程的所以状态
线程的状态是⼀个枚举类型Thread.State
public static void main(String[] args){
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
• NEW:安排了⼯作,还未开始⾏动
• RUNNABLE:可⼯作的.⼜可以分成正在⼯作中和即将开始⼯作.
• BLOCKED:这⼏个都表⽰排队等着其他事情
• WAITING:这⼏个都表⽰排队等着其他事情
• TIMED_WAITING:这⼏个都表⽰排队等着其他事情
• TERMINATED:⼯作完成了.
3.2 线程状态和状态转换的意义
把李四、王五找来,还是给他们在安排任务,没让他们⾏动起来,就是NEW状态;
当李四、王五开始去窗⼝排队,等待服务,就进⼊到RUNNABLE 状态。该状态并不表⽰已经被银⾏⼯作⼈员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;当李四、王五因为⼀些事情需要去忙,例如需要填写信息、回家取证件、发呆⼀会等时,进⼊BLOCKED 、 WATING 、 TIMED_WAITING 状态,⾄于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为TERMINATED 状态。
所以,之前我们学过的isAlive()⽅法,可以认为是处于不是NEW和TERMINATED的状态都是活着的。
3.3 观察线程的状态和转移
观察1:关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换
Thread t = new Thread(() -> {
for (int i = 0; i < 1000_0000; i++) {
}
}, "李四");
System.out.println(t.getName() + ": " + t.getState());
;
t.start();
while (t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());
;
}
System.out.println(t.getName() + ": " + t.getState());
;
}
观察2:关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
使⽤jconsole可以看到t1的状态是TIMED_WAITING,t2的状态是BLOCKED
修改上⾯的代码,把t1中的sleep换成wait
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
使⽤jconsole可以看到t1的状态是WAITING
结论:
• BLOCKED表⽰等待获取锁,WAITING和TIMED_WAITING表⽰等待其他线程发来通知.
• TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在⽆限等待唤醒
4.多线程带来的的⻛险-线程安全(重点)
4.1 观察线程不安全
public class ThreadSecur1 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
为什么不是100000呢?这就是因为线程不安全
4.2 线程安全的概念
想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
4.3 线程不安全的原因
线程调度是随机的
这是线程安全问题的罪魁祸⾸
随机调度使⼀个程序在多线程环境下,执⾏顺序存在很多的变数.
程序猿必须保证在任意执⾏顺序下,代码都能正常⼯作.
修改共享数据
//多个线程修改同⼀个变量
上⾯的线程不安全的代码中,涉及到多个线程针对 count 变量进⾏修改.
此时这个 count 是⼀个多个线程都能访问到的?"共享数据"
什么是原子性
我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B是不是也可以进⼊房间,打断A在房间⾥的隐私。这个就是不具备原⼦性的。
那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。
有时也把这个现象叫做同步互斥,表⽰操作是互相排斥的。
⼀条java语句不⼀定是原⼦的,也不⼀定只是⼀条指令
⽐如刚才我们看到的n++,其实是由三步操作组成的:
1. 从内存把数据读到CPU
2. 进⾏数据更新
3. 把数据写回到CPU
不保证原⼦性会给多线程带来什么问题
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的
这点也和线程的抢占式调度密切相关.如果线程不是"抢占"的,就算没有原⼦性,也问题不⼤.
可⻅性
可⻅性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到
Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型.
⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果.
线程之间的共享变量存在主内存(Main Memory).
• 每⼀个线程都有⾃⼰的"⼯作内存"(Working Memory)
• 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷⻉到⼯作内存,再从⼯作内存读取数据.
• 当线程要修改⼀个共享变量的时候,也会先修改⼯作内存中的副本,再同步回主内存.
由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的"副本".此时修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化.
1)初始情况下,两个线程的⼯作内存内容⼀致.
2)⼀旦线程1修改了a的值,此时主内存不⼀定能及时同步.对应的线程2的⼯作内存的a的值也不⼀定能及时同步.这个时候代码中就容易出现问题.
此时引⼊了两个问题:
• 为啥要整这么多内存
• 为啥要这么⿇烦的拷来拷
1)为啥整这么多内存?
实际并没有这么多"内存".这只是Java规范中的⼀个术语,是属于"抽象"的叫法.
所谓的"主内存"才是真正硬件⻆度的"内存".⽽所谓的"⼯作内存",则是指CPU的寄存器和⾼速缓存.
2)为啥要这么⿇烦的拷来拷去
因为CPU访问⾃⾝寄存器的速度以及⾼速缓存的速度,远远超过访问内存的速度(快了3-4个数量级,也就是⼏千倍,上万倍)
⽐如某个代码中要连续10次读取某个变量的值,如果10次都从内存读,速度是很慢的.但是如果只是第⼀次从内存读,读到的结果缓存到CPU的某个寄存器中,那么后9次读数据就不必直接访问内存了.效率就⼤⼤提⾼了
既然缓存这么快,为什么不都是缓存,当然是因为贵啦,虽然内存比缓存慢很多,但是又要比硬盘快很多
指令重排序
什么是代码重排序
⼀段代码是这样的:
1. 去前台取下U盘
2. 去教室写10分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进⾏优化,⽐如,按?1->3->2的⽅式执⾏,也是没问题,可以少跑⼀次前台。这种叫做指令重排序
重排序是⼀个⽐较复杂的话题,涉及到CPU以及编译器的⼀些底层⼯作原理,此处不做过多讨论
4.4 解决之前的线程安全问题
public class ThreadSecur1 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {//加锁
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object){//加锁
count++;
}}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
这里用到的方法,后面马上会说
5. synchronized关键字-监视器锁 monitor lock
5.1 synchronized的特性
1).互斥
synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏到同⼀个对象synchronized就会阻塞等待.
进⼊synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁
理解"阻塞等待".
针对每⼀把锁,操作系统内部都维护了⼀个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程,
再来获取到这个锁.
注意:
上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁.⽽是要靠操作系统来"唤醒".这也就
是操作系统线程调度的⼀部分⼯作.
假设有ABC三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C
都在阻塞队列中排队等待.但是当A释放锁之后,虽然 B⽐C先来的,但是B不⼀定就能获取到锁,
⽽是和C重新竞争,并不遵守先来后到的规则.
synchronized的底层是使⽤操作系统的mutex lock实现的
2)可重入
synchronized同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
锁死的理解:一个线程没有释放锁,然后又重新上锁,按照之前锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第⼆个锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进⾏解锁操作.这时候就会死锁.(这样的锁称为不可重入锁)
Java中的synchronized是可重⼊锁,因此没有上⾯的问题.
在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.
• 如果某个线程加锁的时候,发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.
•解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)
5.2 synchrenized 使用实例
synchronized本质上要修改指定对象的"对象头".从使⽤⻆度来看,synchronized也势必要搭配⼀个具体的对象来使⽤.
1)修饰代码块:明确指定锁哪个对象.
锁任意对象
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2)直接修饰普通方法:锁的SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3)修饰静态⽅法:锁的SynchronizedDemo类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
我们重点要理解,synchronized锁的是什么.两个线程竞争同⼀把锁,才会产⽣阻塞等待
如果锁的是不同的对象,就不会有竞争,就不会有阻塞
5.3 Java标准库中的线程安全类
Java标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,⼜没有任何加锁措
施.
• ArrayList
• LinkedList
• HashMap
• TreeMap
• HashSet
• TreeSet
• StringBuilder
但是还有⼀些是线程安全的.使⽤了⼀些锁机制来控制.
• Vector(不推荐使⽤)
• HashTable(不推荐使⽤)
• ConcurrentHashMap
• StringBuffer
还有的虽然没有加锁,但是不涉及"修改",仍然是线程安全的
String
6.volatile关键字
volatile修饰的变量,能够保证"内存可⻅性".
代码在写⼊volatile修饰的变量的时候,
• 改变线程⼯作内存中volatile变量副本的值
• 将改变后的副本的值从⼯作内存刷新到主内存
代码在读取volatile修饰的变量的时候,
• 从主内存中读取volatile变量的最新值到线程的⼯作内存中
• 从⼯作内存中读取volatile变量的副本
前⾯我们讨论内存可⻅性时说了,直接访问⼯作内存(实际是CPU的寄存器或者CPU的缓存),速度⾮常快,但是可能出现数据不⼀致的情况.
加上volatile,强制读写内存.速度是慢了,但是数据变的更准确了
代码示例:
在这个代码中:创建两个线程t1和t2, t1中包含一个循环,这个循环以flag==0 为循环条件,t2从键盘读入一个整数,并把这个整数赋值给flag
import java.util.Scanner;
public class volatile_test {
static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(flag==0){
}
System.out.println("循环结束");
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
t1读的是自己工作内存中的内容
当t2对flag变量进行修改,此时t1感知不到flag的变化
如果给flag加上volatile,就可以解决问题.(之所以会这样是因为一下子执行了太多次了,系统感觉还没有变化,所以就认为不会变化了,所以之后改变了也没用了(volatile有可见性和代码优化两个功能))
volatile不保证原子性
volatile和synchronized有着本质的区别,synchronized能够保证原子性,volatile只保证可见性
public class Count1 {
volatile int count =0;
public void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
final Count1 counter = new Count1();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
此时可以看到,最终count的值仍然⽆法保证是100000.
7. notify和wait
由于线程之间是抢占式执⾏的,?因此线程之间执⾏的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序.
比如:球场上的每个运动员都是独⽴的"执⾏流",可以认为是⼀个"线程".⽽完成⼀个具体的进攻得分动作,则需要多个运动员相互配合,按照⼀定的顺序执⾏⼀定的动作,线程
1先"传球",线程2才能"扣篮".
完成这个协调动作,主要涉及三个主要方法:
wait() / wait(long timeout) :让当前线程进入等待状态
notify / notifyAll() :唤醒当前在等待的线程
注意:这三个方法都是Object类方法
7.1 wait() 方法
wait()要做的事情有;
使当前线程的状态变为等待态(进入等待序列)
释放当前锁
满足一定状态,重新尝试获取当前锁
wait要搭配synchronized使用,不然会抛出异常.
wait结束等待的条件:
• 其他线程调⽤该对象的notify⽅法.
• wait等待时间超时(wait⽅法提供⼀个带有timeout参数的版本,来指定等待时间).
• 其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出 InterruptedException 0异常.
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
7.2 notify方法
notify⽅法是唤醒等待的线程.
• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程。(并没有"先来后到")
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏
完,也就是退出同步代码块之后才会释放对象锁。
代码⽰例:使⽤notify()⽅法唤醒线程
• 创建WaitTask类,对应⼀个线程,run内部循环调⽤wait.
• 创建NotifyTask类,对应另⼀个线程,在run内部调⽤⼀次notify
• 注意,WaitTask和NotifyTask内部持有同⼀个Objectlocker.WaitTask和NotifyTask要想配合就
需要搭配同⼀个Object.
WaitTask
public class WaitTask implements Runnable{
private Object object;
public WaitTask(Object object) {
this.object = object;
}
@Override
public void run() {
synchronized (object) {
while (true) {
try {
System.out.println("wait开始");
object.wait();
System.out.println("wait结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
NotifyTask
public class NotifyAll implements Runnable{
private Object object;
public NotifyAll(Object object) {
this.object=object;
}
@Override
public void run() {
synchronized (object){
System.out.println("notify开始");
object.notify();
System.out.println("notify结束");
}
}
}
Main
public class WaitMain {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(new NotifyTask(object));
Thread t2 = new Thread(new WaitTask(object));
t1.start();
Thread.sleep(1000);
t2.start();
}
}
7.3 NotifyAll方法
notify⽅法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.
范例:使⽤notifyAll()⽅法唤醒所有等待线程,在上⾯的代码基础上做出修改
• 创建3个WaitTask实例.1个NotifyTask实例.
public class WaitTask implements Runnable{
private Object object;
public WaitTask(Object object) {
this.object = object;
}
@Override
public void run() {
synchronized (object) {
while (true) {
try {
System.out.println("wait开始"+Thread.currentThread().getName());
object.wait();
System.out.println("wait结束"+Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
public class NotifyAll implements Runnable{
private Object object;
public NotifyAll(Object object) {
this.object=object;
}
@Override
public void run() {
synchronized (object){
System.out.println("notifyall开始");
object.notifyAll();
System.out.println("notifyall结束");
}
}
}
public class WaitMain {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(new WaitTask(object));
Thread t2 = new Thread(new WaitTask(object));
Thread t3 = new Thread(new WaitTask(object));
Thread t4 = new Thread(new NotifyAll(object));
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
t4.start();
}
}
此时可以看到,调⽤notifyAll能同时唤醒3个wait中的线程
注意:虽然是同时唤醒3个线程,但是这3个线程需要竞争锁.所以并不是同时执⾏,⽽仍然是有先有后
的执⾏
理解notify和notifyAll
notify只唤醒等待队列中的⼀个线程.其他线程还是乖乖等着
notifyAll⼀下全都唤醒,需要这些线程重新竞争锁
7.4 wait()和sleep()
其实理论上wait和sleep完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的,⼀个是让线程阻塞⼀段时间,
唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.
wait需要搭配synchronized使⽤sleep不需要
wait是Object的⽅法sleep是Thread的静态⽅法.
8. 多线程案例
8.1 单例模式
单例模式是校招中最常考的设计模式之⼀.
什么是设计模式呢?
设计模式就是:大佬给一般般的人设计出来的套路,棋谱一样,按着套路走,错也不会错到哪去.所以就有了模式.
单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽不会创建出多个.
这⼀点在很多场景上都需要.⽐如JDBC中的DataSource实例就只需要⼀个
单例模式有很多种,最常见的就是饿汉和懒汉模式.
饿汉模式:
public class Singlemon {
public Singlemon() {
}
private static Singlemon instance = new Singlemon();
public static Singlemon getInstance(){
return instance;
}
}
懒汉模式:
public class Singletonn {
public Singletonn() {
}
public static Singlemon instance = null;
public static Singlemon getInstance(){
if(instance==null){
instance = new Singlemon();
}
return instance;
}
}
但是这样子的懒汉模式是不安全的.
如果有两个地方都调用了懒汉,就有可能会创建多个.
所以我们应该加上synchronized.
public class Singletonn {
public Singletonn() {
}
public static Singlemon instance = null;
public static synchronized Singlemon getInstance(){
if(instance==null){
instance = new Singlemon();
}
return instance;
}
}
懒汉模式-多线程版(改进版)
public class Singlemonnplus {
public Singlemonnplus() {
}
public static Singlemonnplus instance = null;
public static Singlemonnplus getInstance() {
if (instance == null) {
synchronized (Singlemonnplus.class) {
if (instance == null) {
instance = new Singlemonnplus();
}
return instance;
}
}
}
}
理解双重if判定/volatile:
加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此
后续使⽤的时候,不必再进⾏加锁了.
外层的if就是判定下看当前是否已经把instance实例创建出来了.
同时为了避免"内存可⻅性"导致读取的instance出现偏差,于是补充上volatile.
当多线程⾸次调⽤getInstance,⼤家可能都发现instance为null,于是⼜继续往下执⾏来竞争锁,其中竞争成功的线程,再完成创建实例的操作.
当这个实例创建完了之后,其他竞争到锁的线程就被⾥层if挡住了.也就不会继续创建其他实例.
例子:
1. 有三个线程,开始执⾏getInstance ,通过外层的if (instance == null) 知道了实例
还没有创建的消息.于是开始竞争同⼀把锁
2. 其中线程1率先获取到锁,此时线程1通过⾥层的 if (instance == null) 进⼀步确认实例
是否已经创建.如果没创建,就把这个实例创建出来.
3. 当线程1释放锁之后,线程2和线程3也拿到锁,也通过⾥层的 if (instance == null) 来
确认实例是否已经创建,发现实例已经创建出来了,就不再创建了.
4. 后续的线程,不必加锁,直接就通过外层 if (instance == null) 就知道实例已经创建了,
从⽽不再尝试获取锁了.降低了开销
8.2 阻塞队列
在Java标准库中内置了阻塞队列.如果我们需要在⼀些程序中使⽤阻塞队列,直接使⽤标准库中的即
可.
• BlockingQueue是⼀个接⼝.真正实现的类是LinkedBlockingQueue.
• put⽅法⽤于阻塞式的⼊队列,take⽤于阻塞式的出队列.
• BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// ⼊队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
阻塞队列实现
• 通过"循环队列"的⽅式来实现.
• 使⽤synchronized进⾏加锁控制.
• put插⼊元素的时候,判定如果队列满了,就进⾏wait.(注意,要在循环中进⾏wait.被唤醒时不⼀定
队列就不满了,因为同时可能是唤醒了多个线程).
• take取出元素的时候,判定如果队列为空,就进⾏wait.(也是循环wait)
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
public class MyBlockQueue {
public String[] elem = null;
int head = 0;
int tail = 0;
int size = 0;
Object object = new Object();
public MyBlockQueue(int capacity) {
this.elem = new String[capacity];
}
public void put(String e) throws InterruptedException {
synchronized (object) {
while(size >= elem.length) {
object.wait();
}
elem[tail] = e;
tail++;
if (tail >= elem.length) {
tail = 0;
}
size++;
object.notify();
}
}
public String take() throws InterruptedException {
synchronized (object){
while(elem.length<= 0){
object.wait();
}
String e = elem[head];
head++;
if(head>=elem.length){
head = 0;
}
size--;
object.notify();
return e;
}
}
}
8.3 定时器
定时器是⼀种实际开发中⾮常常⽤的组件.
⽐如⽹络通信中,如果对⽅500ms内没有返回数据,则断开连接尝试重连.
⽐如⼀个Map,希望⾥⾯的某个key在3s之后过期(⾃动删除).
类似于这样的场景就需要⽤到定时器
标准库中的定时器
• 标准库中提供了⼀个Timer类.Timer类的核⼼⽅法为 schedule .
• schedule 包含两个参数.第⼀个参数指定即将要执⾏的任务代码,第⼆个参数指定多⻓时间之后
执⾏(单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
自定义定时器
import java.util.Objects;
import java.util.PriorityQueue;
class MyTimerTask implements Comparable<MyTimerTask>{
private long time;
Runnable runnable;
public MyTimerTask( Runnable runnable,long time) {
this.time = System.currentTimeMillis()+time;
this.runnable = runnable;
}
public void run(){
runnable.run();
}
public long gettime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
}
public class MyTimer {
Thread t = null;
PriorityQueue<MyTimerTask> queue = new PriorityQueue();
Object locker = new Object();
public void schedule(Runnable runnable,long time){
MyTimerTask task = new MyTimerTask(runnable,time);
queue.offer(task);
locker.notify();
}
public MyTimer(){
t = new Thread(()->{
while(true){
synchronized (locker){
if(queue.isEmpty()){
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
MyTimerTask task = queue.peek();
long curtime = System.currentTimeMillis();
if(curtime>=task.gettime()){
queue.poll();
}
else {
try {
locker.wait(task.gettime()-curtime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
t.start();
}
}
8.4 线程池
使用多线程可以进行并发,但是不停的创建和销毁进程,成本很高.
引入了线程(轻量级进程),复用资源的方式,来提高了销毁的效率.
但是随着创建销毁线程的频率再次提高,已经不足以满足了.
所以我们又引入了
1.协程/纤程(轻量级线程)->这里暂时不讨论
2.线程池(提前把要使用的线程提起在线程池中创建好,需要用就从池子里取出来,用完之后也是还给池子.->纯用户态代码,就比从内核中拿取和存储快)
标准库中,ThreadPoolExecutor表示线程池.
corePoolSize:正式员⼯的数量.(正式员⼯,⼀旦录⽤,永不辞退)
• maximumPoolSize:正式员⼯+临时⼯的数⽬.(临时⼯:⼀段时间不⼲活,就被辞退).
• keepAliveTime:临时⼯允许的空闲时间.
• unit:keepaliveTime的时间单位,是秒,分钟,还是其他值.
• workQueue:传递任务的阻塞队列
• threadFactory:创建线程的⼯⼚,参与具体的创建线程⼯作.通过不同线程⼯⼚创建出的线程相当于对⼀些属性进⾏了不同的初始化设置.
• RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理.
◦ AbortPolicy():超过负荷,直接抛出异常.
◦ CallerRunsPolicy():调⽤者负责处理多出来的任务.
◦ DiscardOldestPolicy():丢弃队列中最⽼的任务.
◦ DiscardPolicy():丢弃新来的任务.
这两个线程池添加线程的方式都是用submit.
下面我们来自定义一个线程池
import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingDeque; import java.util.concurrent.BlockingQueue; public class MyThreadPoll { List<Thread> list = new ArrayList<>(); BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000); public MyThreadPoll(int n){ for (int i = 0; i < n; i++) { Thread t = new Thread(()->{ while(true){ Runnable task = null; try { task = queue.take(); task.run(); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); list.add(t); } } public void schedule(Runnable runnable) throws InterruptedException { queue.put(runnable); } }public class Maint_Tread { public static void main(String[] args) throws InterruptedException { MyThreadPoll myThreadPoll = new MyThreadPoll(4); for (int i = 0; i < 1000; i++) { int n =i;//实事final myThreadPoll.schedule(new Runnable() { @Override public void run() { System.out.println("任务i="+n+" 线程="+Thread.currentThread().getName()); } }); } } }
最后大家都有一个疑问:线程池的线程数量应该怎么确定呢
只要说出具体数字就是错的!!!!!