一、进程和线程的概念
进程:一次程序的执行称为一个进程,每个 进程有独立的代码和数据空间,进程间切换的开销比较大,一个进程包含1—n个线程。进程是资源分享的最小单位。
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,线程是CPU调度的最小单位。
多进程:指操作系统能同时运行多个任务(程序)。
多线程:指同一个程序中有多个顺序流在执行,线程是进程内部单一控制序列流。
二、多线程的优势
单线程的特点就是排队执行,也就是同步。而多线程能最大限度的利用CPU的空闲时间来处理其他的任务,系统的运行效率大大提升,使用多线程也就是在执行异步。
三、使用多线程
实现多线程编程的方式主要有两种,一种是继承Thread类,另一种是实现Runable接口。其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单根继承,所以为了支持多继承,完全可以实现Runnable接口,一边实现一边继承。但是这两种方式创建的线程在工作时的性质是一样的,没有本质的差别。
public class Thread1 extends Thread {
private int count=5;
@Override
public void run()
{
for (int i=0;i<2;i++){
System.out.println("现在是线程"+currentThread().getName()+"在执行:"+count--); }
}
}
public class Thread2 implements Runnable {
private int count=5;
@Override
public void run() {
for(int i=0;i<2;i++){
System.out.println("现在是线程"+Thread.currentThread().getName()+"在执行:"+count--); }
}
}
public class Test{
public static void main(String[] args){
//集成Thread类
Thread1 thread1=new Thread1();
Thread t1=new Thread(thread1,"A");
Thread t2=new Thread(thread1,"B");
Thread t3=new Thread(thread1,"C");
t1.start();
t2.start();
t3.start();
//实现Runable接口
Thread2 thread2=new Thread2();
Thread t4=new Thread(thread2,"A2");
Thread t5=new Thread(thread2,"B2");
Thread t6=new Thread(thread2,"C2");
t4.start();
t5.start();
t6.start();
}}
演示这个结果是为了说明以下两点:
1、CPU对线程的调度具有不确定性,采用“抢占式”调度。
2、对于网上经常说的,实现 Runnable 接口的线程可以实现共享数据,而继承 Thread 的线程就不能。其实不然,它们两者的区别仅是单继承的限制以及一些用法的不同(比如 如果你想对这个Thread对象做点别的事情(比如getName),那么你就必须通过调用Thread.currentThread()方法得到对此线程的引用),没有实质的差别。
四、synchronized 关键字
多线程的锁机制,通过在多线程要调用的方法前加入synchronized 关键字,使多个线程在执行方法时,要首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronize里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到拿到这把锁。synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区” 或 “临界区”。
使用synchronized关键字主要是为了保证当前线程在执行过程中,不被其他线程抢占并修改了共享的资源,从而导致线程不安全的情况出现。
五、常用线程方法
1、Thread.currentThread()方法:返回代码段正在被哪个线程调用的信息。最常见的就是Thread.currentThread().getName()。
2、isAlive()方法:判断当前的线程是否处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程正在运行或准备开始运行的状态,就认为线程是“存活”的。
3、Thread.sleep()方法:在指定的毫秒数内让"正在执行的线程"休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
4、getId()方法:取得该线程的唯一标识。
5、Thread.interrupt()方法:用于中断线程,这里需要注意Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,并且清除中断标志,使之变为false。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
Thread thread = new Thread(() -> {
// 通过这样来检查这个中断标志位是否设置为 true 来判断是否进行程序逻辑,不要使用废弃的 Thread.stop, Thread.suspend, Thread.resume
while (!Thread.interrupted()) {
// do more work.
}
});
thread.start(); // 一段时间以后
thread.interrupt();
值得一提的是,判断线程是否中断有两个办法:
- interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除的false的功能。(这里需要特别注意的是即使是MyThread.interrupted(),测试的仍然是当前线程(this.currentThread())的状态)。
- isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志。
通过抛出异常来中断线程:
public class MyThread extends Thread {
@Override
public void run(){
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!我要退出了");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
} catch (InterruptedException e) {
System.out.println("进入MyThread.java类run方法中的catch了!");
e.printStackTrace();
}
}
}
另外,还可以通过retuen的方法来中断线程。不过还是建议"抛异常"的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。
6、Thread.yield()方法:放弃当前的CPU资源,将它让给其他的任务去占用CPU的执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
7、setPriority()方法:为了程序的可移植性,建议植使用 MAX_PRIORITY , NORM_PRIORITY , MIN_PRIORITY 三个级别。设置优先级并不意味着优先级低的就得不到调用,只是CPU更倾向于让高的优先级先执行,但是CPU具体调用那个线程是无法确定的,设置优先级只能保证说这个线程被调用的频率比较高。
8、setDaemon(true):守护线程。守护线程是一个特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了。
9、join()方法:等待该线程终止。join() 方法主要是让调用该方法的thread完成run方法里面的东西后, 再执行join()方法后面的代码,对join()方法的调用可以被中断,做法是调用线程上的的interrupt()方法。
六、其他
1、stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
2、suspend()方法暂停线程。resume()方法恢复线程的执行。在使用 suspend() 和resume()时,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题 。这是一个典型的线程对立的例子,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的。
3、以上提到的原因导致 stop()、suspend()、resume() 被废弃,不建议使用。
3、Daemon 线程是一种支持型线程,主要用作程序中后台调度以及支持性工作。JVM 不存在非 Daemon 线程的时候,Java 虚拟机将会退出。所以不能依靠 Daemon 线程的 finally 块来确保执行关闭或清理资源的逻辑。
4. 启线程前,最好为这个线程设置线程名字,因为这样在使用 jstack 分析程序或者问题排查时,能够找到一个切入点。