Java多线程
一、进程和线程
进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。
二、线程的实现
在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。
- 实现 Runnable 接口
public class Thrund implements Runnable {// 实现runnable接口
private String name;
public Thrund(String name) {
this.name = name;
}
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "运行,i=" + i) ;
}
}
}
public static void main(String[] args) {
Thrund mt1=new Thrund("线程一");
Thrund mt2=new Thrund("线程二");
Thread t1=new Thread(mt1);
Thread t2=new Thread(mt2);
t1.start();
t2.start();
}
继承 Thread 类
public class Thrund extends Thread{ // 继承Thread类,作为线程的实现类
private String name;
public Thrund(String name) {
this.name = name;
}
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "运行,i=" + i) ;
}
}
}
public static void main(String[] args) {
Thrund t1=new Thrund("线程一");
Thrund t2=new Thrund("线程二");
t1.start();
t2.start();
}
从程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。
Thread 类和 Runnable 接口
Thread 类也是 Runnable 接口的子类,但在Thread类中并没有完全实现 Runnable 接口中的 run() 方法。在 Thread 类中的 run() 方法调用的是 Runnable 接口中的 run() 方法,也就是说此方法是由 Runnable 子类完成的,所以如果要通过继承 Thread 类实现多线程,则必须覆写 run()。
实际上 Thread 类和 Runnable 接口之间在使用上也是有区别的,如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。
三、线程的状态
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:
- 新建阶段
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread
类的构造方法来实现,例如 “Thread thread=new Thread()”。 - 就绪阶段 新建线程对象后,调用该线程的 start()
方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。 - 运行阶段 当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run()
方法定义该线程的操作和功能 - 阻塞阶段 一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU
暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait()
等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。 - 死亡阶段 线程调用 stop() 方法时或 run()
方法执行结束后以及发生Error或Exception时,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
Java 程序每次运行至少启动几个线程?
答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。
四、线程的操作
- 取得和设置线程的名称
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "运行,i=" + i) ;
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
new Thread(t).start();;
new Thread(t,"线程一").start();
}
线程的强制运行
在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "运行,i=" + i) ;
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t);
t1.start();
for (int i = 0; i < 10; i++) {
if(i>3) {
try {
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("Main线程"+i);
}
}
线程的休眠
在程序中允许一个线程进行暂时的休眠,直接使用 Thread.sleep() 即可实现休眠。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行,i=" + i) ;
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t);
t1.start();
}
中断线程
当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
System.out.println("1、进入run()方法");
try {
Thread.sleep(100000);//线程休眠10秒钟
System.out.println("2、线程已完成休眠");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("3、异常情况线程被终止");
return;
}
System.out.println("4、run()正常结束");
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t);
t1.start();
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
System.out.println("5、线程发生异常");
}
t1.interrupt();
}
后台线程
在 Java 程序中,只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用 setDaemon() 方法即可。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "在运行。");
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t);
t1.setDaemon(true);
t1.start();
}
在线程类 Thrund 中,尽管 run() 方法中是死循环的方式,但是程序依然可以执行完,因为方法中死循环的线程操作已经设置成后台运行。
线程的优先级
- 在 Java 的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "运行,i = " + i); // 取得当前线程的名字
}
}
}
public static void main(String[] args) {
Thread t1=new Thread(new Thrund(),"线程一");
Thread t2=new Thread(new Thrund(),"线程二");
Thread t3=new Thread(new Thrund(),"线程三");
t1.setPriority(Thread.MAX_PRIORITY);//最高等级
t2.setPriority(Thread.MIN_PRIORITY);//最低等级
t3.setPriority(Thread.NORM_PRIORITY);//中等等级
t1.start();
t2.start();
t3.start();
}
从程序的运行结果中可以观察到,线程将根据其优先级的大小来决定哪个线程会先运行,但是需要注意并非优先级越高就一定会先执行,哪个线程先执行将由 CPU 的调度决定。
线程的礼让
- 在线程操作中,也可以使用 yield() 方法将一个线程的操作暂时让给其他线程执行。
public class Thrund implements Runnable {// 实现runnable接口
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行,i = " + i); // 取得当前线程的名字
if(i==2) {
System.out.print("线程礼让:");
Thread.currentThread().yield();
}
}
}
}
public static void main(String[] args) {
Thread t1=new Thread(new Thrund(),"线程一");
Thread t2=new Thread(new Thrund(),"线程二");
t1.start();
t2.start();
}
五、同步和死锁
一个多线程的程序如果是通过 Runnable 接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。
- 同步代码块
public class Thrund implements Runnable {// 实现runnable接口
private int piaoshu=5;//基础票数5张
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) {//同步当前对象
if(piaoshu>0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("卖票:piaoshu="+piaoshu--);
}
}
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t,"线程一");
Thread t2=new Thread(t,"线程二");
t1.start();
t2.start();
}
同步方法
除了可以将需要的代码设置成同步代码块外,也可以使用 synchronized 关键字将一个方法声明为同步方法。
public class Thrund implements Runnable {// 实现runnable接口
private int piaoshu=5;//基础票数5张
@Override
public void run() {
for (int i = 0; i < 100; i++) {
this.sopt();
}
}
private synchronized void sopt() {
if(piaoshu>0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("卖票:piaoshu="+piaoshu--);
}
}
}
public static void main(String[] args) {
Thrund t=new Thrund();
Thread t1=new Thread(t,"线程一");
Thread t2=new Thread(t,"线程二");
t1.start();
t2.start();
}
线程的挂起与唤醒
在使用 wait 和 notify 之前,我们需要先了解对象的控制权(monitor)。在 Java 中任何一个时刻,对象的控制权只能被一个线程拥有。无论是执行对象的 wait、notify 还是 notifyAll 方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。所以我们通过同步synchronized 代码块来保证我们对对象的控制权。
public class ThrundTest {
private final Object flag = new Object();
public static void main(String[] args) {
ThrundTest threadTest = new ThrundTest();
ThreadA threadA = threadTest.new ThreadA();
threadA.start();
ThreadB threadB = threadTest.new ThreadB();
threadB.start();
}
class ThreadA extends Thread {
@Override
public void run() {
synchronized (flag) {
for (int i = 1; i <= 100; i += 2) {
flag.notify();
System.out.println(i); // 奇数
try {
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadB extends Thread {
@Override
public void run() {
synchronized (flag) {
for (int i = 2; i <= 100; i += 2) {
flag.notify();
System.out.println(i); // 偶数
if (i == 100) {
// 当输出了最后一个数字的时候,不能再wait了
break;
}
try {
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
六、线程安全和线程非安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
线程安全是通过线程同步控制来实现的,也就是synchronized关键字。
非线程安全是指多线程操作同一个对象可能会出现问题。而线程安全则是多线程操作同一个对象不会有问题。
线程安全必须要使用很多synchronized关键字来同步控制,所以必然会导致性能的降低。
所以在使用的时候,如果是多个线程操作同一个对象,那么使用线程安全的类;否则,就使用效率更高的类。
非线程安全!=不安全