多线程
一、线程简介
-
Java 语言提供了并发机制,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其他线程并发执行,这种机制称之为多线程。
-
Windows 操作系统是多任务操作系统,以进程为单位;一个进程就是一个包含自身地址的程序,每个独立的程序都称为进程,如正在运行 Typora;每个进程中都可以同时包含多个线程。
-
系统可以分配给每个进程一段有限的使用 CPU 的时间(CPU 时间片),CPU 在这段时间中执行某个进程;同理,进程里的每一个线程都可以分配到一小段执行时间,基于此,一个进程可以具有多个并发执行的线程;然后下一个时间片有跳至另一个进程执行。由于 CPU 转换较快,所以使得每个进程好像在同时执行一样。
-
Windows 操作系统的执行模式如下图:
二、实现线程的两种方式
- Java 中主要提供两种方式实现多线程,分别为继承 java.lang.Thread 类与实现 java.lang.Runnable 接口。
1、继承 Thread 类
-
Thread 类是 java.lang 包中的一个类,从这个类中实例化的对象代表线程。
-
通过继承 Thread 类创建并执行一个线程的步骤如下:
1)创建一个继承自 Tread 类的子类。
2)覆写 Thread 类的 run 方法。
3)创建线程类的一个对象。
4)通过线程类的对象调用 start 方法启动线程,之后会自动调用覆写的 run 方法执行线程。
-
Thread 类常用构造方法如下:
1)public Thread():创建一个新的线程对象。
2)public Thread(String threadName):创建一个名称为 threadName 的线程对象。
-
继承 Thread 类创建一个新的线程的语法如下:
public class ThreadTest extends Thread {}
-
Thread 类常用方法如下表:
方法 说明 interrupt() 中断线程 join() 等待该线程终止 join(long millis) 等待该线程终止的时间最长为 millis 毫秒 run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法,否则,该方法不执行任何操作并返回 setPriority(int newPriority) 更改线程的优先级 sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) start() 使该线程开始执行; Java 虚拟机调用该线程的 run 方法 yield() 暂停当前正在执行的线程对象,并执行其他线程。 -
完成线程真正的功能的代码放在类的 run 方法里。run 方法必须使用以下格式:
public void run() {}
-
如果 start 启动一个已经启动的线程,系统将会抛出 IllegalThreadStateException 异常。
-
实例:
public class ThreadTest extends Thread{ private int count = 10; public void run() { do { System.out.print( count + " " ); --count; } while (count != 0); } public static void main(String[] args) { ThreadTest test = new ThreadTest(); test.start(); // 启动线程 } } /* 输出如下: 10 9 8 7 6 5 4 3 2 1 */
2、实现 Runnable 接口
-
当需要继承非 Thread 类,又要实现多线程,便可以使用 Runnable 接口来实现,例如 一个拓展 JFrame 的 GUI 程序不可能再继承 Thread 类,因为 Java 语言不支持多继承。
-
实现 Runnable 接口的语法如下:
public class Thread extends Object implements Runnable
-
实质上,Thread 类实现了 Runnable 接口,其中的 run() 方法正是对 Runnable 接口中的 run() 方法的具体实现。
-
Runnable 接口启动新的线程的步骤如下:
1)建立 Runnable 对象。
2)使用参数为 Runnable 对象的构造方法创建 Thread 实例。
3)调用 start() 方法启动线程。
-
通过 Runnable 接口创建线程,首先得编写一个实现 Runnable 接口的类,接着实例化。
-
例子:
import java.awt.Container; import java.net.URL; import javax.swing.*; public class SwingAndThread extends JFrame{ private JLabel jl = new JLabel( ); private int count = 0; // 声明计数变量 private Container container; { container = getContentPane(); } private SwingAndThread() { setBounds( 300,200,250,100 ); container.setLayout( null ); // 使用绝对布局 try { URL url = SwingAndThread.class.getResource( "1.gif" ); Icon icon = new ImageIcon( url ); jl.setIcon( icon ); } catch (NullPointerException ex) { System.out.println( "图片不存在!!" ); } jl.setHorizontalAlignment( SwingConstants.LEFT ); jl.setBounds( 10,10,200,50 ); jl.setOpaque( true ); // 声明线程对象 Thread t = new Thread( new Roll() ); t.start(); container.add(jl); setVisible( true ); setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); } class Roll implements Runnable { Roll() { } @Override public final void run() { while (count <= 200) { // 将标签的横坐标用变量表示 jl.setBounds( count,10,200,50 ); try { Thread.sleep( 1000 ); // 为了体现滚动效果,使线程休眠 1000 毫秒 } catch (Exception e) { e.printStackTrace(); } count += 4; if (count == 200) { // 当图标到达标签最右边时,使其回到标签最左边 count = 10; } } } } public static void main(String[] args) { new SwingAndThread(); } }
-
运行效果:
三、线程的生命周期
-
线程具有生命周期,有五种状态:出生状态、就绪状态、运行状态、暂停状态(包括休眠、等待和阻塞等)和死亡状态。
-
五种状态之间的关系如下图:
[外链图片转存失败(img-RfVFgXUs-1563517852082)(D:\编程笔记\java\线程生命周期.jpg)]
四、操作线程的方法
- 操作线程的方法有很多种,这些方法都可以使线程从某一种状态到另一种状态。
1、线程的休眠
-
让线程休眠的方法是 sleep(),其参数为指定线程休眠的时间,sleep() 方法通常在 run() 的内循环中使用。
-
使用例子:
try{ Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
-
实例:
import javax.swing.*; import java.awt.*; import java.util.Random; public class SleepMethodTest extends JFrame { private static final long serialVersionUID = 1L; private Thread t; private static Color[] color = { Color.BLACK, Color.BLUE,Color.CYAN,Color.GREEN, Color.ORANGE,Color.YELLOW}; private static final Random rand = new Random( ); // 创建随机对象 private static Color getC() { // 获取随机颜色值的方法 // 随机产生一个color数组长度 return color[rand.nextInt( color.length )]; } private SleepMethodTest() { t = new Thread( new Draw() ); // 创建匿名线程对象 t.start(); } class Draw implements Runnable { // 定义内部类,用来在窗体中绘制线条 int x = 30; // 定义初识坐标 int y = 50; @Override public void run() { // 重写线程接口方法 while(true) { try { Thread.sleep( 100 ); //线程休眠0.1秒 } catch (InterruptedException e) { e.printStackTrace(); } // 获取绘图组件上下文对象 Graphics graphics = getGraphics(); graphics.setColor( getC() ); // 设置绘图颜色 // 绘制直线并递增垂直坐标 graphics.drawLine( x,y,100, y ); y++; if(y >= 80) { y = 50; } } } } // 初识化程序界面 public static void init(JFrame frame, int width, int height) { frame.setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); frame.setSize( width,height ); frame.setVisible( true ); } public static void main(String[] args) { init(new SleepMethodTest(),100,100); } }
-
运行效果如下图:
2、线程的加入
-
假如存在一个线程A,现在需要插入线程B,并且要求线程B先执行完毕,再执行线程A;对于此种操作可以使用 Thread 类中的 join() 方法来完成。
-
当某个线程使用 join() 方法加入另一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
-
例子:
import javax.swing.*; import java.awt.*; public class JoinTest extends JFrame { private Thread threadB; private JoinTest() { super(); setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); setSize( 100, 100 ); setVisible( true ); // 将进度条设置在窗体最北面 // 定义两个进度条组件 JProgressBar progressBar = new JProgressBar(); getContentPane().add( progressBar, BorderLayout.NORTH ); // 将另一条进度条放置在窗体最南面 JProgressBar progressBar2 = new JProgressBar(); getContentPane().add( progressBar2, BorderLayout.SOUTH ); // 设置进度条显示数字 progressBar.setStringPainted( true ); progressBar2.setStringPainted( true ); //使用匿名内部类初始化 Thread 实例 // 定义两个线程 Thread threadA = new Thread( new Runnable() { int count = 0; @Override public void run() { while (true) { ++count; progressBar.setValue( count ); try { Thread.sleep( 100 ); if (count == 20) { threadB.join(); } } catch (Exception e) { e.printStackTrace(); } } } } ); threadA.start(); threadB = new Thread( new Runnable() { int count = 0; @Override public void run() { do { ++count; progressBar2.setValue( count ); try { Thread.sleep( 100 ); } catch (Exception e) { e.printStackTrace(); } } while (count != 100); } } ); threadB.start(); } public static void main(String[] args) { new JoinTest(); } }
-
运行效果如下图:
3、线程的中断
-
在 run 中使用无线循环的形式,然后使用一个布尔型标记,来控制循环的停止。例如下面这个例子:
public class InterruptedTest implements Runable{ private boolean isContinue = false;//设置一个标记,默认值为 false public void run() { while(true){ //.... if( isContinue) //当isContinue 变为 true 时,停止线程 break; } } public void setContinue() { // 定义设置 isContinue 变量为 true 的方法 this.isContinue = true; } }
-
实例:
import javax.swing.*; import java.awt.*; public class InterruptedSwing extends JFrame { private InterruptedSwing(){ setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); setSize(100,100); setVisible( true ); final JProgressBar progressBar = new JProgressBar( ); // 创建进度条 // 将进度条放置到合适的位置 getContentPane().add(progressBar, BorderLayout.NORTH ); progressBar.setStringPainted( true ); // 设置进度条上显示数字 // 执行线程停止方法 Thread thread = new Thread() { int count = 0; public void run() { while (true) { ++count; progressBar.setValue( count ); try { if (count == 30) { interrupt(); // 执行线程停止方法 } Thread.sleep( 100 ); } catch (InterruptedException e) { System.out.println( "当前线程被中断" ); break; } } } }; thread.start(); } public static void main(String[] args) { new InterruptedSwing(); } }
5、线程的优先级
- 每个线程都有自己的优先级,线程的优先级可以表明在程序中该线程的重要性;当有很多线程处于就绪状态时,系统会根据线程的优先级决定线程的运行次序。
- Thread 类中定义了一些常量用来表示线程的优先级,Thread.MIN_PRIORITY~Thread.MAX_PRIORITY (常数1~常数10),默认情况下,现成的优先级都是 Thread.NORM_PRIORITY。每个线程的产生都继承了父线程的优先级。
- 多线程的执行本身就是多个线程交换执行,并非同时执行,通过设置线程的优先级的高低,只是说明该线程或被执行或暂不执行的概率更大一些,并不能保证优先级高的线程就一定会比优先级低的线程先执行。
- 实例:
public class Priority extends Thread { private String threadName; // 线程的名字 private String output; // 以线程名、控制台输出的信息为参数的构造方法,利用构造方法初始化变量 Priority(String threadName, String output) { this.threadName = threadName; this.output = output; } @Override public void run() { // 线程要执行的任务 System.out.print( threadName + ":" + output + " " ); } } public class PriorityTest { public PriorityTest() { } public static void main(String[] args) { for(int i=0;i<100;i++) { // 通过循环控制线程的启动次数 Priority test1 = new Priority( "加","+" ); test1.setPriority( Thread.MIN_PRIORITY ); // 设置优先级最低 Priority test2 = new Priority( "减","-" ); test2.setPriority( 3 ); Priority test3 = new Priority( "乘","×" ); test3.setPriority( 8 ); Priority test4 = new Priority( "除","÷" ); test4.setPriority( Thread.MAX_PRIORITY ); // 设置优先级最高 // 启动线程 test1.start(); test2.start(); test3.start(); test4.start(); System.out.println( ); // 换行 } } }
六、线程的同步
- 线程的同步机制,是为了在多线程时,避免出现资源访问的冲突。
1、线程安全
- 线程安全问题主要考虑线程同时存取同一对象的数据。
- 实例:
public class ThreadSafeTest implements Runnable { private int num = 10; // 设置当前票总数 public final void run() { while (true) { if (num > 0) { try { Thread.sleep( 100 ); } catch (Exception e) { e.printStackTrace(); } System.out.printf( "%s票数%d\n", Thread.currentThread().getName(), num ); 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(); } } // 输出结果: 线程二票数10 线程三票数10 线程四票数10 线程一票数10 线程二票数6 线程三票数5 线程四票数5 线程一票数3 线程二票数2 线程三票数1 线程一票数1 线程四票数1 线程二票数-2
2、线程同步机制
- 基本上所有解决多线程资源冲突问题的方法都是采用给定时间只允许一个线程访问共享资源,即给共享资源上一道锁。
- 锁的主要作用是为了防止不同线程在同一时间访问同一代码块,如果在同一时间访问同一代码块,有可能出现死“死锁”——两个线程运行时都在等待对方的锁,从而造成程序的停滞。
2.1、同步块
- 同步机制使用 synchronized 关键字,使用该关键字包含的代码块称为同步块,也称为临界区,语法如下:
synchronzied(Object){ } // 例子: public class SynchronizedTest implements Runnable { private int num = 10; public void run() { while (true) { synchronized (this) { if (num > 0) { try { Thread.sleep( 100 ); } catch (Exception e) { e.printStackTrace(); } System.out.printf( "%s票数%d\n", Thread.currentThread().getName(), num ); num--; } else break; } } } public static void main(String[] args) { SynchronizedTest t = new SynchronizedTest(); 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(); } } 输出: 线程一票数10 线程一票数9 线程三票数8 线程四票数7 线程四票数6 线程二票数5 线程二票数4 线程四票数3 线程三票数2 线程一票数1
2.2、同步方法
- 同步方法就是在方法前面加上关键字 synchronized 修饰,语法:synchronized void f(){ }
- 实例:
public class SynchronizedTest implements Runnable { private int num = 10; private synchronized void doit() { if (num > 0) { try { Thread.sleep( 100 ); } catch (Exception e) { e.printStackTrace(); } System.out.printf( "%s票数%d\n", Thread.currentThread().getName(), num ); num--; } else System.exit( 0 ); } public final void run() { while(true) { doit(); } } public static void main(String[] args) { SynchronizedTest t = new SynchronizedTest(); 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(); } } //输出效果和上面一个的例子一样
七、线程的暂停和恢复
- 线程的暂停和恢复主要是通过顶级父类 Object 提供的 wait() 方法与 notify() 方法实现。其中 wait() 用于暂停线程,notify() 用于唤醒正在等待的单个线程,即恢复线程。
- 实例:
import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Random; public class ThreadSuspendFrame extends JFrame { private JLabel label; //显示数字的标签 private ThreadSuspend t; // 自定义线程类 /* * 在主类中创建内部类,自定义线程类 */ class ThreadSuspend extends Thread { /* * 线程挂起状态,若 suspend 为 false,线程正常运行;反之,线程会处于挂起状态 */ private boolean suspend = false; // 线程安全地暂停的方法 final synchronized void toSuspend() { suspend = true; } // 线程安全地恢复运行的方法,将 suspend 变为 false 的同时调用超级父类的 notify() 方法 final synchronized void toRun() { suspend = false; notify(); } @Override public void run() { // 定义中奖号码池 String[] phoneNums = {"13610780204", "13847928544", "18457839454", "18423098757", "17947928544", "19867534533"}; while (true) { synchronized ( this ) { while(suspend) { //判断线程是否要暂停 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } // 获取一个 phoneNums 数据的随机索引 int randomIndex = new Random( ).nextInt( phoneNums.length ); String phoneNum = phoneNums[randomIndex]; label.setText( phoneNum ); } } } private ThreadSuspendFrame() { setTitle( "手机号码抽奖" ); // 窗口标题 setDefaultCloseOperation( EXIT_ON_CLOSE ); setBounds( 200,200,300,150 ); // 初始化标签 label = new JLabel( "0" ); label.setHorizontalAlignment( SwingConstants.CENTER ); label.setFont( new Font( "楷体",Font.PLAIN,42 ) ); getContentPane().add(label,BorderLayout.CENTER); JButton btn = new JButton( "暂停" ); getContentPane().add(btn,BorderLayout.SOUTH); t = new ThreadSuspend(); t.start(); btn.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String btnText = btn.getText(); if(btnText.equals( "暂停" )) { t.toSuspend(); btn.setText( "继续" ); } else { t.toRun(); btn.setText( "暂停" ); } } } ); setVisible( true ); } public static void main(String[] args) { new ThreadSuspendFrame(); } }