线程
首先理解三个概念。程序是一段静态的代码,应用执行的蓝本。进程是针对操作系统而言的一个概念,一个系统可以有多个进程。线程是相对于进程而言的一个概念,一个进程可以有多个线程,每个线程都有自己独立的执行流程,放到内存中分析,就是每个线程都有一个方法调用栈。同一个进程内的各个线程相互独立,但是会互相争夺资源,比如内存对象,文件或者是数据库中的记录和表。共享资源必然就会产生争用资源的问题,所以多线程问题,最需要解决的就是安全性(同一张票只能卖给一个人)和并发性(同时给多个人提供服务,但每个人都感觉系统在只为它提供服务)的问题了。
Java 号称一次编写到处运行?
对于线程管理, JVM 有自己的优先级别和调度机制,但是因为 JVM 本质上也只是操作系统中的一个应用进程,它也是受限于操作系统。毕竟操作系统才是老大嘛,是它主管的硬件资源,所以这里存在两种不同的线程管理规定,导致同样的程序在不同的操作系统和硬件中有不同的执行情况(Java 会采用随机调度而操作系统会采用先来先服务、时间片轮转的调度算法)。一次编写、到处运行在线程这块成了被打脸了。怎么解决呢?加锁什么的,明天会讲
Java 线程的优先级别
java 的优先级别有 [1,10] 十种,但是通常只会用到 1 、5 和 10 这三个级别。这一点可以从 java.lang.Thread 类的仅有的三个静态字段中看出来。主线程默认优先级是 5 级,它创建的线程优先级默认也是 5 级,这从字段的命名中就可以看出来了,默认就是 5 级嘛。源码如下:
这里有一个 final 修饰的变量,只能叫做单值变量,而不能称作常量,常量位于方法区的常量数据区,而这里明显就位于静态数据区,如果未添加 static 关键字,那它应该是在堆区内的对应对象内存空间中。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
关于 Thread 类
线程的默认命名方式为"Thread-"+线程序号
,线程序号从 0 开始,所以命名为Thread-0 , Thread-1
等,也可以通过public static Thread currentThread()
方法得到当前线程对象的引用,再调用public final void setName(String name)
方法指定线程名字。
Thread 类的常见方法
其中常用的睡眠方法也是一个静态方法public static void sleep(long millis)throws InterruptedException
。
Thread 类还有一个让步方法public static void yield()
,会把当前执行机会让给同优先级的其他线程执行。
Thread 类有一个 final 修饰的方法public final boolean isAlive()
,用来判断该线程是否还活着。
public void interrupt()
,线程对象调用此方法,会立即出现中断异常(InterruptedException)。虚拟机强行使得此线程从阻塞态转成运行态,然后执行 catch 代码段。
主线程死亡后,会立即强行唤醒处于阻塞状态的正在睡眠的线程,进入到就绪态,然后再转成运行态,然后 JVM 会在 sleep() 方法中//产生一个 中断异常 抛出来。这个中断方法要配合 sleep() 方法使用,已经得到验证,如果没有 sleep 方法,即使调用了 interrupt() 方法仍然没有任何效果,这里本来就是利用会抛出这个异常//来进行逻辑处理啊。
@Deprecated
public final void stop()
TODO:这两个方法需要进一步去整理
线程的生命周期
主线程的入口方法是 main() 方法,主方法结束,主线程结束。其他线程的入口方法就是 run() 方法,run() 方法结束,该线程就结束了。有了多线程后,主线程结束,其他线程仍然会继续执行,直到所有线程结束,程序才结束。main() 方法是 JVM 调用的,run() 方法当然也是由 JVM 调用的。
如果将某个线程采用public final void setDaemon(boolean on)
方法设置为后台线程的话,当所有前台线程都结束了,那么后台线程无论是否执行完都立即会结束。当然如果这个时候,轮到它的时间片还在运转的话,这个时间片内还是会继续执行的。
线程的生命周期有五个状态:
线程生命周期的状态.png使用 new 新建线程对象的时候,线程处于新建状态,仅仅在堆区开辟了一个线程对象空间而已,线程对象和其他对象在内存中没有任何区别啊。
线程对象调用 .start() 方法的时候,就处于就绪状态,会分配好所有资源.。也就是创建对应的方法调用栈和分配程序计数器。
再由 JVM 调用的时候,线程就处于运行状态,会占用处理机。
当调用了睡眠方法或者正在等待 IO 操作就会进入阻塞状态。
JVM 也是有能力强行把运行状态的线程直接转为就绪状态的,比如时间片到了。当线程结束后,会释放掉所有资源,只等着 GC 进行垃圾回收。
创建线程的方式
方式一:首先定义一个类继承 Thread 类,重写 run() 方法。然后在主方法或者其他方法中调用线程类对象的 start() 方法,让线程处于就绪状态,分配好所需资源。
方式二:定义一个类实现 java.lang.Runnable 接口,并实现 run() 方法。将这类对象作为参数传递给 Thread 类的构造函数,Thread 类 new 出来的才是线程对象,实现了 java.lang.Runnable 接口的类对象并不是线程,它只是给线程提供了一个入口方法。
方式三:通过匿名内部类的方式,使用 Thread 类的构造方法public Thread(Runnable target)
传递一个匿名内部类对象。
线程同步锁
一个进程中有多个线程,这多个线程之间是互相共享进程的资源的,确切点的说就是共享堆区的对象。所以,这里的共享资源问题引出来了安全性的问题,为了保证安全性就不得不添加 synchronized 同步锁,以保证原子操作能够正确执行,不至于因为线程调度的不确定性而受到干扰。
同步锁可以分为对象锁、方法锁和类锁。
对象锁
Object obj = new Object();
synchronized(obj){
//synchronized 代码块内部的代码就一定会是以个原子操作
}
如上述代码所示:首先明确一点,java 中每一个对象都有一个锁。JVM 在遇到 sychronized 关键字的时候,首先会检查该对象有没有被锁住。如果没上锁,会先给对象上锁,再来执行 sychronized 代码块的内容,执行完后,再来释放锁。如果对象锁未被释放期间,其他线程想要执行 sychronized 代码快的内容,是不被允许的。只能在锁池中等待,该线程就从运行状态,转为阻塞状态。
方法锁
public synchronized void sell(){
//对应的原子操作
}
这里讲的是实例方法锁,实例方法锁其实本质上就是对象锁,它锁的是调用这个方法的对象,也就是 this 。更确切的说,一个对象的任意一个方法被锁住了的情况下,也就是这个对象被锁住了,那么这个类的其他方法也就无法被调用了。
类锁
也就是静态方法加锁就是类锁,锁的是这个类的反射对象。这和实例方法同理,也就是说,同一时刻只能有一个静态方法被执行。
线程之间的协作
因为用户想要同时使用多款应用程序,所以操作系统需要支持多进程。而且在使用单个应用程序的时候,还想要同时做多件事情,这就有了线程的产生。线程之间有时可能需要共享资源,因为线程调度的不确定性,所以需要使用 sychronized 加同步锁来保证各个线程彼此的独立性。线程有时候还需要互相协作,以完成某件事情。线程之间协作的最典型的一个应用实例就是生产者和消费者问题了。
首先要弄明白以下内容:Java 中每一个内存对象都有一个专门用来存放线程对象的锁池和等待池。java.lang.Object 类中的public final void wait() throws InterruptedException
方法和public final void notify()
方法都是专门针对线程的协作而设计的方法。
但是为什么它会定义在 Object 类中去呢?按道理应该设计在 Thread 类中的啊!这样设计的目的就是可以在任何添加了 sychronized 同步锁的代码块中调用任意对象的 wait() 和 notify() 方法,而不用考虑这个对象是否是继承了 Thread 类或者实现了 Runnable 接口。而且这两个方法必须在添加了同步锁的代码块中使用,如果没有,编译阶段会通过,但是在运行阶段会抛出 IllegalMonitorStateException 异常。为什么要设计这样报出异常呢?因为只有在同步锁代码块中才有存在的意义啊,所以 JVM 就干脆给一个异常错误。
当对象 t1 调用 wait() 方法时,其所在的线程对象,会进入这个对象 t1 的等待池中,此时处于阻塞状态,同时会释放掉对象 t1 的锁。此时这个线程对象在对象 t1 的等待池中只能等待其它线程调用该对象 t1 的 notify() 或者 notifyAll() 方法才能够使得它从等待池中释放。对象的锁池能够自动开启,等待池的线程对象只能被其他的线程调用 notify() 方法,才能够开启锁池。
其中 Object 类的 wait() 方法有三个重载的方法public final void wait() throws InterruptedException
,表示等待无限长时间和public final void wait(long timeout) throws InterruptedException
,表示等待指定长时间,以及public final void wait(long timeout, int nanos) throws InterruptedException
等待指定长时间。
其中 Object 类还有两个重载方法public final void notify()
和public final void notifyAll()
。第一个方法只能释放对象 t1 等待池中的随机的一个线程对象,第二个方法则可以释放掉所有的等待池对象。它们全都从对象 t1 的等待池中进入到对象 t1 的锁池中去,还会自动给对象 t1 加锁。
public class Test1 {
public static void main(String[] args) {
Tree q = new Tree();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
p.start();
c.start();
}
}
class Producer extends Thread {
Tree q;
Producer(Tree q) {
this.q = q;
}
public void run() {
for (int i = 0; i < 10; i++) {
q.put(i);
System.out.println("Producer put " + i);
}
}
}
class Consumer extends Thread {
Tree q;
Consumer(Tree q) {
this.q = q;
}
public void run() {
while (true) {
System.out.println("Consumer get " + q.get());
}
}
}
/* 树类 **/
class Tree {
int hole;// 树洞
boolean bFull = false;
// 放情报
public synchronized void put(int i) {
if (!this.bFull)// 如果为空
{
this.hole = i;// 把情报放入树洞
this.bFull = true;// 设为非空
/*
* 从该对象的等待队列中释放消费者线程进入该对象锁池 使该线程将再次成为可运行的线程
*/
this.notify();
}
try {// 生产者线程进入this对象的等待队列开启锁池
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// 取情报
public synchronized int get() {
if (!this.bFull)// 如果为空
{
try {
this.wait();// 消费者线程进入this对象的等待队列开启锁池
} catch (Exception e) {
e.printStackTrace();
}
}
bFull = false;// 设置为空
/*
* 从该对象的等待队列中释放生产者线程进入该对象锁池 使该线程将再次成为可运行的线程
*/
this.notify();
int value = hole;
return value;
}
}
以上生产者消费者的案例中,分析过程如下:
内存图如下:
_消费者生产者内存模型.png假设先执行的是消费者线程,消费者线程执行对象 q 的 get() 方法时,先判断对象 q 是否加锁了,加锁就进入锁池中等待,如果是未加锁就加锁,然后判断树洞是否为空,空则进入到等待池中等待,释放对象锁。如果不空则取出树洞中的内容,并释放对象等待池中的线程对象。
消费者线程调用 get() 方法,加对象锁,判断树洞为空,调用 wait() 方法进入等待池中,释放对象锁。
生产者调用 put() 方法,加对象锁,判断树洞为空,就将情报放入树洞中,再调用 notify() 方法释放处于等待池中的线程对象,随即进入到对象锁池中,然后生产者线程再次调用 wait() 方法,进入到等待池中,释放对象锁。
消费者线程对象此时处于运行状态,开始加对象锁,拿到树洞中的情报,后调用 notify() 方法从等待池中释放生产者线程对象,生产者对象进入锁池中,返回拿到的情报,对象锁自动开启。