初识多线程
前言:
不按时更新的我突然回来更新一波多线程操作,多多点击,多多分享,多多支持。希望本期内容有所帮助。
(本文中如有任何错误的观点说法,还请路过的大佬不吝赐教,感激不尽)
一.初识多线程
这个世界上很多事物可以同时完成很多工作。例如人体可以在进行呼吸的时候,血液循环系统也在运转。例如我正在使用的计算机,在我编写博客文章的同时还可以听音乐、挂机打游戏等。如果一次只完成一件事情,会很容易实现,但是在现实世界中,很多事情都是在同时进行的。这种思想在Java语言中被称为并发,而将并发完成的每一件事情称为线程。
在Java语言中为了模拟这种状态,引入了线程机制。在Java中,这种机制非常重要,但是并不是所有的程序语言都支持线程。在以往的程序中,多以一个任务完成以后再进行下一个项目的模式进行开发,这样下一个任务的开始必须等待前一个任务的结束。Java语言提供了并发机制,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其他的线程并发执行,这种机制称为多线程。
多线程是非常复杂的机制,Java中的多线程在每个操作系统中的运行方式也存在差异,例如Windows操作系统。Windows操作系统是多任务操作系统,它以进程为单位。一个进程是一个包含有自身地址的程序,每个独立执行的程序都称为进程,也就是正在执行的程序。系统可以分配给每个进程一段有限的使用CPU的时间(也可以称为CPU时间片),CPU在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。由于CPU转换较快,所以使得每个进程好像是同时执行。
画个简单的图,表明Windows操作系统的执行模式:
一个线程则是进程中的执行模式,一个进程可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程。在单线程中,程序代码按照调用顺序依次向下执行,如果需要一个进程同时完成多段代码的操作,就需要产生多线程。
二.实现线程对的两种方式
在Java中,主要提供了两种实现线程的方式,分别为继承java.lang.Thread类与实现java.lang.Runnable接口。
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()方法执行线程。
Thread对象需要一个任务来执行,任务是指线程在启动时执行工作,该工作的功能代码被写在run()方法中。
run()方法必须使用以下语法格式:
public void run() {
}
当执行一个线程程序时,就自动产生一个线程,主方法正式在这个线程上运行的。当不再启动其他线程时,该程序就为单线程程序。主方法线程启动由Java虚拟机负责,程序员负责启动自己的线程。
代码示例:
public class ThreadTest extends Thread {
public static void main(String[] args) {
new ThreadTest().start();
}
}
注意:
如果start()方法调用一个已经启动的线程,系统将抛出IllThreadStateException异常。
举个继承Thread类的栗子:
/**
* 使用继承Thread类的方法创建线程
*/
public class ThreadTest extends Thread {
private int count = 10;
public void run() { //重写run方法
while (true) {
System.out.print(count + " "); //打印count变量
if (--count == 0) { //使count变量自减,直至减为0的时候,退出循环
return;
}
}
}
//执行线程
public static void main(String[] args) {
new ThreadTest().start();
}
}
运行结果:
上述栗子,在main方法中,使线程执行需要调用Thread类中的start()方法,start()方法调用被覆盖的run()方法,如果不调用start()方法,线程永远都不会启动,在主方法没有调用start()方法之前,Thread对象只是一个实例,而不是真正的线程。
2.2实现Runnable接口
到目前为止,线程都是通过扩展Thread类来创建的,如果程序员需要继承其他类(非Thread类),而且还要使当前类实现多线程,那么可以通过Runnable接口来实现。例如,一个扩展JFrame类的GUI程序不可能再继承Thread类,因为Java语言中不支持多继承,这时该类就需要实现Runnable接口使其具有使用线程的功能。
实现Runnable接口的语法如下:
public class Thead extends Object implements Runnable{
}
说明:
实质上Thread类实现了Runnable接口,其中的run方法正是对Runnable接口中的run()方法的具体实现。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联,Thread类中有以下两个构造方法:
-
public Thread(Runnable target)
-
public Thread(Runnable target,String name)
这两个构造方法的参数中都存在Runnable实例,使用以上构造方法就可以将Runnable实例与Thread实例相关联。
使用Runnable接口的步骤:
- 建立Runnable对象
- 使用参数为Runnable对象的构造方法创建Thread实例
- 调用start()方法启动线程。
通过Runnable接口创建线程是,首先需要编写一个实现Runnable接口的类,然后实例化该类的对象,这样就建立了Runnable对象;接下来使用相应的构造方法创建Thread实例;最后使用该实例调用Thread类中start()方法启动线程。
实现Runnable接口创建线程的流程图:
线程最引人注目的部分应该是与Swing相结合创建GUI程序,在这里演示一个GUI程序,实现图标滚动功能。
代码示例:
import javax.swing.*;
import java.awt.*;
import java.net.URL;
public class SwingAndThread extends JFrame {
private JLabel jl = new JLabel(); //声明JLabel对象
private static Thread t; //声明线程对象
private int count = 0; //声明计数变量
private Container container = getContentPane(); //声明容器
public SwingAndThread(){
setBounds(300,200,250,100); //绝对定位窗体大小与位置
container.setLayout(null); //使窗体不适用任何布局管理器
URL url = SwingAndThread.class.getResource("/1.gif"); //获取图片的url
Icon icon = new ImageIcon(url); //实例化一个Iocn
//调节Icon的大小
((ImageIcon) icon).setImage(((ImageIcon) icon).getImage().getScaledInstance(50,50,Image.SCALE_DEFAULT));
jl.setIcon(icon);
//设置图片在标签的最左方
jl.setHorizontalAlignment(SwingConstants.LEFT);
jl.setBounds(10,10,200,50); //设置标签的位置与大小
jl.setOpaque(true);
t = new Thread(new Runnable() { //定义匿名内部类,该类实现Runnable接口
@Override
public void run() { //重写run方法
while (count <= 200){ //设置循环条件
//将标签的横坐标用变量表示
jl.setBounds(count,10,200,50);
try {
Thread.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
count += 1; //使横坐标每次增加4
if (count == 200) {
//当图标达到标签最右边时,使其回到标签最左边
count = 10;
}
}
}
});
t.start(); //启动线程
container.add(jl); //将标签添加到容器中
setVisible(true); //使窗体可见
//设置窗体关闭方式
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
public static void main(String[] args) {
new SwingAndThread(); //实例化一个SwingAddThread对象
}
}
运行结果:
这段代码中,为了使图标具有滚动功能,需要在类的构造方法中创建Thread实例。在创建该实例的同时需要Runnable对象作为Thread类构造方法的参数,然后使用内部类形式实现run()方法。在run()方法中主要循环图标的横坐标位置,当图标横坐标到达标签的最右方时,再次将图标的横坐标置于图标滚动的初始位置。
注意:
启动一个新的线程,不是直接调用Thread子类对象的run()方法,而是调用Thread子类的start()方法,Thread类的start()方法产生一个新的线程,该线程运行Thread子类的run()方法。
三、、线程的生命周期
线程具有生命周期,其中包含七种状态,这七种状态分别为出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。出生状态就是线程被创建时处于的状态,在用户使用该线程实例创建start()方法之前线程都处于出生状态;当用户调用start()方法后,线程处于就绪状态(又被称为可执行状态);当线程得到系统资源后就进入运行状态。一旦线程进入可执行状态,它会在就绪和运行状态下转换,同时也有可能进入等待、休眠、阻塞或者死亡状态。
当处于运行状态下的线程调用Thread类中的wait()方法时,该线程就进入了等待状态,进入等待状态的线程必须调用Thread类中的notify()方法才能被唤醒,而notifyAll()方法是将所有处于等待状态下的线程唤醒;当线程调用Thread类中的sleep()方法时,则会进入休眠状态。如果一个线程在运行状态下发出输入/输出请求,该线程将会进入阻塞状态,在其等待输入/输出结束时线程进入就绪状态。对于阻塞的线程来说,即使系统资源空闲,线程依然不能回到运行状态。当线程的run()方法执行完毕时,线程进入死亡状态。
虽然看起来多线程像同时执行,但是事实上在同一时间点上只有一个线程被执行,只是线程之间切换较快,所以才会使人产生线程是同时进行的假象。在Windows操作系统中,系统会为每个线程分配一段较小CPU时间片,一旦时间片结束就会将当前线程切换为下一个线程,即使该线程没有结束。
是线程进入就绪状态有以下几种方法:
-
调用sleep()方法
-
调用wait()方法
-
等待输入/输出完成
当线程处于就绪状态后,可以用以下几种方法使线程再此进入运行状态。
- 线程调用notify()方法。
- 线程调用notifyAll()方法。
- 线程调用interrupt()方法。
- 线程的休眠时间结束
- 输入/输出结束
下面这张图描绘了线程生命周期的各种状态:
四、操作线程的方法
操作线程有很多方法,这些方法可以使线程从某一种状态过渡到另一种状态。
4.1线程的休眠
一种能控制线程行为的方法是调用sleep()方法,sleep()方法需要一个参数用于指定该线程休眠的时间,该时间参数以毫秒为单位。
sleep()方法的语法如下:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
上述代码会使线程在2s之内不仅如此就绪状态。由于sleep()方法的执行可能会抛出InterruptedException异常,所以将sleep()方法的调用放在try-catch块中。虽然使用了sleep()方法的线程会在一段时间内醒来,但是并不能保证它醒来之后进入运行状态,只能保证他进入就绪状态。
举个栗子:
import javax.swing.*;
import java.awt.*;
import java.util.Random;
/**
* 实现在窗体中自动画线段的功能,并且为线段设置颜色,颜色是随机产生的
*/
public class SleepMethodTest extends JFrame {
private Thread t;
//定义颜色数组
private static Color[] color = {Color.BLACK,Color.BLUE,Color.CYAN,
Color.GREEN,Color.ORANGE,Color.YELLOW,Color.RED,Color.PINK,Color.LIGHT_GRAY};
private static final Random rand = new Random(); //创建随机对象
private static Color getC(){
return color[rand.nextInt(color.length)];
}
public SleepMethodTest(){
t = new Thread(new Runnable() {
//定义初始坐标
int x = 30;
int y = 50;
@Override
public void run() { //覆盖线程接口方法
while (true) { //无线循环
try {
Thread.sleep(100); //休眠0.1s
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取组件绘图上下文对象
Graphics graphics = getGraphics();
graphics.setColor(getC()); //设置绘图颜色
//绘制直线并递增垂直坐标
graphics.drawLine(x,y,100,y++);
if (y >= 80) {
y = 50;
}
}
}
});
t.start(); //启动线程
}
//主方法
public static void main(String[] args) {
init(new SleepMethodTest(),100,100);
}
//初始化线程界面
public static void init(JFrame frame, int width , int height){
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width,height);
frame.setVisible(true);
}
}
运行结果:
这段代码中定义了getC()方法,该方法用于随机产生Color类型的对象,并且在产生线程的匿名内部类中使用getGraphics()方法获取Graphics对象,使用该对象调用setColor()方法为图像设置颜色;调用drawLine()方法绘制一条线段,同时线段会根据纵坐标的变化自动调整。
4.2线程的加入
如果当前某程序为多线程程序,这个程序中存在一个线程A,现在需要再插入一个线程B,并且让线程B先执行完毕然后再继续执行线程A。此时就可以使用Thread类中的join()方法来完成。当某个线程使用join()方法加入到另一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
代码示例:
import javax.swing.*;
import java.awt.*;
/**
* 使用join()方法,实现线程的加入
*/
public class JoinTest extends JFrame{
//定义两个线程
private Thread threadA;
private Thread threadB;
//定义两个进度条组件
final JProgressBar progressBar = new JProgressBar();
final JProgressBar progressBar2 = new JProgressBar();
int count = 0;
public static void main(String[] args) {
init(new JoinTest(),100,100);
}
public JoinTest(){
super();
//将进度条设置在窗体最上面
getContentPane().add(progressBar, BorderLayout.NORTH);
//将进度条设置在窗体最下面
getContentPane().add(progressBar2,BorderLayout.SOUTH);
//设置进度条显示数字字符
progressBar.setStringPainted(true);
progressBar2.setStringPainted(true);
//使用匿名内部类初始化Thread实例
threadA = new Thread(new Runnable() {
int count = 0;
@Override
public void run() { //重写run方法
while (true) {
progressBar.setValue(++count);
try {
Thread.sleep(100); //使线程A休眠100毫秒
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadA.start(); //启动线程A
threadB = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
progressBar2.setValue(++count); //设置进度条的当前值
try {
Thread.sleep(100); //使线程B休眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//当count变量增长为100时跳出循环
if (count == 100) {
break;
}
}
}
});
threadB.start(); //启动线程B
}
//设置窗体的各种属性的方法
public static void init(JFrame frame,int width,int height){
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width,height);
frame.setVisible(true);
}
}
运行结果:
在线程的run()方法中使线程B的对象调用join()方法,而join()方法使当前运行线程暂停,直到调用join()方法的线程执行完后再执行,所以线程A等待线程B执行完毕后再开始执行,即下面的进度条滚动完毕后,上面的滚动条才开始滚动。
4.3线程的中断
以往会使用stop()方法停止线程,但是现版本的JDK已经废除stop()方法,所以不建议使用stop()方法来停止一个线程的运行。现在提倡在run()方法中使用循环的形式然后使用布尔变量标记控制循环的停止。
代码示例:
public class Interrupted {
private boolean isContinue = false; //设置一个标记变量,默认值为false
public void run(){ //重写run()方法
while (true) {
//......
if (isContinue) { //当isContinue变量为true时,停止线程
break;
}
}
}
public void setContinue(){ //定义设置isContinue变量为true的方法
this.isContinue = true;
}
}
如果线程因为使用了sleep()或者wait()方法进入了就绪状态,可以使用Thread类中的interrupt()方法使线程离开run()方法,同时结束线程,单程序会抛出InterruptedException异常,可以在处理该异常时完成线程的中断业务处理,如终止while循环。
举个栗子:
写一段代码,演示某个线程使用interrupted()方法,同时抛出InterruptedException异常,在异常处理时结束了while循环。在项目中经常在这里执行关闭数据库连接和关闭Socket连接等操作。
创建一个类,这个类实现了Runnable接口,创建一个进度条,在表示进度条的线程中使用interrupted()方法。
import javax.swing.*;
import java.awt.*;
public class InterruptedSwing extends JFrame {
Thread thread;
public static void main(String[] args) {
init(new InterruptedSwing(),100,100);
}
public InterruptedSwing(){
super();
final JProgressBar progressBar = new JProgressBar(); //创建进度条
//将进度条放在窗口中的合适位置
getContentPane().add(progressBar, BorderLayout.NORTH);
progressBar.setStringPainted(true); //设置进度条上显示数字
thread = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true) {
progressBar.setValue(++count);
try {
thread.sleep(1000); //使现场休眠1000毫秒
//捕捉InterruptException异常
} catch (InterruptedException e) {
System.out.println("当前线程被中断");
break;
}
}
}
});
thread.start(); //启动线程
thread.interrupt(); //中断线程
}
public static void init(JFrame frame, int width, int height) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width,height);
frame.setVisible(true);
}
}
运行结果:
4.4线程的礼让
Thread类提供了一种礼让方法,使用yield()方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程,单这仅仅是一种暗示,没有任何一种机制保证当前线程会让资源礼让。
yield()方法使具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时会在度回到就绪状态。对于支持多任务的操作系统来说,不需要调用yield()方法,因为操作系统会为线程自动分配CPU时间片来执行。
五、线程的优先级
每个线程都具有各自的优先级,线程的优先级可以表明在程序中该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。但这并意味着低优先级的线程得不到运行,而只是它运行的几率小,如垃圾回收线程的优先级就比较低。
Thread类中包含的成员变量代表了线程的某些优先级,如Thread.MIN_PRIORITY(常数1)、Thread.MIN_PRIORITY(常数10)、Thread.NORM_PRIORITY(常数5)。其中每个线程的优先级都在Thread.MIN_PRIORITY到Thread.MAX_PRIORITY之间,在默认情况下其优先级都是Thread.NORM_PRIORITY。每个新产生的线程都继承了父线程的优先级。
在多任务操作系统中,每个线程都会得到一段CPU时间片运行,在实践中结束时,将轮换另一个线程进入运行状态,这时系统会选择与当前线程优先级相同的线程予以运行。系统始终选择就绪状态下优先级较高的线程进入运行状态。
线程的优先级可以使用setPriority()方法调整,如果使用该方法设置的优先级不在1-10之内,将会产生IllegalArgumentException异常。
六、线程同步
6.1线程安全
在实际的开发中,使用多线程程序的情况有很多,如银行排号系统,火车站售票系统等等。这种多线程的程序通常会发生问题。以火车站的售票系统为例,在代码中判断当前票数是否大于0,如果大于0,则执行将车票出售给乘客,但是当两个线程同时访问这段代码时,这时却只剩下一张票,第一个线程将票出售后,与此同时第二个线程也已将执行判断是否有票的操作,并且已经得出票数大于0的结果,这样第二个线程也执行售票操作,这样就会产生负数。所以在编写程序的时候应该考虑到线程安全的问题。实质上线程安全问题来孕育两个线程同时存取单一对象的数据。
举个栗子:模拟火车站的售票系统的功能
public class ThreadSafeTest implements Runnable {
int num = 10; //设置当前总票数
public void run(){
while (true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest(); //实例化类对象
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
//分别启动线程
tA.start();
tB.start();
tC.start();
tD.start();
}
}
运行结果:
从运行结果中可以看到,最后为负值,这样就出现了问题。这是鱿鱼板创建了四个线程,这四个线程执行了run()方法,在num变量为1时,四个线程都对num变量有存储功能,当线程1执行run()方法时,还没有来得及做递减操作,
就指定它调用sleep()方法进入就绪状态,这时候线程2、线程3和线程4都进入run()方法,发现num变量依然大于0,但是此时线程1的休眠时间已经结束,将num变量值递减,同时线程2、线程3和线程4页都对num变量进行递减操作,从而产生了负值。
6.2线程同步机制
基本上所有解决多线程资源冲突问题得方法都是采用给定时间只允许一个线程访问共享资源,这时就需要给共享资源上一道锁。
6.2.1同步块
在Java中提供了同步机制,可以有效地防止资源冲突。同步机制使用synchronized关键字。
举个栗子:
public class ThreadSafeTestTwo implements Runnable {
int num = 10; //设置当前总票数
public void run(){
while (true) {
synchronized ("") {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest(); //实例化类对象
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
//分别启动线程
tA.start();
tB.start();
tC.start();
tD.start();
}
}
运行结果:
这个运行结果打印到最后没有出现负数,这就是因为将资源防止在了同步块中。这个同步块也被称为临界区,他使用synchronize关键字建立。
语法:
synchronize(Object){
}
通常将共享资源的操作放置在synchronize定义的区域内,这样当其他线程也获取到这个锁时,必须等待锁被释放时才能进入该区域。Object为任意一个对象,每个对象都存在一个标志位,并且具有两个值,分别为0和1.一个线程运行到同步块时首先检查该对象的标志位,如果为0状态,表明此同步块存在其他线程在运行。这时该线程处于就绪状态,知道处于同步块中的线程执行完同步块中的代码为止。这时该对象的标志位被设置为1,该线程才能执行同步块中的代码,并将Object对象的标志位置设置为0,防止其他线程执行同步块中的代码。
6.2.2同步方法
同步方法就是在方法前面修饰synchronize关键字的方法
语法:
synchronize void f(){
}
当某个对象调用了同步方法时候,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。
举个栗子:修改ThreadSafeTestTwo类
public synchronized void doit(){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tickets" + --num);
}
}
public void run(){
while (true) {
doit();
}
}
多线程编程思想与以往的编程思想不同,随着大多数操作系统对多线程的支持,很多程序语言都已经支持扩展多线程,应该积极转换编程思维从而适应多线程编程的思维方式。
多线程本身是一种非常复杂的机制,本期内容只是冰山一角,需要完全理解多线程编程思想还要付出更多的时间和经历慢慢理解。希望本期内容对您有所帮助,谢谢。
关注我,持续更新!!!