一 多线程
1 进程
定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、IDEA开发工具等
说明: 在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。 在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。 由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。
2 线程
定义: 在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。 注意: 操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
(1)单线程
单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果
(2)多线程
多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行 多线程可以充分利用CUP资源,进一步提升程序执行效率。 多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉
3 线程的创建
创建:Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。
方式一:继承Thread类,重写run()方法 方式二:实现Runnable接口,重写run()方法 方式三:实现Callable接口,重写call()方法,并使用Futrue来获取call()方法的返回结果
(1)Thread类实现多线程
说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程
步骤: 第一步:创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法; 第二步:创建该子类的实例对象,并通过调用start()方法启动线程。
第一步:
package thread; //创建一个类继承Thread public class MyThread extends Thread{ @Override public void run() { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); } } }
第二步:
//创建线程MyThread线程类对象 MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); //设置线程的名字 t1.setName("线程1"); t2.setName("线程2"); t3.setName("线程3"); //启动3个线程 t1.start(); t2.start(); t3.start();
结果:
(2)Runnable实现多线程
说明: Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。
步骤: 第一步:创建一个Runnable接口的实现类,同时重写接口中的run()方法; 第二步:创建Runnable接口的实现类对象; 第三步:使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入; 第四步:调用线程实例的start()方法启动线程。
代码:
//第一步:创建Runnable接口实现类 public class MyRunnable implements Runnable{ @Override public void run() { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); } } }
//第二步:创建Runnable接口实现类对象 MyRunnable runnable = new MyRunnable(); //创建Thread类对象,并传入Runnable接口实现类对象 Thread t1 = new Thread(runnable); Thread t2 = new Thread(runnable); Thread t3 = new Thread(runnable); //第三步:设置线程名字 t1.setName("线程1"); t2.setName("线程2"); t3.setName("线程3"); //第四步:启动线程 t1.start(); t2.start(); t3.start();
结果:
(3)Callable接口实现多线程
说明: 通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。 为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。
使用: Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。
步骤: 第一步:创建一个Callable接口的实现类,同时重写Callable接口的call()方法; 第二步:创建Callable接口的实现类对象; 第三步:通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象; 第四步:使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例; 第五步:调用线程实例的start()方法启动线程。
代码:
import java.util.concurrent.Callable; //第一步:创建一个Callable接口的实现类 public class MyCallable implements Callable<String> { @Override public String call() throws Exception { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); } return Thread.currentThread().getName()+"执行完毕!";//线程返回值 } }
//第二步:创建Callable接口实现类对象 MyCallable myCallable = new MyCallable(); //第三步:创建FutureTask对象 FutureTask<String> futureTask1 = new FutureTask<>(myCallable); FutureTask<String> futureTask2 = new FutureTask<>(myCallable); FutureTask<String> futureTask3 = new FutureTask<>(myCallable); //第四步:创建Thread线程对象,设置线程名字 Thread t1 = new Thread(futureTask1,"线程1"); Thread t2 = new Thread(futureTask2,"线程2"); Thread t3 = new Thread(futureTask3,"线程3"); //第五步:开启线程 t1.start(); t2.start(); t3.start(); //获取线程返回值 String s1 = futureTask1.get(); String s2 = futureTask2.get(); String s3 = futureTask3.get(); System.out.println("线程返回值是:"+s1); System.out.println("线程返回值是:"+s2); System.out.println("线程返回值是:"+s3);
结果:
4 后台线程
说明: 对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。 这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程。
使用: 如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。
代码:
//创建Runnble接口实现类 MyRunnable runnable = new MyRunnable(); //创建3个线程 Thread t1 = new Thread(runnable,"线程1"); Thread t2 = new Thread(runnable,"线程2"); Thread t3 = new Thread(runnable,"后台线程"); //设置t3为后台线程 t3.setDaemon(true); //设置线程的优先级 t1.setPriority(9); t2.setPriority(9); t3.setPriority(1); //启动线程 t1.start(); t2.start(); t3.start();
结果:
5 线程的生命周期
说明: 在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。 当Thread对象创建完成时,线程的生命周期便开始了。 当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。
线程状态图:
新建: 创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。
RUNNABLE(可运行状态): 新建状态的线程调用start()方法,就会进入可运行状态。 在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。 RUNNABLE内部状态转换: 就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行; 运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。
BLOCKED(阻塞状态) 运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。 线程进入阻塞状态的两种情况: 当线程A运行过程中,试图获取同步锁时,却被线程B获取; 当线程运行过程中,发出IO请求时。
WAITING(等待状态) 运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会转换为等待状态。 等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。 例如: 调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程; 调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止
TIMED_WAITING(定时等待状态) 运行状态中的线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就会转换为定时等待状态。 定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。 例如: 调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。
TERMINATED(终止状态) 线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。 一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。
6 线程的调度
程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。 Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
(1)线程的优先级
在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。 优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。 线程的优先级用1~10之间的整数来表示,数字越大优先级越高。 除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级
Thread**类的静态常量** | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,相当于值10 |
static int MIN_PRIORITY | 表示线程的最低优先级,相当于值1 |
static int NORM_PRIORIY | 表示线程的普通优先级,相当于值5 |
说明: 程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。 使用: 可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量
//创建Runnble接口实现类 MyRunnable runnable = new MyRunnable(); //创建3个线程 Thread t1 = new Thread(runnable,"线程1"); Thread t2 = new Thread(runnable,"线程2"); Thread t3 = new Thread(runnable,"线程3"); //设置线程的优先级 t1.setPriority(10); t2.setPriority(5); t3.setPriority(1); //启动线程 t1.start(); t2.start(); t3.start();
(2)线程的休眠
如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。 该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。 sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
//创建Runnable接口实现类 public class MyRunnable implements Runnable{ @Override public void run() { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); //线程1休眠 if(Thread.currentThread().getName().equals("线程1")){ try { Thread.sleep(1);//休眠1毫秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
结果:
(3)线程让步
线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。 当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。
//创建Runnable接口实现类 public class MyRunnable implements Runnable{ @Override public void run() { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); //线程2让步 if(Thread.currentThread().getName().equals("线程2")){ if(i==50){ Thread.yield();//当i=50,线程2让步 } } } } }
结果:
(4)线程插队
在Thread类中也提供了一个join()方法来实现线程插队功能。 当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。 Thread类中还提供了带有时间参数的线程插队方法join(long millis)。 当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。
package thread; //创建Runnable接口实现类 public class MyRunnable implements Runnable{ @Override public void run() { //线程执行内容 for (int i=1;i<=100;i++){ //Thread.currentThread().getName():获取当前线程名字 System.out.println(Thread.currentThread().getName()+":"+i); //线程插队 if(i==50){ try { //当i=50,插入线程2 MyThread myThread = new MyThread(); myThread.setName("线程2"); myThread.start(); myThread.join();//线程2插队 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
public static void main(String[] args) { //创建Runnble接口实现类 MyRunnable runnable = new MyRunnable(); //创建线程 Thread t1 = new Thread(runnable,"线程1"); t1.start(); }
结果;
7 线程同步
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。 例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。 为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
(1)线程安全
线程安全问题其实就是由多个线程同时处理共享资源所导致的。 要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。 为此,Java中提供了线程同步机制。 同步代码块 同步方法
(2)同步代码块
synchronized(lock){ // 操作共享资源代码块 ... }
注意:
Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块 lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。 锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁
执行原理:
当线程执行同步代码块时,首先会检查lock锁对象的标志位; 默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0; 当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后; 锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。
public class SaleTicketRunnable implements Runnable{ private int num = 50; @Override public void run() { while(true){ synchronized (this){//同步代码块 if(num>0){ System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张火车票!"); num--; }else{ System.out.println("火车票卖完了!"); break; } } } } }
public static void main(String[] args) { SaleTicketRunnable saleTicketRunnable = new SaleTicketRunnable(); Thread t1 = new Thread(saleTicketRunnable,"窗口1"); Thread t2 = new Thread(saleTicketRunnable,"窗口2"); Thread t3 = new Thread(saleTicketRunnable,"窗口3"); t1.start(); t2.start(); t3.start(); }
(3)同步方法
[修饰符] synchronized 返回值类型 方法名([参数1,……]){}
注意:
在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。 被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。 同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。 Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。 同步代码块和同步方法解决多线程问题有好处也有弊端。 同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
public class SaleTicketRunnable implements Runnable{ private int num = 50; @Override public synchronized void run() {//同步方法 while(true){ if(num>0){ System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张火车票!"); num--; }else{ System.out.println("火车票卖完了!"); break; } } } }