内容:
1、什么是多线程
2、两种创建线程方式
3、线程的匿名内部类使用
4、线程安全
5、线程状态图
1、什么是多线程
学习多线程之前,我们先要了解几个关于多线程有关的概念。
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,
进程是处于运行过程中的程序,并且每个进程都具有一定独立功能
线程:线程是进程中的一个执行单元,来完成进程中的某个功能
进程实例:
线程实例:
一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序可以称之为多线程程序
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
什么是多线程呢?即就是一个程序中有多个线程在同时执行。
通过下图来区别单线程程序与多线程程序的不同:
单线程程序:
- 若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。
- 去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
多线程程序:
- 若有多个任务可以同时执行。
- 去网吧上网,网吧能够让多个人同时上网。
主线程(单线程程序):
回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的
class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程
遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被马上执行的。如下代码演示:
1 class Person{ 2 String name; 3 Person(String name){ 4 this.name = name; 5 } 6 void music() { 7 for (int i=1;i<=20;i++ ) { 8 System.out.println(name+"在听第"+i+"首歌"); 9 } 10 } 11 void eat() { 12 for (int i=1;i<=20;i++ ) { 13 System.out.println(name+"在吃第"+i+"口饭"); 14 } 15 } 16 17 } 18 19 class MainThreadDemo{ 20 public static void main(String[] args) { 21 Person p = new Person("xxx"); 22 p.music(); 23 p.eat(); 24 System.out.println("听完歌吃完饭了,该睡觉了zzZZ~~~"); 25 } 26 }
若在上述代码中music方法中的循环执行次数很多,这时在p.music();下面的代码是不会马上执行的,并且在dos窗口
会看到不停的输出”xxx在吃第几口饭”,这样的语句。为什么会这样呢?
原因:
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为
主线程(main线程)。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上
执行下面的程序,需要等待循环结束后能够执行。
那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?
答:当然能够实现同时执行,只要通过Java中的多线程技术来解决该问题。
2、两种创建线程方式
(1)创建线程的两种方式
- 将类声明为 Thread 的子类,子类重写Thread类的run方法。创建对象,开启线程。run方法相当于其他线程的main方法
- 声明实现Runnable接口的类,实现 run 方法,然后创建实现类对象并传入到某个线程的构造方法中,开启线程
(2)Thread类
Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程,Thread类就是我们说的线程类
构造方法:
- public Thread(); // 创建一个默认名字的线程对象
- public Thread(String name); // 创建一个指定名字的线程对象
常用方法:
- public void start(); // 使该线程开始执行,Java虚拟机调用该线程的run方法
- public void run(); // 该线程要执行的操作,比如循环100次打印变量的值
- public static void sleep(long millis); // 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- public static Thread currentThread(); // 返回对当前正在执行的线程对象的引用
- public String getName(); // 返回该线程的名称
实例:
1 // 1、继承 2 class Mythread extends Thread { 3 // 2、重写run方法 4 public void run() { 5 // 3、具体任务代码 6 for (int i = 1; i < 20; i++) { 7 System.out.println("线程任务中的" + i); 8 } 9 } 10 } 11 12 public class ThreadDemo { 13 14 public static void main(String[] args) { 15 // 4、创建子类线程对象 16 Mythread mt = new Mythread(); 17 // 5、开启线程 JVM自动告诉CPU去执行线程任务代码 18 mt.start(); 19 for (int i = 100; i < 120; i++) { 20 System.out.println("main线程的" + i); 21 } 22 } 23 24 }
(3)Runnable接口
Runnable接口用来指定每个线程要执行的任务。包含了一个 run方法,需要由接口实现类重写该方法
然后创建Runnable的实现类对象,传入到某个线程的构造方法中,开启线程
此时某个线程的构造方法如下:
创建线程的步骤:
- 定义类实现Runnable接口
- 覆盖接口中的run方法
- 创建Thread类的对象
- 将Runnable接口的子类对象作为参数传递给Thread类的构造函数
- 调用Thread类的start方法开启线程
实例如下:
// 1、实现Runnable接口 class MyRunnable implements Runnable { // 2、重写run方法 public void run() { for (int i = 0; i < 20; i++) { System.out.println("创建线程的任务" + i); } } } public class RunnableDemo { public static void main(String[] args) { // 3、创建实现类对象 MyRunnable mr = new MyRunnable(); // 4、创建Thread对象,并把刚刚的实现类对象作为参数传递 Thread td = new Thread(mr); // 5、开启线程 td.start(); for(int i=100; i<120; i++){ System.out.println("main线程的任务" + i); } } }
(4)线程的执行原理
线程对象调用run方法和调用start方法区别?
线程对象调用run方法不开启线程,仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行
Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢而要继承Thread类呢?
如下:
Thread t1 = new Thread();
t1.start();
这样做没有错,开启一个线程,由线程自己去调用run方法,那么这个run方法就在新的线程中运行起来了,但是我们直接创建
Thread对象,调用start方法,该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中
并没有定义我们需要让线程执行的代码。java设计师认为他们不知道程序员会用run方法执行什么代码,所有没写run方法
(5)两种创建线程方式的比较
程序设计遵循的原则:开闭原则,对修改关闭,对扩展开放,减少线程本身和任务之间的耦合性
从耦合性分析:
第一种方式继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务
第二种方式实现Runnable接口避免了单继承的局限性,同时还对线程对象和线程任务进行解耦。
实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。
从代码的拓展性分析:
第一种方式是由于继承Thread类,那么子线程不能继承别的类了
第二种方式是由于实现Runnable接口,同时可以继承别的类
第二种方式的拓展性更好
综上所述,开发中强烈使用第二种方法来创建线程
3、线程的匿名内部类使用
匿名内部类:快速创建一个类的子类对象或一个接口的实现类对象
格式:
1 new 父类(){ 2 重写方法 3 }; 4 5 new 接口{ 6 实现方法 7 };
实例:
1 public class nimiDemo { 2 3 public static void demo1() { 4 // 方式1:创建线程对象时,直接重写Thread类中的run方法 5 new Thread() { 6 public void run() { 7 for (int x = 0; x < 40; x++) { 8 System.out.println(Thread.currentThread().getName() 9 + "...X...." + x); 10 } 11 } 12 }.start(); 13 } 14 15 public static void demo2() { 16 // 方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法 17 Runnable r = new Runnable() { 18 public void run() { 19 for (int x = 0; x < 40; x++) { 20 System.out.println(Thread.currentThread().getName() 21 + "...Y...." + x); 22 } 23 } 24 }; 25 new Thread(r).start(); 26 27 } 28 29 public static void main(String[] args) { 30 31 } 32 33 }
4、线程安全
(1)什么是线程安全
当有多个线程在同时运行,这些线程同时运行一段代码(即同一个任务,同一个run方法),操作同一个共享数据时,这时候可能就会出现
线程的安全问题,即线程不安全的.
注意:如果是单线程,或者多线程操作的还是不同数据,那么一般是没有问题的
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,
这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全
(2)Synchronized
java中提供了线程同步机制,它能够解决上述的线程安全问题。
线程同步的方式有两种:
- 方式1:同步代码块
- 方式2:同步方法
1 // 同步代码块: 在代码块声明上 加上synchronized 2 synchronized (锁对象) { 3 4 可能会产生线程安全问题的代码 5 6 } 7 // 注:同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。 8 9 10 // 同步方法:在方法声明上加上synchronized 11 public synchronized void method(){ 12 13 // 可能会产生线程安全问题的代码 14 15 } 16 // 注:同步方法中的锁对象是 this
(3)Lock接口
查阅API,查阅Lock接口描述,Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作
Lock接口的实现类:ReentrantLock类
Lock接口中的常用方法:
- public void lock(); // 获取锁
- public void unlock(); // 释放锁
1 class X { 2 private final ReentrantLock lock = new ReentrantLock(); 3 public void run() { 4 lock.lock(); // block until condition holds 5 try { 6 // ... method body 7 } finally { 8 lock.unlock() 9 } 10 } 11 }
5、线程状态图
如果我们多次调用线程对象的start方法,那么久会出现一个异常,查阅API关于IllegalThreadStateException这个异常说明信息
发现,这个异常的描述信息为:指示线程没有处于请求操作所要求的适当状态时抛出的异常。这里面说适当的状态,
这是什么意思呢?难道是说线程还有状态吗?
每个线程是有自己状态的,就好比人的一生从出生到死亡一样,线程也有,具体状态可以查看Thread的一个内部枚举Thread.State.