一、什么是线程
- 进程:操作系统中每个独立执行的程序都可称为一个进程。
- 线程:一个程序中能够同时运行的执行单元。
例:如果将QQ看作一个进程,则其中同时打开的聊天窗口则可看为一个个线程。
注:多线程程序运行时,每个线程之间是独立的,可以并发执行。但并不是同时执行,CPU同一时刻只能执行一个线程。
二、创建线程的两种方式
1、继承Thread类
Thread类位于java.lang包下,通过覆写Thread类的run()方法实现多线程。
示例:
public class Example_1 { public static void main(String[] args) { MyThread one = new MyThread(); one.start(); while(true){ System.out.println("main()方法在运行!"); } } } class MyThread extends Thread{ //重写run()方法 public void run(){ while(true){ System.out.println("MyThread类的run()方法在运行!"); } } }
注:有局限性,Java中一个类只能继承一个父类。
2、实现Runnable接口
其中只有一个run方法,通过构造函数Thread(Runnable target)创建线程对象。避免了局限性。
示例:
public class Example_2 { public static void main(String[] args) { MyThread_2 two = new MyThread_2(); //创建MyThread_2实例对象 Thread thread = new Thread(two); //创建线程对象 thread.start(); //开启线程 while(true){ System.out.println("main()方法在运行!"); } } }class MyThread_2 implements Runnable{ public void run(){ while(true){ System.out.println("MyThread_2类的run()方法在运行"); } } }
3、两种方式的对比
实现Runnable接口相对于继承Thread类来说有以下好处:
① 适合多个相同程序代码的线程取处理同一个资源的情况,把线程同程序代码,数据有效的分离,体现了面向对象的特点。
② 可以避免由于java的单继承带来的局限性。
示例:
public class Example_2 { public static void main(String[] args) { new TicketWindow().start(); //创建一个线程对象TicketWindow并开启 new TicketWindow().start(); //创建一个线程对象TicketWindow并开启 new TicketWindow().start(); //创建一个线程对象TicketWindow并开启 new TicketWindow().start(); //创建一个线程对象TicketWindow并开启 //TicketWindow_2 tw = new TicketWindow_2(); //创建TicketWindow_2实例对象 //new Thread(tw,"窗口1").start(); //创建线程并命名,开启线程 //new Thread(tw,"窗口2").start(); //创建线程并命名,开启线程 //new Thread(tw,"窗口3").start(); //创建线程并命名,开启线程 //new Thread(tw,"窗口4").start(); //创建线程并命名,开启线程 } } class TicketWindow extends Thread{ 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-- + "张票"); } } } } class TicketWindow_2 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-- + "张票"); } } } }
4、后台线程
在java程序中,只要还有一个前台程序在运行,这个线程就不会结束,如果一个程序中只有后台线程运行,这个进程就会结束。
①新创建的线程默认都是前台线程
②如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。
注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法之前调用。否则会引发
IllegalThreadStateException异常。
代码:
class DamonThread implements Runnable{ public void run(){ while(true){ System.out.println(Thread.currentThread().getName() + "---is running."); } } } public class Example_3 { public static void main(String[] args) { System.out.println("main线程是后台线程么?" + Thread.currentThread().isDaemon()); DamonThread one = new DamonThread(); //创建一个DamonThread实例对象 Thread thread = new Thread(one,"后台线程"); //创建线程thread共享one资源 System.out.println("thread线程是后台线程么?" + Thread.currentThread().isDaemon()); thread.setDaemon(true); //将线程thread设为后台线程 thread.start(); //开启thread线程 for(int i = 0; i < 10; i++){ System.out.println(i); } } }
三、线程的生命周期
从Thread对象创建完毕开始,当run()方法中代码正常执行完毕或者抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。
①、新建状态(New)
创建一个线程后,该线程对象就处于就绪状态。仅仅由Java虚拟机为其分配了内存,不能运行。
②、就绪状态(Runnable)
当线程对象调用了start()方法后,该进程就进入就绪状态。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件。
③、运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。
注:只有处于就绪状态的线程才可能转换到运行状态。
④、阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU使用权,进入阻塞状态。
注:线程进入阻塞状态后就不能进入排队队列。只有当引起阻塞原因被消除后,线程才可以转入就绪状态。
线程由运行状态转化成阻塞状态的原因,及如何从阻塞状态转换为就绪状态:
- 当线程需要从其他线程获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程进入阻塞状态。如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
- 当线程调用了一个阻塞式的IO方法时,该线程进入阻塞状态,如果想进入就绪状态就必须等到这个阻塞的IO方法返回。
- 当线程调用了某个对象的wait()方法时,该线程进入阻塞状态,可使用notyfy()方法唤醒该线程使其进入就绪状态。
- 当线程调用了Thread的sleep(long millis)方法时,线程进入阻塞状态,需等线程睡眠时间到了以后,线程就会自动进入就绪状态。
- 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,需要等到新加入的线程运行结束后才会结束阻塞状态进入就绪状态。
⑤、死亡状态(Terminated)
线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。
注:一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他其他状态。
四、线程的调度
即Java虚拟机按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制称为线程的调度。
1、线程的优先级
线程的优先级用1-10之间的整数来表示,数字越大优先级越高。可以用Thread类中提供的静态常量来表示线程的优先级。
Thread类的优先级常量:
static int MAX_PRIORITY 表示线程的最高优先级,相当于值10
static int MIN_PRIORITY 表示线程的最低优先级,相当于1
static int NORM_PRIORITY 表示线程的普通优先级,相当于5
注:main线程具有普通优先级。可以通过Thread类的setPriority(int newPriority)方法对其进行设置。
示例:
class MaxPriority implements Runnable{ public void run(){ for(int i=0; i<=10; i++){ System.out.println(Thread.currentThread().getName() + "正在输出" + i); } } } class MinPriority implements Runnable{ public void run(){ for(int i=0; i<=10; i++){ System.out.println(Thread.currentThread().getName() + "正在输出" + i); } } } public class Example_4 { public static void main(String[] args) { //创建两个线程 Thread minPriority = new Thread(new MinPriority(),"优先级较低的线程"); Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程"); minPriority.setPriority(Thread.MIN_PRIORITY); maxPriority.setPriority(10); //开启两个线程 maxPriority.start(); minPriority.start(); } }
2、线程休眠
可以通过静态方法sleep(long millis)将CPU让给别的线程,该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。
注:sleep(long millis)方法声明抛出InterruptedException异常,因此调用该方法时应该捕捉异常,或者声明抛出该异常。
示例:
class SleepThread implements Runnable{ public void run(){ for(int i=0; i<=10; i++){ if(i == 3){ try{ Thread.sleep(200); }catch(InterruptedException e){ e.printStackTrace(); } } System.out.println("线程一正在输出:" + i); try{ Thread.sleep(500); }catch(InterruptedException e){ e.printStackTrace(); } } } } public class Example_5 { public static void main(String[] args) throws InterruptedException { //创建一个线程 new Thread(new SleepThread()).start(); for(int i=1; i<=10; i++){ if(i == 5){ Thread.sleep(2000); } System.out.println("主线程正在输出" + i); Thread.sleep(500); } } }
注:sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。
3、线程让步
线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,区别是yield()方法不会阻塞该线程,它只是将线程转换成就绪状态让系统重新调度一次。
注:当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
示例:
class YieldThread extends Thread{ public YieldThread(String name){ super(name); //调用父类构造方法 } public void run(){ for(int i=0; i<=500; i++){ System.out.println(Thread.currentThread().getName() + "---" + i); if(i == 3){ System.out.println("线程让步:"); Thread.yield(); //线程运行到此,做出让步 } } } } public class Example_6 { public static void main(String[] args) { //创建两个线程 Thread one = new YieldThread("线程A"); Thread two = new YieldThread("线程B"); //开启线程 one.start(); two.start(); } }
4、线程插队
在Thread类中提供了一个join()方法来实现“插队”,即当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完毕后它才会继续运行。
示例:
class EmergencyThread implements Runnable{
public void run(){
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "输入:" + i);
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public class Example_7 {
public static void main(String[] args) throws Exception {
//创建线程
Thread thread = new Thread(new EmergencyThread(),"线程一");
thread.start(); //开启线程
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "输入:" + i);
if(i == 2){
thread.join(); //调用join()方法
}
Thread.sleep(500);
}
}
}
五、多线程同步
为了解决多个线程访问统一个资源时引发的一些安全问题。
解决方法:实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
1、同步代码块
同步代码块:同步机制
当多个线程使用一个共享资源时,可以将处理共享资源的代码放在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块
语法格式:
synchronized(lock){
操作共享资源代码块
}
* #lock是一个锁对象是同步代码块的关键
* #锁对象的类型可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的
* #锁对象的创建代码不能放到run()方法中。
示例:
class SaleThread implements Runnable{
private int tickets = 10;
Object lock = new Object(); //定义任意一个对象,作为同步代码块的锁
public void run(){
while(true){
synchronized(lock){ //定义同步代码块
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
}else{
break;
}
}
}
}
}
public class Example_8 {
public static void main(String[] args) {
SaleThread one = new SaleThread(); //创建saleThread对象
//创建并开启四个线程
new Thread(one,"线程一").start();
new Thread(one,"线程二").start();
new Thread(one,"线程三").start();
new Thread(one,"线程四").start();
}
}
2、同步方法
方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,能实现和同步代码块相同的功能。
具体格式为:
synchronized 返回值类型 方法名([参数 1, ......]){}
被synchronized修饰的方法某一时刻值允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。
示例:
class Ticket1 implements Runnable{ private int tickets = 10; public void run(){ while(true){ saleTicket(); if(tickets <= 0){ break; } } } private synchronized void saleTicket(){ if(tickets > 0){ try{ Thread.sleep(10); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--); } } } public class Example_9 { public static void main(String[] args) { Ticket1 ticket = new Ticket1(); //创建并开启四个线程 new Thread(ticket,"线程一").start(); new Thread(ticket,"线程二").start(); new Thread(ticket,"线程三").start(); new Thread(ticket,"线程四").start(); } }
注:同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。
优点:同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。
3、死锁问题
死锁问题:两个线程运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
示例:
public class Example_10 { public static void main(String[] args) { //创建两个DeadLockThread对象 DeadLockThread one = new DeadLockThread(true); DeadLockThread two = new DeadLockThread(false); //创建并开启两个线程 new Thread(one,"Chinese").start(); new Thread(two,"American").start(); } } class DeadLockThread implements Runnable{ static Object chopsticks = new Object(); //定义Object类型的chopsticks对象 static Object knifeAndFork = new Object(); //定义Object类型的knifeAndFork对象 private boolean flag; //定义布尔类型变量flag DeadLockThread(Boolean flag){ this.flag = flag; } public void run() { if(flag){ while(true){ synchronized(chopsticks){ //chopsticks锁对象上的同步代码块 System.out.println(Thread.currentThread().getName()+"---if---chopsticks"); synchronized(knifeAndFork){ System.out.println(Thread.currentThread().getName()+"---if---knifeAndFork"); } } } }else{ while(true){ synchronized(knifeAndFork){ //knifeAndFork锁对象上的同步代码块 System.out.println(Thread.currentThread().getName()+"---else---knifeAndFork"); synchronized(chopsticks){ System.out.println(Thread.currentThread().getName()+"---else---chopsticks"); } } } } } }