学习目的
- 了解进程与线程的概念和关系
- 了解java多线程
- 掌握java线程的创建与使用
- 了解并发与线程的关系
- 了解多线程与并发(高并发),并掌握如何处理高并发
- 掌握synchronized关键字
一、进程与线程
1.1 进程
- 概念
进程是计算机中的程序 关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的运行实体。 - 实质
一个进程就是一个应用程序,在操作系统中每启动一个应用程序就会相应的启动一个进程。
1.2 线程
- 概念
一个进程是一个运行程序,线程则是进程中的一个执行场景,线程就是运行程序的其中一部分功能。一个进程可以启动多个线程,如启动一个音乐播放器就是启动了一个进程,而打开音乐播放器的音乐播放、搜索音乐等功能属于同一个进程中的不同执行场景,即线程(播放与暂停属于进程的状态牵制)。 - 底层实质
线程是执行方法的最小单位,线程就是虚拟机栈,java中的方法都由线程执行。无论是哪个类哪个对象的方法,都由线程压栈执行,因此可自定义设置规定让线程按照规定执行方法。 - 特点
- 支持并发:在一个应用程序中,多个线程(执行单元)同时运行和处理程序就是并发;
1.2.1 单线程
- 概念
指的是一个进程(运行程序)中只有一个执行场景,单线程的进程中所有的程序功能(方法)都要由单一的一个线程来执行,因此导致大多数的功能方法需要等待当前的功能执行完释放才能轮到下一个功能执行。 - 缺点
只有一个执行场景,效率低下,如一个旅游景点(进程)只有一个售票窗口(线程)。 - 底层实质
一个线程就是JVM中的一个虚拟机栈,单线程就是该程序运行时只分配一个虚拟机栈。
1.2.2 多线程
- 概念
多线程指的是一个进程(运行程序)中有多个执行场景,多线程的进程中所有的程序功能(方法)可以分开由不同的线程来执行,因此采用多线程来完成一个程序的运行,可以提高运行效率。 - 底层实质
一个线程就是JVM中的一个虚拟机栈,多线程即JVM中多个虚拟机栈, -
多线程图解
1.2.3 用户线程
- 概念
用户线程就是用户编写,或用户可以看得见的线程,日常开发中经常遇见的都是用户线程,如main主线程和分支线程等。
1.2.4 守护线程
- 概念
守护线程又称为后台线程,属于JVM后台默默执行的线程,是用户看不见的线程,如垃圾回收器线程等。守护线程的目的就是守护用户线程的执行。 - 特点
- 死循环:守护线程是一个一直重复做某一件事情的线程;
- 自动结束:当所有的用户线程结束时,守护线程也会结束,因为守护线程守护的就是用户线程。
- 应用场景
- 数据备份:每天00:00的时候系统数据自动备份;
- 实现方式
将一个线程设置为守护线程,必须在该线程start()启动之前设置,采用线程对象.setDaemon(boolean on)方法就可以将该线程设置为守护线程。 - 实现实例
public class ThreadTest14 {
public static void main(String[] args) {
//创建一个分支线程对象
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将分支线程设置为守护线程
t.setDaemon(true);
//启动线程
t.start();
// 主线程执行:主线程是用户线程,用户线程结束,守护线程也结束
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循环,由于该线程设置为守护线程,当用户线程结束,守护线程也自动终止
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.3 线程和进程的区别与联系
- 联系
- 进程=运行时数据区:一个应用程序启动,等于启动一个进程,而每一个进程在JVM中都有一块属于自己的运行时数据区,其中运行时数据区中的每一个虚拟机栈 = 一个线程。这就意味着一个运行时数据区可以拥有多个虚拟机栈。
- 线程是进程的执行单元:
- 区别
- 内存资源不共享:每个进程之间都是独立的,进程之间的资源互不共享;
- 线程=栈:一个线程就是一个虚拟机栈,栈是独立的,资源不共享,但堆内存和方法区内存资源共享;
-
内存模型图解
二.线程实现
- 步骤
- 创建线程对象:重写Thread类或Runnable接口的run()方法;
- 启动线程:调用start()方法(开辟新的栈内存空间);
- 运行线程:自动调用run()方法。
- 实现方式
- 继承Thread类:并重写其run()方法,实例对象本身是可运行的线程;
- 实现Runnable接口:并重写其run()方法,实例对象本身不是可运行的线程,需要Thread类构造器将实例对象包装;
- Thread类构造器:在Thread类构造器参数中,使用匿名内部类创建出Runnable的对象,将该对象封装成可运行的线程;
- 实现Callable接口:
2.1 Thread类(java.lang.Thread)
- 概念
Thread类是java程序中的执行线程类,Java 虚拟机允许应用程序并发地运行多个执行线程(只要一个类继承Thread类就可以实现多线程并发)。 - 字段
- int NORM_PRIORITY = 5:分配给线程的默认优先级;
- int MAX_PRIORITY = 10:线程可以具有的最高优先级;
- int MIN_PRIORITY = 1:线程可以具有的最低优先级。
- 重要方法
Thread():无参构造,创建一个线程对象;
Thread(Runnable target):带参构造,创建一个线程对象(该对象将实现了Runnable接口的对象 包装成可运行的线程对象);
native Thread currentThread():返回对当前正在执行的线程对象,类似this返回当前对象;
synchronized void start():启动该线程,开辟一块栈内存空间(新线程),并让该线程对象就绪;
void run():若该线程是实现Runnable接口构造的,则调用Runnable对象的run()方法开始运行线程;否则,该方法不执行任何操作并返回。
void checkAccess():判定当前运行的线程是否有权修改该线程;
void interrupt():中断线程的休眠(唤醒sleep()的线程),如果当前线程没有中断它自己,则checkAccess()方法会被调用;
boolean interrupted():判断是否已中断该线程;
native void yield():暂停当前正在执行的线程对象,并执行其他线程;
native void sleep(long millis):静态方法,在指定毫秒数内 让当前正在执行的线程休眠(暂停执行让出时间片),休眠时间结束再继续执行;
public class ThreadTest05 {
public static void main(String[] args) {
//获取当前执行线程名称--Thread.currentThread()在哪里调用就是哪个线程对象
//String name = Thread.currentThread().getName();
//创建分支线程对象
Thread t = new Thread(new MyRunnable2());
//修改分支线程的名称
t.setName("tname");
System.out.println(t.getName());//tname
System.out.println(Thread.currentThread().getName());//main
//启动分支线程
t.start();
// 主线程休眠,希望5秒之后主线程休眠结束,t线程醒来
try {
Thread.sleep(1000 * 5);//主线程休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt(); //interrupt()执行,让sleep()抛出InterruptedException: sleep interrupted
}
}
class MyRunnable2 implements Runnable {
// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类Runnable中没有抛出任何异常,子类不能比父类抛出更多的异常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 分支线程睡眠1年(原本的设定,但可以打破)
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
//interrupt()执行,sleep()抛出InterruptedException并被catch,catch结束休眠结束
e.printStackTrace();
}
//分支线程1年之后才会执行这里(原本的设定)
System.out.println(Thread.currentThread().getName() + "---> end");
}
}
- 常用方法
- void setName(String name):set方法设置当前线程的名字,给线程命名;
- String getName():get方法,获取当前线程的名字;
- ThreadGroup getThreadGroup():返回该线程所属的线程组,若该线程已经终止(停止运行),则返回 null;
- void setPriority(int newPriority):更改线程的优先级,设置该线程可以优先抢占时间片;
- int getPriority():返回线程的优先级;
- void join():等待该线程终止;
- void suspend():已过时,挂起线程;
- void resume():已过时,重新开始(唤醒)挂起的进程;
- void exit():强制退出该线程;
- void setDaemon(boolean on):将该线程设置为守护线程或用户线程,当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程start()前调用。
public class ThreadTest10 {
public static void main(String[] args) {
//创建Runnable实现类对象
MyRunable4 r = new MyRunable4();
//创建线程对象--将Runnable包装成可执行线程对象
Thread t = new Thread(r);
t.setName("t");
//启动线程
t.start();
// 主线程休眠5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 合理终止线程--将修改标记为false
r.run = false;
//r.stop();//结束进程,不是结束线程,运行时整个进程结束会导致每个分支线程的数据可能丢失
}
}
class MyRunable4 implements Runnable {
// 分支线程可运行的标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
//ture说明分支线程可以执行
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// 遇到运行标记为false,分支线程结束,终止当前线程
// r.save();//保存数据
// return终止前,可以保存可能丢失的数据
return;
}
}
}
}
- 代码示例
//继承 Thread类并重写run()方法
public class MyThread extends Thread {
public void run() {
for (int i = 0;i<100;i++){
System.out.println("MyThread分支线程:" +i);
}
}
}
public static void main(String[] args) {
//创建线程对象
MyThread mt = new MyThread();
//启动多线程--start()即启动线程,调用后瞬间结束
mt.start();
//start()调用后会开辟一块新的虚拟机栈,而后自动调用run()方法压入栈,不需要手动调用run()
//mt.run();
for (int i =0;i<50;i++){
System.out.println("main主线程:" + i);
}
}
2.2 Runnable接口
概念
Runnable接口是java提供的,由那些 打算通过某一线程 执行其实例的类来实现。实现Runnable接口的类必须重写其run()方法,并且实现了Runnable接口的类创建出的对象 还不是可直接运行的线程对象,而是将该对象作为Thread类的构造参数进行包装返回,才是可执行的线程对象。目的
作为一个标记接口,为希望在活动时执行代码的对象 提供一个公共协议。重要方法
- void run():Runnable接口只有唯一的一个方法,实现Runnable接口的类必须重写该方法。
- 代码示例
//实现 Runnable接口并重写run()方法
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0;i<100;i++){
System.out.println("MyRunnable分支线程:" +i);
}
}
}
public static void main(String[] args) {
//创建Runnable对象--非可运行线程对象
MyRunnable mr = new MyRunnable();
//创建线程对象--将Runnable对象包装成可运行的线程对象
Thread t = new Thread(mr);
//Thread t = new Thread( new MyRunnable());//合并
//启动线程
t.start();
//start()调用后会开辟一块新的虚拟机栈,而后自动调用run()方法压入栈,不需要手动调用run()
//mt.run();
for (int i =0;i<50;i++){
System.out.println("main主线程:" + i);
}
}
2.3 Thread(匿名内部类)
概念
具体实现
Thread(new XXXRunnable(){})代码示例
//实现 Runnable接口并重写run()方法
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0;i<100;i++){
System.out.println("MyRunnable分支线程:" +i);
}
}
}
public static void main(String[] args) {
// 采用匿名内部类方式--创建线程对象
Thread t = new Thread( new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("t线程---> " + i);
}
}
});//匿名内部类
// 启动线程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("main线程---> " + i);
}
}
2.4 Callable接口(java.util.concurrent.Callable)
- 概念
JDK8新版本的特性,Callable接口属于java工具类中并发编程包下的一个接口。Callable接口类似于Runnable接口,作为一个参数来创建线程对象,不同的是Runnable接口实现类直接作为参数,而Callable接口先要作为"未来任务对象的参数",再通过未来任务来创建线程对象。 - 特点
- 有返回值:Callable接口在定义时拥有泛型,因此Callable接口的方法也拥有泛型返回值;
- 方法
- V call():Callable接口唯一的方法,拥有返回值,返回线程执行的结果。
2.4.1 FutureTask类(java.util.concurrent.FutureTask)
- 概念
FutureTask是实现了RunnableFuture接口(同Runnable接口)的一个"未来任务类",意为可被线程执行的未来任务。该类的实现主要是希望线程在未来通过该类能够完成某一部分功能。 - 特点
- 可抛出异常:
- 方法
- FutureTask(Callable
callable):带参构造,创建一个可被线程执行的未来任务对象; - V get():获取Callable线程对象执行完未来任务后,返回的执行结果;
- void run():
- 代码示例
public class CallableTest {
public static void main(String[] args) {
//创建未来任务对象 -- 匿名内部类方法
FutureTask ft = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("FutureTask begin...");
Thread.sleep(1000*3);
System.out.println("FutureTask over");
int i = 20;
int j = 20;
return i*j;//返回值类型是Object,此处int-->Integer-->Object
}
});
//创建分支线程对象 -- 执行未来任务
Thread t = new Thread(ft);
//启动线程
t.start();
//获取未来任务线程执行的返回值--在get()执行未获取得到返回值前,未来任务线程一直占据当前时间片
try {
Object o = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 未来任务线程执行get(),会导致主线程进入阻塞态
//判断FutureTask未来任务线程的执行是否阻塞主线程main
System.out.println("main begin");
}
}
//
class MyTask implements Callable{
}
三.线程生命周期
3.1 线程状态
新建态
新建态即是线程对象刚刚使用Thread类构造器new创建出来的状态,就绪态
当新建态的线程对象调用了start()方法后,该线程进入就绪态(仅是开辟栈内存空间,未压入方法)。
就绪态的线程拥有抢占CPU时间片的权利,当就绪态抢占到CPU分配的时间片后,准备线程执行。运行态
当已经就绪态的线程对象(自动)调用run()方法后,即就绪态抢到了CPU时间片,该线程进入运行态。
因为start()方法调用后,即开辟了一块新的栈内存空间(新开了一个线程),run()方法的调用则是和main()方法一样,始终是最先压入栈内存的底部最先执行。run()方法压入栈内存,意味着线程开始运行。阻塞态
当正在运行态的线程对象在正常运行过程中遇到用户输入(如Scanner)或 时间片被抢占时,线程对象由运行态进入阻塞态。
阻塞态的线程即是虽然还占有未完成的时间片,但是却不能做任何事情,直到时间片耗尽回到就绪态重新等待分配新的时间片。由阻塞态回到就绪态的线程对象重新获得时间片后,会继续执行未执行完的事情方法,而不是重头重零开始。挂起/死亡状态
当运行态的线程对象执行完run()方法,即run()方法从该线程栈内存的底部出栈时,意味着该线程执行结束,进入挂起状态(死亡状态)。
3.2 线程调度(模型)
JVM调度
线程由就绪态和运行态两种状态之间的不断切换,称为JVM的调度。抢占式调度模型
抢占式调度就是根据线程的优先级来抢占CPU时间片,优先级高的线程抢占CPU时间的概率会越高,java中采用的就是此种模型。均分式调度模型
均分式调度就是平均分配CPU时间片,每个线程占有的CPU时间片长度一样,一切平分平等。
3.3 线程调度与线程状态
- 新建态 --> 就绪态
- start()方法:创建出的线程对象执行start()方法后,启动线程区,等待抢占时间片;
- 就绪态 --> 运行态
- run()方法:
- 抢占得到时间片:
- 运行态 --> 就绪态
- 时间片到:每一次正常运行完占有的时间片,回到就绪态重新抢占;
- yield()方法:让出时间片,当前线程对象执行yield()方法后,给其他线程让出CPU,当前线程对象回到就绪态重新抢占时间片;
- 运行态 --> 阻塞态
- sleep():
- 接收用户输入:
- join()方法:执行join()方法的对象 加入到另外一个正在运行的线程当中,另外一个线程由运行态进入到阻塞态,而执行join()方法的对象开始运行直到执行结束;
- synchronized关键字:运行态 --> 对象锁池 --> 阻塞态。当一个正在运行的线程对象,执行遇到某个synchronized关键字修饰的代码块时,运行态进入线程共享对象的对象锁池,释放之前占有的时间片(释放后去寻找并占有对象锁,可能找到,也可能没找到),之后回到就绪态重新等待抢占时间片。
- 阻塞态 --> 就绪态
- 抢占运行态的时间片到:
- 运行态 --> 挂起/死亡态
- run()结束/出栈:run()方法是最先进入分支线程栈底部的方法,run()方法弹出说明该分支线程栈内无可执行方法,线程结束;
- stop():结束整个进程的应用程序(方法已过期)。
四.线程安全(重点掌握)
概念
在开发中,我们的项目是运行在服务器当中,而服务器是一个允许多线程并发执行的环境,当编写的项目程序需要在一个多线程的环境下运行时,项目中的数据就要考虑到线程安全的问题。产生线程安全问题的条件
- 多线程并发:
- 有共享数据:
- 共享数据存在修改的行为:
- 解决线程安全
- 局部变量替代:尽量使用局部变量代替“实例变量和静态变量”,局部变量在栈内存中(不共享,线程安全),实例变量和静态变量在堆内存和方法区(数据共享,不安全);
- 多个对象:如果必须是实例变量,可以创建多个对象,多个实例变量的内存不会共享(每个对象的堆内存地址不同)。一个线程对应1个对象,100个线程对应100个对象,对象地址不同不共享,数据安全;
- 线程同步机制:使用synchronized关键字实现线程同步,让所有线程排队执行,不能并发,用线程排队执行解决线程安全问题。
4.1 线程同步机制
- 实质
线程排队执行。如线程t1 和 线程t2,在线程t1执行时,t2线程必须等待t1执行结束;或者在t2线程执行时,t1线程必须等待t2执行结束,两个线程之间发生先后等待关系。 - 特点
线程排队执行,效率较低,以牺牲一部分效率保证数据安全。
4.2 线程异步机制
- 实质
线程并发执行。如线程t1 和 线程t2,同时各自执行各自的,t1不管t2,t2不管t1,无需等待,同时并发执行。 - 特点
多线程并发,执行效率较高。
4.3 变量与线程安全
- 实例变量
- 静态变量
- 局部变量:局部变量不存在线程安全问题,因为局部变量的作用域仅限于当前方法体中,出了该方法无效;
- 常量:常量不存在线程安全问题,因为常量使用final修饰,不可修改。
4.4 synchronized关键字(排他锁--锁对象)
概念
synchronized关键字是一个允许将其修饰的代码块或变量,由非线程安全变成线程安全的,主要是让其修饰的代码块或变量由进入同步模式,由线程同步执行(排队执行)。依靠点
synchronized的实现依靠java的对象锁机制, 在java中,任何一个对象都有"一把锁",这把锁就是标记,一般称为"对象锁"。1个对象对应1把锁,100个对象100把锁。synchronized(共享对象)的关键点在于"共享对象一定指明是哪些线程共享的"。执行原理
- 假设 t1线程 和 t2线程 并发,开始执行代码的时候,肯定有一个先一个后;
- 假设 t1线程先执行,遇到synchronized关键字,此时t1线程自动寻找synchronized()括号里面的线程共享对象 的对象锁;
找到之后并占有这把对象锁,然后执行同步代码块中的程序,并且在程序执行过程中一直占有这把锁,直到同步代码块代码结束,这把锁才会释放。 - 假设 t1线程已经占有对象锁,此时t2线程也开始执行并遇到synchronized关键字,同样去查找并占有共享对象的对象锁;
结果发现这把锁已被t1线程占有,t2只能在同步代码块外面等待t1执行,直到t1把同步代码块执行结束 归还对象锁; - t1线程已将同步代码块执行结束并归还对象锁,此时t2线程得到对象锁并开始占有,进入同步代码块执行程序;
- 以上步骤就是synchronized的线程同步排队执行原理。
- 作用
- 同步代码块:锁定对象锁,锁定(方法体内)指定代码块,让多个线程对该代码块同步执行(排队执行);
- 同步实例对象/变量/方法:锁定实例方法,相当于锁定当前对象;
- 同步静态代码块:锁定类锁,静态代码块属于类级别;
- 注意点
- synchronized不能嵌套使用:一个代码块中同时使用synchronized嵌套修饰,容易产生"死锁"。
- 非线程安全示例
//银行账户类
public class MyAccount {
//银行账号
private String acno;
//账户余额
private double balance;
//省略无参/带参构造方法和get/set方法
//取款方法
public double withdraw(double money){
double before = this.getBalance();
double after;
if (before < 100 | before < money){
System.out.println("你的余额不足!");
}
after = before - money;
this.setBalance(after);
System.out.println("本次成功取款" + money + "元");
return after;
}
//存款方法
public void save(double money){
double before = this.getBalance();
double after;
if (money < 100){
System.out.println("不好意思!请存入正数存款金额");
}
//存入后的金额 = 原账户余额 + 存入金额
after = before + money;
this.setBalance(after);
System.out.println("存款成功!剩余余额为:" + after + "元");
}
}
//对银行账户操作的线程
public class User extends Thread{
private MyAccount account;
//省略无参/带参构造方法和get/set方法
@Override
public void run() {
//假定每次取款2000,后期可以动态设置取款金额
double money = 2000;
//调用银行账户取款方法
account.withdraw(money);
System.out.println(Thread.currentThread().getName()
+ "线程本次对账户" + account.getAcno()
+ "取款" + money
+ ",余额" + account.getBalance());
}
}
//模拟多个线程对同一个银行账户对象并发操作
public class ThreadSafeTest01 {
public static void main(String[] args) {
//创建银行账户对象
MyAccount ma = new MyAccount("actno-001",10000);
//创建多个并发线程对同一个银行账户取款
Thread tt = new User(ma);
Thread th = new User(ma);
//给每个分支线程命名
tt.setName("TT");
th.setName("TH");
//启动线程--启动后自动调用run()
tt.start();
th.start();
}
}
4.4.1 synchronized代码块
- 特点
写法灵活。 - 关键
synchronized()后面小括号中传的这个“数据”必须是多线程共享的数据,才能达到多线程排队。 - 写法格式
synchronized(被线程共享的对象){
同步代码块;
}
//银行账户类
public class MyAccount {
//银行账号
private String acno;
//账户余额
private double balance;
//省略构造方法和get/set方法
//Object obj = new Object();//实例变量--可以线程共享
//取款方法--使用synchronized改进以上非线程安全代码块,使其同步
public double withdraw(double money){
//取款执行中,线程共享的是银行账户对象,因此在本类中共享的对象是当前对象this
//synchronized ("abc") { // "abc"在字符串常量池当中,常量可以共享
//synchronized (null) { // 线程的共享对象不能是空指针,抛异常
//synchronized (obj) { // obj属于实例变量,可以线程共享
//Object obj2 = new Object();// 在方法体中new = 局部变量
//synchronized (obj2) { // obj2是局部变量--不是线程的共享对象
synchronized (this){
double before = this.getBalance();
double after;
if (before < 100 | before < money){
System.out.println("你的余额不足!");
}
after = before - money;
this.setBalance(after);
System.out.println("本次成功取款" + money + "元");
return after;
}
}
}
4.4.2 synchronized实例变量/方法/对象
- 特点
synchronized修饰的实例方法,可以让该方法同步执行(线程排队执行),但是synchronized修饰实例方法,锁定的一定是当前对象this。 - 缺点
- 锁定当前对象this:synchronized修饰实例方法一定锁定当前对象,不够灵活;
- 同步范围扩大:synchronized修饰实例方法,可能导致方法体内本不需要同步的代码块也一并锁定,导致效率低下。
- 写法格式
public synchronized 返回类型 方法名(参数列表){
代码块
}
//取款方法 -- 使用synchronized关键字修饰以上实例方法,让该方法同步执行(线程排队执行)
public synchronized double withdraw(double money){
double before = this.getBalance();
double after;
if (before < 100 | before < money){
System.out.println("你的余额不足!");
}
after = before - money;
this.setBalance(after);
System.out.println("本次成功取款" + money + "元");
return after;
}
4.4.3 synchronized静态代码块
- 特点
synchronized不管修饰静态代码块、静态变量、静态方法,静态的都属于类级别,锁定的是类锁。类锁只有一个,对象锁则有多个(1个对象1个对象锁,100个对象100个对象锁),因此只要对静态成员使用synchronized,锁定的都是类对象。 - 写法格式
public class AAA{
// 锁静态变量--锁类对象,对所有线程同步
synchronized static int i;
//锁静态代码块--锁类对象,对所有线程同步
synchronized static{
同步代码块;
}
// 锁静态方法--锁类对象,对所有线程同步
public synchronized static void doSome(){
同步代码块;
}
}
4.4.4 synchronized嵌套与死锁
synchronized不能嵌套使用,一个代码块中同时使用synchronized嵌套修饰,容易产生"死锁"。发生"死锁时",编译不会报错,但也不会有输出结果,很难测试发现。
-
死锁图解
死锁代码示例
public class DeadLock {
public static void main(String[] args) {
// 创建线程共享的实例对象o1、o2
Object o1 = new Object();
Object o2 = new Object();
// 创建线程对象t1和t2,并共享实例对象o1、o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
// 启动两个分支线程对象o1、o2
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
// o1、o2两实例对象都对线程共享
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
// T1线程先锁住o1对象,再锁住o2对象
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
// T2线程先锁住o2对象,再锁住o1对象
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
4.5 锁
- 概念
4.5.1 排他锁
- 概念
4.5.2 互斥锁
- 概念
4.6 定时器(java.util.Timer)
- 概念
Timer定时器是java提供的一种工具,线程可以用定时器安排 以后在后台线程中执行的任务,可安排任务执行一次,或定期重复执行。 与每个 Timer对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。 - 应用场景
- 每周进行银行账户的总账操作;
- 每天进行数据的备份操作;
- Spring框架中提供的SpringTask框架底层就是Timer定时器。
- 实现原理
- Timer():无参构造,创建一个定时器对象;
- Timer(boolean isDaemon):带参构造,创建一个定时器对象并设置其状态为守护线程,"定时器就是线程";
- Timer(String name):带参构造,创建一个定时器对象并设置名称,底层是设置线程的名称并启动线程,"定时器就是线程";
- Timer(String name, boolean isDaemon):带参构造,创建一个定时器对象,同时设置其名称和状态为守护线程后 启动线程,"定时器就是线程";
- void schedule(TimerTask task, long delay):安排在指定的延迟时间delay毫秒后,执行指定的任务;
- void schedule(TimerTask task, Date time):安排在指定的时间,执行指定的任务;
- void schedule(TimerTask task, long delay, long period):安排指定的任务,从指定的延迟时间delay毫秒后开始执行任务,每隔指定间隔period毫秒重复执行;
- void schedule(TimerTask task, Date firstTime, long period):安排指定的任务,在指定的时间开始第一次执行任务,每隔指定间隔period毫秒重复执行。
- 实现方式
- 创建定时器对象:
- 给定时器对象安排定时任务,并指明定时器状态:
public class TimerTest {
public static void main(String[] args) throws Exception {
// 创建定时器对象
Timer timer = new Timer();
// 创建定时器对象,并指定守护线程执行
//Timer timer = new Timer(true);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//第一次执行的时间
Date firstTime = sdf.parse("2020-03-14 09:34:30");
// 指定定时任务
//timer.schedule(定时的任务, 第一次执行时间,执行间隔);
timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
//匿名内部类方式 -- 替代TimerTask类的继承和实现
timer.schedule(new TimerTask(){
@Override
public void run() {
// 定时执行的任务......
}
} , firstTime, 1000 * 10);
}
}
// 定时任务类,继承TimerTask类,并在该类中编写定时执行的任务程序
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 编写定时执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
// 提示在何时完成了定时任务
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}
- 存在问题
计时器任务应该迅速完成,如果完成某个计时器任务的时间太长,那么它会"独占"计时器的任务执行线程。因此,可能延迟后续任务的执行,而这些任务就可能"堆在一起"。
4.6.1 定时任务(java.util .TimerTask)
- 概念
定时任务即在指定时间内需要完成的任务,java提供的一个工具类TimerTask就是一个定时任务。 - 实质
TimerTask底层实现了Runnable接口,因此可以认为定时器任务TimerTask是一个"线程"。但是TimerTask是一个抽象类,没有具体实现,必须编写子类继承TimerTask并重写run()方法,在run()方法内编写具体要执行的定时任务。 - 扩展点
可以用匿名内部类替代TimerTask类的继承与实现。
4.7 wait()和notify()方法
- 使用
wait()和notify()方法是Object类的方法,不是线程对象的方法,任何继承于Object类的java对象自带这两个方法。因此wait()方法和notify()方法是通过Object对象.wait() 或 Object对象.notify()来调用,而不是通过线程对象调用。 - wait()方法
让正在Object对象上进行活动的线程进入等待状态,并且是无期限等待,直到被notify()唤醒为止。Object对象.wait()方法的调用,会让"当前线程"(正在obj对象上活动的线程)进入等待状态,实质是wait()让当前线程 释放占有的对象锁。 - notify()方法
notify()是唤醒正在Object对象上由wait()导致等待的线程,并不会释放锁也不会占有锁。还有一个notifyAll()方法,唤醒Object对象上处于等待的所有线程。 - 本质
wait()方法和notify()方法都是基于synchronized关键字实现的方法,但wait()和notify()针对的是线程对象,synchronized针对的是线程共享的对象。
4.7.1 生产者与消费者模式
- 概念
生产者和消费者是一种基于线程数量平衡的一种模式。为了安全性,共享对象上执行操作的线程不能太多;为了高效率,共享对象上执行操作的线程不能过少。因此需要实现一种一边生产线程一边消费线程的动态平衡。 - 原理
- 组成:存储数据的仓库(建议使用集合),生产线程,消费线程
- 必要条件:无论是生产线程还是消费线程,都是对仓库操作,要对仓库加锁
- 假设生产先行(也可以假设消费先行):生产前先对仓库中判断是否拥有可消费的数据,拥有可消费数据则wait()休眠等待并释放占有的锁,等候消费线程消费;若仓库没有可消费数据,创建新数据添加到仓库中,再notify()唤醒消费线程来消费。
-
唤醒消费线程后(也可以唤醒生产线程):消费之前先判断仓库中是否拥有可消费数据,没有可消费数据则wait()休眠等待并释放占有的锁,等待生产线程生产数据;若仓库中拥有可消费数据,则开始消费仓库数据,消费完notify()唤醒生产线程继续生产。
- 实现方式
- 生产者:生产者即生产在Object对象上进行操作的线程,生产出来的线程对象必须及时由消费者消费,否则堆积过多导致共同在Object对象操作--产生的安全问题也越多;
- 消费者:消费者即在Object对象上消费那些由生产者生产出来的线程,当生产的线程堆积过多时,必须由消费者消费,从而达到生消平衡。
- 代码示例
public class ThreadTest16 {
public static void main(String[] args) {
// 创建1个共享仓库对象
List list = new ArrayList();
// 创建线程对象:生产者线程
Thread t1 = new Thread(new Producer(list));
// 创建线程对象:消费者线程
Thread t2 = new Thread(new Consumer(list));
// 给分支线程命名
t1.setName("生产者线程");
t2.setName("消费者线程");
// 启动分支线程:启动时分支线程有前有后执行
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 共享的仓库对象(实例对象)
private List list;
// 构造器,传入共享线程对象
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
//模拟生产线程可以一直生产
while(true){
// 给仓库对象list加锁(保证线程安全)
synchronized (list){
if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素
try {
// 当前线程进入等待状态,并且释放生产线程Producer之前占有的list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到这里说明仓库是空的,可以生产线程加到仓库中
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 仓库中中拥有线程后,唤醒消费者进行消费(满足一边生产一边消费--不堆积)
list.notifyAll();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 共享的仓库对象(实例对象)
private List list;
// 构造器,传入共享线程对象
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
//模拟消费线程可以一直消费产生的线程
while(true){
synchronized (list) {
if(list.size() == 0){
try {
// 仓库已经空了,消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,可以进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 仓库中拥有线程后,唤醒消费者进行消费(满足一边生产一边消费--不堆积)
list.notifyAll();
}
}
}
}
五.并发 (重点掌握)
- 概念
并发指的是两件或以上的事情同时发生,在java中指的是一个进程(应用程序)中的多个线程(执行场景)同时发生执行,互不干扰。 - 特点
- 同时执行:多个线程并发即多个线程同时执行运行程序;
- 执行时间分配不均:虽然多个线程并发可以同时执行,但是由于计算机系统给每一个线程分配的可执行时间(时间片)不一样,或优先级不一样,导致并发的线程是交替占用计算机系统来执行的。
5.1 多线程与并发同步
- 描述
多线程的存在必定引起线程的并发执行,而并发执行必定引起线程的数据不安全问题,即一个进程中的内存和资源被并发的多个线程共享读/写。 - 原由
进程中共享的内存是堆内存和方法区内存,栈内存不共享,每个线程(就是一个独立的栈)有自己的栈内存。 - 解决多线程并发安全
- 多线程同步(即排序执行)
- 给并发的多个线程加锁,保证每个线程在并发执行时,数据没有互相影响,从而确保数据安全。
- 为每一个线程创建自己的操作对象,执行时每个线程对象的状态不再共享(每个线程拥有自己的堆内存、方法区内存)
- 多线程可以解决的问题
多个线程可以并发执行,节省程序以往串行执行(排队执行)的时间,提高程序的执行效率。如龟兔赛跑、追及问题,让龟和兔成为并发的线程同时进行,让追和及同时进行,而不是等前面一个执行完再到另外一个执行。 - 多线程并发常见场景
各大电商平台,巨大流量的用户同时进行搜索、浏览、查看详情、加入购物车、付款、退款等等。
常见面试题
- Thread类的run()方法 和 start()方法的区别?
答:
start()方法:继承Thread类的线程对象调用start()方法时,会在JVM运行时数据区开辟一块新的栈区,即启动一个线程;并且start()调用后,开辟栈内存后瞬间结束;
-
run()方法:当一个线程对象调用了start()方法,strat()方法开辟栈内存后立刻结束,继而自动调用线程对象重写的run()方法;因此新的线程启动后(新栈内存开辟后),第一个压入新栈的方法就是run(),因此run()方法和主线程的main()有一样的优先级,会压入栈内存底部。
-
不执行start()直接执行run():若没有执行start()方法前,就直接执行线程对象重写的run()方法,将不会开辟新的栈内存空间,也就不会创建新的线程,因此run()方法还是在原来的栈内存(main主线程)中运行。即没有并发执行,只是单线程执行。
sleep()方法的作用?
答:
sleep()方法是Thread类中的一个静态方法,属于类级别。因此无论是哪一个Thread对象调用sleep()方法,只会让sleep()所在位置的当前线程休眠,与Thread对象无关。sleep()在哪里出现,哪个线程就会休眠,且sleep()带的参数为线程休眠的毫秒数。interrupt()方法的作用?
答:
interrupt()方法是Thread类中的方法,用于中断当前线程的执行,经常用于唤醒一个正在sleep()休眠的线程。
interrupt()方法的实现依靠Java异常机制,因为interrupt()方法一旦执行,会临时让sleep()方法的执行抛出异常,由于在sleep()方法调用的位置本身已进行try...catch捕获异常,所以sleep()方法出现异常try...catch就停止,sleep()方法也就终止(线程休眠中断)。从而让之前休眠的线程重新醒来执行干活。synchronized关键字的作用?
synchronized关键字和volatile关键字得区别?
答:
- volatile关键字:线程同步的轻量级实现,volatile性能比synchronized关键字要好;
volatile关键字只能用于变量;多线程访问volatile关键字不会发生阻塞,volatile关键字主要用于解决变量在多个线程之间的可见性;volatile关键字能保证数据的可见性和有序性,但不能保证数据的原子性。 - synchronized关键字:synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁 和 释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized关键字的场景还是更多一些;
synchronized关键字可以修饰方法以及代码块;多线程访问synchronized关键字可能会发生阻塞,synchronized关键字解决的是多个线程之间访问资源的同步性;synchronized关键字都能保证数据的可见性、有序性和原子性。
- 使用synchronized关键字模拟"死锁"的实现?
答:
- 存在两个对线程共享的实例变量
- 两个实例变量使用synchronized关键字嵌套加锁
- 存在两个线程对象,两线程都对两个实例对象同步(但每个线程对实例对象同步的顺序不一样)
//线程对象A共享两个实例对象
public class MyThread1 extends Thread{
Object obj1;
Object obj2;
public MyThread1(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
public void run(){
// 该线程先锁住o1对象,再锁住o2对象
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
//线程对象B共享两个实例对象
public class MyThread2 extends Thread{
Object obj1;
Object obj2;
public MyThread2(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
public void run(){
// 该线程先锁住o2对象,再锁住o1对象
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
- 使用多线程完成奇偶数的交替输出,t1线程只输出奇数,t2线程只输出偶数?
答:
- 两个线程,对一个数字Num进行操作(Num为共享)
- t1线程奇数输出,偶数等待
- t2线程偶数输出,奇数等待
- Num可以完成自增++