多线程
java提供了并发机制,可以在程序中执行多个线程,每一个线程完成一个功能,并与其他线程并发执行,这种机制称为多线程。
18.2 实现线程的两种方法
java中提供了两种方式实现多线程,分别为继承java.lang.Thread类和实现java.lang.Runnable接口。
18.2.1 继承Thread类
继承Thread类创建一个新的线程:
public class ThreadTest extends Thread { ... }
完成线程真正功能的代码放在类的run()方法中,当一个类继承了Thread类之后,就可以在该类中覆盖run()方法,将实现该线程功能的代码写入到run()方法中,同时调用Thread类中的start()方法执行线程,也就是调用run()方法。
Thread对象需要一个任务来执行,任务实质线程在启动时执行的工作,该工作的功能代码被写在run()方法中,run()方法碧玺使用如下的语法格式:
public void run() { ... }
当执行一个线程程序的时候,就会自动产生一个线程,主方法正式在这个线程中运行的。当不再启动其他线程的时候,这个程序就是一个单线程程序,主方法线程的启动由java虚拟机负责,程序员负责启动自己的线程。
开启自己的线程的语法如下:
public static void main(String args[]) { new ThreadTest().start(); }
使用多线程倒序输出10个数字:
package thread_18_1; public class ThreadTest extends Thread{ private int count = 10; public void run() { while(true) { System.out.println(count + " "); if(--count == 0) return; } } public static void main(String args[]) { new ThreadTest().start(); } }
继承了Thread类,然后在类中覆盖了run方法,通常在run()方法中使用无限循环的形式是的线程一直运行下去,所以需要指定一个跳出循环的条件,如本例中使用变量count的值为0作为跳出循环的条件。
在main方法中,使线程执行需要调用Thread类中start()方法,start()方法调用被覆盖的run()方法,如果不调用start()方法,线程永远不会启动,在主方法中没有调用start方法之前,Thread对象只是一个实例,而不是一个真正的线程。
package thread_18_1; public class ThreadT3 { public static void main(String args[]) { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if(i == 30) { Thread myThread1 = new ThreadTest3(); Thread myThread2 = new ThreadTest3(); myThread1.start(); myThread2.start(); } } } } class ThreadTest3 extends Thread { private int i = 0; public void run() { for(i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
18.2.2 实现Runnable接口
如果需要继承其他类(非Thread类),而且需要使用当前类实现多线程,那么必须通过Runnable接口来实现。例如,一个扩展JFrame类的GUI程序不可能再继承Thread类,因为java不支持多继承(广义上的多继承),所以此时必须实现Runnable接口使其具有使用线程的功能。
实现Runnable接口的语法如下:
public class Thread extends Object implements Runnable
实质上Thread类实现了Runnable接口,其中的run()方法正是对Runnable接口中run()方法的具体实现。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象和Thread对象相关联。Thread类中有以下两个构造方法:
public Thread(Runnable r)
public Thread(Runnable r, String name)
这两个构造方法的参数中都存在Runnable实例,使用上述构造方法就可以将Runnable实例与Thread实例相关联。
使用Runnable接口启动新线程的过程如下:
(1)新建Runable对象;
(2)使用参数为Runnable对象的构造方法创建Thread实例;
(3)调用start()方法启动线程。
通过Runnable接口创建线程时,需要首先编写一个实现Runnable接口的类,然后实例化这个类的对象,这样就建立了Runnable对象;接下来使用相应的构造方法创建Thread实例;最后使用该实例调用Thread的start()方法启动线程。
package thread_18_1; 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; import javax.swing.WindowConstants; 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, 500, 100); container.setLayout(null);//使窗口不使用任何布局管理器 URL url = SwingAndThread.class.getResource("C:\Users\motiv\Desktop\1.png"); 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(1000); } catch(Exception e) { e.printStackTrace(); } count += 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(); } }
在本例中,为了是图标具有滚动功能,需要在类的构造方法中创建Thread实例。在创建实例的同时需要Runnable对象作为Thread类构造方法的参数,然后使用内部类形式实现run()方法。在run()方法中主要循环图标的横坐标位置,当图标横坐标到达标签的最右方的时候,再次将图标置于图标滚动的初始位置。
启动一个新线程,不是直接调用Thread子类对象的run()方法,而是调用Thread子类的start()方法,Thread类的start()方法产生一个新的线程,该线程运行Thread子类的run()方法。
package Runnable_test; public class MyRunnable { public static void main(String args[]) { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if(i == 30) { Runnable myRun = new myRun();//创建一个Runnable实现类的对象 Thread thread1 = new Thread(myRun);//将myRun作为Thread target创建新的进程 Thread thread2 = new Thread(myRun); thread1.start();//调用start方法使得线程进入就绪状态 thread2.start(); } } } } class myRun implements Runnable { public void run() { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
18.3 线程的生命周期
线程具有生命周期,分别为:出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。在用户使用该线程实例调用start()方法之前线程都处于出生状态;当用户调用start()方法后,线程处于就绪状态(又被称为可执行状态);当线程得到系统资源之后,就会进入运行状态。
一旦线程进入可执行状态,它会在就绪与运行状态下切换,同时也有可能进入等待、休眠、阻塞或死亡状态。当处于运行状态下的线程调用Thread类中的wait(0方法时,该线程便进入等待状态,进入等待状态的线程必须调用Thread类中notify()方法才能被唤醒,而notifyAll()方法将所有处于等待状态下的线程唤醒;当线程调用Thread类中的sleep()方法时,则会进入休眠状态。如果一个线程在运行状态下发出输入/输出请求,该线程就会进入阻塞状态,在其等待输入/输出结束时线程进入就绪状态,对于阻塞的线程来说,即使系统资源闲置,线程依然不能回到运行状态。当线程run()方法执行完毕时,线程进入死亡状态。
虽然多线程看起来像是同时执行的,但是事实上在同一时间只有一个线程被执行,只是线程之间的切换速度很快,在windows系统中,系统为每个线程分配一小片cpu时间,一旦cpu时间片结束就会将当前线程换位下一线程, 即便该线程没有结束。
18.4 操作线程的方法
一种能控制线程行为的方法是调用sleep()方法,时间参数单位是毫秒,sleep()方法通常在run()方法的循环中被使用。
try { Thread.sleep(2000); } catch(InterruptedException e) { e.printStackTrace(); }
上述代码能够是线程在2秒之内不会进入就绪状态。由于sleep()方法的执行有可能抛出InterruptedException异常,所以将sleep()方法的调用放在try/catch块中。虽然使用了sleep()方法的线程在一段时间内会醒来,但是并不能保证醒来后进入运行状态,只能保证进入就绪状态。
package thread_18_1; import java.awt.Color; import java.awt.Graphics; import java.util.Random; import javax.swing.JFrame; public class SleepMethodTest extends JFrame { private Thread t; //定义颜色数组 private static Color[] color = {Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.GREEN, Color.ORANGE, Color.YELLOW, 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; public void run() { while(true) { try { Thread.sleep(100); } 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); } }
18.4.2 线程的加入
当某个线程使用join()方法加入到另外一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
package thread_18_1; 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 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; public void run() { while(true) { progressBar.setValue(++count); try { Thread.sleep(100); threadB.join(); } catch(Exception e) { e.printStackTrace(); } } } }); threadA.start(); threadB = new Thread(new Runnable() { int count = 0; public void run() { while(true) { progressBar2.setValue(++count); try { Thread.sleep(100); } catch(Exception e) { e.printStackTrace(); } } } }); threadB.start(); } public static void init(JFrame frame, int width, int height) { frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(width, height); frame.setVisible(true); } }
在本实例中创建了两个线程,这两个线程都负责进度条的滚动,在现场A的run()方法中使线程B的对象调用join方法,而join()方法使得当前进程暂停,直到调用join()方法的线程执行完毕后再执行,所以线程A等待线程B执行完毕后再开始执行。
18.4.3 线程的中断
以往有时候会使用stop()方法来停止当前进程,而现在不建议这么使用,现在提倡的是在run()方法中使用无限循环的形式,然后使用一个布尔型的标记控制循环的停止。
package thread_18_1; public class InterruptedTest implements Runnable { private boolean isContinue = false; public void run() { while(true) { System.out.println(1); } if(isContinue) { break; } } public void setContinue() { this.isContinue = true; } }
如果线程使用了sleep()或者wait()方法进入就绪状态,可以使用Thread类中的interrupt()方法使线程离开run()方法,同时结束线程,但程序会抛出InterruptedException异常,用户可以在处理该异常时完成线程的中断业务处理,如终止while循环。
package thread_18_1; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JProgressBar; 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; public void run() { while(true) { progressBar.setValue(++count); try { thread.sleep(1000); } 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); } }
18.4.4 线程的礼让
Thread类中提供一种礼让的方法,使用yield()方法表示,它只是给出当前正处于运行状态下的线程的一个提醒,告诉它可以将资源让给其他线程,但是这仅仅是一种暗示,没有任何机制保证当前线程会将资源礼让。
yield()方法使具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时会再度回到就绪状态。对于支持多任务的操作系统来说,不需要调用yield()方法,因为操作系统会为线程自动分配CPU时间片来执行。
18.5 线程的优先级
每个线程都有自己的优先级,线程的优先级可以表明在程序中该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级决定首先使哪个线程进入就绪状态。但这不意味着低优先级的线程得不到运行,而是运行的几率小,比如说垃圾回收线程的优先级比较低。
在多任务操作系统中,每个线程都有一小段的CPU时间片运行,在时间结束后,将轮换到另一个线程进入运行状态,这时系统会选择与当前线程优先级相同的线程予以运行。系统始终选择就绪状态下优先级较高的线程进入运行状态。
优先级高的先得到时间片,之后轮换到优先级较低的线程。
线程的优先级可以使用setPriority()方法调整,需要使用该方法将优先级设置在1~10之间。
setPriority("thread", 5, threadA);
...
public static void setPriority(String threadName, int priority, Thread t)
{
t.setPriority(priority);
t.setName(threadName);
t.start();
}
18.6 线程同步机制
1.同步块
在java中提供了同步机制,可以有效防止资源冲突,同步机制使用synchronized关键字。
package ThreadSafe; public class ThreadSafeTest implements Runnable { int num = 10; public void run() { while(true) { synchronized("") { if(num > 0) { try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } System.out.println("ticket" + --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(); } }
运行结果:
ticket9 ticket8 ticket7 ticket6 ticket5 ticket4 ticket3 ticket2 ticket1 ticket0
synchronized(Object) { ... }
通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程也获取到这个锁的时候,必须等待锁被释放的时候才能进入到这个区域。Object为任意的一个对象,每个对象都有一个标志位,并具有两个值,分别为0和1,一个线程运行到同步块的时候首先检查这个对象的标志位,如果为0状态,表明此同步块中存在其他线程在运行,这时候该线程处于就绪状态,知道处于同步块中的线程执行完同步块中的代码为止。这个时候该对象的标志位会被设置为1,该线程才能执行同步块中的代码,i并将Object对象的标志位置为0,防止其他线程执行同步块中的内容。
2.同步方法
同步方法就是在方法前面修饰synchronized关键字的方法,其语法如下:
synchronized void f() { ... }
当某个对象调用了同步方法的时候,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问资源的方法修饰为synchronized,否则会出错。
package ThreadSafe; public class test1 implements Runnable { int num = 10; public synchronized void doit() { if(num > 0) { try { Thread.sleep(1000); }catch(Exception e) { e.printStackTrace(); } System.out.println("ticket" + --num); } } public void run() { while(true) { doit(); } } public static void main(String args[]) { test1 t = new test1(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } }