Java 多线程

2023-12-15 21:53:09

?Java 多线程

??? ? 如果一次只完成一件事情,很容实现。但现实生活中,很多事情都是同时进行的。Java中为了模拟这种状态,引入了线程机制。简单地说,当程序同时完成多件事情时,就是所谓的多线程。多线程应用相当广泛,使用多线程可以创建窗口程序、网络程序等。



一? ?线程简介

? ? ? ?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。

? ? ? ?线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,它们共享进程的内存空间和系统资源,但是每个线程又有自己的独立运行堆栈和程序计数器。线程能够很好地提高程序的并发执行效率,可以将一个进程拆分成多个并发的任务,每个任务由一个线程负责处理,通过利用多核处理器和多线程技术,可以提高系统的处理速度和资源利用率。

? ? ? 一个进程可以有多个线程:

? ? ? ?当我们打开电脑的任务管理器时就可以看到进程,如下:

?线程的特点:



二? ?创建线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

2.1? ? 继承THread类

? ? Thread类是java.lang包中的一个类,从这个类中实例化的对象代表线程,程序员启动一个新线程需要建立Thread实例。Thread类中常用的两个构造方法如下:

public?Thread):创建一个新的线程对象。
public?Thread(String?threadName):创建一个名称为threadName的线程对象。

继承Thread类创建一个新的线程的语法如下:
public?class?ThreadTest?extends?Thread{

}

? ? ?完成线程真正功能的代码放在类的run()方法中,当一个类继承Thread类后,就可以在该类中覆盖run()方法,将实现该线程功能的代码写入run()方法中,然后调用Thread类中的start()方法执行线程,也就是调用run0方法。

? ? ?Thread对象需要一个任务来执行,任务是指线程在启动时执行的工作,该工作的功能代码被写在run()方法中。run()方法必须使用以下语法格式:
public?void?run(){
}

【注意】
如果start?方法调用一个已经启动的线程,系统将抛出IllegalThreadStateException异常。
?

??? ? 循环打印0~10模拟实现线程的创建与运行:

【代码实列】


