Java多线程学习笔记 | 多线程基础知识
一. 线程与进程
-
进程是一个应用程序,线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
-
在 java 程序运行时,先启动 JVM 进程,然后JVM进程将启动一个线程调用 main 方法。同时 JVM 还会启动一个GC线程。因此一个 Java 程序至少存在两个线程并发。
-
进程间内存独立不共享,而线程间堆内存与方法区内存共享,栈不共享,一个线程一个栈
-
主线程(执行main方法的线程)结束后,Java 程序进程可能任然在运行。因为主线程结束,其他线程依然可以继续运行。
二. Java中创建线程
1.编写继承 Thread 的类,重写 run 方法
package org.example;
public class ThreadTest {
public static void main(String args[]) {
MyThread myThread = new MyThread();
//start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间
//这段代码任务完成后,start()方法瞬间就结束了
//线程启动成功,自动调用run()方法,并且run()方法在分支栈底部
myThread.start();
for(int i = 0 ; i < 100 ; i++) {
//输出用于测试,主线程
System.out.println("The main thread:"+i);
}
}
}
class MyThread extends Thread {
@Override
void run() {
for(int i = 0 ; i < 100 ; i++) {
//输出用于测试,新创建的线程
System.out.println("The second thread:"+i);
}
}
}
- 注意
start()
方法的作用是分配新的分支栈并启动run()
方法(由JVM完成)。若直接在主方法中调用run()
方法只会执行在主线程的栈中执行run()
方法 start()
方法只有瞬间结束或者说不持续运行,才能执行start()
方法以下的主线程中的代码。
2. 编写实现 Runnable 的类,重写 run 方法
package org.example;
public class ThreadTest {
public static void main(String args[]) {
//创建可运行对象
MyRunnable myRunnable = new MyRunnable();
//将可运行对象传入线程类,使其封装为一个线程,从而执行线程
Thread myThread = new Thread(myRunnable);
myThread.start();
for(int i = 0 ; i < 100 ; i++) {
System.out.println("The main thread:" + i);
}
}
}
//定义一个实现Runnable接口的类重写run()方法
class MyRunnable implements Runnable {
@Override
void run() {
for(int i = 0 ; i < 100 ; i++) {
System.out.println("The second thread:" + i);
}
}
}
- 若使用接口的线程创建方式,则这个类可以继承其他类。
3. 其他方式的创建
-
匿名内部类创建线程
package org.example; public class ThreadTest { public static void main(String args[]) { //此处是使用实现了Runnable接口的匿名内部类对象创建了线程对象 Thread myThread = new Thread(new Runnable { public void run() { for(int i = 0 ; i < 100 ; i++) { System.out.println("The thread:" + i); } } }); } }
-
Lambda 表达式创建线程(Java8)
package org.example; public class ThreadTest { public static void main(String args[]) { //Thread接口只有一个待实现的方法,因此可视为函数接口来使用Lambda表达式 Thread myThread = new Thread(()->{ for(int i = 0 ; i < 100 ; i++) { System.out.println("The thread:" + i); } }); } }
三. 线程的生命周期
- 由 new 出来的线程对象,进入新建状态
- 调用线程对象的
start()
方法后,进入就绪状态,又称为可运行状态。此时的线程具有抢夺CPU时间片的权利,即抢夺执行权。 - 当一个处于就绪状态的线程抢夺到CPU时间片后,开始执行
run()
方法,run()
方法的执行标志着线程进入运行状态 - 当前的CPU时间片耗尽之后,线程重新进入就绪状态,抢夺CPU时间片。直到再次获得CPU时间片,则再次进入运行状态,接着
run()
方法上次运行的状态继续运行。就绪状态与运行状态的切换由JVM调度 - 若遇到阻塞事件,发生阻塞事件的线程进入阻塞状态。阻塞状态的线程会放弃之前占有的CPU时间片
- 若线程的阻塞事件结束,则该现场重新进入就绪状态,抢夺时间片
- 当
run()
方法运行结束,线程进入死亡状态
四. 获取线程对象
1. 线程名称
-
线程的默认名称
支线线程的默认名字规律为
Thread-0
Thread-1
…
Thread-n
按照创建的顺序进行命名
主线程的名字为:main
-
获取线程的名称
thread.getName()
-
设置线程的名称
thread.setName()
2. 获取线程对象
-
获取当前线程
通过 Thread 类中的静态方法
currentThread()
获取当前线程。Thread currentThread = Thread.currentThread();
该方法在什么线程中调用,得到的就是什么线程的对象。如在主线程中调用,获取的对象则为主线程对象。
五. 线程操作
1. 线程睡眠,阻塞状态
-
sleep方法
通过 Thread 中的静态方法
sleep()
来使线程进入休眠(阻塞)状态。sleep方法的参数值为休眠的毫秒数。在什么线程中调用该方法,什么线程就会进入休眠状态。try{ //静态方法 Thread.sleep(1000 * 5); }catch(InterruptedException e) { e.printStackTrace(); }
-
sleep阻塞唤醒
对线程对象进行
interrupt()
唤醒sleep的线程thread.interrupt();
在用
Interrupt()
方法唤醒后,在唤醒线程的sleep()
方法处将会出现 InterruptedException 异常
2. 线程终止
-
使用
stop()
强行终止线程thread.stop()
该方法是强行终止线程。该方法已过时,使用该方法会丢失数据。
-
使用传递值控制线程程序化的终止
在线程内创建一个变量,线程的执行会轮询该变量,若变量的值改变到某状态时,则程序化的结束run方法,从而实现程序化的结束线程。
package org.example; class MyRunnable implements Runnable { boolean isRun = true; @Override public void run() { for(int i = 0 ; i < 10 ; i++) { //如果在该线程运行的某时间段获取的isRun为false,则运行else语句块 if(isRun) { System.out.println("The thread is running" + i); try { Thread.sleep(1000 * 5); }catch (InterruptedException e){ e.printStackTrace(); } }else{ //在else语句块内可对该线程中的资源进行回收保存,从而避免数据的丢失 //run方法结束,线程进入死亡状态 return; } } } }
六. 线程调度模型
1. 常见的线程调度模型
-
抢占式调度模型
Java 的多线程采用的就是抢占式的调度模型,按照优先级的不同,优先级高的线程更有可能抢到CPU 时间片
-
均分式调度模型
平均分配 CPU 时间片。每个线程占有的 CPU 时间长度一样,平均分配
2. Java中的线程调度操作
-
Java线程的优先级
-
默认优先级,最低优先级,最高优先级
Java中的最高线程优先级为10,最低为1,默认为5
-
获取一个线程的优先级
thread.getPriority()
获取thread线程的优先级 -
设置一个线程的优先级
thread.setPriority(10)
设置线程thread的优先级为10
-
-
线程让步
static void yield()
让步方法的方法签名
调用方法与 sleep()
方法相同
Thread.yield()
让步方法的调用
暂停当前正在执行的线程对象(处于运行状态的线程),并执行其他线程。yield()
方法不会让线程进入阻塞状态而是让线程进入就绪状态。该线程的让步只是一瞬间,很快该线程又有机会抢夺到 CPU 时间片重新进入运行状态。
-
线程合并
thread.join()
线程合并的调用(在有别于 thread 线程的其他线程中调用)若在主线程中运行该代码,将会使得 thread 线程与主线程线程合并,主线程将会等待 thread 线程执行完成再继续执行该代码以下的主线程中剩下的任务。相当于单线程
该方法并不会消除 thread 线程栈的存在,而是等待 thread 线程运行结束
七. 多线程的安全
满足三个条件时,会出现多线程的数据安全问题:
- 多线程并发
- 存在多线程共享的数据
- 共享数据会被修改
在 Java 的三大变量(实例变量存储于堆中,静态变量存储于方法区,局部变量存储于栈中),局部变量不存在线程安全问题。因为局部变量在栈中不共享。
1. 线程同步
线程排队执行,此时多线程不能并发,只能排队等候执行。这种机制被称为线程同步机制
2. 同步编程模型,异步编程模型
-
异步编程模型
线程t1月t2,各自执行,互不干扰,不等候,这种编程模型被称为异步编程模型。即多线程并发,效率高
-
同步编程模型
线程t1与t2,在t1执行的时候,必须等待t2线程执行结束,或者说在t2执行的时候,必须等待t1的执行结束。即线程排队,效率较低。所谓同步就是排队。
3. Java中实现线程同步
使用关键字 synchronized 创建 synchronized 语句块来实现多线程操作数据时的线程同步
//synchronized语句块
synchronized(Object) {
//操作
}
-
小括号中填入的是线程共享的数据或对象。若要对t1与t2操作的数据或对象进行线程同步则填入的是t1与t2共同操作的数据对象。若是n个线程都进行同步,则填入n个线程将会共同访问的对象,进行线程同步。
-
实现原理
- 假设 t1 和 t2 并发,开始执行,直到 synchronized 语句块必然有先后。
- 假设 t1 先执行,遇到了 synchronized,这时候自动找共享对象的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在执行过程中一直都是占有这把对象锁。直到同步代码块结束,这把锁才释放。
- 假设 t1 占有这把锁,此时 t2 也遇到了 synchronized 关键字,也会去占有后面共享对象的这把锁,结果这把锁被 t1 占有,t2 只能等待 t1 执行完并释放该锁。直到锁释放,t2 得到这把锁,然后 t2 占有该锁并进入同步代码块执行程序
-
锁池 lockpool
运行当中的线程遇到了 synchronized 关键字,会放弃 CPU 时间片,进入 lockpool 中寻找共享对象的对象锁。若没有找到共享对象的对象锁,则在锁池中等待,直到找到共享对象锁(例如其他线程释放了共享对象的对象锁)。若找到了共享对象锁则进入就绪状态,继续抢夺 CPU 时间片
-
方法使用关键词 synchronized
在实例方法上可以使用 synchronized。若这么使用锁的共享对象一定是 this。
存在缺点:synchronized 出现在实例方法上,标识整个方法都需要同步,可能会出现无故扩大同步的范围,降低程序执行效率,所以这种方法不常用。
-
在静态方法上使用 synchronized
在静态方法上使用 synchronized 标识寻找类锁。类锁只有一把,与对象数无关。 而对象锁是100个对象100个对象锁,类锁则是一个类一个类锁。类锁用于保障静态变量的线程安全。
-
常用类的线程安全问题
说明 线程安全 非线程安全 集合类 Vector ArrayList 哈希表类 Hashtable HashMap HashSet 字符串类 StringBuffer StringBuilder … … … - 可根据类的原码,观察方法是否有 synchronized 来判断是否为线程安全的
- 若对应的类对象时作为局部变量不存在线程安全问题,则建议使用非线程安全的类。因为非线程安全的类执行效率上高于线程安全的类。因为线程安全的类即便作为局部变量也会在运行的过程中前往锁池寻找对象锁
-
synchronized 代码块的效率问题
因为 synchronized 代码块中的代码在一个线程执行的时候,另一个线程必须等待,因此synchronized 代码块中的代码越多(或代码所需的执行时间越长),另一个线程等待的时间越长(一般来说),从而影响运行效率。也就是说同步范围越大效率越低
八. 死锁
手写的死锁示例
package org.example;
public class DeadLock {
public static void main(String []args) {
//创建试验对象
Object o1 = new Object();
Object o2 = new Object();
//创建自定义的试验线程对象
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() {
//运行时,先会索要o1对象的锁,不释放,并索要o2对象的锁
synchronized(o1) {
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() {
//运行时,先会索要o2对象的锁,不释放,并索要o1对象的锁
synchronized(o2) {
synchronized(o1) {
}
}
}
}
-
死锁原理
- 线程A与线程B同时运行。开始线程A得到对象A的锁,线程B得到对象B的锁
- 线程A并未释放对象A的锁,继续索要对象B的锁,但对象B的锁已经被线程B获取,因此等待对象B的锁释放。与此同时线程B并未释放对象B的锁同时索要对象A的锁,但线程A并未释放对象A的锁所以进行等待。
- 此时线程A与线程B互相持有对方所需的锁不释放,形成锁死的局面,双方都陷入等待对方释放锁的局面。
九. 守护线程
所谓守护线程也就是后台线程。Java 中有两类线程,一类是用户线程,用于执行各种逻辑业务,例如 main 线程。一类是守护线程,在后台运行,例如 GC 的线程。
-
守护线程的特点
一般守护线程是一个死循环。一般的所有的用户线程结束,守护线程会自动结束
-
守护线程的实现
package org.example; public class ThreadTest { public static void main(String args[]) { Thread t = new DataRThread(); //设置线程t为守护线程,若其他用户线程结束则自动结束守护线程,即便守护线程是死循环,也会被强制结束 t.setDaemon(true); t.start(); } } class DataRThread extends Thread { public void run() { int i = 0; while(true) { System.out.println(Thread.currentThread().getName() + (++i)); try { Thread.sleep(1000); }catch (Exception e) { e.printStackTrace(); } } } }
十. 线程的其他相关功能
1. 定时器
使用java.util.Timer构建定时器
package org.example;
import java.util.Timeer;
public class TimerTest {
public static void main(String args[]) {
Timer timer = new Timer;
SimpleDateFormat now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Data firstTime = sdf.parse("2020-03-14 09:30:00");
//设置定时器的执行任务,首次执行时间,执行间隔
timer.schedule(new LogTimerTask(),firstTime,1000*10);
}
}
//定时任务类
//TimerTask为抽象类
class LogTimerTask extends TimerTask {
@Override
public void run() {
//此处编写要执行的任务
SimpleDateFormat now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = now.format(new Data());
Systme.out.println(strTime + "pLog");
}
}
2. 实现线程的第三种方式
实现 Callable 接口(JDK8新特性),相当于 Android 的异步线程任务(AsyncTask)。是系统委派一个线程去完成一个任务,该线程完成该任务后,可能会有一个执行结果,可以获取该结果。
实现 Callable 接口
package org.example;
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String args[]) {
//用匿名内部类创建线程任务
FutureTask task = new FutureTask(new Callable() {
@override
public Object call() throws Exception {
System.out.println("Call method begin");
Thread.sleep(1000*10);
System.out.println("Call method finish");
int a = 100;
int b = 200;
return a+b;//自动装箱为Integer对象
}
});
//创建执行任务的线程
Thread t = new Thread(task);
t.start();
//在主线程中获取异步线程任务执行结果
//主线程运行到这里为了获取异步线程的任务执行结果可能需要等待很久。主线程将进入阻塞状态
Object obj = task.get();
}
}
-
优点
与
run()
方法返回值为void不同,这种方法可以获取线程的执行结果。 -
缺点
这种线程方法会使得调用获取结果的线程陷入阻塞状态,效率较低。
3. 生产者与消费者模式
-
wait()
与notify()
方法wait()
与notify()
方法不是线程对象的方法,是 java 中任何一个 java 对象都有的方法,这两个方法在 Object 中。这两个方法不是通过线程对象调用。-
wait()
的作用Object o = new Object(); o.wait();
上述代码表示,让正在o对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止。
wait()
方法会让正在o对象上活动的线程进入等待状态,并释放之前占有的o对象的锁。 -
notify()
方法o.notify()
唤醒正在o对象上等待的线程。此外还有
notifyAll()
方法,可以唤醒o对象上处于等待的所有线程。notify()
方法只会通知线程,不会释放之前占有的o对象的锁。
-
-
生产者消费者模式
生产者和消费者模式是为了专门解决某个特定需求的
图示
-
生产者线程
生产者线程负责生产需要的资源,生产的资源进入资源仓库。
-
消费者线程
消费者线程负责从资源仓库中获取资源并进行消费,较少资源仓库中的资源。
-
资源仓库
资源仓库是共享资源,因此要注意线程安全问题。资源仓库有容量限制。仓库对象最终调用
wait()
与notify()
方法,而wait()
方法与notify()
方法建立在 synchronized 线程同步的基础上,保证线程数据安全。
-