JAVA——从基础学起(十四)多线程

14 多线程

14.1 线程概述

人们在生活中可以同时完成很多工作,比如边上厕所边刷手机=.=等。而Java中这种同时完成多个工作的思想叫做并发。而并发完成的每一个工作叫做线程。Java中并发机制非常重要,由于这种机制,程序员可以在程序中值行多个线程,每个线程执行一个功能并与其他线程并发执行,这种机制我们称为多线程。

14.2 实现现成的两种方式

在Java中提供两种方式实现线程。分别为继承java.lang.Thread类和实现java.lang.Runnable接口。

14.2.1 继承Thread类

Thread类中实例化出的对象代表线程。程序员每启动一个线程,就需要实例化一个Thread对象,Thread类中常用的构造方法有:

  1. public Thread();
  2. public Thread(String threadName);
    真正完成线程功能的方法放在类的run()方法中,当一个类继承Thread类后,就可以通过重写run()方法,然后调用Thread类中的start()方法执行run()方法。Thread对象需要一个任务来执行,任务是指线程在启动时执行的工作,该工作的功能代码被放在run方法中。run方法的语法形式必须为:public void run();
    当start()方法调用了一个已经启动的线程时,系统将抛出IllegalThreadStateException异常。

当还行一个线程程序时,将会自动生成一个线程,而主方法就是在这个线程上运行的。当不再启动其他线程时,该程序就为单线程程序。主方法线程的启动由Java来控制,而程序员则负责控制自己的线程。
下面使用实例来说明:

package com.mw01;
public class ThreadTest extends Thread{                  //通过继承Thread类来创建线程
	private int count = 10;
	public void run() {                                 //重写run方法
		while (true) {
			System.out.print(count + " ");              //达因变量count
			if (--count == 0) {
				return;
			}
		}
	}
	public static void main(String[] args) {
		new ThreadTest().start();
	}
}

在上述实例中,继承了Thread类并覆盖了其中的run方法。通常在run方法中会使用while无限循环的形式使得线程一直进行下去,所以需要制定一个跳出循环的条件。在main方法中,使线程执行需要调用start方法。start方法会调用被覆盖后的run方法。如果不调用start方法,线程就永远不会启动,Thread对象也就只是一个实例而不是真正的线程。

14.2.2 实现Runnable接口

如果程序员需要继承其他非Thread类时,而且还需要实现多线程,可以通过实现Runnable接口来实现。语法如下:public class extends Object implements Runnable{}
而实际上,Thread类也是实现了Runnable接口,其中的run方法正时对Runnable接口中的run方法的具体实现。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联。Thread类中提供了如下构造方法:

  1. public Thread(Runnable target);
  2. public Thread(Runnable target , String name);

使用以上两种构造方法就可以实现Runnable实例与Thread实例相关联。

通过实现Runnable接口来启动多线程的步骤为:

  1. 实例化Runnable对象
  2. 使用Runnable对象作为参数实例化Thread对象实例
  3. 调用创建好的Thread对象的start()方法来启动线程

下面通过实例来进行介绍:

package com.mw02;

import java.awt.Container;
import java.net.URL;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;

public class SwingAndThread extends JFrame{
	private JLabel jl = new 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");
		Icon icon = new ImageIcon(url);
		jl.setIcon(icon);
		jl.setHorizontalAlignment(SwingConstants.LEFT);
		jl.setBounds(10,10,200,50);
		jl.setOpaque(true);
		t = new Thread(new Runnable() {
			public void run() {
			    while (count < 200) {
			    	jl.setBounds(count,10,200,50);
			    	
			    	try {
						Thread.sleep(10);
					} catch (Exception e) {
						e.printStackTrace();
					}
			    	
			    	count += 4;
			    	if (count == 200) {
						count = 0;
					}	
				}
			}
		});
		t.start();
		container.add(jl);
		setVisible(true);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
	}
	
	public static void main(String[] args) {
		new SwingAndThread();
	}
}

本实例中使用匿名内部类来对线程进行创建,通过Thread类中的sleep()方法使得线程周期性休眠,从而达到标签滚动的效果。
启动一个新的线程,并不是直接调用Thread类中的run方法即可,而是需要使用start方法。这个方法会产生一个新的线程,在该县城内运行Thread子类的run方法。