public class ThreadTest extends Thread{
	public void run() {
		for(int i=0;i<=10;i++) {
			System.out.println(i);
			try {
				Thread.sleep(1000);//设置休眠事件为1000ms=1s
			} catch (InterruptedException e) {
			   e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		ThreadTest t = new ThreadTest();
		t.start();
	}

}

【运行效果】

从截屏可以看出,截屏时结果没有全部出现,这是因为设置了阻塞状态,通过sleep()函数,让程序1秒输出一个。?

??? ? ?模拟同时运行两个线程:

【代码实列】


public class Deno2 {
	public static void main(String[] args) {
		ThreadA A = new ThreadA();
		ThreadB B = new ThreadB();
		A.start();
		B.start();
		
	}

}
class ThreadA extends Thread{
	public void run() {
		for(int i=1;i<=8;i++) {
			System.out.println(i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

class ThreadB extends Thread{
	public void run() {
			for(char i='A';i<='H';i++) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
	}
}

【运行效果】

两个线程的启动是有先后顺序的,但他们几乎同时运行的,但他们的运行顺序和执行时间不是代码控制的是由cpu来控制的?


?2.2? ?实现Runnable接口

? ? ? ?到目前为止,线程都是通过扩展Thread类来创建的,如果程序员需要继承其他类(非Thread类),而且还要使当前类实现多线程,那么可以通过Runnable接口来实现。例如,一个扩展JFrame类的GUI程序不可能再继承Thread类,因为Java语言中不支持多继承,这时该类就需要实现Runnable接口使其具有使用线程的功能。实现Runnable接口的语法如下:
public?class?Thread?extends?Object?implements?Runnable
?

?

?

? ? ? ?实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联。Thread类中有以下两个构造方法:
public?Thread(Runnable?target)。
public?Thread(Runnable?target,String?name)。

这两个构造方法的参数中都存在Runnable实例,使用以上构造方法就可以将Runnable实例与Thread实例相关联。
使用Runnable接口启动新的线程的步骤如下:
(1)建立Runnable对象。
(2)使用参数为Runnable对象的构造方法创建Thread实例。
(3)调用start)方法启动线程。

线程最引人注目的是与Swing相结合创建一个GUI程序,下面我们模拟实现一下:

【代码实列】

import java.awt.Container;
import javax.swing.*;

public class SwingAndThread extends JFrame {
	int count = 0; // 图标横坐标

	public SwingAndThread() {
		setBounds(300, 200, 250, 100); // 绝对定位窗体大小与位置
		Container container = getContentPane();// 主容器
		container.setLayout(null); // 使窗体不使用任何布局管理器

		Icon icon = new ImageIcon("src/1.gif"); // 图标对象
		JLabel jl = new JLabel(icon);// 显示图标的标签
		jl.setBounds(0, 0, 25, 50); // 设置标签的位置与大小
		Thread t = new Thread() { // 定义匿名线程对象
			public void run() {
				while (true) {
					jl.setBounds(count,0, 25, 50); // 将标签的横坐标用变量表示
					try {
						Thread.sleep(500); // 使线程休眠500毫秒
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					count += 4; // 使横坐标每次增加4
					if (count >= 200) {
						// 当图标到达标签的最右边时,使其回到标签最左边
						count = 10;
					}
				}
			}
		};
		t.start(); // 启动线程
		container.add(jl); // 将标签添加到容器中
		setVisible(true); // 使窗体可见
		// 设置窗体的关闭方式
		setDefaultCloseOperation(EXIT_ON_CLOSE);
	}

	public static void main(String[] args) {
		new SwingAndThread(); // 实例化一个SwingAndThread对象
	}
}

?【运行效果】



?

三? 线程的生命周期

? ?线程和人一样也有出生,死亡等状态,我们将这种成为生命周期:

? ? ? ?线程具有生命周期,其中包含7种状态,分别为出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用start)方法之前线程都处于出生状态;当用户调用start(方法后,线程处于就绪状态(又被称为可执行状态);当线程得到系统资源后就进入运行状态。
? ? ? 一旦线程进入可执行状态,它会在就绪与运行状态下转换,同时也有可能进入等待、休眠、阻塞或死亡状态。当处于运行状态下的线程调用Thread类中的wait()方法时,该线程便进入等待状态,进入等待状态的线程必须调用Thread类中的notify0方法才能被唤醒,而调用notifyAll方法可将所有处于等待状态下的线程唤醒;当线程调用Thread类中的sleep(方法时,则会进入休眠状态。如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入/输出结束时线程进入就绪状态,对于阻塞的线程来说,即使系统资源空闲,线程依然不能回到运行状态。当线程的run)方法执行完毕时,线程进入死亡状态。
?

?

?

  • 新建状态(出生):

    使用?new?关键字和?Thread?类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序?start()?这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行?run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态(暂停):

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

?



四? ?操作线程的方法

4.1? ?线程的休眠

一种能控制线程行为的方法是调用sleep方法,sleep0方法需要一个参数用于指定该线程休眠的时间,该时间以毫秒为单位。它通常是在run()方法内的循环中被使用。sleep()方法的语法如下:
try{
? ? ? ?Thread.sleep(2000);
}catch(InterruptedException?e){
? ? ? ?e.printStackTrace();

}


上述代码会使线程在2秒之内不会进入就绪状态。由于sleep)方法的执行有可能抛出InterruptedException异常,所以将sleep0方法的调用放在try-catch块中。虽然使用了sleep()方法线程在一段时间内会醒来,但是并不能保证它醒来后进入运行状态,只能保证它进入就绪状态。在前面循环打印数字时已经展示了sleep()的用法和效果,这里就不在演示了。


4.2? ?线程的加入

? ? ?如果当前某程序为多线程程序,假如存在一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后再继续执行线程A,此时可以使用Thread类中的join()方法来完成。这就好比此时读者正在看电视,突然有人上门收水费,读者必须付完水费后才能继续看电视。
? ? ?当某个线程使用join)方法加入另外一个线程时,另一个线程会等待该线程执行完毕后再继续执行。

?

使用进度条来模拟一下线程的加入效果:

【代码实列】

?

import java.awt.BorderLayout;
import javax.swing.*;

public class JoinTest extends JFrame {
	private Thread A;//线程A
	private Thread B;//线程B
	private JProgressBar a1 = new JProgressBar();//进度条1
	private JProgressBar b1 = new JProgressBar();//进度条2
	
	
	
	public JoinTest() {
		setBounds(200, 200, 200, 100);//设置窗体位置,大小
		setDefaultCloseOperation(EXIT_ON_CLOSE);//设置关闭方式
		getContentPane().add(a1, BorderLayout.NORTH);//设置进度条a1在北
		getContentPane().add(b1, BorderLayout.SOUTH);//设置进度条b1在南
		a1.setStringPainted(true);//设置进度条显示字符
		b1.setStringPainted(true);
		A=new Thread() {
			int count = 0;
			public void run() {
				while(true) {
					a1.setValue(++count);
					try {
						Thread.sleep(100);
						if(count==20) {
							B.join();
						}
						
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		A.start();
		B=new Thread() {
			int count = 0;
			public void run() {
				while(true) {
					b1.setValue(++count);
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					if(count==100) {
						break;
					}
				}
			}
		};
		B.start();
		setVisible(true);
	}

	public static void main(String[] args) {
		JoinTest t = new JoinTest();
		
	}
}

【运行效果】

?


4.3? ?线程的中断?

? ? ? ?如果线程是因为使用了sleep()或wait()方法进入了就绪状态,可以使用Thread类中?interrupt()方法使线程离开run()方法,同时结束线程,但程序会抛出InterruptedException异常,用户可以在处理该异常时完成线程的中断业务处理,如终止while循环。

【代码实列】

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;

public class InterruptedSwing extends JFrame {

	public static void main(String[] args) {
		new InterruptedSwing();
	}

	public InterruptedSwing() {
		setBounds(100, 100, 200, 100);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		JProgressBar progressBar = new JProgressBar(); // 创建进度条
		getContentPane().add(progressBar, BorderLayout.NORTH); // 将进度条放置在窗体合适位置
		JButton button = new JButton("停止");
		getContentPane().add(button, BorderLayout.SOUTH);
		progressBar.setStringPainted(true); // 设置进度条上显示数字
		Thread t = new Thread(new Runnable() {
			int count = 0;

			public void run() {
				while (true) {
					progressBar.setValue(++count); // 设置进度条的当前值
					try {
						Thread.sleep(100); // 使线程休眠100毫秒
					} catch (InterruptedException e) { // 捕捉InterruptedException异常
						JOptionPane.showMessageDialog(null, "当前线程被中断", "提示!", JOptionPane.PLAIN_MESSAGE);
						break;
					}
				}
			}
		});

		button.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				t.interrupt(); // 中断线程
			}
		});
		t.start(); // 启动线程
		setVisible(true);
		
	}

}

?【运行效果】


4.4? ?线程的礼让

? ? ? Thread类中提供了一种礼让方法,使用yield0方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程,但这仅是一种暗示,没有任何一种机制保证当前线程会将资源礼让。yield)方法使具有同样优先级的线程有进入可执行状态的机会,在当前线程放弃执行权时会再度回到就绪状态。对于支持多任务的操作系统来说,不需要调用yield0方法,因为操作系统会为线程自动分配CPU时间片来执行。



五? ?线程的优先级?


? ? ? ? 每个线程都具有各自的优先级,线程的优先级可以表明在程序中该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。但这并不意味着低优先级的线程得不到运行,而只是它运行的概率比较小,如垃圾回收线程的优先级就较低。
Thread?类中包含的成员变量代表了线程的某些优先级,如Thread.MIN?PRIORITY(常数1)、Thread.MAX_PRIORITY(常数10)、Thread.NORM_PRIORITY(常数5)。其中,每个线程的优先级都在?Thread.MIN_PRIORITY~Thread.MAX_PRIORITY,在默认情况下其优先级都是Thread.NORMPRIORITY。每个新产生的线程都继承了父线程的优先级。
? ? ? ? 在多任务操作系统中,每个线程都会得到一小段CPU时间片运行,在时间结束时,将轮换另一个线程进入运行状态,这时系统会选择与当前线程优先级相同的线程予以运行。系统始终选择就绪状态下优先级较高的线程进入运行状态。

? ? ? 处于各个优先级状态下的线程的运行顺序如图所示。在图中,优先级为5的线程A首先得到CPU时间片;当该时间结束后,轮换到与线程A相同优先级的线程B;当线程B的运行时间结束后,会继续轮换到线程A,直到线程A与线程B都执行完毕,才会轮换到线程C;当线程C结束后,才会轮换到线程D。

?

? ? ?线程的优先级可以用setPriority()方法调整,如果使用该方法设置的优先级不在1~10,将产生IllegalArgumentException异常。

【代码实列】



public class PriorityTest implements Runnable {
	String name;
	public PriorityTest(String name) {
		this.name = name;
	}

	public static void main(String[] args) {
		Thread A = new Thread(new PriorityTest("A"));
		A.setPriority(1);
		Thread B = new Thread(new PriorityTest("B"));
		B.setPriority(3);
		Thread C = new Thread(new PriorityTest("C"));
		C.setPriority(5);
		Thread D = new Thread(new PriorityTest("D"));
		D.setPriority(7);
		Thread E = new Thread(new PriorityTest("E"));
		E.setPriority(10);
		A.start();
		B.start();
		C.start();
		D.start();
		E.start();
		
		
	}

	

	public void run() {
		String tmp = "";
		for(int i=0;i<50000;i++) {
			tmp=tmp+1;
		}
		System.out.println(name+"完成任务.");
	}


}

?【运行结果】

我们可以看到运行的结果并没有全部按照优先级执行,这是由于线程的执行顺序是由cpu决定的,即使线程设定了优先级也是作为cpu的参考数据。?

?



?六? ?线程同步

? ? ? ? 在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,但是如果使用多线程程序,就会发生两个线程抢占资源的问题,如两个人同时说话、两个人同时过同一个独木桥等。所以,在多线程编程中需要防止这些资源访问的冲突。Java提供了线程同步的机制来防止资源访问的冲突。


6.1? ?线程安全


? ? ? ?实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。这种多线程的程序通常会发生问题,以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于0的结论,于是它也执行售出操作,这样就会产生负数。所以,在编写多线程程序时,应该考虑到线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。

模拟实现未考虑线程安全情况下的售票系统:

【代码实列】



public class  ThreasafeTest implements Runnable {
	int count =10;//票池


	public static void main(String[] args) {
		ThreasafeTest d = new ThreasafeTest();//实列化类对象
		Thread A = new Thread(d,"售票一");
		Thread B = new Thread(d,"售票二");
		Thread C = new Thread(d,"售票三");
		A.start();
		B.start();
		C.start();
		
	
		
		
	}
	public void run() {
		while(true) {
			if(count>0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"----票数"+count--);
			}
		}
		
	}



}

【运行效果】

从这个结果可以看出,最后打印出的剩下的票数为负值,这样就出现了问题。这是由于同时创建了3个线程,这3个线程执行run()方法,在count变量为1时,线程一、线程二、线程三、都对num变量有存储功能,当线程一执行run()方法时,还没有来得及做递减操作,就指定它调用sleep0方法进入就绪状态,这时线程二、线程三也都进入了run()方法,发现count变量依然大于0,但此时线程一休眠时间已到,将count变量值递减,同时线程二、线程三也都对count变量进行递减操作,从而产生了负值。

? ? ? ?那么,该如何解决资源共享的问题呢?所有解决多线程资源冲突问题的方法基本上都是采用给定时间只允许一个线程访问共享资源的方法,这时就需要给共享资源上一道锁。这就好比一个人上洗手间时,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。


6.2? ?线程同步机制

为了解决资源共享问题,保证线程的安全性,java提供了同步机制(也就是上锁).同步机制使用synchronised关键字。共有两种方法:

1.同步块

使用synchronized关键字包含的代码块称为同步块,也称为临界区,语法如下:
synchronized?(Object) {
}
通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才可以进入该区域。Object为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1。一个线程运行到同步块时首先检查该对象的标志位,如果为0状态,表明此同步块内存在其他线程,这时当期线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位设置为1,当期线程才能开始执行同步块中的代码,并将Object对象的标识位设置为0.以防止其他线程执行同步块中的代码。

?

2.同步方法

同步方法就是在方法前面用synchronized关键字修饰的方法,其语法如下:
synchronized?void?f(){

}
当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。
?

?

上述两种情况的运行效果一样:
?

上述结果打印的结果并没有出现负数,这是运用了资源同步机制,使其不能够共享,也就不会产生线程不安全(脏数据)。?

文章来源:https://blog.csdn.net/2301_77599154/article/details/134900164
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。