多线程编程学习一(Java多线程的基础).

时间:2024-06-11 10:03:38

一、进程和线程的概念

进程:一次程序的执行称为一个进程,每个 进程有独立的代码和数据空间,进程间切换的开销比较大,一个进程包含1—n个线程。进程是资源分享的最小单位。

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,线程是CPU调度的最小单位。

多进程:指操作系统能同时运行多个任务(程序)。

多线程:指同一个程序中有多个顺序流在执行,线程是进程内部单一控制序列流。

线程和进程一样包括:创建、就绪、运行、阻塞、销毁 五个状态:
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

多线程编程学习一(Java多线程的基础).

二、多线程的优势

单线程的特点就是排队执行,也就是同步。而多线程能最大限度的利用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();
}}

多线程编程学习一(Java多线程的基础).

演示这个结果是为了说明以下两点:

1、CPU对线程的调度具有不确定性,采用“抢占式”调度。

2、对于网上经常说的,实现 Runnable 接口的线程可以实现共享数据,而继承 Thread 的线程就不能。其实不然,它们两者的区别仅是单继承的限制以及一些用法的不同(比如 如果你想对这个Thread对象做点别的事情(比如getName),那么你就必须通过调用Thread.currentThread()方法得到对此线程的引用),没有实质的差别。

多线程执行时为什么调用的是start()方法而不是run()方法?
    如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象就不会交给“线程规划器”来进行处理。而是由main主线程来调用run()方法,也就是说必须要等到run()方法中的代码执行完成后才可以执行后面的代码。start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程,通过Thead类中start()方法来启动的线程处于就绪状态(可运行状态),此时并没有运行,一旦得到CPU时间片,就自动开始执行run()方法。 

四、synchronized 关键字

多线程的锁机制,通过在多线程要调用的方法前加入synchronized 关键字,使多个线程在执行方法时,要首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronize里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到拿到这把锁。synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区” 或 “临界区”。

使用synchronized关键字主要是为了保证当前线程在执行过程中,不被其他线程抢占并修改了共享的资源,从而导致线程不安全的情况出现。

多线程编程学习一(Java多线程的基础).

五、常用线程方法

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 分析程序或者问题排查时,能够找到一个切入点。