14.3 线程的生命周期

线程具有生命周期,包括七种状态:出生状态,就绪状态,运行状态,等待状态,休眠状态,阻塞状态和死亡状态。在使用start方法启动线程之前线程都处于出生状态;当启动线程后,就进入了就绪状态;在得到系统资源后进入运行状态。当线程进入可执行状态后,就会在就绪与运行状态下进行转换,同时也可能进入等待,休眠,阻塞和死亡状态。当使用wait()方法后,线程进入等待状态,必须使用notify()方法将其唤醒才能进行运行;当调用sleep()方法后,线程会进入休眠状态;如果在运行状态下发出IO请求,则线程会进入阻塞状态,IO操作结束后回到就绪状态等待系统资源分配。当run方法执行完毕后,线程进入死亡状态。

使得线程进入就绪状态有以下方法:

  1. 调用sleep方法
  2. 调用wait方法
  3. 等待输入输出结束、

当线程处于就绪状态后,使得线程进入运行状态:

  1. 使用notify方法
  2. 调用notifyAll方法
  3. 线程调用interrupt方法打断休眠
  4. 休眠时间结束
  5. 输入输出结束

14.4 操作线程的方法

操作线程有很多方法,这些方法可以实现使线程从一状态过渡到另一状态

14.4.1 线程的休眠

Thread类中的sleep方法可以实现使线程在一段时间内不会进入就绪状态。使用了sleep方法的线程在一段时间后会醒来,但是不能保证其进入运行状态,只能保证其进入就绪状态。sleep方法的使用语法如下:

try{
    Thread.sleep();
}catch(InterruptedException e){
    e.printStackTrance;
}

下面使用实例来对sleep方法进行介绍:

package com.mw03;

import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;

import javax.swing.JFrame;

public class SleepMethodTest extends JFrame{
	private Thread thread ;
	
	private static Color[] colors = {Color.BLACK,Color.RED,Color.GREEN,Color.BLUE,Color.CYAN,Color.GRAY,Color.ORANGE,Color.PINK};
	
	private static final Random random = new Random();

	public Color getColor() {
		return (colors[random.nextInt(colors.length)]);
	}
	
	public SleepMethodTest() {
		thread = new Thread(new Runnable() {
			int x = 30;
			int y = 50;
			
			public void run() {
				while (true) {
					try {
						Thread.sleep(80);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					Graphics graphics = getGraphics();
					graphics.setColor(getColor());
					graphics.drawLine(x, y, 500, y++);
					if (y>=800) {
						y = 50;
					}
				}
			}
		});
		thread.start();
	}
	
	public static void main(String[] args) {
		init(new SleepMethodTest(), 1000, 1000);
	}
	
	public static void init(JFrame jFrame , int width , int height) {//初始化界面的方法
		jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
		jFrame.setSize(width,height);
		jFrame.setVisible(true);
	}
}

本实例实现了线程在运行中休眠80ms,从而实现在窗体中自动画线段的功能。需要注意的是,我们并没有创建一个实现了Runnable接口的类,因此在创建Thread对象时需要使用匿名内部类来进行Thread对象的实例化。

14.4.2 线程的加入

如果当前程序为多线程程序,假如存在多线程A,现在需要插入多线程B,并要求线程B先执行完毕然后执行线程A,可以使用Thread类中的join方法实现。当某个线程使用join方法加入到另一个线程中,另一个线程必须等待该线程执行完毕后才可以继续。
下面使用实例介绍:

package com.mw04;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JProgressBar;


public class JoinTest extends JFrame{
	private Thread threadA ;
    
	private Thread threadB ;
	
	final JProgressBar jProgressBar1Bar = new JProgressBar();
	final JProgressBar jProgressBar2Bar = new JProgressBar();   //定义两个进度条组件
	
