一、线程概述
1.1进程
在一个操作系统中,每个独立执行的程序都可称为一个进程。
在多任务操作系统中,进程并不是同时运行的。在计算机中所有的应用程序都是由cpu执行的,对于一个cpu而言同一时间点只能运行一个程序。由于运行速度快,看起来像同时执行多个进程。
1.2线程
一个进程中可以有多个执行单元同时运行,称为线程。操作系统中的每一个进程中都至少存在一个线程。
多线程是指一个进程在执行过程中可以产生多个单线程,单线程程序在运行时相互独立且可以并发执行。
线程与进程一样由cpu轮流执行。
二、线程的生命周期及状态转换
线程的生命周期可以分为五个阶段,分别是新建状态、就绪状态、运行状态、阻塞状态和死亡状态,具体关系如下图。
1.新建状态(New)
创建了线程对象后该线程对象就处于新建状态,此时仅占有内存,不能运行。
2.就绪状态(Runnable)
调用了线程对象的start()方法后该线程对象进入就绪状态。处于就绪状态的线程位与线程队列中,等待系统的调度。
3.运行状态(Running)
只有处于就绪状态的线程才可能转换到运行状态。一个线程启动后不会一直处于运行状态。
4.阻塞状态(Blocked)
一个正在执行的线程在特殊情况下会暂时中止自己的执行,进入阻塞状态。县城从阻塞状态只能进入就绪状态,结束阻塞的线程需要重新进入可运行池中,等待系统调度。
5.死亡状态(Terminated)
调用stop()或run()方法正常执行完毕,或线程抛出一个未捕获的异常(Exception)、错误(Error),线程进入死亡状态。一旦进入死亡状态,县城不再拥有运行的资格,也不能再转换到其他状态。
三、线程的创建
3.1继承Thread类创建多线程
//单线程
class Mythread{
public void run(){
while(true){
System.out.println("Mythread类的run()方法正在执行");
}
}
}
public class Main{
public static void main(String[] args){
Mythread mythread = new Mythread();
mythread.run();
}
}
通过继承Thread类,并重写Thread类中的run()方法可以实现多线程。Thread类中提供了start()方法用于启动新线程,线程启动后虚拟机会自动调用run()方法。
//多线程
class Mythread extends Thread{
public void run(){
while(true){
System.out.println("Mythread类的run()方法正在执行");
}
}
}
public class Main{
public static void main(String[] args){
Mythread mythread = new Mythread();
mythread.start();
while(true){
System.out.println("Main()方法正在执行");
}
}
}
单线程的程序在运行时会按照代码的调用顺序执行,多线程中同时运行,互不影响。
3.2实现Runnable接口创建多线程
为了克服多重继承的弊端,Thread类提供了另一个构造方法Thread(Runnable target),使用该构造方法时,需为该方法传递一个实现了Runnable接口的实例对象。
其中Runnable是一个接口,只有一个run()方法。
//多线程
class Mythread implements Runnable{
public void run(){
while(true){
System.out.println("Mythread类的run()方法正在执行");
}
}
}
public class Main{
public static void main(String[] args){
Mythread mythread = new Mythread();
Thread thread = new Thread(mythread);
thread.start();
while(true){
System.out.println("Main()方法正在执行");
}
}
}
3.3两种方式的比较
多个线程运行同一个程序,即资源共享,需要用到第二种方式创建多线程。
class TicketWindow implements Runnable{
private int tickets = 100;
public void run(){
while(true){
if(tickets > 0){
Thread th = Thread.currentThread(); //获取当前线程
String th_name = th.getName();
System.out.println(th_name"正在发售第"+tickets--+"张票");
}
}
}
}
public class Main{
public static void main(String[] args){
TicketWindow tw = new TicketWindow();
new Thread(tw,"窗口1").start();
new Thread(tw,"窗口2").start();
new Thread(tw,"窗口3").start();
new Thread(tw,"窗口4").start();
}
}
注意,每个线程都有自己的名字,主线程默认名字为"main",用户创建的第n个线程的名字默认为"Thread-n-1".如果希望指定线程的名字,可以调用setName(String name)方法为线程设置名称。
四、线程的调度
Java虚拟机会按照特定的机制为程序中的每个线程分配cpu的使用权,这种机制称为线程的调度。
线程调度有分时调度模型和抢占式调度模型两种。
分时调度模型,是指让所有的线程轮流获得cpu的使用权,平均分配每个线程占用cpu的时间片。
抢占式调度模型,是指让可以运行池中优先级高的线程优先占用cpu,优先级相同则随机选择,时java虚拟机的默认调度模型。
4.1线程的优先级
线程的优先级用1-10的整数表示,数字越大优先级越高。也可以用Thread类中提供的3个静态常量表示,static int MAX_PRIORITY/MIN_PRIORITY/NORM_PRIORITY。
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,如main线程具有普通优先级。可以通过Thread类的setPriority(int newPriority)方法设置。
4.2线程休眠
可以使用Thread的静态方法sleep(long millis)使正在执行的线程暂停。该方法声明会抛出InterruptedException异常,因此调用时应该捕获异常,或者声明抛出异常。
4.3线程让步
可以通过Thread的yield()方法实现线程让步,让正在执行的线程将cpu资源让给其他线程执行。
yield()方法与sleep()方法的区别是,它不会阻塞该线程,而是将其转换成就绪状态。
调用该方法后,只有与当前线程优先级相同或者更高的线程才能获得执行机会。
4.4线程插队
在某个线程中调用其他线程的join()方法,该线程会被阻塞,直到被join方法加入的线程执行完成后,被阻塞的线程才会继续运行。
五、多线程同步
5.1线程安全问题
线程安全问题由多个线程同时处理共享资源所导致,要想解决该问题,必须保证任何时刻只有一个线程访问共享资源。
5.2同步代码块
当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,该代码块称为同步代码块。
synchronized(lock){
操作共享资源代码块
}
lock是个锁对象,是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,发生阻塞。
等当前线程执行完同步代码块后,所有的线程开始抢夺执行权,抢到的进入同步代码块。
注意,同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。
锁对象的创建代码不能放到run()方法中。
5.3同步方法
也可以在方法前用synchronized关键字修饰,称为同步方法。
同步方法也有锁,他的锁是调用该方法的对象。
5.4死锁问题
两个线程都需要对方所占用的锁,且无法释放自己的锁,都处于挂起状态时,称为死锁。