学习内容:多线程
文章目录
1. 线程的概述
1、什么是进程?什么是线程?
进程是一个应用程序。线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
2、进程和线程是什么关系?
进程可以看做是现实生活当中的公司。
线程可以看做是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
java中之所有多线程机制,目的就是为了提高程序的处理效率。
2. 多线程
1、什么是多线程并发?
t1线程执行t1的,t2线程执行t2的。t1和t2不会相互影响。
单核的CPU表示只有一个“大脑”,但是可以做到给人一种“多线程并发的感觉”。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时在做。
2、java语言中,实现线程有三种方式。
(java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就行了)
第一种方式:编写一个类,直接继承java.lang.Thread
,重写run方法。
/*
实现线程的第一种方式:
编写一个类,直接继承`java.lang.Thread`,重写run方法。
怎么创建线程对象? 直接new就行
怎么启动线程?调用线程对象的start()方法
*/
public class Test01 {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行
//新建一个分支线程对象
MyThread myThread=new MyThread();
//启动线程
//myThread.run();//不会启动线程,不会分配新的分支栈
//start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间(这段代码瞬间就结束了)
//启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部
myThread.start();
//这里的代码还是运行在主线程中
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.Runnable
接口,实现run方法。
/*
实现线程的第二种方式:
编写一个类,实现`java.lang.Runnable`接口
*/
public class Test02 {
public static void main(String[] args) {
//创建一个可运行的对象
//MyRunnable r=new MyRunnable();
//将可运行的对象封装成一个线程对象
//Thread t=new Thread(r);
Thread t=new Thread(new MyRunnable());//合并上面两行代码
//启动线程
t.start();
for (int i=0;i<100;i++){
System.out.println("主线程:--->"+i);
}
}
}
//这并不是一个线程类,是一个可运行的类,它还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("分支线程:--->"+i);
}
}
}
也可以采用匿名内部类方式:
public class Test03 {
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("分支线程:--->"+i);
}
}
});
t.start();
for (int i=0;i<100;i++){
System.out.println("主线程:--->"+i);
}
}
}
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活。
(第三种方法在后面)
3、线程的生命周期
3. 常用方法
3.1 void setName(String name)
修改线程对象的名字
3.2 String getName()
获取线程对象的名字
3.3 static Thread currentThread()
获取当前线程对象
public class Test01 {
public static void main(String[] args) {
//创建线程对象
MyThread mt=new MyThread();
//获取线程的名字(这里的名字是默认的)
String mtName=mt.getName();
System.out.println(mtName);//Thread-0
//设置线程名字
mt.setName("One");
String s=mt.getName();
System.out.println(s);//One
//启动线程
mt.start();
MyThread mt2=new MyThread();
System.out.println(mt2.getName());//Thread-1
//获取当前线程
//currentThread就是当前线程对象
//这个代码出现在main方法中,使用当前线程就是主线程
Thread currentThread=Thread.currentThread();
System.out.println(currentThread.getName());//main
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
//currentThread就是当前线程对象
//当mt执行run方法,当前线程就是mt
//当mt2执行run方法,当前线程就是mt2
Thread currentThread=Thread.currentThread();
System.out.println(currentThread.getName()+"--->"+i);
}
}
}
当线程没有设置名字的时候,默认的名字有什么规律?
Thread-0
Thread-1
Thread-2
…
3.4 static void sleep(long millis)
:
1、静态方法
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用
public class Test02 {
public static void main(String[] args) {
//让当前线程进入休眠,睡眠5秒
//当前线程是主线程
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World");
}
}
3.5 void interrupt()
中断线程的睡眠
public class Test02 {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.setName("t");
t.start();
//中断t线程的睡眠(这种中断睡眠的方式依靠了java的异常处理机制)
t.interrupt();
}
}
class MyRunnable implements Runnable{
//run()方法当中的异常不能throws,只能try catch
//因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->begin");
try {
//睡眠1年
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
e.printStackTrace();//打印异常信息
}
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName()+"--->end");
}
}
3.6 void stop()
强行终止线程(缺点:容易丢失数据。因为这种方式是直接将线程杀死了,没有保存的数据将会丢失,不建议使用)
怎么合理的终止一个线程的执行:
public class Test03 {
public static void main(String[] args) {
MyRunnable2 r=new MyRunnable2();
Thread t=new Thread(r);
t.setName("t");
t.start();
//终止线程
r.run=false;
}
}
class MyRunnable2 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);
}else {
//终止当前线程
return;
}
}
}
}
4. 线程安全问题
什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:
条件1:多线程并发
条件2:有共享数据
条件3:共享数据有修改的行为
可以使用“线程同步机制”(线程排队执行,不能并发)去解决。但是会牺牲一部分效率。
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,其实就是多线程并发(效率高)
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
模拟两个线程对同一个账户取款:
(未使用线程同步机制)
public class Account {
//账号
private String actno;
//余额
private double balance;
//取款的方法
public void withdraw(double money){
//t1和t2并发这个方法(t1和t2是两个栈。两个栈操作堆中同一个对象)
//取款之前的余额
double before=this.getBalance();//10000
//取款之后的余额
double after=before-money;
//在这里模拟一下网络延迟,100%会出问题
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这里了,但是还没来得及执行这行代码,t2线程进来withdraw方法了,此时一定出问题。
this.setBalance(after);
}
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 class AccountThread extends Thread{
//两个线程必须共享同步一个账户对象
private Account act;
//通过构造方法传递过来账户对象
public AccountThread(Account act){
this.act=act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money=5000;
//取款
act.withdraw(money);
System.out.println(Thread.currentThread().getName()+"对账户"+act.getActno()+"取款成功,余额"+act.getBalance());
}
}
public class Test01 {
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();
t2.start();
}
}
使用线程同步机制:
线程同步机制的语法是:
synchronized (){
//线程同步代码块
}
这个小括号里面传入的数据必须是多线程共享的数据,才能达到多线程排队
(其他代码同上,只用修改一下代码)
public void withdraw(double money){
//以下这几个代码必须是线程排队的,不能并发
//一个线程把这里的代码全部执行结束之后,另一个线程才能进来
/*
线程同步机制的语法是:
synchronized (){
//线程同步代码块
}
这个小括号里面传入的数据必须是多线程共享的数据,才能达到多线程排队
*/
//这里的共享对象是账户
//不一定是this,只要是多线程共享的对象就行
synchronized (this){
double before=this.getBalance();//10000
double after=before-money;
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁)
代码执行原理:
1、假设t1和t2线程并发,开始执行以上代码的时候,肯定有一个先一个后。
2、假设t1先执行,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有了这把锁,然后执行代码块中的程序,在程序执行过程中一种都是占有这把锁的。直到同步代码块代码执行结束,这把锁才会归还。
3、假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有“后面共享对象”的这把锁,结果锁被t1占有,t2只能在同步代码快外面等待t1的结束,知道t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后 t2占有这把锁之后,进入同步代码块执行程序。
就类似于t1和t2去卫生间洗澡,t1先进去洗,t2就只能在外面等t1洗完了再进去洗。
这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
哪些变量有线程安全问题?
实例变量:在堆中
静态变量:在方法区中
局部变量:在栈中
以上三大变量中:局部变量永远都不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中。
实例变量在堆中,堆只有一个
静态变量在方法区中,方法区只有一个
堆和方法去都是多线程共享的,使用可能存在线程安全问题。
synchronized有三种写法:
第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上面使用synchronized表示共享对象一定是this,并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized,表示找类锁,类锁永远只有一把。
5. 死锁概述
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个例子:
甲拿了A号密室的钥匙进了A号密室,乙拿了B号密室的钥匙进了B号密室。
甲在A号密室里面得打开一个箱子,但是打开箱子需要B号密室的钥匙(在乙身上)。
乙在B号密室里面得打开一个箱子,但是打开箱子需要A号密室的钥匙(在甲身上)。
他们只有打开箱子之后才能拿到出来的信息,但是打开箱子所需要的钥匙都在对方身上,于是就是都只能在密室里面等着,也出不来。
6. 守护线程概述
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
守护线程的特点:
一般守护线程是一个死循环,所以的用户线程只要结束,守护线程自动化结束。
(注意:主线程main方法是一个用户线程)
public class Test02 {
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();
}
}
}
}
7. 定时器概述
定时器作用:间隔特定的时间,执行特定的程序。
java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,等到整个时间点醒来,执行任务。这种方式是最原始的定时器(比较low)
在java的类库种已经写好了一个定时器,java.util.Timer。可以直接拿来用。
目前使用较多的是Spring框架种提供的SpringTask框架,整个框架只要进行简单的配置,就可以完成定时器的功能
public class Test03 {
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("2021-12-09 16:20:00");//这里异常因为繁琐就直接抛了,
timer.schedule(null,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+":成功完成了 一次数据备份");
}
}
8. 实现线程的第三种方式:实现Callable接口(jdk8新特性)
这种方式实现的线程可以获取线程的返回值。
上面两种方式是无法获取线程返回值的,因为run方法返回void。
例子:
系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到整个执行结果呢?
public class Test04 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个“未来任务类对象”
//参数非常重要,需要给一个Callable接口的实现类对象
FutureTask task=new FutureTask(new Callable() {
@Override
//call()方法就相当于run方法。只不过整个有返回值
public Object call() throws Exception {
//线程执行一个任务,执行之后可能会有一个执行结果
//模拟执行
int a=100;
int b=200;
return a+b;//自动装箱
}
});
//创建线程对象
Thread t=new Thread(task);
//启动线程
t.start();
//这里是main方法,这是在主线程中
//在主线程中怎么获取t线程的返回结果?
//get()方法的执行会导致“当前线程阻塞”
//它会一直等到上面的call方法结束了,获取到返回值了才继续往下执行代码
Object obj=task.get();
}
}
了解内容
了解内容:关于线程的调度
1.1、常见的线程调度模型有哪些?
抢占式调度模型:
哪个线程的优先级比较高,强盗的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。
均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片长度一样。平均分配,一切平等。
有一些编程语言,线程调度模型采用的是这种方式。
1.2、java中提供 哪些方法是和线程调度有关系的呢?
实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
最低优先级1
默认优先级是5
最高优先级10
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield() 方法不是阻塞方法。让当前线程让位,让给其他线程使用。
yield() 方法的执行会让当前线程从“运行状态”回到“就绪状态”。
实例方法:
void join() 合并线程
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t=new MyThread2();
t.join();//当前线程进入阻塞,t线程执行,知道t线程结束。当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}