目录
一、认识多线程
1.1 概念
(1) 线程是什么
(2)为啥要有线程?
(3) 进程和线程的区别
(4)Java 的线程和操作系统线程的关系
1.2 创建线程
方法1 继承 Thread 类
方法2 实现Runnable接口
使用Runnable定义任务有啥好处?
其他变形:
1.3 Thread 类及常见方法
(1)Thread 的常见构造方法
编辑
(2) Thread 的几个常见属性
(3)启动一个线程-start()
(4)中断一个线程
(5) 线程等待
(6) 获取当前线程
(7)休眠当前线程
1.4 线程的状态
二、多线程带来的风险--线程安全
2.1 什么是线程安全?
2.2 线程不安全的原因
(1) 修改共享数据
(2)线程是抢占是执行的
(3)原子性
(4)内存可见性
(5)有序性
三、Synchronized&volatile&wait¬ify的使用
3.1 Sychronized锁
Sychronized的使用
Sychronized的特性
3.2 volatile 关键字
3.3 wait 和 notify
wait()方法
notify()方法
notifyAll()方法
一、认识多线程
1.1 概念
(1) 线程是什么
一个线程就是一个 " 执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行着多份代码.
举个栗子:
我们设想如下场景:一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread )。
(2)为啥要有线程?
面试题:什么是并发?什么是并行?并发:一会干这件事,一会干那件事(同时只能干一件事)并行:一边干这件事,一边干那件事(真正意义上的同时进行)在编程方面,我们并不区分是并行还是并发,统一称为并发编程。
举个栗子:
(3) 进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
比如多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别 人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。
(4)Java 的线程和操作系统线程的关系
1.2 创建线程
方法1 继承 Thread 类
public class Demo01_Thread {
public static void main(String[] args) {
// 创建自己定义的线程对象
MyThread thread = new MyThread();
// 执行这个线程 start方法是启动线程,并通知操作系统加入CPU调度
thread.start();
}
}
// 继承Thread类并实现run方法
class MyThread extends Thread {
// run方法中的代码,就表示线程要执行的任务
@Override
public void run() {
System.out.println("hello thread...");
}
}
方法2 实现Runnable接口
public class Demo03_Runnable {
public static void main(String[] args) {
// 创建Runnable对象
MyRunnable runnable = new MyRunnable();
// 创建线程
Thread thread = new Thread(runnable);
// 启动线程,参与CPU调度
thread.start();
}
}
// 实现Runnable接口
class MyRunnable implements Runnable {
// 表示的是线程要执行的任务
@Override
public void run() {
while (true) {
System.out.println("生产皮包,金币+1...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
使用Runnable定义任务有啥好处?
- 解耦:把定义线程,与定义任务分开(把不同的功能都给分开,如果要修改或查找相应的功能的时候可以直接在指定的位置查找)
- 把创建线程,与定义任务分开,以便修改代码时,可以统一修改
其他变形:
-
匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象 Thread t1=new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } };
-
匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象 Thread t2=new Thread(newRunnable() { @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.3 Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。 -
(1)Thread 的常见构造方法
Thread t1=new Thread(); Thread t2=new Thread(newMyRunnable()); Thread t3=new Thread("这是我的名字"); Thread t4=new Thread(newMyRunnable(), "这是我的名字");
(2) Thread 的几个常见属性
public class Demo10_Properties {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("hello thread...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "大圣");
// 启动线程
thread.start();
// 打印Thread对象中的属性
System.out.println("线程Id = " + thread.getId());
System.out.println("线程名 = " + thread.getName());
System.out.println("线程状态 = " + thread.getState());
System.out.println("线程优先级 = " + thread.getPriority());
System.out.println("线程是否后台 = " + thread.isDaemon());
System.out.println("线程是否存活 = " + thread.isAlive());
System.out.println("线程是否中断 = " + thread.isInterrupted());
}
}
(3)启动一个线程-start()
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
(4)中断一个线程
public class Demo13_Interrupted01 {
// 定义一个中断标识
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!isQuit) {
System.out.println("hello thread...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程任务结束");
});
// 启动线程
thread.start();
// 主线程休眠3秒,模拟子线程正在处理任务
Thread.sleep(3000);
// 设置中断标志位为true
isQuit = true;
// 让子线程先结束
Thread.sleep(1000);
System.out.println("主线程结束");
}
}
public class Demo14_Interrupted02 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
// 注意判断条件
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 异常处理来中断线程
// 方式一:啥也不做
// 方式二:处理具体的逻辑
// 方式三:真正的中断
break;
}
}
System.out.println("是否中断:" + Thread.currentThread().isInterrupted());
System.out.println("线程任务结束");
});
// 启动线程
thread.start();
// 主线程休眠3秒,模拟子线程正在处理任务
Thread.sleep(3000);
// 中断线程,修改Thread中的中断标志
thread.interrupt();
// 让子线程先结束
Thread.sleep(1000);
System.out.println("主线程结束");
}
}
(5) 线程等待
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnabletarget= () -> {
for (inti=0; i<10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() +": 我结束了!");
};
Threadthread1=newThread(target, "李四");
Threadthread2=newThread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
(6) 获取当前线程
返回当前线程的引用
public class ThreadDemo {
public static void main(String[] args) {
Threadthread=Thread.currentThread();
System.out.println(thread.getName());
}
}
(7)休眠当前线程
public class ThreadDemo {
public static void main(String[] args) throwsInterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3*1000);
System.out.println(System.currentTimeMillis());
}
}
1.4 线程的状态
二、多线程带来的风险--线程安全
2.1 什么是线程安全?
2.2 线程不安全的原因
(1) 修改共享数据
多个线程修改了同一个共享变量
多个线程修改不同的变量,不会出现线程安全问题。多个线程读取同一个变量,也不会出现线程安全的问题,单线程环境下也不会出现线程安全问题。
(2)线程是抢占是执行的
多个线程在CPU上调度是随机的,顺序是不可预知的
(3)原子性
什么是原子性?
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
要么全都执行,要么全都不执行
(4)内存可见性
多线程环境下,某一线程修改的共享变量的值,另一个线程没有感受到最新的值
引入JAVA内存模型的概念JMM
1、主内存指的是硬件的内存条进程在启动的时候会申请一些资源,包括内存资源用来保存所有的变量,
2、工作内存指的是线程独有的内存空间,他们之间不能够相互访问,起到了线程之间内存隔离的作用,
3、JMM规定一个线程在修改某个变量的值时,必须把这个变量从主内存中加载到自己的工作内存,修改完成后再修,再刷新回主内存
4、每个工作内存之间是相互隔离的。
为什么要用JMM?
因为JAVA是一个跨平台的语言,把不同的计算设备和操作系统对内存的管理做了一个统一的封装。
(5)有序性
编译过程,JVM调用本地接口CPU执行指令过程中指令的有序性。指令在特殊情况下会打乱顺序,并不是按程序员的预期去执行的。
三、Synchronized&volatile&wait¬ify的使用
3.1 Sychronized锁
- 加锁之后可以把多线程并发执行并成单线程串行执行
- 由于是单线程执行的,所以第二个线程读到的值一定是第一个线程修改过的值,从而实现的内存可见
- Sychronized可以解决原子性内存可见性,但是不能解决有序性问题
Sychronized的使用
public class SynchronizedDemo {
public synchronized void methond() {
}
}
public class SynchronizedDemo {
public synchronized static void method() {
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
Sychronized的特性
3.2 volatile 关键字
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
保证内存可见性
static class Counter {
public int flag=0;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(() -> {
while (counter.flag==0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2=new Thread(() -> {
Scanner scanner=new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag=scanner.nextInt();
});
t1.start();
t2.start();}
不保证原子性
static class Counter {
volatile public int count=0;
void increase() {
count++;
}
}
public static void main(String[] args) throwsInterruptedException {
final Counter counter=new Counter();
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.
3.3 wait 和 notify
球场上的每个运动员都是独立的 " 执行流 " , 可以认为是一个 " 线程 ".而完成一个具体的进攻得分动作 , 则需要多个运动员相互配合 , 按照一定的顺序执行一定的动作 , 线程1 先 " 传球 " , 线程 2 才能 " 扣篮 ".
完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
wait()方法
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
public static void main(String[] args) throwsInterruptedException {
Object object=new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
notify()方法
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
sleep(long)让线程进入休眠状态 在指定的时间内不会被调度到CPU上执行
notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.