	public JoinTest() {
		super();
		getContentPane().add(jProgressBar1Bar,BorderLayout.NORTH);// 将第一个进度条设置在窗体北边
		getContentPane().add(jProgressBar2Bar,BorderLayout.SOUTH);//将第二个进度条设置在窗体南边
		jProgressBar1Bar.setStringPainted(true);
		jProgressBar2Bar.setStringPainted(true);
		
		threadA = new Thread(new Runnable() {
			int count = 0;
	
			public void run() {
				while (true) {
					jProgressBar1Bar.setValue(count++);
					try {
						threadA.sleep(100);
						threadB.join();      //使用join方法加入B线程,只有当B线程完成后A线程才能继续执行
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				
			}
		});
		threadB = new Thread(new Runnable() {
			int count = 0;
			public void run() {
				while (true) {
					jProgressBar2Bar.setValue(count++);
					try {
						threadB.sleep(100);
					} catch (Exception e) {
						e.printStackTrace();
					}
					if (count > 100) {
						break;
					}
				}
			}
		});
		threadA.start();
		threadB.start();
	}
	
	public static void init(JFrame jFrame , int width , int height) {
		jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
		jFrame.setSize(width, height);
		jFrame.setVisible(true);
	}
	public static void main(String[] args) {
		init(new JoinTest(), 100, 100);
	}
}

该实例实现了两个进度条进度的显示,只有线程B控制的进度条满,也即该线程执行结束后,线程A控制的进度条进度才能够增加。

14.4.3 线程的中断

当前版本的JDK废除了stop方法,而是提倡在run方法中使用无限循环的形式,然后设置一个布尔型标记控制循环的停止来实现停止线程。而如果线程通过调用sleep或wait方法进入了就绪状态,则可以使用Thread类中的interrupt方法使线程离开run方法,同时结束线程,但是程序会抛出InterruptedException异常,可以在此同时结束循环。在项目中通常会在此执行关闭数据库连接和关闭Socket连接等操作。
下面使用实例进行介绍:

package com.mw05;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JProgressBar;

public class InterruptThread extends JFrame{
	Thread thread ;
	public static void main(String[] args) {
		init(new InterruptThread(), 100, 100);
	}
	
	public static void init(JFrame jFrame , int width , int height) {
		jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
		jFrame.setSize(width, height);
		jFrame.setVisible(true);
	}
	
	public InterruptThread() {
		final JProgressBar jProgressBar = new JProgressBar();
		getContentPane().add(jProgressBar,BorderLayout.SOUTH);
		jProgressBar.setStringPainted(true);
		
		thread  = new Thread(new Runnable() {
			int count = 0;
			@Override
			public void run() {
				while (true) {
					jProgressBar.setValue(count ++ );
					try {
						thread.sleep(100);
						if (count > 50) { //当进度条执行到百分之50以上时,结束线程
							thread.interrupt();
						}
					} catch (InterruptedException e) {
						System.out.println("线程中断啦!!!!!");
						break;
					}
				}
			}
		});
		thread.start();
	}
}

在本实例中,由于调用了interrput方法,在抛出InterruptedException异常的同时结束了循环,线程被关闭,因此进度条在百分之51时停止增加。

14.4.4 线程的礼让

Thread类中提供了yield()方法来表示礼让,在线程中使用该方法,告知其可以将系统资源礼让给其他线程。但是这并不是说明保证当前线程一定会将系统资源让给其他线程,这仅仅是一种暗示。而对于支持多任务的操作系统来说,不需要调用yield方法,因为操作系统会自动为线程分配CPU时间片进行执行。

14.5 线程的优先级

每个线程都具有优先级,线程的优先级代表着该程序中该线程的重要性。但是这并不意味着低优先级的线程得不到运行,他只是运行的概率较小,如垃圾回收线程的优先级就比较低。

Thread类中所包含的成员变量代表了县陈大哥某些优先级,如Thread.MAX_PRIORITY(常数10),Thread.MIN_PRIORITY(常数1)等。默认情况下线程的优先级都为NORM_PRIORITY(常数5),每个新产生的线程都会继承父线程的优先级。只有当高优先级的线程执行完毕后,才会轮到低优先级的线程得到执行。线程的优先级可以使用setPriority()方法来进行设置,参数范围为1-10。
下面以进度条为例进行介绍:

package com.mw06;

import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JProgressBar;

public class PrioriteTest extends JFrame{
	private Thread threadA ;
	private Thread threadB ;
	private Thread threadC ;
	private Thread threadD ;
	
	public PrioriteTest() {
		final JProgressBar jProgressBar1 = new JProgressBar();
		final JProgressBar jProgressBar2 = new JProgressBar();
		final JProgressBar jProgressBar3 = new JProgressBar();
		final JProgressBar jProgressBar4 = new JProgressBar();
		getContentPane().add(jProgressBar1,BorderLayout.NORTH);
		getContentPane().add(jProgressBar2,BorderLayout.EAST);
		getContentPane().add(jProgressBar3,BorderLayout.WEST);
		getContentPane().add(jProgressBar4,BorderLayout.SOUTH);
		
		//创建四个线程并分别使用
		threadA = new Thread(new MyThread(jProgressBar1,"A"));
		threadB = new Thread(new MyThread(jProgressBar2,"B"));
		threadC = new Thread(new MyThread(jProgressBar3,"C"));
		threadD = new Thread(new MyThread(jProgressBar4,"D"));
		
		setPriorite(threadA, 10, "A线程");
		setPriorite(threadB, 10, "B线程");
		setPriorite(threadC, 5, "C线程");
		setPriorite(threadD, 1, "D线程");
		
		
		
	}
	
	public void setPriorite(Thread thread , int i, String threadName){
		thread.setName(threadName);
		thread.setPriority(i);
		thread.start();              
	}
	
	public static void init(JFrame jFrame , int width , int height) {
		jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
		jFrame.setVisible(true);
		jFrame.setSize(width, height);
	}
	
	public static void main(String[] args) {
		init(new PrioriteTest(), 500, 500);
	}
}


public class MyThread implements Runnable{
//自定义类实现Runnable接口
    private final JProgressBar jProgressBar ;
    private String name;
    int count;
    
    public MyThread(JProgressBar bar , String name) {
		jProgressBar = bar;
		this.name = name;
	}
	
	public void run() {
		jProgressBar.setStringPainted(true);
		jProgressBar.setString(name);
		while (true) {
			jProgressBar.setValue(count += 10);
			jProgressBar.setName(name);
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

本实例中定义了4个线程,线程A,B首先执行,而线程C,D会在他们之后执行,也即A,B线程休眠期间执行。

14.6 线程同步

在单线程程序中,每次只能做一件事情,但是如果使用多线程的话,不可避免的会发生两个线程抢占系统资源的问题。Java提供了线程同步的机制来防止资源访问的冲突。

14.6.1 线程安全

实际开发中,在使用多线程时需要考虑到多线程的安全问题。多线程的安全问题实际上来源于不同线程对同一对象数据进行访问时。下面我们使用实例来对多线程的安全问题进行介绍:

package com.mw07;

public class ThreadSafe implements Runnable{
	int ticketNum = 10;
	
	@Override
	public void run() {
		while (true) {
			try {
				Thread.sleep(100);
				if (ticketNum <= 0) {
					Thread.interrupted();
					break}
			} catch (Exception e) {
				e.printStackTrace();
			}
			System.out.println("还剩下"+ ticketNum --+ "火车票!" );
		}
	}

	public static void main(String[] args) {
		ThreadSafe tSafe = new ThreadSafe();
		Thread thread = new Thread(tSafe);
		Thread thread1 = new Thread(tSafe);
		Thread thread2 = new Thread(tSafe);
		Thread thread3 = new Thread(tSafe);
		thread.start();
		thread1.start();
		thread2.start();
		thread3.start();		
	}
}

运行结果为:

还剩下10火车票!
还剩下9火车票!
还剩下8火车票!
还剩下7火车票!
还剩下6火车票!
还剩下4火车票!
还剩下5火车票!
还剩下3火车票!
还剩下2火车票!
还剩下1火车票!
还剩下1火车票!

可见当多个线程启动时会发生资源共享的问题,如何解决这种资源冲突的问题呢?

14.6.2 线程同步机制

基本上所有解决多线程资源冲突问题的方法都是采用给定时间内只允许一个线程访问共享资源,这样就能够保证各个线程在访问共享资源时不会发生冲突。

  1. 同步块
    在Java中提供了同步机制,可以有效的防止资源冲突。同步机制使用synchronized关键字,下面使用实例来进行介绍:
package com.mw07;

public class ThreadSafe implements Runnable{
	int ticketNum = 10;
	
	@Override
	public void run() {
		while (true) {
			synchronized(""){            //使用同步块表示临界区,表明同一时间只能有一个线程执行此块
			    try {
				Thread.sleep(100);
				if (ticketNum <= 0) {
					Thread.interrupted();
					break;
				}
			    } catch (Exception e) {
				    e.printStackTrace();
			    }
			    System.out.println("还剩下"+ ticketNum --+ "火车票!" );
			}
		}
	}

	public static void main(String[] args) {
		ThreadSafe tSafe = new ThreadSafe();
		Thread thread = new Thread(tSafe);
		Thread thread1 = new Thread(tSafe);
		Thread thread2 = new Thread(tSafe);
		Thread thread3 = new Thread(tSafe);
		thread.start();
		thread1.start();
		thread2.start();
		thread3.start();		
	}
}

得到结果:

还剩下10火车票!
还剩下9火车票!
还剩下8火车票!
还剩下7火车票!
还剩下6火车票!
还剩下5火车票!
还剩下4火车票!
还剩下3火车票!
还剩下2火车票!
还剩下1火车票!

通常将需要共享的资源放置在同步块中,当其他线程获取到这个锁,必须等待锁被释放时才能进入该区域。所有对象都有着标志位,当线程运行到同步块时会首先检查该对象的标志位,标志位为0说明此同步块中存在其他线程正在运行。这样线程会处于就绪状态,当对象的标志位变为1时,该线程才会执行同步块中的代码。

  1. 同步方法
    同步方法就是在方法前面修饰synchronized关键字的方法,其语法如下:
    synchronized void f(){}
    当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完成后再能被执行。而且必须要将每个能访问资源共享的方法都修饰为synchronized,否则就会出现问题。

通过修改上面实例来进行介绍:

package com.mw07;

public class ThreadSafe implements Runnable{
	int ticketNum = 100;
	
	@Override
	public void run() {
		while (true) {
			boolean temp = doIt();
			if (temp) {
				break;
			}
		}
	}

	public static void main(String[] args) {
		ThreadSafe tSafe = new ThreadSafe();
		Thread thread = new Thread(tSafe);
		Thread thread1 = new Thread(tSafe);
		Thread thread2 = new Thread(tSafe);
		Thread thread3 = new Thread(tSafe);
		thread.start();
		thread1.start();
		thread2.start();
		thread3.start();		
	}
	public synchronized boolean doIt() {
			try {
				Thread.sleep(100);
				if (ticketNum <= 0) {
					Thread.interrupted();
					return true;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			System.out.println("还剩下"+ ticketNum --+ "火车票!" );
			return false;
	}
}

也就是将同步块内的代码使用synchronized修饰的方法封装起来,当线程需要使用的时候直接调用。这样封装性好的同时也保证了线程安全。


咳咳:
和IO一样,多线程的控制及使用比较简单,大多是Java预定义好的方法,只要实例化对象进行使用即可。在本章节中,我们对匿名内部类进行了一些使用,如:

thread = new Thread(new Runnable() {
			int x = 30;
			int y = 50;
			
			public void run() {
				while (true) {
					try {
						Thread.sleep(80);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					Graphics graphics = getGraphics();
					graphics.setColor(getColor());
					graphics.drawLine(x, y, 500, y++);
					if (y>=800) {
						y = 50;
					}
				}
			}
		});

可能在一开始的时候匿名内部类会显得非常复杂,晦涩难懂。但是当实际应用时就会感受到它的巧妙。在这里我们对Thread对象进行实例化时,需要使用继承了Runnable接口的类作为参数,但是由于我们没有定义一个这样的类,因此可以在参数列表部分使用匿名内部类进行直接定义。在很多情况下,都可以对抽象类或者接口进行这样的使用。在没有必要为某个对象实例创建一个类的情况下,就可以使用匿名内部类来进行代替。

2020.6.15

你可能感兴趣的:(Java)