多线程和定时器详解及Object类中的wait和notify方法(生产者和消费者模式)
多线程和定时器详解及Object类中的wait和notify方法(生产者和消费者模式)
程序、进程和线程之间的关系及多线程
程序: 是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程: 是一个应用程序(一个进程是一个软件)
- 进程A与进程B的内存独立不共享
线程: 是一个进程中的执行场景/执行单元
- 线程A和线程B,堆内存和方法区内存共享;但是栈内存独立,不共享资源
一个进程可以启动多个线程
多线程并发: 假设启动10个线程,会有10个栈空间,每个栈和每 个栈之间互不干扰,各自执行各自的
多线程机制: 目的是为了提高程序的处理效率
简而言之: 一个程序运行后至少有一个进程,一个进程中可以包含多个线程
---->注:使用多线程机制之后,main方法结束,有可能程序也不会结束。因为main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈,弹栈。
run()方法和start()方法的
run()方法:
start()方法:
作用: 启动一个分支线程,在JVM中开辟出一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
区别:
-
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
-
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由JVM的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。
sleep()方法和wait()方法
sleep()方法:
static void sleep(long millis)
1.静态方法:Thread。sleep(1000);
2.参数是毫秒
3.作用:让当前线程进入休眠,进入“阻塞状态”,
放弃占有的CPU时间片,让给其它线程使用
这行代码出现在A线程,A线程就会进入休眠
这行代码出现在B线程,B线程就会进入休眠
4.Thread.sleep()方法,可以做到如下效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次
public static void main(String[] args) {
//让当前线程进入休眠,睡眠5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒之后执行这里的代码
System.out.println("hello world");
}
wait()方法 :
wait()方法可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个方法的同时,应该用notifyAll()方法通知所有由于使用了这个同步方法而处于等待的线程结束等待,曾中断的线程就会从刚才中断处继续执行这个同步方法(并不是立马执行,而是结束等待),并遵循“先中断先继续”的原则。
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify()方法(notify()并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify()方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy()方法后增加一个等待和一些代码,看看效果)
public static void main(String[] args) throws InterruptedException {
new Thread(new Thread1()).start();
Thread.sleep(5000);
//主动让出CPU,让CPU去执行其他的线程。
//在sleep指定的时间后,CPU回到这个线程上继续往下执行
new Thread(new Thread2()).start();
}
class Thread1 implements Runnable{
@Override
public void run() {
synchronized (MultiThread.class){
System.out.println("进入线程1");
try{
System.out.println("线程1正在等待");
Thread.sleep(5000);
//MultiThread.class.wait();
/*wait是指一个已经进入同步锁的线程内
(此处指Thread1),让自己暂时让出同步锁*/
/*以便其他在等待此锁的线程
(此处指Thread2)可以得到同步锁并运行*/
}catch(Exception e){
System.out.println(e.getMessage());
e.printStackTrace();
}
System.out.println("线程1结束等待,继续执行");
System.out.println("线程1执行结束");
}
}
}
class Thread2 implements Runnable{
@Override
public void run() {
synchronized (MultiThread.class){
System.out.println("进入线程2");
System.out.println("线程2唤醒其他线程");
MultiThread.class.notify();
/*Thread2调用了notify()方法,但该方法不会释放
对象锁,只是告诉调用wait方法的线程可以去参与获得
锁的竞争了。但不会马上得到锁,因为锁还在别人手里,
别人还没有释放。如果notify()后面的代码还有很多,
需要执行完这些代码才会释放锁。*/
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2继续执行");
System.out.println("线程2执行结束");
}
}
}
区别:
- “sleep是Thread类的方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是有区别的。
- Thread.sleep()不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep()不会让线程释放锁。如果能够帮助你记忆的话,可以简单认为和锁相关的方法都定义在Object类中,因此调用Thread.sleep()是不会影响锁的相关行为。
- Thread.sleep()和Object.wait()都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify()/notifyAll()才能够重新获得CPU执行时间。
- 线程的状态参考 Thread.State的定义。新创建的但是没有执行(还没有调用start())的线程处于“就绪”,或者说Thread.State.NEW状态。
Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,因为锁不能获取到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。BLOCKED状态下线程,OS调度机制需要决定下一个能够获取锁的线程是哪个,这种情况下,就是产生锁的争用,无论如何这都是很耗时的操作。 - 两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁 。
- 两者都可以暂停线程的执行。
Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。
线程的实现
java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就行。
方式一:编写一个类,直接继承java.lang.Thread,重写run()方法
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
/*
这段代码的任务只是为了开启一个新的栈空间,
只要新的栈空间开出来,start()方法就结束了,
线程启动成功。
启动成功的线程会自动调用run方法,
并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈底部,main方法在主栈的栈底部。
run和main是平级的
*/
//这里的代码还在运行在主栈中
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方法
public static void main(String[] args) {
/*
//创建一个可运行的对象
Runnable r = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
*/
Thread t = new Thread(new Runnable());
//启动线程
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
//这并不是一个线程类,是一个可运行的类。它还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
}
注:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活
采用匿名内部类的方式
public static void main(String[] args) {
//创建线程对象,采用匿名内部类
//这是通过一个没有名字的类,new出来的对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("t线程--->"+i);
}
}
});
//启动线程
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程--->"+i);
}
}
方式三:实现Callable接口
这种方式实现的线程可以获取线程的返回值。前两种方式是无法获取线程返回值得,因为run方法返回void
优点: 可以获取线程的执行结果
缺点: 在获取t线程执行结果的时候,当前线程受阻塞,效率较低
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个"未来任务类"对象
//参数非常重要,需要给一个Callable接口实现类对象
FutureTask task = new FutureTask(new Callable() {
//call()方法就相当于run方法。只不过有返回值
//线程执行一个任务,执行之后肯会有一耳光执行结果
//模拟执行
@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;//自动装箱(300结果变成Integer)
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//在主线程获取t线程的返回结果
//get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果" + obj);
//main方法这里的程序想要执行必须等待get()方法的结束
//而get()方法可能需要很久。
//因为get()方法为了拿另一个线程的执行结果
//另一个线程执行是需要时间的。
}
线程的五种状态及生命周期
-
新建状态
刚new出来的线程对象
-
就绪状态
就绪状态又叫做可运行状态,表明当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,ran方法的开始执行标志着线程进入运行状态
-
运行状态
ran方法的开始执行标志着线程进入运行状态,当之占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上次的代码继续往下执行
-
阻塞状态
当一个线程遇到一个阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片
当阻塞解除,由于之前的时间片没了,需要再次回到就绪状态,抢夺CPU时间片
-
死亡状态
图示:
终止线程的睡眠——interrupt()方法
不是终断线程的执行,是终止线程的睡眠
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
//希望5秒之后。t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终断t线程的睡眠
///(这种终断睡眠的方式依靠了java的异常处理机制)
t.interrupt();
}
class MyRunnable2 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");
}
}
终止线程的执行
注:不建议用t.stop()方法,容易损失数据
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
//模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
//你想什么时候终止t的执行,
//那么你把标记修改为false,就结束了
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;
}
}
}
}
定时器
在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的
作用:
间隔特定的时间,执行特定的程序
实现方式:
方式一: 使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式最原始的定时器(low)
方式二: java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在很多高级框架都是支持定时任务的。在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
实现
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer = new Timer();
//指定定时任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date fisrttime = sdf.parse("2021-1-29 23:37:37");
timer.schedule(null,fisrttime,1000*10);
}
//编写一个定时任务类,因为TimerTask是一个抽象类,没法直接new
class LogTimerTask extends TimerTask{
@Override
public void run() {
//编写你需要执行的任务就行了
}
}
Object类中的wait和notify方法(生产者和消费者模式)
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类自带的
wait方法和notif方法建立在synchronized线程同步的基础之上
wait()方法
Object o = new Object;
o.wait();
表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态,直到最终调用o.notify()方法。
o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁
notify()方法
Object o = new Object;
o.notify();
表示:唤醒正在o对象上等待的线程
还有一个notifyAll()方法:唤醒o对象上处于等待的所有线程
o.notify()方法只会通知,不会释放之前占有的o对象的锁
生产者和消费者模式
生产者和消费者模式是为了专门解决某个特定需求的,最终要达到生产和消费必须均衡