- 进程A和进程B的内存独立不共享;
- 线程A和线程B,堆内存和方法区共享,但是栈内存独立,一个线程一个栈;
- 多线程并发:假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行;
- 使用了多线程机制之后,main方法结束,有可能程序也不会结束。main方法结束只是主线程结束了,主栈空了,其它的线程有可能还在压栈弹栈;
- 单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以给人一种"多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个线程同时在执行!!!
1、 编写一个类,直接继承java.lang.Thread,重写run方法
–编写MyThread类继承Thread
class MyThread extends Thread{
@Override
public void run() {
}
}
–创建线程对象
MyThread myThread = new MyThread()
–启动线程
myThread.start();
-调用run方法
注意1:start()方法的作用是启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程启动成功。
注意2:线程启动成功之后会自动调用run方法,并且run方法在分支栈的栈底部。main方法在主栈的栈底部。run和main是平级的。
注意:如果不执行start()方法,直接调用run()方法的话实际上并没有开启线程,只是普通的方法调用 ,两种情况的内存图如下所示
2、编写一个类,实现java.lang.Runnable接口,实现run()方法
-定义一个可运行的类
class MyRunnable implements Runnable{
@Override
public void run() {
}
}
}
-创建一个可运行的对象
MyRunnable r = new MyRunnable();
-将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
-启动线程
t.start();
3、 匿名内部类
-创建线程对象,采用匿名内部类方式
Thread t = new Thread(new Runnable() {
@Override
public void run() { }
});
-启动线程
t.start();
-其他线程的代码
线程的生命周期-
调用start()方法,进入就绪状态。就绪状态的线程又叫做可运行状态,表示当前线程具有抢守CPU时间片的权利(CPU时间片就是执行权),当一个线程抢到时间片之后开始执行run方法,run方法 的执行标志着线程进入运行状态
-
run方法的开给执行标志着这个线程进入运行扶态·当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU世间片,当再次抢到CPU时间片之后,会重新进入run方法接着上一次的代码进续往下执行。
-
线程在就绪和运行两个状态中频繁的变化;
-
当一个线程遇到阻塞事件时,例如接收用户键盘的输入,或者sleep方法等,此时线程就会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片;
-获取线程对象
Thread currentThread = Thread.currentThread();
-获取线程对象的名字
String tName = t.getName(); Thread.currentThread().getName();
-修改线程对象的名字
t.setName("tttttttttt");
sleep()方法
1、关于sleep()方法
-
static void sleep(long millis, int nanos)
-
是一个静态方法,静态方法的调用与调用对象无关,Thread.sleep(1000)
-
让当前线程进入休眠,进入“阻塞状态”,放弃占有的CPU时间片,让给其它的线程使用
2、sleep()方法的测试
- 测试1:可以实现每隔相等的时间执行某块代码
public static void main(String[] args) {
for (int i=0 ;i<5 ;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} //输出结果:每隔1秒输出 main--->0 main--->1 main--->2 main--->3 main--->4
- 测试2:sleep()为静态方法,对象.sleep()并不会造成该对象线程休眠
public class ThreadTest{
public static void main(String[] args) {
Thread t = new MyThread();
t.setName("t");
t.start();
try {
t.sleep(1000*2);//由于sleep方法为静态方法,该语句不会造成线程t休眠,而主线程会休眠
//执行的时候还是会转换成 Thread.sleep(1000) 且当前线程是main ,则main线程会进入休眠。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world!");
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
} //输出结果:瞬间输出t--->0、t--->1、t--->2、t--->3、t--->4,2秒之后输出 hello world!
终止线程的睡眠:依靠java的异常处理机制
public class ThreadTest{
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
//以下代码实现5秒之后t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程的睡眠
t.interrupt();
}
}
class MyRunnable implements Runnable{
//重点: run()当中的异常不能throws ,只能try catch
//因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常
@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");
}
}
//执行结果 5秒之后输出 t--->end
t--->begin
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.MyRunnable2.run(ThreadTest08.java:29)
at java.lang.Thread.run(Thread.java:748)
t--->end
终止线程的执行
1、强行终止线程的执行 :t.stop() ----->不建议使用
public class ThreadTest09 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable3());
t.setName("t");
t.start();
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();
}
}
}
}
//执行结果 每隔1秒输出 t--->0、t--->1、t--->2、t--->3、t--->4,5秒之后线程被杀死,后续代码不再执行
2、合理终止线程的执行 通过修改boolean类型的run来结束线程;
public class ThreadTest10 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒之后终止线程
r.run=false;
}
}
class MyRunnable4 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;
}
}
}
}
多线程并发的安全问题
1、 满足以下3个条件时会存在线程安全问题
- 多线程并发
- 有共享数据
- 共享数据有修改的行为
2、线程同步机制(线程排队)
-
异步编程模型(并发)
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1 —>其实就是多线程并发
-
同步编程模型(排队)
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间存在等待关系。
synchronized (){
//线程同步代码块
}
- synchronized后面小括号中传的这个”数据“是相当关键的,这个数据必须是多线程共享的数据,才能实现多线程排队;假设t1、t2、t3、t4、 t5有5个线程,你只希望t1、t2、t3排队,t4、t5不需要排队。怎么办?一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。
- 对象锁:
- 假设t1先执行了,遇到了synchronized,此时自动找后面共享对象"的对象锁,找到之后,占有这把锁,然后执行同步代码块中的程序,在程序执行过程t1一直占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
- 假设t1已经占有这把锁,此时t2也遇到synchronized关健字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,当t1执行结束并归还这把锁后,t2占有这把锁之后,进入同步代码块执行程序。
3、java中的三大变量
- 实例变量:存储在堆中
- 堆只有1个,是多线程共享的,可能存在线程安全问题
- 静态变量:存储在方法区中
- 方法区只有1个,是多线程共享的,可能存在线程安全问题
- 局部变量:在栈中
- 局部变量永远不存在线程安全问题,因为局部变量不共享,一个线程一个栈
- 局部变量在栈中,所以局部变量永远不会共享
- 局部变量+常量:不会存在线程安全问题
- 成员变量(包括实例变量和静态变量):可能会有线程安全问题
4、 synchronized的三种方法
- 同步代码块
synchronized (线程共享对象){
同步代码块;
}
-
在实例方法上使用synchronized
表示共享对象一定是this,并且同步代码块是整个方法体;
-
在静态方法上使用synchronized
表示找类锁,类锁永远只有1把,就算创建了100个对象,类锁也只有1把
- 尽量使用局部变量代替实例变量和静态变量
- 如果必须是实例变量,则可以考虑创建多个对象
- 如果不能使用局部变量,对象也不能创建多个,只能选择synchronized线程同步机制
- java语言中线程分为两大类:用户线程和守护线程(后台线程)
- 守护线程具有代表性的是垃圾回收线程
- 守护线程一般是死循环,所有的用户线程只要结束,守护线程自动结束
- 将线程t设置为守护进程
t.setDaemon(true);定时器
- 间隔特定的时间执行特定的程序
-每周进行银行账户的总账操作
-每天进行数据的备份操作 - java.util.Timer
- 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架
public class TimerTask {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("YYY-MM-dd HH:mm:ss");
Date firstTime =sdf.parse("2021-09-30 15:48:00");
timer.schedule(new java.util.TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("YYY-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime+"成功完成了一次数据备份");
}
},firstTime, 1000 * 10);
}
}//执行结果 在firstTime之后每隔10秒钟进行一次输出
实现线程的另一种方式:FutureTask方式,实现Callable接口
-实现Callable接口这种线程实现方式 可以获取线程的返回值
public class ThreadTest15 {
public static void main(String[] args) throws Exception {
//创建未来任务类对象
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 end");
int a=100;
int b=200;
return a+b;
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//在main主线程中怎么获得t线程的执行结果
Object obj = task.get();
System.out.println("执行结果:"+obj);
//main方法这里的程序要想执行必须等待get()方法的结束,而get()方法可能需要很久。
//因为get()方法是为了拿另一个线程的执行结果,另一个线程执行是需要时间的
//因此主线程会在 Object obj = task.get()位置出阻塞
System.out.println("hello world");
}
}
明知道没有结果、没有未来,你会去试试吗?



