Java多线程
多线程
1.进程与线程
进程:进程是程序的一次动态执行过程,它需要经历从 代码加载
、 代码执行
、 执行完毕
的一整个过程。也对应着进程本身从产生、发展到消亡的过程。
线程:线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。
多线程:多线程是实现并发机制的一种有效手段,多线程是指一个程序在执行过程中可以产生多个更小的程序单元。这些更小的程序单元就称为线程,这些线程可以同时存在、同时运行。
一个进程中可能包含多个同时执行的线程。
举例: 通过word的使用了解进程与线程的区别
当我们启动一个word对于操作系统而言就相当于启动了一个系统的进程,在这个进程之上又有许多其他程序在运行(比如:拼写检查),那么这些程序就是一个个小的线程。如果word关闭了,则这些拼写检查的线程也将会消失,但是反过来如果拼写检查的线程消失了,也并不定会让word的进程消失。
2.Java中线程的实现
Java中实现多线程:
- 继承thread类
- 实现Runnable接口
1.继承Thread类
Thread类是在java.lang包中定义的,一个类只要继承了Thread类,此类就称为多线程实现类。在Thread子类中,必须明确地覆写Thread类中的run()方法,此方法为线程的主体。
线程类的定义:
class 类名称 extends Thread{ // 继承Thread类 属性...; //类中定义属性 方法...; //类中定义方法 public void run(){ //覆写Thread类中的run()方法,此方法是线程的主体 线程主体; } }
多线程实现:
启动线程:不能直接调用run()方法,而应该调用从Thread类中继承而来的start()方法。⭐⭐⭐
提问:为什么启动线程不能直接使用run()方法。
回答:线程的运行需要本机操作系统的支持。
代码:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); //这里调用start0()方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); 实际上调用的是
start0()
方法,此方法使用native
关键字声明,此关键字表示调用本机的操作系统函数,因为多线程的实现需要依靠底层操作系统支持。
注意:
一个类通过继承Thread类来实现多线程,只能调用一次start()方法,如果调用多次则会抛出
IllegalThreadStateException
异常。例如:
class MyThread extends Thread{ private String name; public MyThread(String name){ this.name = name; } public void run(){ for (int i = 0; i < 10; i++) { System.out.println(name + "运行, i = " + i); } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread1 = new MyThread("线程A"); myThread1.start(); //在这里调用两次start方法 myThread1.start(); //在这里调用两次start方法 } }
2.实现Runnable接口
在Java中也可以通过实现Runnable接口的方式实现多线程,Runnable接口中只定义了一个抽象方法:public abstract void run();
使用Runnable接口实现多线程的格式:
class 类名 implements Runnable{ 属性...; 方法...; public void run(){ //覆写Runnoable接口中的run()方法 线程主体; } }
实现Runnable接口:
class MyThread implements Runnable{ //实现Runnable接口 private String name; public MyThread(String name){ this.name = name; } public void run(){ //覆写Runnable中的run()方法 for (int i = 0; i < 10; i++) { System.out.println(name + "运行, i = " + i); } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread1 = new MyThread("线程A"); //实例化Runnable子类对象 MyThread myThread2 = new MyThread("线程B"); //实例化Runnable子类对象 Thread thread1 = new Thread(myThread1); //实例化Thread类对象 Thread thread2 = new Thread(myThread2); //实例化Thread类对象 thread1.start(); //启动线程 thread2.start(); //启动线程 } }
需要注意:
要想启动一个多线程必须使用start()方法完成,如果继承Thread类,则可以直接从Thread类中继承并使用start()方法,但是现在实现的是Runnable接口,此接口中并没有start()方法的定义。所以还是要依靠Thread类完成启动,在Thread类中提供了两个构造方法:
public Thread(Runnable target)
和public Thread(Runnable target,String name)
这两个构造方法都可以接收Runnable的子类实例对象,所以可以依靠这一点来启动多线程。
无论是继承Thread类或者实现Runnable接口来实现多线程都必须依靠Thread类才能启动多线程;
3.Thread类和Runnable接口
Thread类的定义:
public class Thread implements Runnable
Thread类的部分定义:
private Runnable target; public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { ...... this.target = target; } @Override public void run() { if (target != null) { target.run(); } }
从定义中可以看出,在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果通过继承Thread类实现多线程,则必须覆写run()方法。
此代码的操作形式与代理设计类似:
Thread和Runnable的子类同时实现了Runnable接口,之后将Runnable的子类实例放到Thread类中。
Thread类和Runnable接口之间是有区别的:
- 继承Thread类不能资源共享
- 实现Runnable接口可以资源共享
继承Thread类不能资源共享
class MyThread extends Thread{ private int tickets = 5; //一共5张票 public void run(){ for (int i = 0; i < 100; i++) { if (tickets > 0) { System.out.println("卖票:ticket = " + tickets--); } } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); MyThread myThread3 = new MyThread(); myThread1.start(); myThread2.start(); myThread3.start(); } }
程序中启动了3个线程,但是3个线程却分别卖了各自的5张票,并没有达到资源共享的目的。
实现Runnable接口实现资源共享
class MyThread implements Runnable { private int tickets = 5; //一共5张票 public void run(){ for (int i = 0; i < 100; i++) { if (tickets > 0) { System.out.println("卖票:ticket = " + tickets--); } } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); new Thread(myThread).start(); //启动3个线程 new Thread(myThread).start(); //启动3个线程 new Thread(myThread).start(); //启动3个线程 } }
程序中启动了3个线程,3个线程一共卖了5张票,即ticket属性被所有的线程对象共享。
实现Runnable接口相对于继承Thread类来说,有如下优势:
- 适合多个相同程序代码的线程去处理同一资源的情况
- 可以避免由于java单继承特性带来的局限
- 增强程序健壮性,代码能够被多个线程共享,代码与数据是独立的。
3.线程的状态
线程一般具有5种状态:
- 创建
- 就绪
- 运行
- 阻塞
- 终止
线程状态转换图:
-
创建状态
在程序中使用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时它拥有相应的内存空间和其他资源,但是还处于不可运行的状态。
新建一个线程对象可采用Thread类的构造方法来实现,例如"
Thread thread = new Thread();
" -
就绪状态
调用新建线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
-
运行状态
当处于就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
-
阻塞状态
一个正在执行的线程在某些特殊情况下,例如:人为挂起、需要执行耗时的输入输出操作时,会让出CPU并暂时终止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
-
死亡状态
线程调用stop()方法或run()方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
4.线程操作的相关方法
Thread类中的主要方法
序号 | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public Thread(Runnable target) | 构造 | 接收Runnable接口子类对象,实例化Thread对象 |
2 | public Thread(Runnable target, String name) | 构造 | 接收Runnable接口子类对象,实例化Thread对象,并设置线程名称 |
3 | public Thread(String name) | 构造 | 实例化Thread类对象,并设置线程名称 |
4 | public static native Thread currentThread() | 普通 | 返回目前正在执行的线程 |
5 | public final String getName() | 普通 | 返回线程名称 |
6 | public final int getPriority() | 普通 | 返回线程优先级 |
7 | public boolean isInterrupted() | 普通 | 判断目前线程是否被中断,如果是,返回true,否则返回false |
8 | public final native boolean isAlive() | 普通 | 判断线程是否在活动,如果是,返回true,否则返回false |
9 | public final void join() throws InterruptedException | 普通 | 等待线程死亡 |
10 | public final synchronized void join(long millis) throws InterruptedException | 普通 | 等待millis毫秒后线程死亡 |
11 | public void run() | 普通 | 执行线程 |
12 | public final synchronized void setName(String name) | 普通 | 设定线程名称 |
13 | private native void setPriority0(int newPriority) | 普通 | 设定线程的优先级 |
14 | public static native void sleep(long millis) throws InterruptedException | 普通 | 使目前正在执行的线程休眠millis毫秒 |
15 | public synchronized void start() | 普通 | 开始执行线程 |
16 | public String toString() | 普通 | 返回代表线程的字符串 |
17 | public static native void yield() | 普通 | 将目前正在执行的线程暂停,允许其他线程执行 |
18 | public final void setDaemon(boolean on) | 普通 | 将一个线程设置成后台运行 |
1.取得和设置线程名称
在Thread类中可以通过:
getName()
方法取得线程的名称。setName()
方法设置线程的名称。
注意:
线程名称一般在启动线程前设置,也允许为已经运行的线程设置名称。允许两个线程对象有相同的名称,但是尽量避免这种情况。
如果没有给线程设置名称,系统会自动为其分配名称。
class MyThread implements Runnable { public void run(){ for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); new Thread(myThread).start(); //系统自动设置线程名称 new Thread(myThread,"线程-A").start(); //手工设置线程名称 new Thread(myThread,"线程-B").start(); //手工设置线程名称 new Thread(myThread).start(); //系统自动设置线程名称 } }
输出: Thread-0运行,i=0 线程-B运行,i=0 线程-B运行,i=1 线程-B运行,i=2 Thread-1运行,i=0 Thread-1运行,i=1 Thread-1运行,i=2 线程-A运行,i=0 Thread-0运行,i=1 Thread-0运行,i=2 线程-A运行,i=1 线程-A运行,i=2 进程已结束,退出代码0
注意: 主方法实际上也是一个线程。
在Java中所有的线程都是同时启动的,哪个线程先抢占到了CPU资源,哪个线程就先运行。
class MyThread implements Runnable { public void run(){ for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); // 获取线程的名称 } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); new Thread(myThread,"线程-A").start(); //手工设置线程名称 myThread.run(); } }
主方法直接通过Runnable接口的子类对象调用其中的run()方法。
问题:Java程序每次启动至少启动多少个线程?
回答:至少两个线程,一个是main线程,另一个垃圾收集线程。
2.判断线程是否启动
方法: isActive()
方法来测试线程是否已经启动而且仍在运行。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } } }; public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread = new Thread(myThread,"线程"); // 判断线程是否启动 System.out.println("线程开始执行之前-->" + thread.isAlive()); // 启动线程 thread.start(); System.out.println("线程开始执行之后-->" + thread.isAlive()); for (int i = 0; i < 3; i++) { System.out.println("main运行 -->" + i); } System.out.println("代码执行之后-->" + thread.isAlive()); } }
输出:
注意:主线程有可能比其他线程先执行完。
因为线程操作的不确定性,所以主线程有可能最先执行完,那么此时其他线程不会受到任何影响,并不会随着主线程的结束而结束。
3.线程的强制运行(等待线程死亡)
方法: join()
方法可以让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread = new Thread(myThread,"线程"); // 启动线程 thread.start(); for (int i = 0; i < 50; i++) { if (i > 10) { try{ thread.join(); //线程thread进行强制运行 }catch (Exception e){} } System.out.println("main运行 -->" + i); } } }
由输出可以得到:当i>10的时候,线程thread会一直执行完,然后主线程才能开始继续执行。
4.线程的休眠
方法: Thread.sleep()
方法可以实现线程的休眠。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ for (int i = 0; i < 5; i++) { try{ Thread.sleep(500); //线程休眠 }catch (Exception e){} //需要异常处理 System.out.println(Thread.currentThread().getName() + "运行,i=" + i); } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread = new Thread(myThread,"线程"); // 启动线程 thread.start(); } }
在程序执行时,每次输出都会间隔500ms。
5.中断线程
方法: interrupt()
方法可以中断线程的运行状态。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ System.out.println("1.进入run方法"); try{ Thread.sleep(10000); //休眠10s System.out.println("2.已经完成休眠"); }catch (Exception e){ System.out.println("3.休眠被终止"); return; } System.out.println("4.run方法正常结束"); } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread = new Thread(myThread,"线程"); // 启动线程 thread.start(); try{ Thread.sleep(2000); //稍微停2s再继续中断 }catch (Exception e){} thread.interrupt(); // 中断线程执行 } }
6.后台线程
方法: setDaemon()
方法
在Java程序中,只要前台有一个线程在运行,则整个Java进程都不会消失,所以可以设置一个后台进程,这样即使Java进程结束了,此后台线程仍然会继续执行。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ while(true){ //无限循环 System.out.println(Thread.currentThread().getName() + "在运行。"); } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread = new Thread(myThread,"线程"); // 此线程在后台运行 thread.setDaemon(true); // 启动线程 thread.start(); } }
在线程类MyThread中,尽管run()方法中是死循环的方式,但是程序依然可以执行完,因为方法中死循环的线程操作已经设置成后台运行了。
7.线程的优先级
在Java的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时哪个线程的优先级高,哪个线程就有可能会被先执行。
方法: setPriority()
方法可以设置一个线程的优先级,在Java的线程中一共有3种优先级。
序号 | 定义 | 描述 | 表示的常量 |
---|---|---|---|
1 | public final static int MIN_PRIORITY = 1; | 最低优先级 | 1 |
2 | public final static int NORM_PRIORITY = 5; | 中等优先级,是线程的默认优先级 | 5 |
3 | public final static int MAX_PRIORITY = 10; | 最高优先级 | 10 |
举例:测试线程优先级。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ for (int i = 0; i < 5; i++) { try{ Thread.sleep(500); }catch (Exception e){} System.out.println(Thread.currentThread().getName() + "运行,i = " + i); } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread,"线程A"); Thread thread2 = new Thread(myThread,"线程B"); Thread thread3 = new Thread(myThread,"线程C"); thread1.setPriority(Thread.MIN_PRIORITY); thread2.setPriority(Thread.MAX_PRIORITY); thread3.setPriority(Thread.NORM_PRIORITY); // 启动线程 thread1.start(); thread2.start(); thread3.start(); } }
注意: 并不是哪个线程的优先级越高就一定会先执行,哪个线程先执行将由CPU的调度决定。
主方法的优先级是NORM_PRIORITY
8.线程的礼让
方法: yield()
方法将一个线程的操作暂时让给其他线程执行。
//实现Runnable接口 class MyThread implements Runnable { //覆写run()方法 public void run(){ for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "运行,i = " + i); if (i == 3){ System.out.println("线程礼让:"); Thread.currentThread().yield(); //线程礼让 } } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread,"线程A"); Thread thread2 = new Thread(myThread,"线程B"); // 启动线程 thread1.start(); thread2.start(); } }
从程序运行结果可以发现,每当线程满足条件(i==3),就会将本线程暂停,让其他线程先执行。
5.同步锁 与死锁 ⭐⭐⭐
背景:
一个多线程的程序如果是通过Runnable接口实现的,这意味着类中的属性将被多个线程共享,那么这样一来就会造成一种问题,如果多个线程要操作同一资源时就有可能出现资源的同步问题。例如前面的卖票程序,如果多个线程同时操作时就有可能出现卖出票为负数的问题。
案例:
通过Runnable接口实现多线程,并产生3个线程对象,同时卖5张票。
import javax.swing.*; //实现Runnable接口 class MyThread implements Runnable { private int ticket = 5; //覆写run()方法 public void run(){ for (int i = 0; i < 100; i++) { if (ticket > 0){ try{ Thread.sleep(500); // 加入延时 }catch (Exception e){ e.printStackTrace(); } System.out.println("卖票: ticket = " + ticket--); } } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); Thread thread3 = new Thread(myThread); // 启动线程 thread1.start(); thread2.start(); thread3.start(); } } 可能会出现ticket等于负数的情况。
因为可能一个线程还没有对票数进行减操作之前,其他线程就已经将票数减少了。
1.同步
1.同步代码块
代码块:就是指使用"{}"括起来的一段代码。
同步代码块:在代码块上加上synchronized
关键字,此代码块就称为同步代码块。
格式:
synchronized(同步对象){ 需要同步的代码; }
注意:在使用同步代码块时必须指定一个需要同步的对象,但是一般都是将当前对象(this)设置成同步对象。
synchronize 是什么?
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码
synchronize的作用:
根据上述言简意赅的解释,我们已经知道了这“家伙的”用途了,说抽象点,就是给修饰的对象(可以是方法、对象、或者是段代码段)加了一把"锁",什么是锁,我打个比方吧:假如你要上厕所,厕所只有一间,一间只有一个坑,上厕所的人可不只有你一个,怎么办,难道大家都拥挤进来,共享这一个坑吗? 当然不是,我们最惬意的方式就是一个人独享厕所,为了做到独享,我们需要排队(先获得锁的人有优先蹲坑权),为了不让其他人在自己蹲坑的时候闯进来,我们需要在上厕所的时候给门上把锁,把其他人"锁"在外面,防止自己在蹲坑的时候有人不遵守规矩"硬"闯进来;这样一来的话,只有等我们上完出来,把锁打开(释放锁)后,下一个人才能进来,独享他自己的蹲坑时间;
注意:
当然,程序中锁的释放不是由我们自己写代码手动控制的(区别于Lock接口中的unlock方法),而是由JVM说的算的,如果同步块中的代码异常的话,JVM会主动释放当前线程获得的锁,如果线程顺利执行完毕后,JVM也会主动释放锁,反之,如果线程持有对象的锁却始终处于dosomething状态时,那么其他想要获得该对象锁的线程则会一直处于wait状态,即阻塞在那;
2.同步方法
除了将需要的代码块设置成同步代码块外,也可以使用synchronized关键字将一个方法声明成同步方法。
格式: synchronized 方法返回值 方法名称(参数列表){}
当某段代码需要互斥时,可以用 synchronized 关键字修饰,这里讨论 synchronized 关键字修饰方法时,是如何互斥的。
synchronized 修饰方法时锁定的是调用该方法的对象。它并不能使调用该方法的多个对象在执行顺序上互斥。
1.使用synchronized修饰方法:⭐⭐⭐
- synchronized修饰非静态方法,实际上是对
调用该方法的对象
加锁,俗称“对象锁”。 - synchronized修饰静态方法,实际上是对该
类对象
加锁,俗称“类锁”。 - 对于类锁synchronized static,是通过该类直接调用加类锁的方法,而对象锁是创建对象调用加对象锁的方法,两者访问是不冲突的,对于同一类型锁锁住的方法,同一对象是无法同时访问的.
1.synchronized修饰非静态方法
synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
如果采用形象的比喻就是,对于这个对象可能有很多方法(房间),有一些加了锁(锁门的房间),而这些房间共用一把钥匙,导致当一个被锁方法被访问时,无法访问其他带锁的方法。
但是需要注意:一个对象可以拿一把钥匙,多个对象可以插队执行。
class Number{ public synchronized void getOne(){ try{ Thread.sleep(2000); System.out.println("one"); }catch (Exception e){ e.printStackTrace(); } } public synchronized void getTwo(){ System.out.println("Two"); } } public class TestSyn { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); } }
输出: one Two
这里就number一个对象,相当于对number对象上锁。
因为getOne()虽然要停止2秒,但上了对象锁,getTwo()不能超车(访问),要等getOne()被调用完,把钥匙换回来,才能继续执行
2.Synchronized修饰静态方法
synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
类锁和对象锁的访问是不冲突的
3.方法定义的完整格式
访问权限{public|default|protected|private}[final][static][synchronized] 返回值类型|void 方法名称(参数类型 参数名称,...)[throws Exception1,exception2]{[return [返回值|返回调用处]]}
2.死锁
什么是死锁:
两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
产生死锁的四个必要条件:
-
互斥使用(资源独占)
一个资源每次只能给一个线程使用
说明:对象锁只有一把,同一时间,只能有一个线程持有,其他线程需等待
-
不可强占(不可剥夺)
资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放
说明:当线程A先拿到对象锁时,线程B除了等待线程A主动释放该对象锁时,什么都干不了(想都别想)
-
请求和保持
一个线程在申请新的资源的同时保持对原有资源的占有
说明:线程A原本持有对象M的锁,但又想要申请获取对象N的锁,这时候,如果获得对象N的锁遇到阻塞时,就会导致线程A原本持有的对象M的锁无法得到释放,这就导致其他想要获取对象M锁的线程陷入无限的等待中
-
循环等待
存在一个线程等待队列 {T1 , T2 , … , Tn},,其中T1等待T2释放占有的资源,T2等待T3释放占有的资源,…,Tn等待T1释放 占有的资源,形成一个线程等待环路
说明:A说B写的模块代码有问题,B说C写的模块代码有问题,C又反过来说是A写的不对
如何避免死锁:
上述产生死锁的四个必要条件只要有一个不成立,就可以推翻或者排除出现死锁的可能,因此,我们在使用多线程开发程序之前,一定要好好设计和斟酌一下,防止写出来的程序在线程调度上出岔子,造成死锁就麻烦了,慎重!
死锁案例
张三想要李四的画,李四想要张三的书,张三对李四说:"把你的画给我,我就给你书",李四对张三说:"把你的书给我,我就给你画"。此时张三等着李四的答复,李四也等着张三的答复。
class Zhangsan{ public void say(){ System.out.println("张三对李四说:\"你把画给我,我就给你书。\""); } public void get(){ System.out.println("张三得到了画。"); } } class Lisi{ public void say(){ System.out.println("李四对张三说:\"你把书给我,我就给你画。\""); } public void get(){ System.out.println("李四得到了书。"); } } //实现Runnable接口 class MyThread implements Runnable { private static Zhangsan zs = new Zhangsan(); //实例化static型对象,数据共享 private static Lisi ls = new Lisi(); //实例化static型对象,数据共享 public boolean flag = false; //声明标记,用于判断哪个对象先执行 //覆写run()方法 public void run(){ if (flag){ synchronized (zs){ zs.say(); try{ Thread.sleep(500); }catch (Exception e){ e.printStackTrace(); } synchronized (ls){ zs.get(); } } }else { synchronized (ls){ ls.say(); try{ Thread.sleep(500); }catch (Exception e){ e.printStackTrace(); } synchronized (zs){ ls.get(); } } } } } public class ClassDemo01 { public static void main(String[] args){ MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); myThread1.flag = true; myThread2.flag = false; Thread thread1 = new Thread(myThread1); Thread thread2 = new Thread(myThread2); // 启动线程 thread1.start(); thread2.start(); } }
3.关于同步和死锁
多个线程共享同一资源时需要进行同步,也就是加同步锁,以保证资源操作的完整性,但是过多的同步就有可能产生死锁。
6.线程操作案例--生产者及消费者
生产者不断生产,消费者不断取走生产者生产的产品。
可以看出,生产者生产出信息后将其放在一个区域中,消费者从此区域中取走数据。
但是由于线程运行存在不确定性,所有会存在一下三个问题:
- 加入生产者线程刚向数据存储空间添加了信息的名称,还没有添加该信息的内容,程序就切换到了消费者线程,消费者线程会把信息的名称和上一个信息的内容联系到一起。
- 生产者线程可能已经放了若干次数据,消费者线程才开始取数据。
- 消费者线程取完一个数据后,还没等生产者线程放入新的数据,又重复取出已经取出过的数据。
程序的基本实现
可以定义一个保存信息的类:Info(生产的信息)
Info类
class Info{ private String name; private String content; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 因为生产者和消费者要操作同一个空间的内容,所以生产者和消费者分别实现Runnable接口,并接收Info类的引用
生产者
//实现Runnable接口 class Producer implements Runnable { private Info info = null; //保存Info类的引用 public Producer(Info info) { this.info = info; } //覆写run()方法 public void run(){ boolean flag = false; //定义标记位 for (int i = 0; i < 50; i++) { if (flag){ this.info.setName("小狗"); try{ Thread.sleep(90); }catch (Exception e){ e.printStackTrace(); } this.info.setContent("犬科动物"); flag = false; }else { this.info.setName("小猫"); try{ Thread.sleep(90); }catch (Exception e){ e.printStackTrace(); } this.info.setContent("猫科动物"); flag = true; } } } } 消费者
class Consumer implements Runnable{ private Info info = null; public Consumer(Info info) { this.info = info; } @Override public void run() { for (int i = 0; i < 50; i++) { try{ Thread.sleep(100); }catch (Exception e){ e.printStackTrace(); } // 取出信息 System.out.println(this.info.getName() + "-->" + this.info.getContent()); } } } 测试程序
public class ClassDemo01 { public static void main(String[] args){ Info info = new Info(); //实例化Info对象 Producer producer = new Producer(info); //实例化生产者 Consumer consumer = new Consumer(info); //实例化消费者 new Thread(producer).start(); //启动生产者线程 new Thread(consumer).start(); //启动消费者线程 } } 输出
从输出结果中可以发现以上提到的三个问题都有出现。
先解决第一个问题 --- 加入同步锁
将设置名称和姓名定义为一个同步方法。
修改Info类
额外设置了set、和get方法。
class Info{ private String name; private String content; public synchronized void set(String name, String content){ this.setName(name); try{ Thread.sleep(300); }catch (Exception e){ e.printStackTrace(); } this.setContent(content); } public synchronized void get(){ try{ Thread.sleep(300); }catch (Exception e){ e.printStackTrace(); } System.out.println(this.getName() + "-->" + this.getContent()); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 修改生产者
class Producer implements Runnable { private Info info = null; //保存Info类的引用 public Producer(Info info) { this.info = info; } //覆写run()方法 public void run(){ boolean flag = false; //定义标记位 for (int i = 0; i < 50; i++) { if (flag){ this.info.set("小狗","犬科动物"); flag = false; }else { this.info.set("小猫","猫科动物"); flag = true; } } } } 修改消费者
class Consumer implements Runnable{ private Info info = null; public Consumer(Info info) { this.info = info; } @Override public void run() { for (int i = 0; i < 50; i++) { try{ Thread.sleep(100); }catch (Exception e){ e.printStackTrace(); } this.info.get(); } } } 测试程序
public class ClassDemo01 { public static void main(String[] args){ Info info = new Info(); //实例化Info对象 Producer producer = new Producer(info); //实例化生产者 Consumer consumer = new Consumer(info); //实例化消费者 new Thread(producer).start(); //启动生产者线程 new Thread(consumer).start(); //启动消费者线程 } } 输出
此时可以发现信息错乱的问题已经解决,但是依然存在重复读取的问题。
如果要解决这个问题就需要使用Object类对线程的支持--等待与唤醒。
7.Object类对线程的支持--等待与唤醒
Object类是所有类的父类,在此类中有以下几种方法是对线程操作有所支持的。
序号 | 方法 | 类型 | 描述 |
---|---|---|---|
1 | public final void wait() throws InterruptedException | 普通 | 线程等待 |
2 | public final native void wait(long timeout) throws InterruptedException; | 普通 | 线程等待,并指定等待的最长时间,以毫秒为单位 |
3 | public final void wait(long timeout, int nanos) throws InterruptedException | 普通 | 线程等待,并指定等待的最长时间,单位毫秒及纳秒 |
4 | public final native void notify(); | 普通 | 唤醒第一个等待的线程 |
5 | public final native void notifyAll(); | 普通 | 唤醒全部等待的线程 |
根据这些方法,我们可以把一个线程设置为等待状态,但是对于唤醒操作却有两个,分别是notify()和 notifyAll()。一般来说,所有等待的线程会按照顺序进行排列,如果使用 notify()方法,则会唤醒第一个等待的线程执行,如果使用了 notifyAll()方法,则会唤醒所有的等待线程,那个线程的优先级高,那个线程就有可能先执行。
解决生产者重复生产和消费者重复取走的问题:
方法:
增加一个标志位,如果标志位的值为true,则表示可以生产但是不能取走,此时如果线程执行到消费者线程则消费者线程应该等待;如果标志位的值为false,则表示可以取走但是不能生产,如果执行到了生产者线程,生产者线程应该等待。
直接修改Info类即可:修改Info类:
class Info{ private String name; private String content; private boolean flag = false; public synchronized void set(String name, String content){ if (!flag){ //标志位false,生产者不可以生产 try{ super.wait(); //等待消费者取走 }catch (Exception e){ e.printStackTrace(); } } this.setName(name); try{ Thread.sleep(300); }catch (Exception e){ e.printStackTrace(); } this.setContent(content); flag = false; //修改标志位,表示可以取走 super.notify(); //唤醒等待线程 } public synchronized void get(){ if (flag){ try{ super.wait(); }catch (Exception e){ e.printStackTrace(); } } try{ Thread.sleep(300); }catch (Exception e){ e.printStackTrace(); } System.out.println(this.getName() + "-->" + this.getContent()); flag = true; super.notify(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
注意: wait()
方法在执行的时候会释放锁。⭐⭐⭐
wait()的意思就是放弃已经持有的锁然后等待。
线程在运行的时候占用了计算机的共享资源,因为当前线程在使用它,然而当前线程进行了休眠例如 wait() 很浅显的道理,当前线程已经停止了,那意味着这个资源空闲了下来。那么作为万恶的剥削者"程序员"肯定不会让这个资源空闲着,你们说对吧!!!
因此很容易推断出wait()是会释放锁的,而锁的奥义就是控制指定的线程持有共享资源,既然线程都进行了等待,肯定是要需要释放锁的!!!
举例:
很简单,用两个线程同时用一把锁,其中一个线程先执行,并且进行
wait()
,如果释放了锁,那么是不是对于另外一个线程来说它就可以抢占到这个锁呢(因为它空闲下来了)
wait()会立刻释放sycronized(obj)中的obj锁,以便其他线程可以执行obj.nodify() 但是nodify()不会立刻立刻释放sycronized(obj)中的obj锁,必须要等nodify()所在线程执行完sycronized(obj)块中的所有代码才会释放这把锁
8.线程的生命周期
方法介绍:
suspend()
:暂时挂起线程。resume()
:恢复挂起的线程。stop()
:停止线程。
注意:
suspend()、resume()、stop()、这3种方法并不推荐使用,因为这三种方法在操作时会产生死锁的问题。
suspend()、resume()、stop()方法使用了@Deprecated声明。
@Deprecated属于Annotation的语法,表示此操作不建议使用。所以一旦使用了这些方法之后将会出现警告信息。
思考:既然以上方法不推荐使用,那么该如何停止一个线程的执行呢?
答:在多线程的开发中可以通过设置标志位的方式停止一个线程的运行。
举例:停止线程。
class MyThread implements Runnable{ private boolean flag = true; @Override public void run() { int i = 0; while(this.flag){ System.out.println(Thread.currentThread().getName() + "运行,i = " + (i++)); } } // 编写停止方法 public void stop(){ // 修改标志位 this.flag = false; } } public class StopThreadDemo { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.start(); try{ Thread.sleep(10); }catch (Exception e){ e.printStackTrace(); } myThread.stop(); } } 以上程序一旦调用stop()方法就会将MyThread类中的flag变量设置为false,这样run()方法就会停止运行。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~