Java多线程

多线程

1.进程与线程

进程:进程是程序的一次动态执行过程,它需要经历从 代码加载代码执行执行完毕的一整个过程。也对应着进程本身从产生、发展到消亡的过程。

线程:线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。

多线程:多线程是实现并发机制的一种有效手段,多线程是指一个程序在执行过程中可以产生多个更小的程序单元。这些更小的程序单元就称为线程,这些线程可以同时存在、同时运行。

一个进程中可能包含多个同时执行的线程。

举例: 通过word的使用了解进程与线程的区别

当我们启动一个word对于操作系统而言就相当于启动了一个系统的进程,在这个进程之上又有许多其他程序在运行(比如:拼写检查),那么这些程序就是一个个小的线程。如果word关闭了,则这些拼写检查的线程也将会消失,但是反过来如果拼写检查的线程消失了,也并不定会让word的进程消失。

2.Java中线程的实现

Java中实现多线程:

  1. 继承thread类
  2. 实现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接口之间是有区别的:

  1. 继承Thread类不能资源共享
  2. 实现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类来说,有如下优势:

  1. 适合多个相同程序代码的线程去处理同一资源的情况
  2. 可以避免由于java单继承特性带来的局限
  3. 增强程序健壮性,代码能够被多个线程共享,代码与数据是独立的。

3.线程的状态

线程一般具有5种状态:

  1. 创建
  2. 就绪
  3. 运行
  4. 阻塞
  5. 终止

线程状态转换图:

  1. 创建状态

    在程序中使用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时它拥有相应的内存空间和其他资源,但是还处于不可运行的状态。

    新建一个线程对象可采用Thread类的构造方法来实现,例如"Thread thread = new Thread();"

  2. 就绪状态

    调用新建线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。

  3. 运行状态

    当处于就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。

  4. 阻塞状态

    一个正在执行的线程在某些特殊情况下,例如:人为挂起、需要执行耗时的输入输出操作时,会让出CPU并暂时终止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  5. 死亡状态

    线程调用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类中可以通过:

  1. getName()方法取得线程的名称。
  2. 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修饰方法:⭐⭐⭐
  1. synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
  2. synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
  3. 对于类锁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.死锁

什么是死锁

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

产生死锁的四个必要条件:

  1. 互斥使用(资源独占)

    一个资源每次只能给一个线程使用

    说明:对象锁只有一把,同一时间,只能有一个线程持有,其他线程需等待


  1. 不可强占(不可剥夺)

    资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放

    说明:当线程A先拿到对象锁时,线程B除了等待线程A主动释放该对象锁时,什么都干不了(想都别想)


  2. 请求和保持

    一个线程在申请新的资源的同时保持对原有资源的占有

    说明:线程A原本持有对象M的锁,但又想要申请获取对象N的锁,这时候,如果获得对象N的锁遇到阻塞时,就会导致线程A原本持有的对象M的锁无法得到释放,这就导致其他想要获取对象M锁的线程陷入无限的等待中


  3. 循环等待

    存在一个线程等待队列 {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.线程操作案例--生产者及消费者

生产者不断生产,消费者不断取走生产者生产的产品。

可以看出,生产者生产出信息后将其放在一个区域中,消费者从此区域中取走数据。

但是由于线程运行存在不确定性,所有会存在一下三个问题:

  1. 加入生产者线程刚向数据存储空间添加了信息的名称,还没有添加该信息的内容,程序就切换到了消费者线程,消费者线程会把信息的名称和上一个信息的内容联系到一起。
  2. 生产者线程可能已经放了若干次数据,消费者线程才开始取数据。
  3. 消费者线程取完一个数据后,还没等生产者线程放入新的数据,又重复取出已经取出过的数据。

程序的基本实现

可以定义一个保存信息的类: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.线程的生命周期

方法介绍:

  1. suspend():暂时挂起线程。
  2. resume():恢复挂起的线程。
  3. 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()方法就会停止运行。

posted @   e路有你  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示