最近在复习多线程的相关知识,整理了一下笔记,有很多代码案例,给大家分享一下。
目录
synchronized (共享的数据){}:线程同步语句块
1、什么是进程和线程?
-
进程是一个应用程序(一个程序是一个进程)
-
线程是一个进程中的执行场景/执行单元(一个进程可以启动多个线程)(一个线程一个栈)
2、启动一个Java程序的进程和线程
-
当在DOS窗口中启动一个Java程序时,会先启动JVM,JVM是一个进程;JVM再启动一个主线程调用main方法;同时在启动一个垃圾回收线程负责看护、回收垃圾。
-
一个运行中的Java程序中,至少有两个线程并发,一个是垃圾回收机制,一个是执行main方法的主线程
3、存在问题:主线程结束,其他线程可能还在运行
4、实现线程有三种方式
-
方式一:继承java.lang.Thread类,重写run()方法
-
方式二:(建议)编写一个类,实现java.lang.Runnable接口,实现run()方法
-
方式三:FutureTask方式,实现Callable接口(jdk8新增)
-
注意:建议使用实现接口的方式,因为一个类实现接口,它还可以去继承其他的类,更加灵活
5、方式一:继承Thread类
-
run()方法:
-
该方法在线程创建成功后自动调用
-
该方法的内容是线程需要执行的功能
-
-
start()方法
-
调用start方法启动线程。
-
start方法的任务是开辟一个栈空间,空间开辟出来该方法就结束了(瞬间结束)。开辟成功后,线程就创建成功了,然后会自动调用run方法在分支栈的栈底部(压栈)
-
main方法中start方法不执行完毕不会继续执行下去,但是该方法是瞬间结束
-
-
代码展示
package com.bjpowernode.java.thread;
// 实现多线程的第一种方法:编写一个类,直接继承Thread类,并重写run方法
public class ThreadTest02 {
public static void main(String[] args) {
// 2、新建一个分支线程对象
MyThread myThread = new MyThread();
// 3、调用start方法启动线程。
myThread.start();
// 下面的这段代码运行在主线程中
for (int i=0; i<1000; i++){
System.out.println("主线程 ----> " + i);
}
}
}
// 1、定义线程类“编写一个类,直接继承Thread类,并重写run方法
class MyThread extends Thread {
@Override
public void run() {
// 编写程序。这段程序运行在分支线程(分支栈)中
for (int i=0; i<1000; i++){
System.out.println("分支线程 ----> " + i);
}
}
}
6、方式二:实现Runnable接口
-
代码展
package com.bjpowernode.java.thread;
// 实现线程的方式二:编写一个类,实现java.lang.Runnable接口,实现run()方法
public class ThreadTest03 {
public static void main(String[] args) {
// 2、3合并
Thread t = new Thread(new MyRunnable());//先创建一个可运行对象,再封装成一个线程对象
// 4、调用start方法启动线程。
t.start();
for (int i=0; i<100; i++){
System.out.println("主线程 ----> " + i);
}
}
}
// 1、实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0; i<100; i++){
System.out.println("分支线程 ----> " + i);
}
}
}
-
合并的说明
-
下面两句代码合并成最后那句代码
-
// 2、新建一个可运行对象
MyRunnable myRunnable = new MyRunnable();
// 3、将可运行的对象封装成一个线程对象
Thread t = new Thread(myRunnable);
// 2、3合并
Thread t = new Thread(new MyRunnable());//先创建一个可运行对象,再封装成一个线程对象
7、方式三:FutureTask方式
-
FutureTask方式实现的线程可以获取线程的返回值(前两种方式是无法获取线程返回值的,因为run方法返回的是null)
-
优点:可以拿到返回值;缺点:效率低
-
代码实例
package com.bjpowernode.java.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest15 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一步:创建一个”未来任务类“对象。FutureTask的参数需要一个Callable接口实现类对象
FutureTask task = new FutureTask(new Callable() {
@Override
// call方法相当于run方法,只不过call方法有返回值。线程执行完任务之后会有一个执行结果
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method end");
int a = 100;
int b = 200;
return a+b;
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 在主线程中获取t线程的返回结果
/**
* main方法里的程序要想继续执行必须等待get()方法执行结束,而get方法可能需要很久。
* 因为get方法想要拿到另一个线程的执行结果,就要等待该线程执行结束,而该线程的执行需要一定的时间
*/
Object obj = task.get();
System.out.println("hello world!");
}
}
8、采用匿名内部类实现多线程
-
代码展示
package com.bjpowernode.java.thread;
public class ThreadTest04 {
public static void main(String[] args) {
// 创建线程对象,采用匿名内部类的方式(匿名内部类没有类名,直接new类)
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);
}
}
}
9、线程的生命周期
-
五个状态:
-
新建状态
-
就绪状态
-
运行状态
-
阻塞状态
-
死亡状态
-
-
图
10、线程名字、当前线程对象
-
getName():获取线程对象的名字
String name = 线程对象.getName();
-
setName():修改线程对象的名字
线程对象.setName("线程名字");
-
默认的线程名字
-
主线程:main
-
分支现场:Thread-0、Thread-1、Thread-2、Thread-3……
-
currentThread():获取当前线程对象
-
currentThread()是一个静态方法
-
该代码出现在main方法中,所以当前线程是主线程,主线程的线程名字叫main
-
常使用该方法拿到当前线程对象,对当前线程进行操作
-
Thread 对象名 =Thread.currentThread();
-
代码展示
package com.bjpowernode.java.thread;
// 获取线程对象、获取线程对象的名字、修改线程对象的名字
public class ThreadTest05 {
public static void main(String[] args) {
// currentThread()获取当前线程对象(当前所运行的线程)
Thread currentThread =Thread.currentThread();
System.out.println("分支:" + currentThread.getName());
// 创建线程对象
MyThread2 t = new MyThread2();
// 获取默认的的线程名字
String tName1 = t.getName(); // Thread-0
System.out.println(tName1);
// 设置线程的名字
t.setName("tttt"); // tttt
// 获取修改后的线程名字
String tName2 = t.getName(); // tttt
System.out.println(tName2);
// 启动线程
t.start();
}
}
class MyThread2 extends Thread {
public void run(){
Thread currentThread =Thread.currentThread();
System.out.println("分支:" + currentThread.getName());
for (int i=0; i<10; i++){
System.out.println("分支线程 ----> " + i);
}
}
}
11、sleep()方法:让线程睡眠
-
static void sleep(long millis)方法
-
静态方法(Thread.sleep(毫秒);)
-
参数是毫秒
-
作用:让当前线程进入休眠状态,进入阻塞状态,放弃占有的CPU时间片,让其他线程使用
-
-
sleep():让线程睡眠
-
代码展示
-
public class ThreadTest06 {
public static void main(String[] args) throws InterruptedException {
for (int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
// 睡眠一秒
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
-
注意:
-
sleep是静态方法,t线程不会进入睡眠,而是让当前线程进入休眠,也就是main线程,因为运行时会转换为:Thread.sleep(1000*5)
-
public static void main(String[] args) {
try {
t.sleep(1000*5);// sleep是静态方法,t线程不会进入睡眠,而是让当前线程进入休眠,研究生main线程
// 因为运行时会转换为:Thread.sleep(1000*5)
}
}
-
interrupt():唤醒正在睡眠的线程
-
依靠了异常处理机制,睡眠语句会异常
-
run()方法中的异常不能throws,只能try catch。因为run()方法在父类中没有抛出如何异常,字类不能比父类抛出更多的异常
-
package com.bjpowernode.java.thread;
// 唤醒一个正在睡眠的线程(不是中断线程的执行,而是中断线程的睡眠)
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread((new MyRunnable2()));
t.setName("t");
t.start();
// 希望5秒之后唤醒t线程
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断t线程的睡眠(依靠了异常处理机制,睡眠语句会异常)
t.interrupt();// 干扰
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---->begin");
try {
Thread.sleep(1000*60*60*24*365);// 睡一年
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->end");
}
}
12、终止线程
-
stop():强行终止一个线程(过时,不建议使用)
-
这种方式存在很大的缺点:容易丢失数据,因为这种方式是直接杀死线程,如果还没有来得及保存数据会造成数据丢失
-
package com.bjpowernode.java.thread;
// stop()强行终止一个线程的执行,不建议使用
public class ThreadTest09 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable3());
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 5秒之后强行停止睡眠一年的t线程
t.stop();// 已过时,不建议使用
}
}
class MyRunnable3 implements Runnable{
@Override
public void run() {
for (int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
-
return:合理的方式结束进程
-
在run()方法中,打一个布尔标记,然后通过对布尔标记进行判断,满足终止线程条件时,调用return终止线程
-
在调用return之前,通过可以进行一些终止之前的准备,例如保存数据等
-
package com.bjpowernode.java.thread;
// 合理地终止一个线程的执行(常用)
public class ThreadTest010 {
public static void main(String[] args) {
MyRunable4 r = new MyRunable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程(当想要终止线程时,把run属性值修改为false即可)
r.run = false;
}
}
class MyRunable4 implements Runnable{
// 打一个标记
boolean run = true;
@Override
public void run() {
for (int i=0; i<10; i++){
if (run){
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// 在return之前可以进行一些线程结束的准备工作,如保存数据
System.out.println(Thread.currentThread().getName() + "线程结束了");
return;// 终止线程
}
}
}
}
13、线程调度(了解)
-
常用线程调度模型:
-
抢占式调度模型
-
那个线程的优先级比较高,抢到的CPU时间片的概率就更高。Java采用的就是抢占式调度模型
-
-
均分式调度模型
-
平均分配CPU时间,每个线程抢占到的CPU时间一样长。有一些编程语言采用这种方式
-
-
-
Java提供的和线程调度有关的方法
-
实例方法:
-
viod setPriority(int newPriority):设置线程的优先级
-
int getPriority():获取线程优先级
-
void join():合并线程
-
实例:在main方法中调用t.join();,则主线程进入阻塞,t线程执行,t线程执行结束后,主线程才会执行
-
-
说明:优先级是1至10的整数,默认优先级是5(理论上优先级高会获得更多的时间,但也不完全是)
-
-
静态方法:
-
static void yield():让位方法
-
暂停当前正在执行的线程对象,并执行其他线程。
-
该方法不是阻塞方法,让当前线程让位,让给其他线程使用。会让当前线程的”运行状态“回到”就绪状态“。回到就绪状态后,所有线程一起抢占,有概率继续抢到时间片
-
-
-
14、线程安全(重点)
-
为什么线程安全是重点?
-
在项目开发中,我们的项目是运行在服务器中,而服务器已经将线程的定义、线程对象的创建、线程的启动等都实现了,我们不需要编写多线程
-
我们编写的程序需要放在一个多线程的换季下运行,需要更需要关注的是数据在多线程并发的环境下是否是安全的
-
-
存在线程安全问题的三个条件(条件均需满足):
-
多线程并发
-
有共享数据
-
共享数据有修改行为
-
-
解决线程安全问题
-
当多线程并发,并存在需修改的共享数据时存在线程安全问题
-
解决方案:
-
线程同步机制:线程排队执行(不并发):用排队执行解决线程安全问题
-
线程排队会消耗一定的效率,效率换安全
-
-
-
线程同步的两个专业术语:
-
异步编程模型
-
线程1和线程2,个自执行各自的(独立执行),这种编程模型叫作异步编程模型
-
说白了就是多线程并发,效率较高
-
-
同步编程模型
-
线程1和线程2,在一个线程执行的时候,另一个必须等待正在执行的线程执行结束才会执行,这种编程模型就是同步编程模型
-
线程与线程之间有等待关系,效率较低
-
-
-
synchronized (共享的数据){}:线程同步语句块
-
语法
-
synchronized (共享的数据){
// 要执行的语句
}
-
说明:
-
同步代码块越小,效率越高
-
最好不要嵌套使用,容易导致死锁
-
-
举例:共享数据必须是多线程的数据,才能达到多线程排队当有t1-t5五个线程,123需排队,45不排队,则共享数据要是t1-t3共享的对象,而对于t4t5来说不是共享的
-
原理:
-
假设t1和t2线程并发,开始执行的时候,肯定会有先后
-
假设t1先执行,遇到synchronized语句块,这是会自动找共享对象的对象锁,找到之后占有对象锁,然后执行同步语句块中的代码。在程序执行的过程中,一直都占有对象锁,直到同步语句块执行结束,才归还对象锁(对象锁是一个标记,每个对象都有对象锁)
-
假设t1已经占有这把锁,此时t2也遇到synchronized,此时会去找共享对象的对象锁,但此时这把锁被t1占有,t2只能等待t1归还,然后t2占有对象锁,开始执行同步代码块中的程序
-
-
存在线程安全的变量
-
Java中的三大变量
-
实例变量:在堆中(存在线程安全问题)
-
静态变量:在方法区中(存在线程安全问题)
-
局部变量:在栈中(不存在线程安全问题)
-
-
三大变量中,局部变量永远不会存在线程安全问题,因为局部变量不共享(局部变量在栈中,一个线程一个栈)。实例变量在堆中,静态变量在方法区中,堆和方法区都是只有一个,所有堆和方法区都是多线程共享的,所以可能存在线程安全问题
-
如果使用局部变量,建议使用StringBuilder,因为局部变量不存在线程安全问题,所以选择StringBuilder,而StringBuffer效率比较低
-
-
synchronized可以出现在实例方法中(不常用)
-
但此时锁的一定是this,不能是其他对象,所以这种方式不灵活
-
synchronized出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低
-
优点是:代码量少
-
示范
-
public synchronized void doSome(){
// 代码块
}
-
总结:
-
synchronized有三种写法:
-
第一种:同步代码块
-
表示找对象锁,优点:灵活
-
-
第二种:在实例方法中使用synchronized
-
表示共享对象一定是this,并且同步代码块是整个方法
-
-
第三种:在静态方法上使用synchronized
-
表示找类锁。类锁永远只有一把
-
-
-
-
开发中解决线程安全问题的方案
-
方案一:
-
尽量使用”局部变“量代替”实例变量和静态变量“
-
-
方案二:
-
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存不共享(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有线程安全问题)
-
-
方案三:
-
如果不能使用局部变量,对象也不能创建多个,这个时候只能选择synchronized
-
-
注意:
-
不要一上来就使用synchronized,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,用户体验差。在不得已的情况下才选择synchronized线程同步机制
-
-
15、死锁
-
例子:线程t1需要锁对象1和对象2,而线程t2也需要锁对象1和对象2。如果t1锁了对象1,还没来得及锁对象2,t2就把对象2锁了,这样t1锁不到对象2,t2锁不到对象1,场面僵持不下,就造成了死锁
-
死锁代码:需要会写,面试时可能会让写
package com.bjpowernode.java.deadlock;
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2共享两个线程的o1和o2
Thread t1 = new MyThread1(o1, o2);
Thread t2 = new MyThread2(o1, o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1, Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
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(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
16、守护线程
-
Java语言中线程分为两大类
-
一类是:用户线程
-
一类是:守护线程(后台线程)
-
-
守护线程的特点:
-
一般守护线程是一个死循环,全部用户线程结束,守护线程会随之结束
-
-
setDaemon(true);:将线程设置为守护线程
线程对象.setDaemon(true);
17、定时器
-
作用:间隔特定的时间,执行特定的程序
-
多种实现方式:
-
方式1:可以使用sleep方法,通过设置睡眠时间来定时执行任务(这是最原始的计时器,比较low)
-
方式2:在Java的类库中,已经写好了一个定时器(java.util.Timer),可以直接使用。不过这种方式在开发中很少使用,因为很多高级框架都支持定时任务
-
方式3(常用):使用高级框架中的定时器。目前使用最多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时任务(SpringTask框架是基于java.util.Timer实现的)
-
实例
-
package com.bjpowernode.java.thread;
import javax.xml.crypto.Data;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.SimpleFormatter;
// 使用定时器指定定时任务
public class TimerTest {
public static void main(String[] args) throws ParseException {
// 创建定时器对象
Timer timer = new Timer();
// 可以设置为守护线程
//Timer timer = new Timer(true);
// 指定定时任务
// 语法:timer.schedule(定时任务, 第一次执行时间, 每次执行间隔的时间)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-09-25 21:23:00");
timer.schedule(new LogTimerTask(), firstTime, 1000*10);
}
}
// 编写一个定时任务类(也可以使用匿名内部类)
// 假设这是一个记录日志的定时任务
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 + ":成功完成一次数据备份!");
}
}
18、wait和otify(生产者和消费者模式)
-
wait和notfy方法不是线程对象的方法,是Java如何一个Java对象都有的方法,因为这两个方法是Object自带的
-
wait和notfy方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题
-
生产者和消费者模式
-
生产线程负责生产,消费者线程负责消费。生产线程和消费线程要达到均衡。这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和ontify方法
-
-
wait()方法
Object o = new Object();
o.wait();
-
作用:表示让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒(调用otify方法),会释放掉该线程之前占有的o对象上占有的锁
-
otify()方法
Object o = new Object();
o.otify();
-
作用:表示唤醒正在o对象上等待的线程。只是通知,不会释放o对象上之前占有的锁
-
notifyAll()方法
-
作用:notifyAll()方法可以唤醒o对象上处于等待的所有线程
-
-
图示
19、生产者和消费者模式
-
代码实例
package com.bjpowernode.java.thread;
import java.util.ArrayList;
import java.util.List;
// 使用wait方法和notify方法实现生产者和消费者模式
// 模拟需求:仓库是一个List集合,List集合中假设只能存储一个元素,只存一个元素仓库就满了。
// 如果List集合中元素个数是0,就表示仓库为空
// 保证List集合中永远都是最多存储一个元素
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){
// 当前线程进入等待状态,并且释放list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到此,说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "---->" + obj);
// 唤醒消费者进行消费
list.notify();
}
}
}
}
// 消费线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list){
this.list = list;
}
@Override
public void run() {
// 一直消费
while (true){
// 给仓库对象list加锁
synchronized (list){
if (list.size() == 0){
// 当前线程进入等待状态,并且释放list集合的锁
try {
// 仓库空了,消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "---->" + obj);
// 唤醒生产者生产
list.notify();
}
}
}
}