Java多线程
线程是比进程更小的执行单位,是程序运行的基本执行单元。合理地使用线程是减少开发和维护成本
的必要条件,甚至能够改善复杂应用程序的性能。
线程与进程的区别
-
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大在开销;
-
线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小;
-
多进程:在操作系统中能同时运行多个任务(程序);
-
多线程:在同一应用程序中有多个顺序流同时执行。
所谓的多线程就是一个进程在执行的过程中可以产生多个同时存在、同时运行的线程,多线程机制可以合理利用资源,提高程序运行效率
实现Java多线程的方式:
1. 继承Thread类
2. 实现Runnable接口
方法一:实现Thread类
-
首先继承Thread类
-
实例化一个变量
-
给出一个构造方法
-
重写Three中的run方法
-
给出主线程运行方法
/** * 实现Java多线程方法一: 继承Thread类 */ public class Thread extends java.lang.Thread { //实例化一个变量 private String name; //给出构造方法 public Thread(String name){ this.name = name; } //给出Java多线程实现的业务逻辑(重写run方法) @Override public void run() { synchronized (this){ for (int i = 0; i < 5; i++) { System.out.println(name + "运行了,i = " + i); } } } //给出主线程main运行方法 public static void main(String[] args) { Thread t1 = new Thread("线程A"); Thread t2 = new Thread("线程B"); t1.start(); t2.start(); } }
方法二:继承Runnable接口
-
继承runnable方法
-
重写run方法
-
测试方法(步骤同上)
启动多线程必须依靠Thread类实现
//实现Runnable接口 public class Demo2 implements Runnable { //实例化一个变量 private String name; //给出构造方法 public Demo2(String name){ this.name = name; } //定义逻辑 public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "被执行,当前i = " + i); } } //给出主线程运行方法 public static void main(String[] args) { //实例化runnable Runnable a = new Demo2("A"); Runnable b = new Demo2("B"); //使用thread中的start方法运行 new Thread(a).start(); new Thread(b).start(); } }
Thread类和Runnable接口的区别
-
继承了Thread的类不适合多个线程共享资源
-
而实现了Runnable接口可以方便的实现资源共享
实现多窗口售票功能
方法一:使用Thread类
public class Demo3 extends Thread { //假设现在有五张票 private Integer ticket = 5; //私有变量 private String name; //构造方法 public Demo3(String name){ this.name = name; } @Override public void run() { for (int i = 0; i < 5; i++) { //确认现在有票 才可以出售 if(ticket > 0){ System.out.println( Thread.currentThread().getName() + "买票:ticket = " + ticket--); } } } public static void main(String[] args) { Demo3 a = new Demo3("A"); Demo3 b = new Demo3("B"); Demo3 c = new Demo3("C"); a.start(); b.start(); c.start(); } }
运行结果:
启动程序中的三个线程会各自卖出五张票,因为创建了三个线程对象,就想到与创建了三个资源每个线程都有五张票需要卖出,独立处理各自的资源没有达到资源共享的目的
方法二:实现Runnable接口
public class Demo4 implements Runnable{ private Integer ticket = 5; public void run() { for (int i = 0; i < 5; i++) { //确认现在有票 才可以出售 if(ticket > 0){ System.out.println("卖票:ticket = " + ticket--); } } } public static void main(String[] args) { //实例化 Demo4 d = new Demo4(); //创造线程去执行方法 new Thread(d).start(); new Thread(d).start(); new Thread(d).start(); } }
运行结果:
由运行结果可知,通过 Runnable 接口启动了三个线程,共卖出五张票,即 ticket 属性被所有的线程
对象共享。原因在于每个线程调用的是同一个 Demo4 对象中的 start( )方法。
实现Runnable接口和继承Thread类在实现多线程中的优势
-
适合多个具有相同程序代码的线程处理同一资源。
-
可以避免 Java 的单继承特征带来的局限性。
-
代码能够被多个线程共享且与数据独立存在,从而增加了程序的健壮性。
线程操作方法
-
启动方法(开始执行线程)
new Demo3().start(); //开始执行
-
执行线程
new Demo3().run();//执行
-
获取线程名称
Thread.currentThread().getName() //获取当前线程的名称
-
设置线程的睡眠时间 单位是毫秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
-
暂停当前的线程去执行别的线程(线程礼让)
Thread.yield();
-
线程的优先级
Thread t1 = new Thread("线程A"); Thread t2 = new Thread("线程B"); t1.setPriority(); //设置线程的优先级 //优先级有三个 min低(1-4) norm中(5) max高(6-10) t1.setPriority(MAX_PRIORITY); //--> 优先级最高 t2.setPriority(MIN_PRIORITY); //--> 优先级最低 t1.setPriority(NORM_PRIORITY); //--> 优先级中等 //线程将根据其优先级的高低决定运行顺序 //并非线程优先级越高就一定优先被执行,最终还是由 CPU 的调度来决定最先执行的线程
注意事项:
1. 线程最终启动调用的还是run()方法
线程启动时调用了 start( )方法,系统通过 start( )方法调用 run( )方法,执行线程主体
一段代码若需要在新线程中运行,该段代码就必须放在类的 run( )方法中,同时一个线程类对象只允许调用一次 start( )方法,多次调用将产生异常
2.sleep(睡眠) 方法和 yield(礼让)方法的区别
1) sleep( )方法使当前线程进入停滞状态,所以执行 sleep( )方法的线程在指定时间内肯定不会被执行yield( )方法只是使当前线程重新回到可执行(就绪)状态,所以执行 yield( )方法的线程有可能在进入到可执行状态后马上又被执行。
2) sleep( )方法可以使优先级别低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield( )方法只能使同优先级的线程得到执行的机会。
同步和死锁
线程同步:
什么是线程同步:同步是指同一时间段内只能运行一个线程,其他线程需要等待此线程完成后才可继续执行。同步可以解决线程中资源共享的安全问题,主要通过同步代码块和同步方法两种方式完成
方法一:同步代码块
//语法格式:Synchronized(同步对象){ //需要同步的代码 } //示例代码 @Override public void run() { synchronized (this){ for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + "运行了,i = " + i); } } }
方法二:同步方法
//语法格式:synchronized 方法返回类型 方法名称(参数列表){ } @Override public void run() { //调用 this指代当前 this.test(); } //同步方法 public synchronized void test(){ synchronized (this){ for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + "运行了,i = " + i); } } }
死锁
造成死锁的原因:多个线程共享同一资源需要进行同步,以保证资源操作的完整性,但过多的同步可能产生死锁
出现死锁需要满足的条件(必须同时满足,缺少一个就不能出现死锁):
1. 互斥条件:一个资源每次只能被一个线程使用。
2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3. 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
解决措施:
1) 打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
2) 打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
3) 进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低
4) 避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。
线程的生命周期概括
生命周期包括的五种状态:创建、就绪、运行、阻塞和终止
详细解析
-
创建状态
程序中使用构造方法创建线程对象后,新线程对象即处于创建状态。
线程此时已经具有相应的内存空间和其他资源,但不可运行
-
就绪状态
线程对象创建后调用 start()方法启动线程,即进入就绪状态。
就绪状态下的线程进入线程队列,等待 CPU 调用
-
运行状态
线程获得 CPU 服务后即处于运行状态,此时将自动调用线程对象的 run()方法。run()方法定义了该线程的具体操作和实现功能。
需要注意的是运行状态下的线程调用 yield()方法后,将从运行状态返回至就绪状
-
阻塞状态
运行状态的线程调用 sleep()、wait()等方法后将进入阻塞状态。
线程阻塞条件解除后,线程再次转入就绪状态。
5.终止(死亡状态)
当线程执行 run()方法完毕后处于终止状态(又称死亡状态)处于终止状态下的线程不具有继续运行的能力。
综合示例
public class Demo5 extends Thread { private static Demo5 demo1 = null; @Override public void run() { try { //运行状态 System.out.println("demo1" + demo1.getState()); Thread.sleep(1000); //阻塞状态 System.out.println("demo1" + demo1.getState()); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { //创建状态 demo1 = new Demo5(); System.out.println("demo1" + demo1.getState()); //就绪状态 demo1.start(); } }
本章总结
1、 一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须24
是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
2、 Java 中实现多线程主要有两种方式:继承 Thread 类和实现 Runnable 接口。
3、 线程的生命周期:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。
4、 线程的同步就是指多个线程在同一时间段内只能有一个线程执行指定代码,其他线程要等待此线程完成之后才能继续执行。
5、 线程进行同步主要有两种方式:
➢ 同步代码块:synchronized(要同步的对象){要同步的操作},即有 synchronized 关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
➢ 同步方法:public synchronized void method(){要同步的操作}即有 synchronized 关键字修饰的方法。
补充:观察继承了Thread类的多线程和实现Runnable接口的多线程的区别
首先写一个继承Thread类的类和实现了Runnable接口的类
//继承了Thread的类test1 public class Test1 extends Thread { private Integer count = 0; @Override public void run() { System.out.println(++count+""); } }
//实现Runnable接口的类 test2 public class Test2 implements Runnable { private Integer num = 0; public void run() { System.out.println(++num+""); } }
然后写一个测试主类
/** * 观察继承了Thread类的多线程和实现Runnable接口的多线程的区别 */ public class Demo7 { public static void main(String[] args) { System.out.println("继承了Thread类的多线程:\t"); for (int i = 0; i < 10; i++) { Test1 test1 = new Test1(); //启动继承了Thread类的多线程 test1.start(); } try { //睡眠三秒 让上面的执行完毕 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("------------------"); System.out.println("实现了Runnable的多线程:\t"); Test2 test2 = new Test2(); for (int i = 0; i < 10; i++) { new Thread(test2).start(); } } }
结论:在十次循环中
继承了Thread类的多线程执行结果每次都是1,相当于每次执行都会重值数据-->不会共享数据
而实现Runnable接口的多线程会在上次运行结果上面进行累加 --> 实现了数据共享