一、线程和进程
进程:我们知道计算机的核心是cpu,承担了所有计算任务,是计算机硬件的组成之一,其他的还有内存等等。
在它之上运行着一个管家,起着承上启下的作用,我们称之为操作系统,它负责任务的调度,资源的分配和管理。
而在操作系统之上,运行的是应用程序,是某种功能集的集合。
每一个应用程序都可以看成一个进程,它有自己独立的内存区域,操作系统是以进程为独立单位分配硬件资源的,也是以进程为独立单位进行任务调度的,例如jvm,它就是一个进程,有自己的内存区域。进程中包含了一系列的线程。
线程:一个进程包含一个或多个线程,线程是cpu调度的最小执行单元,同一个进程中的线程可以共享进程的内存空间。
区别:
- 进程是系统资源分配的最小单位,而线程是cpu调度的最小执行单位。
- 进程是一个可以独立执行的应用程序,线程无法独立于进程之外执行。
- 线程间的上下文切换比进程上下文切换快的多
- 进程包含一个或多个线程,且线程可以共享进程的内存空间
二、线程的分类
线程分为用户级线程(ULT)和内核级线程(KLT)。
用户级线程:线程的创建,资源的分配和线程管理是由进程负责,不需要内核的支持,线程的切换是有程序进程控制的,和内无关。
例如一个进程中有5个线程,而服务器是4核cpu,一个cpu资源分配给了进程,而进程又把它分配给了内部的一个用户级线程,其他四个线程未获取到cpu资源,这时其他三个cpu资源即使空闲了,也不会在分配给未执行的线程了。
在内核空间中只维护一个进程表,对线程不关心。
优点:
- 线程的切换有程序自己控制,不用内核控制,性能相比较高。
- 一个进程中只有一个线程可以获得资源运行,可以在不支持多线程的操作系统中使用
缺点:
- 同时只能运行一个线程,无法充分利用多核cpu资源
- 一个线程一旦阻塞,则整个进程阻塞
内核级线程:线程的创建,资源的分配和调度是由内核控制的,线程的切换也是由内核负责的,从用户态切换到内核态,可以很好的运用多核cpu资源。例如jvm内的线程就是依靠内核线程完成工作的。
jvm创建的线程无法直接使用cpu资源,他是创建一个线程后,会在内核空间创建一个与之一一对应的内核线程,有内核线程获取cpu资源执行。
优点:
- 可以充分利用多核cpu资源,同时执行多个线程
缺点:
- 线程在用户态的运行,而线程的调度和管理在内核实现,在控制权从一个线程传送到另一个线程需要用户态到内核态再到用户态的模式切换,比较占用系统资源。(就是必须要受到内核的监控)
java线程和内核线程的关系:
三、线程的生命状态
线程的生命周期分为以下几种:
- 创建,通过new Thread()创建线程对象 此时先并未被启动。
- 就绪,当线程调用start()方法后,线程进入就绪状态,等待cpu调度。
- 运行,当就绪状态的线程拿到cpu时间片,进入线程运行状态。
- 等待/阻塞,当调用了线程中对象的wait()方法/LockSupport.park()/join()方法等,或者进入synchronized块时未获取到锁,线程进入阻塞状态。
- 死亡,线程执行完毕/线程被提前终止/执行中抛出异常,进入死亡状态。
四、线程的创建方式
我一直认为创建线程的方式本质上就一种:new Thread(),而创建线程任务的任务有三种,
- 集成Thread类,重写器run()方法,本质还是通过new Thread()创建线程 然后执行任务。
public class ThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是需要执行的任务
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 50) {
new ThreadTest ().start();
new ThreadTest ().start();
}
}
}
}
- 实现Runnable接口,实现run()方法,然后通过new Thread(new Runnable()),不任务传入线程对象中
public class RunnableThreadTest implements Runnable{
private int i;
public void run()
{
for(i = 0;i <100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args)
{
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
//创建一个任务
RunnableThreadTest rtt = new RunnableThreadTest();
/**
*创建两个线程
*然后将任务传进两个线程中执行
**/
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
- 实现Callable接口,实现call()方法,然后创建一个带有返回值的任务 传入线程中执行。
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 20) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//带有返回值的任务实现
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}
我们可以用过以上三种方式创建线程任务。
五、简单说下wait(),sleep(),join(),yield(),LockSupport.unpark()几个方法的区别
wait():它不仅仅是线程对象的方法,是所有对象的方法,也就是Object提供的方法,一般结合notify/notifyAll方法使用。在线程中,调用对象的wait()方法,当前线程会等待,直到有线程调用对象的notifyAll()方法,该线程才会重新进入就绪状态,等待执行。
该方法用于协调共享数据的存取,多以只有才获取到锁后,才能执行,一般只在synchronized块内部使用,它会释放锁标志,会释放cpu资源,当被notifyAll()方法唤醒后,线程需要重新等待分配cpu资源 然后重新获取到锁才能继续执行。
sleep():线程特有方法,线程调用此方法,进入睡眠阻塞状态,阻塞后,不会释放锁资源,但是会释放cpu资源,也就是线程一旦睡眠,则其他线程有机会获取到CPU资源执行。
join():线程特有的方法,当前线程会等待调用此方法的线程执行完毕后在继续执行。
yield():该方法和sleep()方法相似,调用此方法时,当前线程暂停执行,并且也是不释放锁标志的,不过此方法没有参数的,即调用yeild()方法,只是使当前线程回到可执行状态,所以执行yeild()方法的线程可能在进入可执行状态后立马又被执行。另外,在当前线程暂停执行时,同优先级的线程或者高优先级的线程可以获取执行机会,低优先级的线程没有获取执行机会的可能,这也是与sleep()方法的区别之一。
unpark():java中的锁机制和同步框架的核心AQS中就是通过LcokSupport.park()和LockSupport.unpark()方法实现线程的阻塞与唤醒的。内部是通过获取许可证进行线程的阻塞和唤醒,许可证最大只能有1个。park()是等待一个许可,unpark()是给出一个许可。
例如:线程A调用park()方法,则线程A会等待获取许可,如果许可未获取到,则一直阻塞,直到获取成功。假如此时线程B调用了unpark(A),相当于给A发了一个许可,此时阻塞的A会被唤醒并继续执行。
所以park()和unpark()的调用顺序是可以调换的。
许可默认是被占用的,所以一开始调用park()时,获取不到许可的,除非先调用unpark()先给自己发一个许可,在此调用park()时就不会阻塞。