举个例子:
DOS窗口运行java HelloWorld,先启动JVM,JVM是一个进程,JVM启动一个主线程调用main方法,同时再启动一个垃圾回收线程来负责看护、回收垃圾。(也就是说Java程序至少两线程并发,main方法对应的主线程+GC)
把进程看作是现实生活中的公司,如京东。线程则可看作是其下的某一个职能部门,负责完成某任务,如开发部门。
如启动了10个线程,就会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
🍁Java中的多线程机制,目的就是为了提高程序的处理效率, 如火车站看成是一个进程,则每个售票小窗口就是一个个线程,甲在窗口1买票,乙在窗口2买票,谁也不用等谁 一个个售票窗口就像一个个栈,有自己独立的空间。售票大厅这个共用空间就像堆和方法区。
引入多线程以后,main方法的结束,不再意味着程序的结束。main方法结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈
真正的多线程并发是:t1线程执行t1,t2线程执行t2,t1不影响t2,t2不影响t1。4核的CPU,在同一时间点,可以真正的有4个进程并发执行,单核的CPU,在某一个时间点上实际只能处理一件事情,但由于CPU的处理速度极快,多个线程之间频繁切换,从而造成了多个事情在同时处理的视觉假象。
public class Thread1 {public static void main(String[] args) {m1(); }public static void m1(){m2(); }public static void m2(){m3();}public static void m3(){System.out.println("m3 excute");}}
分析以上:只有一个主线程,主栈,没有分支线程被启动
编写一个类,直接继承java.lang.Thread,重写run方法
class MyThread extends Thread{//这段代码运行在分支线程中public void run(){for(int i=0;i<100;i++){System.out.println("分支线程" + i);}}
}
public class ThreadTest{public static void main(String[] args) {//创建分支线程对象MyThread myThread = new MyThread();//启动分支线程myThread.start();//这里仍然运行在主线程当中for(int i=0;i<100;i++){System.out.println("主线程" + i);}}
}
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这个任务完成后,这段代码就瞬间执行结束了
启动成功后的线程会自动调用我重写的run方法,并且run方法在分支栈的栈底部(main方法在主栈的底部,故run和main是平级的)
myThread.run();
如果直接调用我重写的run方法,则不会启动线程,不会分配新的分支栈,此时,就是单线程了。
运行结果:
编写一个类,实现java.lang.Runable接口,重写run方法
public class ThreadTest {public static void main(String[] args) {//创建一个可运行的对象MyRunnable r = new MyRunnable();//给Thread类的构造方法传入Runnable类的对象//将可运行的对象封装成一个线程对象Thread t = new Thread(r);t.start();for(int i=0;i<100;i++){System.out.println("主线程"+ i);}}
}//这不是线程,仅仅是一个可运行的类
class MyRunnable implements Runnable{public void run(){for(int i=0;i<100;i++){System.out.println("分支线程"+i);}}
}
总结线程的实现:
🍁编写一个类继承Thread类并重写run方法
public class MyThread extends Thread{public void run(){}
}MyThread t = new MyThread();
t.start();
🍁编写一个类,实现Runnable接口并重写run方法
public class MyRunnbale implements Runnable{public void run(){}
}Thread t = new Thread(new MyRunnable());
t.start();
由于Java的单继承,第一种方式中,不能再继承别的类了,而第二种可以,面向接口编程更优。
//方式二的匿名内部类写法:Thread t2 = new Thread(new MyRunnable(){public void run(){for(int i=0;i<100;i++){System.out.println(i);}}});
run()方法中要是有异常,也只能捕捉,不能上抛,因为run方法在父类中没有抛出任何异常,做为子类,重写时不能比父类抛出更多的异常。
🍁 新建状态:
刚new出来的线程对象
🍁就绪状态:
又叫做可运行状态,表示当前线程具有抢夺CPU时间片的能力(CPU时间片就是执行权)当一个线程抢夺到CPU时间片后,开始执行run方法,run方法的执行标志着线程进入运行状态。
🍁运行状态:
run方法开始执行,线程进入运行状态,当之前占有的CPU时间片用完之后,重新回到就绪状态继续抢夺CPU时间片,待抢到后,重新进入run方法上次执行的地方继续执行
🍁死亡状态:
run方法执行结束,线程到达死亡状态
🍁阻塞状态:
当一个线程遇到阻塞事件,如接收用户键盘输入,sleep方法,则进入阻塞状态,此时线程会放弃之前抢占到的CPU时间片
获取线程的名字getName()
MyThread myThread = new MyThread();
String tName = myThread.getName();
//Thread-0
System.out.println(tName);
//更改
myThread.setName("code-9527 's Thread");
System.out.println(myThread.getName());
获取当前线程对象currentThread()
//静态方法获取当前线程对象,返回一个Thread类型对象
Thread currentThread = Thread.currentThread();
//返回线程名
System.out.println(currentThread.getName());
//毫秒
static void sleep(Long millis);
作用是让当前线程进入休眠,进入阻塞状态,放弃占有CPU时间片,让给其他线程去使用,出现的地方,对应的线程休眠
public static void main(String[] args) {try{Thread.sleep(1000*5); //让当前线程(main)休眠五秒}catch(InterruptedException e){e.printStackTrace();}//五秒后被输出System.out.println("sleep结束");
}
实现间隔特定的时间去执行一段特定的代码
sleep是静态方法,若上面的Thread.sleep改成:
Thread t = new MyThread();
...
t.sleep(1000*5);
执行的时候,t.sleep(1000*5);还是会被当作Thread.sleep(1000*5);,被休眠的也还是main线程,而不是t线程。
t.interrupt();
干扰,即中断t线程的睡眠,执行后sleep()出现异常,即catch(InterruptedException e),这种中断睡眠状态的方式,依靠的时Java的异常处理机制。
class MyThread extends Thread{public void run(){try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}for(int i=0;i<100;i++){System.out.println("分支线程" + i);}}
}
class ThreadTest1{public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();//线程myThread计划sleep5秒//扔出interrupt后就提前醒来了myThread.interrupt();}
}
线程对象的引用.stop()
stop方法已过时,容易丢数据。这种方式是直接将线程杀死了,线程没有保存的数据会丢失
MyThread myThread = new MyThread();
myThread.start();
//终止
myThread.stop();
class MyRun implements Runnable{boolean run = true;public void run() {for (int i = 0; i < 100; i++) {if (this.run) {System.out.println(Thread.currentThread().getName() + "--->" + i);System.out.println(this.run);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}} else {System.out.println("这是一些终止线程前要做的事");System.out.println("保存数据中..终止线程成功!");return;}}}
}
public class ThreadTest2 {public static void main(String[] args) {MyRun r = new MyRun();Thread t = new Thread(r);t.start();//sleep主线程三秒try{Thread.sleep(3000);}catch(InterruptedException e){e.printStackTrace();}//终止,改run属性为falser.run = false;}
}
运行效果:
🍁抢占式调度模型:
哪个线程的优先级比较高,抢到CPU时间片的概率就高一些/多一些。Java中采用的就是抢占式调度模型。
🍁均分式调度模型:
平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样
void setPriority(int newPriority)
int getPriority()
//最低优先级为1
static int MIN_PRIORITY
//默认优先级为5
static int NORM_PRIORITY
//最高优先级为10
static int MAX_PRIORITYSystem.out.println(Thread.MAX_PRIORITY);
举例:
Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName()+"的优先级是:"+ currentThread.getPriority());currentThread.setPriority(9);
//静态方法
static void yield()
暂停当前正在执行的线程对象,去执行其他线程,yield方法的执行会让当前线程从运行状态进入就绪状态 ,注意不是阻塞状态。
class MyRunnable2{public void run(){for(int i=0;i<101;i++){//每循环10次,让当前线程暂停让位一下if(i%10 == 0){Thread.yield();}System.out.println(Thread.currentThread().getName() + i);}}
}
实例方法void join(),注意线程合并不是栈的合并
MyThread t = new MyThread();
//让当前线程阻塞,t线程执行,直到t线程执行结束,当前线程才执行
t.join();
线程安全问题的产生条件:
线程安全问题的解决–线程同步机制:
线程同步即线程排队执行,不能并发了(可能会牺牲一部分效率,但数据安全是一切的前提)
🍁异步模型:
异步就是并发,线程t1和线程t2各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,即多线程并发,效率较高。
🍁同步模型:
线程t1和t2,t1执行的时候,必须等待t2执行结束,效率较低。
❀❀❀账户安全问题的代码模拟:
/*** 账户类*/
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;}/*** 取款方法* @param money*/public void withdraw(double money){double before = this.getBalance();double after = before - money;//别立即更新余额,使用休眠模拟网络延迟try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}this.setBalance(after);}
}
public class AccountThread extends Thread{//该类的对象有Account属性//某人“有一个账户”private Account account;//通过构造方法传递账户对象public AccountThread(Account account){this.account = account;}public void run(){double money = 5000;account.withdraw(money);System.out.println(Thread.currentThread().getName() + "对账户:" + account.getActno() +"取款:" + money + ",余额:"+ account.getBalance());}
}class Test{public static void main(String[] args) {Account account = new Account("act-001",10000);//两个线程共用一个账户对象AccountThread t1 = new AccountThread(account);AccountThread t2 = new AccountThread(account);t1.setName("t1");t2.setName("t2");t1.start();t2.start();}
}
运行结果:
🍁语法:
synchronized(){
…
线程同步代码块
…}
小括号中传的是多个线程共享的对象,若有t1、t2、t3、t4、t5线程,t1、t2、t3需要排队,t4、t5不用,则()中是一个t1、t2、t3共享的对象,而这个对象t4、t5不共享
由此,上面例题中的取款方法变为:
public void withdraw(double money){synchronized(this) {double before = this.getBalance();double after = before - money;try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}}
简单的说就是synchronized内部放的是要排队执行的代码块
🍁对象锁:
在Java中,任何一个对象都有“一把锁”,这把锁其本质是一个标记,一个对象一把锁。
当运行状态的线程遇到synchronized关键字:
🍺例:
当t1和t2线程并发:
t1先遇到synchronized,自动找所共享对象的对象锁,找到之后,占有这把锁,然后执行同步代码块中的代码, 直到同步代码块执行结束,这个锁才释放
—>>>
t1占有对象锁后,t2线程若也遇到了synchronized,在找对象锁时,发现被t1占有,则t2在同步代码块外等待t1结束并释放对象锁后,再占有对象锁、执行同步代码块
存在于堆区中的实例变量、存在于方法区中的静态变量,因为堆和方法区均只有一个,且是多线程共享的,有可能存在安全问题。
局部变量存在于栈区中,永远不会有线程安全问题,因为一个线程一个栈。
改为:
public synchronized void withdraw(double money){double before = this.getBalance();double after = before - money;try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}this.setBalance(after);}
这样写的缺点:
🍁
synchronized(线程共享对象){同步代码块;
}
🍁
在实例方法中使用synchronized,表示共享的对象一定是this,并且同步的代码块是整个方法体
🍁
在静态方法中使用synchronized,表示找类锁,类锁永远只有1把(对象锁是100个对象就有100个对象锁)
t1线程执行某同步代码块,用到了对象1和2,即t1线程需要先锁对象1,再锁对象2,全锁以后,算同步代码块执行结束,然后一下释放两个对象锁
t2线程执行另一个同步代码块,需要先锁对象2,再锁对象1才算这个同步代码块执行结束,然后释放两个对象锁。
如此:
t1锁到对象2的时候,发现已被锁,则等待,而另一边:t2锁到对象1的时候,发现对象1已被锁,两个线程同时陷入无休止的等待…尬住了
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{//别着急锁o2,为了保证死锁必现,这里等两秒Thread.sleep(2000);}catch(InterruptedException e){e.printStackTrace();}//synchronized的嵌套//从而实现:对象o1和o2都锁了才算同步代码块结束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(2000);}catch(InterruptedException e){e.printStackTrace();}synchronized(o1){}}}
}public class DeadLock{public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();//启动两个线程,共用对象o1和o2MyThread1 t1 = new MyThread1(o1,o2);MyThread2 t2 = new MyThread2(o1,o2);t1.start();t2.start();}
}
运行结果:
死锁发生后,程序不出异常,也不报错,会一直僵持着,不易发现并调试。
关于synchronized死锁和线程安全的优化:
synchronized会让程序执行效率变低,系统吞吐量降低,用户体验变差。解决线程安全,可考虑:
Java中,线程分为两大类:
守护线程的特点是:
一般守护线程是一个死循环,所有用户线程结束的时候,守护线程自动结束
通过实例方法setDaemon:
public class DaemonTest {public static void main(String[] args) {Thread t = new BackupThread();t.setName("备份守护线程");//传入true,则普通线程变守护线程t.setDaemon(true);t.start();}}
class BackupThread extends Thread{public void run(){//要进行的守护动作写在run方法中即可int i = 0;while(true){//即使是死循环,但做为守护线程,当所有线程都结束的时候,也会自动结束}}
}
🍁作用:
间隔特定的时间,执行特定的程序
🍁应用场景:
如每周进行银行账户的总账操作,每天进行数据库的备份操作
🍁实现方式:
用到的java.util.Timer类中的方法
Timer timer = new Timer();
//传入true,表示以守护线程的方式
Timer timer = new Timer(true);
/**安排任务从指定时间开始进行重复固定的延迟执行
* TimerTask是一个抽象类
* Date firstTime即第一次执行的时间
* Long period 即间隔多少毫秒
*/
void schedule(TimerTask task, Date firstTime , Long period)//安排任务在指定时间执行任务task
void schedule(TimerTask task, Date time)
❀代码实现–编写一个定时器任务类记录日志
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;public class LogTimerTask extends TimerTask {/*** 重写父类TimerTask中的抽象方法run* TimerTask类实现了Runnable接口,所以有run方法*/public void run(){//这里写要执行的任务SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String strTime = sdf.format(new Date());System.out.println(strTime + "日志记录成功");}
}class TimerTest{public static void main(String[] args) {Timer timer = new Timer();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");try {Date firstTime = sdf.parse("2022-12-06 08:10:06");timer.schedule(new LogTimerTask(),firstTime,1000*5);} catch (ParseException e) {e.printStackTrace();}}
}
运行效果:
JDK8新特性,可实现Callable接口。这种方式实现的线程可以获得线程的返回值。前两种实现方式不能获取返回值,因为run方法的返回值类型是void。
优点:
可获取到线程的执行结果
缺点:
效率较低,在获取t线程执行的结果时,当前线程需要等待,等拿到返回值以后再往下执行其余code
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadTest0 {public static void main(String[] args) {//创建一个”未来任务类“的对象,传参为Callable接口实现类的对象FutureTask task = new FutureTask(new Callable(){ //匿名内部类//call方法相当于run方法,不过其有返回值public Object call() throws Exception{System.out.println("call method begin!");Thread.sleep(1000*6);System.out.println("call method end");int a = 100;int b = 200;//线程执行一个任务,执行完可能有返回结果//这里自动装箱return a+b;}});Thread t = new Thread(task);t.start();try {//当前为主线程,获取t线程的执行结果Object obj = task.get();System.out.println("线程执行结果:" + obj);//此处get方法要拿另一个线程的执行结果,可能要很久//但这导致了下面main线程要等待get执行结束。//这就导致了”当前线程的阻塞“System.out.println("这里要等get拿到线程的返回值才能执行");} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
运行结果:
wait 和notify方法是Java中任何一个Java对象都有的方法,因为这两个方法是Object类自带的。不是给线程对象用的,所以别t.wait();
Object o = new Object();
o.wait();
以上让正在o对象上活动的线程进入等待状态,且为无限等待,直到被唤醒为止
T线程在o对象上活动,o.wait()后,T线程进入无限期等待,并且释放o对象的对象锁
o.notify()让正在o对象上等待的线程被唤醒
notifyAll()方法是唤醒o对象上处于等待的所有线程
🍁代码实现:
import java.util.ArrayList;
import java.util.List;/*** 生产线程*/
class Producer implements Runnable{/*** 仓库*/private List list;public Producer(List list){this.list = list;}public void run(){while(true){synchronized(list){if(list.size()>0){ //仓库有东西,则生产线程waittry {list.wait(); //进入等待状态,释放之前占有的list的对象锁} 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;}public void run(){while(true){synchronized(list){if(list.size() == 0){ //仓库已空,暂停消费try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//消费Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "-->" + obj);//唤醒生产线程来生产list.notifyAll();}}}
}/*** 测试*/
public class PC_Moudle {public static void main(String[] args) {//创建一个仓库,生产和消费线程共享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(); }
}
运行效果: