多线程
本章多线程原理能够在操作系统学习记录中找到原理:咖啡ice的操作系统学习记录
1.多线程概述
- 进程:是一个应用程序,或者说是一个软件。进程之间内存的资源不会共享。
- 线程:是进程中的执行场景/执行单元。一个进程可以启动多个线程。线程之间堆内存和方法区内存共享,但栈内存独立,一个线程一个栈。
2.实现多线程方式
-
第一种:编写一个类,直接继承java.lang.Thread,重写run方法。
创建线程对象:new
启动线程:调用线程对象的start()方法
-
注:start方法作用:启动一个分支线程,在JVM中开辟一个新的栈空间,启动完这段代码就结束了,启动成功后新的栈空间会自动调用run方法开始执行自己的任务,且run方法在新的分支栈底部。
-
run方法在分支栈底部,main方法在主栈底部,run和main是平级的。
/*
实现线程的第一种方式:
编写一个类,继承java.lang.Thread,重写run方法
*/
public class ThreadTest {
public static void main(String[] args) {
//这里的main方法,这里代码属于主线程,在主栈中运行
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start(); //start方法作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码启动完就结束了,新的栈空间会开始执行它的任务。
//如果直接调用MyThread.run(),会导致一直执行到run()结束才执行下面的代码,导致不能并发。
//其他代码继续在主线程中执行
for (int i = 0; i <1000; i++) {
System.out.println("主线程——>" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)。
for (int i = 0; i <1000; i++) {
System.out.println("分支线程——>" + i);
}
}
}
- 第二种:编写一个类,实现java.lang.Runable接口,实现run方法。
- 一般使用实现接口的方法,还能够继承(extends)其他类。
public class ThreadTest {
public static void main(String[] args) {
//创建一个可运行的对象,封装成一个线程对象
Thread t = new Thread(new MyRunable());
//启动线程
t.start();
//其他代码继续在主线程中执行
for (int i = 0; i <100; i++) {
System.out.println("主线程——>" + i);
}
}
}
//这不是一个线程类,只是一个可运行的类。不是一个线程。
class MyRunable implements Runnable{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)。
for (int i = 0; i <100; i++) {
System.out.println("分支线程——>" + i);
}
}
}
通过常用匿名内部类实现
public class ThreadTest {
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);
}
}
}
-
第三种:实现Callable接口(JDK8新特性),这种方式优点能够获取返回执行结果,缺点是获取线程执行结果时,本线程会阻塞执行效率低。
这种方式实现线程可以获取线程的返回值,前两种方式的run返回void无法获取返回值。
package studyThread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; //属于JAVA的并发包。
//实现线程的Callable接口
public class ThreadTest13 {
public static void main(String[] args) throws Exception{
//创建一个“未来任务”类对象
//这里需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { //call()方法相当于run方法,只不过这里有返回值。
//线程执行一个任务,执行完会有一个结果。
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method over");
int a = 100;
int b = 200;
return a + b;
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//这里main方法是在主线程当中
//在主线程中获取线程的返回结果使用get()方法
System.out.println(task.get());
//main方法下面的程序想要执行必须等待get()方法结束
//get方法可能需要很久,因为get方法为了拿另一个线程的执行结果。另一个线程需要时间。
//get方法执行会导致当前线程执行阻塞
System.out.println("get()获取完毕");
}
}
3.线程生命周期
- 刚new出来的线程对象为新建状态(创建状态)。
- 调用了start方法进入就绪状态。就绪状态的线程有抢夺CPU时间片(执行权)的权利。
- 当一个线程抢夺到CPU时间片后,执行run方法,标志着线程进入运行状态。
- 当占有的CPU时间片用完后,会线程会重新回到就绪状态继续抢夺CPU时间片,再次得到CPU时间片时会重新进入run方法接着上一次代码继续往下执行。
- run方法执行结束,标志着线程进入死亡状态(终止状态)。
- 当遇到阻塞事件,例如要接受用户键盘输入或者sleep方法等,程序会从执行状态进入到阻塞状态,放弃当前占用的CPU时间片。
- 阻塞解除之后,线程会进入到就绪状态。
本小结涉及到操作系统中的进程管理:操作系统学习第二章进程管理
4.获取线程名字
- 获取线程名字:线程对象.t.getName()
- 设置线程名字:线程对象.setName(“线程名字”);
- 当线程没有设置名字时,默认名字:Thread-0、Thread-1、……
public class ThreadTest {
public static void main(String[] args) {
//创建线程对象
MyThread2 t = new MyThread2();//默认名字:Thread-0
//设置线程名字
t.setName("t1");//修改为t1
//获取线程的名字
System.out.println(t.getName()); //t1
MyThread2 t2 = new MyThread2(); //默认名字:Thread-1
//不修改名称
System.out.println(t2.getName()); //Thread-1
}
}
class MyThread2 extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println("分支线程——>" + i);
}
}
}
- 获取当前线程对象:static Thread currentThread()
public class ThreadTest {
public static void main(String[] args) {
//currentThread表示当前线程对象
//这个代码出现在main方法当中,所以当前线程就是主线程。
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()); //main
//创建线程对象
MyThread2 t = new MyThread2();//默认名字:Thread-0
//设置线程名字
t.setName("t1");//修改为t1
t.start();
//获取线程的名字
System.out.println(t.getName()); //t1
MyThread2 t2 = new MyThread2(); //默认名字:Thread-1
t2.start();
//不修改名称
System.out.println(t2.getName()); //Thread-1
}
}
class MyThread2 extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() + "——>" + i);
}
}
}
5.线程的休眠和终止
- static void sleep(long millis):静态方法、参数是毫秒,作用是让当前进程进入休眠(阻塞)状态,放弃CPU时间片让给其他线程使用。
public class ThreadTest {
public static void main(String[] args) {
//让当前进程进入休眠,睡眠时间5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("咖啡加点ice"); //5秒之后才会打印
}
}
- 可以通过此方法,执行一段特定的代码,每隔一段时间执行一次。
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "——>" + i);
//睡眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- sleep()这是一个静态方法,在哪个线程调用那个线程进入休眠。
public class ThreadTest {
public static void main(String[] args) {
//创建线程对象
Thread t = new MyThread3();
t.setName("t");
t.start();
//调用sleep方法
try {
t.sleep(1000*5); //sleep作为静态方法,在这里调用t.sleep会转换为:Thread.sleep(1000*5)
//这段代码是让当前线程进入休眠,即main方法休眠
//这方法出现在在main方法中,是main方法休眠,而不是t休眠
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程"); //5秒钟后才能打印
}
}
class MyThread3 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("分支线程——>" + i);
}
}
}
- sleep()休眠时间过长,如果需要提前终止线程的休眠:interrupt()
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunable2());
t.setName("t");
t.start();
//希望5秒后t线程终止休眠
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//终止t线程的睡眠,这种中断睡眠的方式,依靠了Java的异常处理机制
t.interrupt(); //这个方法调用,t中的sleep会捕获到异常,就会提前结束
}
}
class MyRunable2 implements Runnable{
//重点:run()方法当中的异常不能throws,只能try.catch
//因为run()方法在父类中没有任何异常,子类不能比父类抛出更多的异常
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "——> begin" );
try {
//睡眠一小时
Thread.sleep(1000*60*60);
} catch (InterruptedException e) {
//打印异常信息
e.printStackTrace();
}
//1小时后才会执行这里
System.out.println(Thread.currentThread().getName() + "——> over" );
}
}
-
强制终止线程:stop(),此方法容易丢失数据,不建议使用。
-
合理的终止一个线程执行,通过设置一个标记进行终止。
public class ThreadTest {
public static void main(String[] args) {
MyRunnable3 r = new MyRunnable3();
Thread t = new Thread(r);
t.setName("t");
t.start();
//主程序延迟5秒执行t线程的终止
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒到了,强制终止t线程
//t.stop();会直接丢失数据不建议使用
r.run=false;
}
}
class MyRunnable3 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) {
throw new RuntimeException(e);
}
}else {
//return就结束了,在return将需要保存的数据进行保存。
return;
}
}
}
}
6.线程调度
-
常见的线程调度模型:
抢占式调度模型:线程的优先级比较高,抢到的CPU时间片概率高。Java采用的就是抢占式调度模型。
均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配一切平等。
-
java中提供与线程调度有关的方法:
-
实例方法
更改线程优先级:void setPriority(int newPriority)
获取线程优先级:int getPriority()
等待某个线程结束:void join()
//线程合并 public class ThreadTest { public static void main(String[] args) { System.out.println("main begin"); Thread t = new Thread(new MyRunnable5()); t.setName("t"); t.start(); try { t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行到结束 } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("main over"); //t执行结束才会执行这里。 } } class MyRunnable5 implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "——>" +i); } } }
最低优先级1常量:MAX_PRIORITY
默认优先级5常量:MIN__PRIORITY
最高优先级10常量:NORM__PRIORITY
//关于线程优先级 public class ThreadTest { public static void main(String[] args) { System.out.println("最高优先级:" + MAX_PRIORITY); //最高优先级:10 System.out.println("最低优先级:" + MIN_PRIORITY); //最低优先级:1 System.out.println("默认优先级:" + NORM_PRIORITY); //默认优先级:5 //获取当前线程对象,获取当前线程优先级 Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority()); //main线程的默认优先级是5 Thread t = new Thread(new MyRunable3()); t.setName("t"); t.start(); } } class MyRunable3 implements Runnable{ @Override public void run() { //获取当前线程优先级 System.out.println(Thread.currentThread().getName() + "线程没有设置时的优先级是:" + Thread.currentThread().getPriority()); //t线程没有设置时的优先级是:5 } }
-
静态方法
暂停当时线程,执行其他线程:static void yield()
注:yield方法不是阻塞方法,让当前线程让位从运行状态回到就绪状态,将CPU时间片让给其他线程使用。
//让当前进行暂停,回到就绪状态,让给其他线程 //静态方法:Thread.yield(); public class ThreadTest { public static void main(String[] args) { Thread t = new Thread(new MyRunnable4()); t.setName("t"); t.start(); for (int i = 1; i <=10000; i++) { System.out.println(Thread.currentThread().getName() + " ——> " + i); } } } class MyRunnable4 implements Runnable { @Override public void run() { for (int i = 1; i <=10000; i++) { //每100让位一次 if(i % 100 == 0){ Thread.yield(); //当前线程暂停一下,让给主线程 } System.out.println(Thread.currentThread().getName() + " ——> " + i); } } }
-
7.synchronized线程同步
-
线程安全:在多线程并发环境下,数据的安全问题。
-
多线程并发环境下存在安全问题的条件:①多线程并发、②有共享数据、③共享数据有修改的行为。
-
线程同步:线程排队执行,用排队执行解决线程安全问题。
-
同步与异步:
异步(并发)模型:线程t1与线程t2互相不影响,各自执行。即多线程并发。
同步(排队)模型:线程t1和线程t2之间,一个线程在执行时,另一个线程要等待前面线程执行结束才能执行。线程排队执行。
-
synchronized(共享对象){ }:假设t1与t2线程并发,t1遇到了synchronized,就占用这把锁,直到代码执行结束这把锁才会释放,在未释放前,t2遇到synchronized只能等待t1执行结束synchronized里边的代码才能够执行synchronized里边的代码。
【例子】模拟两个线程对同一个账户取款。
银行账户设置了线程安全synchronized(){ },如果没有设置线程安全将会出现数据安全问题。
//银行账户
public class Account {
//账号
private String actno;
//余额
private double balance;
public Account() { }
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() { return actno; }
public void setActno(String actno) { this.actno = actno; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
//取款方法
public void withdrad(double money){
//线程同步机制
//synchronized ()括号中数据必须是多线程共享的数据,才能达到线程同步。
//()里填需要线程同步的共享对象
synchronized (this){
//线程同步机制代码块
//一个线程把这里代码全部执行结束后,另一个线程才能进来。
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(after>=0){
this.setBalance(after);
}else {
System.out.println("余额不足");
}
}
}
}
设置线程模拟取款
public class AccountThread extends Thread{
//两个线程必须共享一个账户对象
private Account act;
//通过构造方法传过来账户对象
public AccountThread(Account act){
this.act = act;
}
public void run() {
//run表示取款操作
//假设取款5000
double money = 5000;
//取款
act.withdrad(money);
System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款成功,余额:" + act.getBalance());
}
}
实现测试类
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建一个)
Account act = new Account("act-001",10000);
//创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置线程name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start(); //t1对act-001取款成功,余额:5000.0
t2.start(); //t2对act-001取款成功,余额:0.0
}
}
- 可以直接在实例方法上加上synchronized,有个缺点是会扩大同步的范围,导致程序的执行效率低。
//取款方法
public synchronized void withdrad(double money){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(after>=0){
this.setBalance(after);
}else {
System.out.println("余额不足");
}
}
-
synchronized使用在实例方法上,代码比较简洁了,如果共享的对象是this,并且需要同步的是整个方法体,使用这种方式。
-
synchronized出现在静态方法上(带static方法),是类锁,不管创建几个对象这个方法同步。
8.守护线程
- 守护线程:这是一个死循环,所有用户线程只要结束,守护线程自动结束。
package studyThread;
public class ThreadTest {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("这是一个守护线程");
//在启动线程之前将线程设置成一个守护线程,
t.setDaemon(true);//设置成守护线程之后,如果这里线程结束,即使是死循环线程也会结束
t.start();
//主线程:主线程是用户线程
//在没有设置守护线程时,主线程的for循环结束后,死循环的线程还会继续执行
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "——>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//写一个死循环的线程
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) {
throw new RuntimeException(e);
}
}
}
}
9.定时器
-
定时器的作用:间隔特定的时间执行特定的程序。
-
定时器在java中实现方式:
第一种:使用sleep方法,睡眠。设置睡眠时间,每到这个时间点醒来执行任务。
第二种:java.util.Timer可以直接用,目前开发很少用,很多高级框架支持定时任务。
第三种:目前使用最多的是Spring框架中的SpringTask框架。
public class TimerTest {
public static void main(String[] args) throws Exception {
//创建定时器对象
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("2024-01-22 16:39: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");
System.out.println(sdf.format(new Date()) + ":完成了一次定时任务");
}
}
——本章节为个人学习笔记。学习视频为动力节点Java零基础教程视频:动力节点—JAVA零基础教程视频