一、进程和线程的概念
进程:一次程序的执行称为一个进程,每个 进程有独立的代码和数据空间,进程间切换的开销比较大,一个进程包含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(() -> { while (!Thread.interrupted()) {//通过这样来检查这个中断标志位是否设置为true,是否进行程序逻辑,请不要使用废弃的Thread.stop, Thread.suspend, Thread.resume // 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()方法恢复线程的执行。
3、在使用suspend()和resume()时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。比如因为线程的暂停而导致数据不同步、suspend 和 resume会对访问资源的锁进行独占(i++没有锁、println()具有同步锁)。