进程和线程
说到线程,不得不提到进程,首先我们宏观的了解一下进程和线程。
进程,进程是具有一定独立功能的程序,进程是系统进行资源分配和调度的一个独立单位,竟争计算机系统资源的基本单位。每一个进程都有一个自己的物理地址空间,即进程空间或(虚空间)。一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程,线程是进程下的一个实体,由CPU调度和分配的基本单位,线程基本上不拥有系统资源,只拥有一点在运行过程中必不可少的资源。但是它可以和同属于一个进程的其他线程共享进程所拥有的资源。在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
创建线程
创建线程有如下两种方式:
1,继承java.lang.Thread类,该类有两个主要的方法:
run方法:在流程中直接线程的run方法方法,需要等待run方法体运行完毕才能执行下面的代码;这样就类似一个普通的方法,没有达到写线程目的。
start方法:调用该方法用来启动线程,真正实现了多线程的运行。调用该方法就无需等待run方法体执行完毕,可以继续执行主线程下面的代码。调用start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。当该线程分到CPU时间片时,才调用run()来完成其运行操作, 这里run()方法称为线程体,它包含了要执行的这个线程的内容, run()方法运行结束,此线程终止。然后CPU再调度其它线程。一个线程只能调用一次start()方法,第二次启动时将会抛出java.lang.IllegalThreadExcetpion异常。
2,实现Runnable接口
线程生命周期
初始状态:用new语句创建的线程对象时,这时候该线程就处于初始状态,跟普通的Java对象一样,仅仅分配了堆空间,不过要注意这时调用Thread的isAlive()方法,方法返回的是false。
可运行状态(就绪状态):线程创建以后,调用了该线程的start()方法,它就进入了就绪状态。这个状态的线程位于可运行池中,等待获得CPU的时间片。
运行状态:该线程占用CPU,执行run方法。
阻塞状态:线程处于阻塞状态时,JVM不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到可运行状态。
阻塞的情况:
1、线程运行时,如果执行了某个对象的wait()方法,JVM就会把线程放到这个对象的等待队列中,可以通过notify和notifyAll方法唤醒。
2、线程试图获取每个对象的同步锁或者是某个类的类锁时,如果想要获取的锁被其他线程占用了,则JVM会把该线程放到这个对象的锁池中。
3、线程执行了sleep方法。
4、调用了其他线程的join方法,在等待其他线程执行完毕的时候。
5、线程发出了I/O请求,在等待I/O的时候。
终止状态:
1,运行完run方法就是终止状态,结束了线程的生命周期。
2,异常退出。
终止状态,调用Thread的isAlive()方法,方法返回的是false,除了初始状态和终止状态,其他都是true。
线程调度
JVM是采用抢占式的调度方式,线程的调度不是跨平台的,他不但取决于JVM,也和操作系统密不可分。
如果要调度线程,可以采用以下几种方式:
1,调整线程的优先级。
线程的优先级可以通过Thread类的setPriority(int)和getPriority()来设置和读取线程的优先级,线程的优先级Thread定义了MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY,用户也不适用Thread类中定义的这三个值,但是必须在MIN_PRIORITY < 自定义优先级值 < MAX_PRIORITY,为了保证代码在各个系统的移植性,最好用Thread类中定义的这三个值。
2,线程调用Thread.sleep方法。
调用Thread.sleep方法可以让线程放弃CPU,转为阻塞状态。
3,线程调用Thread.yield方法。
调用Thread.yield方法时,如果此时有相同优先级的其它线程处于就绪状态,那么yield()方法CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。
4,线程调用另一个线程的join()方法
调用另一个线程的join()方法,当前线程会等待被调用的线程执行完毕后在执行。比如线程A调用了线程B的join方法,那么线程A会等待线程B执行完毕后在执行。
sleep和yield的区别
Thread.sleep方法和Thread.yield方法都是Thread类中的静态变量,都会让出CPU执行权给其他线程,他们的区别在于:
1,Thread.sleep方法让出CPU执行权,但是不管线程优先级的高低,仅仅把CPU的执行权让出来,这样无论线程的优先级是高还是低都有机会获取到CPU的执行权。Thread.yield方法只会给相同优先级或者更高优先级的线程一个运行的机会。
2,Thread.sleep(long millis)方法阻塞状态,参数millis指定睡眠时间;当线程执行Thread.yield()方法后,将转到可运行状态(就绪状态)。
3,Thread.sleep方法抛出InterruptedException异常,而yield()方法没有声明抛出任何异常。
4,Thread.sleep方法比Thread.yield方法具有更好的移植性。
线程同步
线程同步是为了保证操作的原子性,在Java规范中,对于基本类型的赋值或者返回值操作,是原子操作。只有long,double这些占用64位的会有些特殊,因为JVM的基础储存单位是32位,所以64位无法再一个时钟内完成。
为了保证操作的原子性,Java引入的同步快的概念,具体的做法是在原子操作的程序块前加synchronized标记,这样的代码块就是同步代码块了。同步代码块需要一个锁,就叫做同步锁,Java中每个对象和类都有且只有一个同步锁,一个同步锁只能被一个线程拥有。后续写一篇文章专门说一下锁。
线程通信
Java.lang.Object类中提供了两个用于线程通信的方法
1,wait:执行该方法的线程释放了对象锁,JVM会把该线程放到对象的等待池中,该线程等待被其他线程通过notify/notifyAll唤醒。
2, notify:执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。
死锁
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。
一个简单避免死锁的方式是让每个线程按照同样的顺序去访问他们。