何为线程:线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区【共享的】(JDK1.8之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈【私有的】。
总结:线程是进程划分成的更小的运行单位,线程和进程的最大不同在于基本上各进程是相互独立的,而各线程则不一定,因为同一进程中的线程极有可能相互影响。线程执行开销小,但不利于资源的管理和保护;而进程则相反。
程序计数器主要有两个作用:
需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
所以,为了保证线程中局部变量不被其他线程访问到,虚拟机栈和本地方法栈都是线程私有的。
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已经被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
New:初始状态,线程被创建出来但没有调用start方法
RUNNABLE:运行状态,线程调用了start()等待运行的状态。
BLOCKED: 阻塞状态,需要等待释放锁。
WAITING:等待状态,表示线程需要等待其他线程做出一些特定动作
TIME_WAITING:等待超时状态,可以在指定的时间后自行返回而不是像WAITING那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。

线程创建之后处于New状态,调用start()方法后开始运行,线程此时处于Ready状态,可运行状态的线程获得了CPU时间片后就处于Running状态。
在操作系统层面,线程有Ready和Running状态;而在JVM层面,只能看到Runnable状态,所以一般将这两个状态称为Runnable状态。
为什么JVM没有区分这两种状态呢:现在的时分多任务操作系统架构通常都是用所谓“时间分片”方式进行抢占式轮转调度。这个时间分片通常是很小的,一个线程一次最多只能在CPU上运行10-20ms的时间(此时处于Running状态),时间片用完后就要被切换下来放入调度队列的队尾等待再次调度(也即回到Ready状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
wait和notify示例代码
package example;
class NotifyExample{
private static class WaitTask implements Runnable {
private Object lock;
public WaitTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
// 此线程在等待lock对象的notify方法唤醒
try {
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
}
}
}
private static class NotifyTask implements Runnable {
private Object lock;
public NotifyTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
//!!!重要,获取到lock的线程才能去唤醒在lock上等待的线程
synchronized (lock) {
System.out.println("准备唤醒");
// 唤醒所有线程(随机)
lock.notifyAll();
System.out.println("唤醒结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
//Object lock2 = new Object();
Object lock = new Object();
// 创建三个等待线程
Thread t1 = new Thread(new WaitTask(lock),"t1");
Thread t2 = new Thread(new WaitTask(lock),"t2");
Thread t3 = new Thread(new WaitTask(lock),"t3");
// 创建一个唤醒线程 Thread notify = new Thread(new NotifyTask(lock2),"notify线程");
Thread notify = new Thread(new NotifyTask(lock),"notify线程");
t1.start();
t2.start();
t3.start();
Thread.sleep(100);
notify.start();
// 当前正在执行的线程数
t1.join();
t2.join();
t3.join();
System.out.println(Thread.activeCount() - 1);
}
}
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所提到的程序计数器、栈信息等,当出现如下情况的时候,线程会从占用CPU状态中退出。
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文,这就是所谓的上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用CPU,内存等系统资源进行处理,也就意味着效率会有一定的损耗,如果频繁切换就会造成整体效率低下。
共同点:两者都可以暂停线程的执行。
区别:
wait是让获得对象锁的线程实现等待, 会自动释放当前线程占有的对象锁。每个对象都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入WAITING状态,自然要操作对应的对象(Object)而费当前的线程(Thread)。
sleep()是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法,这是真正的多线程工作。但是,直接执行run()方法,会把run方法当作一个main线程下的普通方法去执行,并不会在某个线程中执行他。
总结:调用start()方法可启动线程并使线程进入就绪状态,直接执行run()方法就不会以多线程方式执行。
同一时刻执行。关键点在于并发是时间片的调度,宏观上的并行,并行是真正意义上的同时。
在单核CPU上,同一时刻只能有一个线程在运行,其他线程需要等待CPU的时间片分配,如果线程是CPU密集型,那么多个线程同时运行会导致频繁的线程切换,增加了系统开销,降低了效率。如果线程是IO密集型,那么多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。
因此,对于单核CPU来说,如果任务是CPU密集型,那么多线程反而会影响效率;如果任务是IO密集型,那么多线程会提高效率。
如何预防:破坏死锁产生的必要条件
如何避免:借助于银行家算法对资源分配进行计算评估,使其进入安全状态。
银行家算法核心内涵:在资源分配过程中,通过模拟分配后系统状态,确保系统始终处于安全状态,从而避免死锁。