目录
3. 实现callable接口重写call方法,有返回值的(可以仅作为了解)
一、了解线程
世间万物都可以同时完成很多工作,比如用户可以拿电脑听歌也可以用电脑打印文件。java中将同时进行称为并发,将并发完成的每一件事都称为线程。
多线程:同时阅读三本书,首先阅读第一本书的第一章,然后阅读第二本书的第一章,再阅读第三本书的第一章,回过头再阅读第一本书的第二章,以此类推。
windows操作系统是多任务操作系统,它以进程为单位,一个进程是一个包含有自身地址的程序,每一个独立的执行的程序都称之为进程,也就是正在执行的程序,系统可以分配给每个进程一段有限的使用cpu的时间(也就是cpu时间片),cpu在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。由于cpu转换较快,所以使得每个进程好像是同时执行一样,进程是资源分配的最小单位。
一个线程是进程中的执行单元,一个进程中可有多个线程,每个线程都可以得到一小块程序执行时间,线程是程序执行时的最小单位。
二、实现线程的三种方式
1.继承Thread类
Thread类是java.lang包中的一个类,从这个类中实例化的对象代表线程,程序员启动一个新线程需要建立Thread实例。常用的两个构造方法如下:
public Thread(String threadName)
public Thread()
带参数的构造器是创建一个名字为threadName的线程,第二个是创建一个默认的线程
继承Thread类创建一个新的线程:
public class ThreadTest extends Thread{
}
完成线程真正功能的代码放在类的run()方法中,下面是Thread类中的run方法,是重写的Runnable接口中的run方法。
当一个类继承Thread类后,就可以在该类中覆盖run()方法,将实现该线程功能的代码写入run()中,然后调用Thread类中的start()方法执行线程,也就是调用run()方法。
**如果start()方法调用一个已经启动的线程,系统将抛出IllegalThreadStateException**
当执行一个线程程序时,就自动产生一个线程,主方法就是在这个线程上运行的。如果没有别的线程启动,那这个程序就是单线程的。main方法的启动由java虚拟机负责,程序员负责启动自己的线程。
下面是一个继承Thread类的实例:
public class ThreadTest extends Thread{
private int count = 10;//定义全局变量
@Override
public void run() {//重写父类的run方法
while (true){
System.out.println("count = " + count);//打印每一次的count
if (--count == 0)//终止运行条件
return;
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
// threadTest.run();
threadTest.start();
}
}
调用run()方法和调用start()方法的结果是一样的,调用start()方法后,start()再去调用run()方法。
java的线程会自动关闭,当run()方法中的全部执行完后,就关闭了。
如果没有调用start()方法,线程就永远不会启动。
启动一个新的线程,不是直接调用run()方法,先调用start()方法产生新线程,然后由它调用run()方法
2.实现Runnable接口
因为java是单继承多实现,如果程序员需要继承其他类还要使当前类为多线程,就可以通过Runnable接口来实现。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象和Thread对象相关联。Thread类中有以下两个构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
这两个构造方法的参数都存在Runnable实例,使用上述构造方法就可以将Runnable实例与Thread实例相关联。步骤如下:
1.创建Runnable对象
2.使用有Runnable对象为参数的构造方法创建Thread对象
3. 调用start()方法启动线程
下面演示一个GUI程序:
import javax.swing.*;
import java.awt.*;
import java.net.URL;
public class SwingAndThread extends JFrame {
private JLabel jl = new JLabel();//声明一个标签对象
private static Thread t;//声明一个线程对象
private int count = 0;//声明计数变量
private Container c = getContentPane();//声明容器
public SwingAndThread(){//创建本类的空构造方法
setBounds(300,200,250,100);//绝对定位窗体位置和大小
c.setLayout(null);//使窗体不使用任何布局管理器
URL url = SwingAndThread.class.getResource("icon.png");//获取图片所在的url(图片需手动复制到.class文件存放的文件夹中)
Icon icon = new ImageIcon(url);//实例化一个图片icon
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(10);//让线程休眠10毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count += 4;//设置步长
if (count == 200){
count = 10;//当图标到达标签的最右边时,让它回到最左边
}
}
}
});
t.start();//启动线程
c.add(jl);
setVisible(true);
setDefaultCloseOperation(3);
}
public static void main(String[] args) {
new SwingAndThread();
}
}
上面的实例就是一个走了两遍的进度条
3. 实现callable<V>接口重写call方法,有返回值的(可以仅作为了解)
import java.util.concurrent.Callable;
public class Thread3 implements Callable<Integer> {
private Integer num1;
private Integer num2;
public Thread3(Integer num1,Integer num2){
this.num1 = num1;
this.num2 = num2;
}
@Override
public Integer call() throws Exception {
return Math.max(num1, num2);
}
public static void main(String[] args) throws Exception {
Thread3 thread3 = new Thread3(1, 2);
System.out.println("thread3.call() = " + thread3.call());
}
}
在run中实例化一个线程,输出相关属性
Thread t = Thread.currentThread();//代表当前线程
String name = t.getName();//获取线程名字-->如果没有设置线程名则默认分配
long id = t.getId();//获取线程id
int priority = t.getPriority();//优先级
State state = t.getState();//获取当前状态-->七种
boolean alive = t.isAlive();//线程存活状态-->是否存活
boolean interrupted = t.isInterrupted();//线程是否中断
boolean daemon = t.isDaemon();//是否守护线程
三、线程的生命周期
线程具有生命周期,其中包含七种状态,分别为:出生状态(线程被创建时处于的状态,调用start方法之前线程都处于这个状态)、就绪状态(调用start方法后处于这个状态-->可执行状态)、运行状态(线程进入可执行后就会在就绪和运行之间来回切换)、等待状态(调用wait方法时进入此状态,直到调用notify/notifyAll:唤醒所有进行等待的线程方法)、休眠状态(调用sleep方法时进入此状态)、阻塞状态(线程运行状态下发出输入/输出请求时进入该状态)和死亡状态(run方法执行完就是此状态)。
下面给大家画张图帮助理解:
可以看到,同一时间点上只有一个线程被执行,只是线程之间切换较快,造成了同时进行的假象。
当系统分配给线程的时间片结束后,就会将当前线程切换为下一个线程。
总结一下可以使线程处于就绪态的几种方法:
调用sleep()方法
调用wait()方法
等待输入/输出完成
当线程处于就绪态后,可以使用以下几种方式重新进入运行状态
线程调用notify()方法
线程调用notifyAll()方法
线程调用interrupt()方法
线程的休眠时间结束
输入/输出结束
四、操作线程的方法
1.线程的休眠 (sleep()方法)
public void run() {
try {
Thread.sleep(100);//参数是休眠的时间,单位是毫秒,这一句表示线程休眠100毫秒,通常在循环中使用
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
为了使读者更深入的了解线程的休眠方法,来看下面的实例
import javax.swing.*;
import java.awt.*;
import java.util.Random;
public class SleepMethodTest extends JFrame {
private Thread t;
//定义存放颜色的数组
private static Color[] colors = {
Color.red,Color.ORANGE,Color.YELLOW,Color.GREEN,Color.CYAN,Color.blue,Color.MAGENTA,Color.pink,Color.LIGHT_GRAY
};
//创建随机对象
private final static Random rm = new Random();
//获取颜色的方法
private static Color getC(){
return colors[rm.nextInt(colors.length)];//返回颜色数组中随机一个的颜色
}
public SleepMethodTest(){
t = new Thread(new Runnable() {
@Override
public void run() {
//定义在窗口中的初始坐标
int x = 30;
int y = 50;
while (true){
try {
Thread.sleep(100);//线程休眠0.1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//获取组件绘图上下文
Graphics graphics = getGraphics();
graphics.setColor(getC());//设置绘图颜色
//绘制直线并递增垂直坐标
graphics.drawLine(x,y,280,y++);
if (y >= 280)
y = 50;
}
}
});
t.start();//启动线程
}
public static void main(String[] args) {
init(new SleepMethodTest(), 300, 300);
}
//初始化界面
private static void init(JFrame jf,int width,int height) {
jf.setDefaultCloseOperation(3);
jf.setSize(width,height);
jf.setVisible(true);
}
}
2.线程的加入
如果当前某程序为多线程程序,假如存在一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后再继续执行线程A,此时可以用Thread类中的join()方法来完成。
下面是一个实例:
import javax.swing.*;
import java.awt.*;
public class JoinTest extends JFrame {
//定义两个线程
private Thread a;
private Thread b;
//定义两个进度条组件
private JProgressBar aa = new JProgressBar();
private JProgressBar bb = new JProgressBar();
public JoinTest(){
super();//创建窗口
getContentPane().add(aa, BorderLayout.NORTH);//将进度条添加到窗体的最北面
getContentPane().add(bb, BorderLayout.SOUTH);//将进度条添加到窗体的最南面
//设置进度条显示进度数字
aa.setStringPainted(true);
bb.setStringPainted(true);
//初始化Thread类
a = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true) {
aa.setValue(++count);//设置当前进度条的当前值
try {
Thread.sleep(100);//a线程休眠0.1秒
//添加线程b
b.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
a.start();
b = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true){
bb.setValue(++count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 100){
break;
}
}
}
});
b.start();
}
public static void init(JFrame j,int width,int height){
j.setDefaultCloseOperation(3);
j.setSize(width,height);
j.setVisible(true);
}
public static void main(String[] args) {
init(new JoinTest(), 100,100);
}
}
3.线程的中断
之前由stop()方法用来停止线程,现在的jdk已经舍弃这个方法了,不建议使用。现在提倡在run()方法中使用无限循环的形式,然后用一个布尔类型的标记控制循环的停止。
如果使用了sleep()或wait()方法进入就绪状态,可以使用interrupt()方法使线程离开run()方法,同时结束线程,然后我们可以在捕获的异常中做业务处理,如终止循环。
下面是实例代码:
import javax.swing.*;
import java.awt.*;
public class InterruptedSwing extends JFrame {
Thread t;
public InterruptedSwing(){
super();
final JProgressBar jProgressBar = new JProgressBar();//创建进度条
getContentPane().add(jProgressBar, BorderLayout.NORTH);//将进度条放在合适的位置
jProgressBar.setStringPainted(true);//设置显示进度数字
t = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true){
jProgressBar.setValue(++count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("当前线程被中断");
break;
}
}
}
});
t.start();//启动线程
t.interrupt();//中断线程
}
public static void init(JFrame j,int width,int height){
j.setDefaultCloseOperation(3);
j.setSize(width,height);
j.setVisible(true);
}
public static void main(String[] args) {
init(new InterruptedSwing(), 100,100);
}
}
4.线程的礼让
Thread类中提供了一种礼让方法,如yeild()方法,但仅仅是暗示,并不是强制,让不让要看当前线程的心情。
yeild()方法使同优先级的线程有进入可执行状态的机会,如果当前线程放弃执行权,会再度回到就绪态。对于支持多任务的操作系统来说,不需要调用yeild()方法,因为有时间片。
五、线程的优先级(其实是假的)
优先级:Thread.MIN_PRIORITY(常数1)~Thread.MAX_PRIORITY(常数10)
程序会根据优先级让线程进入运行状态,每个新线程都继承了父类的优先级
我们用进度条详细演示一下优先级:
import javax.swing.*;
import java.awt.*;
public class PriorityTest extends JFrame {
//创建四个进度条变量
private JProgressBar aa = new JProgressBar();
private JProgressBar bb = new JProgressBar();
private JProgressBar cc = new JProgressBar();
private JProgressBar dd = new JProgressBar();
//创建四个线程
private Thread a;
private Thread b;
private Thread c;
private Thread d;
public PriorityTest() {
super();
setLayout(new GridLayout(4, 1));
//将进度条放入容器
getContentPane().add(aa);
getContentPane().add(bb);
getContentPane().add(cc);
getContentPane().add(dd);
//设置进度条数字可视
aa.setStringPainted(true);
bb.setStringPainted(true);
cc.setStringPainted(true);
dd.setStringPainted(true);
//分别实例化四个线程
a = new Thread(new MyThread(aa));
b = new Thread(new MyThread(bb));
c = new Thread(new MyThread(cc));
d = new Thread(new MyThread(dd));
setPriority("优先级为5", 5, a);
setPriority("优先级为5", 5, b);
setPriority("优先级为4", 4, c);
setPriority("优先级为3", 3, d);
}
public static void main(String[] args) {
init(new PriorityTest(),100,100);
}
private static void init(JFrame j, int i, int i1) {
j.setVisible(true);
j.setSize(i,i1);
j.setDefaultCloseOperation(3);
}
public static void setPriority(String threadName,int priority,Thread t){
t.setPriority(priority);//设置线程优先级
t.setName(threadName);//设置线程的名字
t.start();//开启线程
}
}
//定义一个实现Runnable接口的类
class MyThread implements Runnable{
private final JProgressBar bar;
int count = 0;
MyThread(JProgressBar bar) {
this.bar = bar;
}
@Override
public void run() {
while (true){
bar.setValue(++count);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
}
}
}
为什么说是假的呢,因为肉眼真的没看出这四个进度条有快慢之分!!!
六、线程同步
1.线程安全
很典型的生活例子就是火车站售票系统。因为是同步的,如果只剩下最后一张票,窗口1把票卖出去了窗口二线程已经在执行了,那么就会出现票数为负的情况,这个时候就要考虑线程安全问题,也就是给线程上一个安全锁(大概就是排队上厕所的时候前一个进去锁门了,后一个想进去就要等前一个开门出来才能进去)
2.线程同步机制
(1)同步块
同步块(临界区):synchronized
语法如下:
synchronized(Object){
}
通常将共享资源放在synchronized定义的区域中(比如或火车站售票系统的票数),它就是一道锁,其他线程想要获取这道锁里的内容,就要等待锁被释放才能进入开区域。参数为任意一个对象,每个对象都存在一个标志位,如果为0状态,表示该同步块中的内容正在被其他线程运行,为1时,当前线程才能访问
下面是火车站售票的一个小实例:
public class sellTicket extends Thread{
private static int ticketNums = 10;
private static Object obj = new Object();
@Override
public void run() {
while (true){
if (ticketNums < 0){
System.out.println("票已售完");
System.exit(0);
}
System.out.println(Thread.currentThread().getName() + "------当前票号:" + ticketNums--);
try {
synchronized (obj){//同步块
sleep(1);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
sellTicket sellTicket = new sellTicket();
sellTicket.setName("窗口1");
sellTicket.start();
sellTicket sellTicket1 = new sellTicket();
sellTicket1.setName("窗口2");
sellTicket1.start();
}
}
(2)同步方法
synchronized void f(){}
当对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错
class synchronizedTest extends Thread{
int num = 10;
public synchronized void f(){
if (num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.print(--num+"\t");
}
}
@Override
public void run() {
while (true) f();
}
public static void main(String[] args) {
new synchronizedTest().start();
}
}
将共享资源操作放在同步块和放在同步方法中的执行结果是一样的。