1.进程和线程
- 进程是一个应用程序(一个进程是一个软件)
- 线程是一个进程中的执行场景/执行单元
- 一个进程可以启动多个线程
- 进程可以看作是现实生活当中的公司
- 线程可以看作是公司当中的某个员工
- 进程A和进程B的内存独立不共享
- 线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈
- 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发
- 多线程并发可以提高效率
- java中之所以有多线程机制,目的就是为了提高程序的处理效率
- 使用了多线程机制之后,main方法结束,只代表主线程结束了,其他线程可能还在执行
2.多线程并发
- 对于多核的cpu电脑来说,真正的多线程并发没有问题
- 4核cpu表示同一个时间点上,可以真正的有4个线程并发执行
- 什么是多线程并发
- t1线程执行t1的,t2线程执行t2的,t1不会影响t2,t2也不会影响t1,这叫做真正的多线程并发
- 单核cpu表示只有一个大脑
- 不能做到真正的多线程并发,但是可以做到给人一种多线程并发的感觉
3.java语言中实现线程有两种方式
- java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就可以了
- 编写一个类,直接继承java.lang.Thread,重写run方法
public class ThreadTest{ public static void main(String[] args){ //新建分支对象 MyThread myTread = new Mythread(); //启动线程 myThread.start(); //代码还是运行在主线程中 for(int i = 0; i < 1000; i++){ System.out.println("主线程-->" + i ); } } } class Mythread extends Thread{ public void run(){ for(int i = 0; i < 1000; i++){ System.out.println("分支线程--->" + i); } } }
void start()
- 使该线程开始执行;Java 虚拟机调用该线程的
run
方法。- 结果是两个线程并发地运行;当前线程(从调用返回给
start
方法)和另一个线程(执行其run
方法)。- 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
- 启动一个分支线程,在jvm中开辟一个新的栈空间,这段代码任务完成以后,瞬间就结束了,这段代码的任务只是为了开辟一个新的栈空间,只要新的栈空间开辟出来,start()方法就结束了。线程就启动成功了
- 启动线程会自动调用run方法,并且run()方法在分支的栈底部(压栈)。
- run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的
void run()
- 如果该线程是使用独立的
Runnable
运行对象构造的,则调用该Runnable
对象的run
方法;否则,该方法不执行任何操作并返回。
Thread
的子类应该重写该方法。编写一个类,实现java.lang.Runnable接口,实现run方法
public class TreadTest01{ public static void main(String[] args){ MyRunnable() r = new MyRunnable(); Tread t = new Tread(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); } } }
第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活
4.线程声明周期
- 就绪状态的线程又叫做可运行状态,表现hi当前线程酷游抢夺cpu时间片的权利(cpu时间片就是执行权)。当一个线程抢到cpu时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态
- run方法的开始执行标志着这个程序进入运行状态,当之前占有的cpu时间片用完之后,会重新回到就绪状态继续抢夺cpu时间片,当再次抢到cpu时间之后,会重新进入run方法,接着上一次的代码继续往下执行
- 当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的时间片
- 之前的时间片没了,需要再次回到就绪状态,抢夺cpu时间片
- 新建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 死亡状态
5.获取线程的名字
public class ThreadTest{ public static void main(String[] args){ Thread currentThread = Thread.currentThread(); //新建分支对象 MyThread t = new Mythread(); //设置线程名字 t.setName("ttt"); //获取线程名字 String tName = t.getName(); //启动线程 t.start(); //代码还是运行在主线程中 for(int i = 0; i < 1000; i++){ System.out.println("主线程-->" + i ); } } } class Mythread extends Thread{ public void run(){ for(int i = 0; i < 1000; i++){ System.out.println("分支线程--->" + i); } } }
final String getName()
- 返回该线程的名称。
final void setName(String name)
- 改变线程名称,使之与参数
name
相同。 static Thread currentThread()
- 返回对当前正在执行的线程对象的引用。
6.关于sleep方法
static void sleep(long millis)
- 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。
millis
- 以毫秒为单位的休眠时间。- 让当前线程进入休眠,进入阻塞状态,放弃占有的cpu时间片,让给其他程序使用
- Tread.sleep()方法,可以做到这样的效果
- 每个特定的时间,去执行一段特定的代码,没个多久执行一次
7.终止线程的睡眠
void interrupt()
- 中断线程。
- 如果线程在调用
Object
类的wait()
、wait(long)
或wait(long, int)
方法,或者该类的join()
、join(long)
、join(long, int)
、sleep(long)
或sleep(long, int)
方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException
。
8.强制终止线程
final void stop()
- 强迫线程停止执行。
- 已过时。 该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查
ThreadDeath
异常的一个自然后果)。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为。stop
的许多使用都应由只修改某些变量以指示目标线程应该停止运行的代码来取代。目标线程应定期检查该变量,并且如果该变量指示它要停止运行,则从其运行方法依次返回。如果目标线程等待很长时间(例如基于一个条件变量),则应使用interrupt
方法来中断该等待- 容易丢失数据,因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用
public class TreadTest{ public static void main(String[] args){ MyRunnable r = new MyRunnable(); Tread t = new Thread(r); t.setName("t"); t.start(); //5秒终止线程 r.run = false; } } class MyRunnable implements Runnable{ //打一个布尔标记 boolean run true; public void run(){ if(run){ System.out.println(Tread.currentThread().getName() + "--->" + i); } try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); }else{ return; } } }
9.线程让位
static void yield()
- 暂停当前正在执行的线程对象,并执行其他线程。
10.线程合并
final void join()
- 等待该线程终止。
- t合并到当前线程中,当前线程受阻塞,t线程执行直到结束
11.关于多线程并发环境下,数据的安全问题
- 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现了。这些代码我们都不需要编写
- 重要的是,编写的程序需要放到一个多线程的环境下运行,你需要关注的是这些数据在多线程并发的环境下是否是安全的
- 什么时候数据在多线程并发的环境下会出现安全问题
- 多线程并发
- 有共享数据
- 共享数据有修改行为
- 怎么解决线程安全问题
- 线程排队执行,不能并发
- 这种机制被称为线程排队机制
- 线程同步就是线程排队,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事。
- 异步编程模型
- 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种模型叫做异步编程模型。其实就是多线程并发(效率较高)
- 异步就是并发
- 同步编程模型
- 线程t1和t2,在线程t1执行的时候,必需等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,着就是同步编程模型。效率较低。线程排队执行
- 同步就是排队
12.同步代码块sychronized
1.线程同步机制语法
sychronized(){
线程同步代码块
}
2.sychronized后面小括号中传递的这个数据是相当关键的,这个数据必须是多线程共享的数。才能达到多线程排队
3.()中些什么
在于你想让哪些线程共享,
假设t1,t2,t3,t4,t5五个线程,你只希望让t1,t2,t3排队,t4,t5不参与排队。
你一定要在()中写一个t1,t2,t3共享的数据,而这个数据对于t4,t5不共享
4.三大变量中
局部变量永远都不会存在线程安全问题。因为局部变量不共享。一个线程一个栈,局部变量在栈中。所以局部变量永远都不会共享
实例变量在堆中,堆只有一个
静态变量在方法区中,方法区只有一个
堆和方法区都是多线程共享的,所以可能存在线程安全问题
局部变量和常量:不会有线程安全问题
成员变量:可能会有线程安全问题
5.如果使用局部变量的话建议使用StrongBuider。因为局部变量不存在线程安全问题。StringBuffer效率较低
6.sychronized有三种方法
第一种:同步代码块
灵活
sychronized(){
同步代码块;
}
第二种:在实例方法上使用sychronized
表示共享对象一定是this,并且同步代码块是整个方法体
第三种:在静态代码块上使用sychronized
表示找类锁
类锁永远只有一把
对象锁:1个对象1把锁,100个对象100把锁
类锁:100个对象,也可能只有1把锁
13.死锁
- 不出现异常,也不出现错误,程序一致僵持在那里。这种程序最难调试
public class DeadLock{ public static void main(String[] args){ Object o1 = new Object(); Object o1 = new Object(); //t1和t2共享o1和o2 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(){ sychronized(o1){ stchronized(o2){ } } } } class Mythread2 extends Thread{ Object o1; Object o2; public Mythread2(Object o1,Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ sychronized(o2){ stchronized(o1){ } } } }
14.开发中应该怎么解决线程安全问题
- 尽量使用局部变量代替实例变量和静态变量
- 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(1个线程1个对象,100个线程100个对象,对象不共享,就没有数据安全问题了)
- 如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择sychronized了,线程同步机制
15.守护线程
- java语言中线程分为两大类
- 用户线程
- 守护线程(后台线程)
- 其中具有代表性的就是:垃圾回收线程(守护)
- 守护线程的特点
- 一般守护线程是一个死循环,所有的用户线程只需要结束,守护线程自动结束
- 注意:主线程main方法是一个用户线程
- 守护线程用在什么地方
- 每天0:00的时候系统数据自动备份
- 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里等着,没到0:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份
final void setDaemon(boolean on)
- 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
- 该方法必须在启动线程前调用。
on
- 如果为true
,则将该线程标记为守护线程。public class ThreadTest{ public static void mian(String[] args){ Thread t = new BackUpDataThread(); t.setName("备份数据") t.setDeamon(true); t.start(); for(int i = 0; i < 10; i++){ System.out.println(Thread.currentThread() + "--->" + (++i)); try{ Thread.sleep(1000) }catch(InterruptedException){ e.printStackTrace(); } } } class BackUpDataThread extends Thread{ int i = 0; while(true){ System.out.println(Thread.currentThread() + "--->" + (++i)); try{ Thread.sleep(1000) }catch(InterruptedException){ e.printStackTrace(); } } }
16.定时器
- 作用:
- 间隔特定的时间执行特定的程序
- java.util.Timer
Timer()
- 创建一个新计时器。相关的线程不 作为守护程序运行。
void schedule(TimerTask task, Date firstTime, long period)
- 安排指定的任务在指定的时间开始进行重复的固定延迟执行。以近似固定的时间间隔(由指定的周期分隔)进行后续执行。
task
- 所要安排的任务firstTime
- 首次执行任务的时间。period
- 执行各后续任务之间的时间间隔,单位是毫秒。 Timer(boolean isDaemon)
- 创建一个新计时器,可以指定其相关的线程作为守护程序运行。如果计时器将用于安排重复的“维护活动”,则调用守护线程,在应用程序运行期间必须调用守护线程,但是该操作不应延长程序的生命周期。
isDaemon
- 如果应该将相关的线程作为守护程序运行,则为 true。public class TimerTest{ public static void main(String[] args){ //创建定时器对象 Timer t = new Timer(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS); Date firstTime = sdf.parse("2022-5-21 9:50:00 000"); timer.schedule(new LogTimerTask(), firstTime, 1000 * 10); } } class LogTimerTask extends TimerTask{ public void run(){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS); String strTime = sdf.format(new Date()); System.out.println(strTime + "成功完成一次数据备份"); } }
17.实现线程的第三种方式:FutureTask方式,实现Callable接口。(jdk8新特新)
- 这种方式实现的线程可以获取线程的返回值
java.util.concurrent.FutureTask<V>public class TreadTest{ public static void mian(String[] args){ //创建未来对象 FutureTask task = new FutureTask(new Callable(){//匿名内部类 public Object call() throws Exception{ System.out.print("call mathod begin"); Tread.sleep(1000 * 10); System.out.print("call mathod end"); int a = 100; int b = 200; return a + b; } }); Tread t = new Thread(task); t.start(); Obkect o = task.get(); } }
18.关于Object类当中的wait和notify方法
- wait和notify方法不是线程对象的方法,是java种任何一个java对象都有的方法,因为着两个方法是Object类自带的
final void wait()
- 在其他线程调用此对象的
notify()
方法或notifyAll()
方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。- 当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用
notify
方法,或notifyAll
方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。Object o = new Object(); o.wait()
表示让正在o对象上活动的线程进入等待状态,无限期等待,直到被唤醒
final void notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个
wait
方法,在对象的监视器上等待。直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
- 通过执行此对象的同步实例方法。
- 通过执行在此对象上进行同步的
synchronized
语句的正文。- 对于
Class
类型的对象,可以通过执行该类的同步静态方法。- 一次只能有一个线程拥有对象的监视器。
Object o = new Object(); o.wait()
唤醒正在o对象上等带的线程
final void notifyAll()
- 唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个
wait
方法,在对象的监视器上等待。- wait和notify方法会建立在sychronized同步的基础之上
- o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁
- o.notify()只会通知,不会释放之前占有的o对象的锁