多线程
-
什么是线程?
- 线程是程序执行的一条路径,一个进程中可以包含多个线程。
- 多线程开发执行可以提高程序的效率,可以同时完成多项工作。
-
多线程的应用场景
- 迅雷开启多线程一起下载
- QQ同时和多个人一起视频
- 服务器同时处理多个客户端请求
并行和并发
-
什么是并行?
并行就是多个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
-
什么是并发?
并发是指多个任务都请求运行,而处理器只能接受一个任务,就把这多个任务安排轮流进行,由于时间间隔非常短,所以看起来感觉是多个任务同时在运行。
举例说明
- 快要开学了,小学生们要补作业。由于时间紧迫,所以小学生们就左手写语文作业、右手同时写数学作业,这叫并行。
- 小学生们都比较爱玩游戏,比如消消乐、刺激战场等。但是只有一台游戏设备,于是玩一下消消乐再赶紧切换到刺激战场打一枪,然后再立刻切换到消消乐消一个冰块,再迅速切换到刺激战场扶队友…循环往复。感觉像是在同时玩两款游戏一样,这叫并发。
Java程序运行原理和JVM的启动是多线程的吗?
-
Java程序运行原理
- Java命令会启动Java虚拟机,启动JVM等于启动了一个应用程序,也就是启动了一个进程。
- 该进程会自动启动一个"主线程",然后主线程去调用某个类的main方法。
-
JVM的启动是多线程的吗?
- JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
多线程实现的三种方式
-
方式一:继承Thread类创建线程(推荐使用)
a. 定义类继承Thread b. 重写run方法 c. 把新线程要做的事情写在run方法中 d. 创建线程对象 e. 开启新线程,内部会自动执行run方法
代码示例:开启线程的第一种方式
public class Demo2_Thread { public static void main(String[] args){ MyThread myThread = new MyThread(); //4,创建Thread类的子类对象 myThread.start(); //5,执行start方法开启线程 for (int i = 0; i < 1000; i++){ System.out.println("bbbbbbbbbbbbbbbbbbb"); } } } class MyThread extends Thread{ //1,继承Thread类 @Override public void run() { //2,重写run方法 for (int i = 0; i < 1000; i++){ //3,将要执行的代码写在run中 System.out.println("啊啊啊啊啊啊啊啊啊啊啊啊"); } } }
注意:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
-
方式二:实现Runnable接口类创建线程
a. 定义类实现Runnable接口 b. 实现run()方法 c. 把新线程要做的事情写在run方法中 d. 创建自定义的Runnable子类对象 e. 创建Thread对象,传入Runnable f. 调用start()开启新线程,内部会自动调用Runnable的run()方法
代码示例:开启线程的第二种方式
public class Demo3_Thread { public static void main(String[] args){ MyRunnable mr = new MyRunnable(); //4,创建Runnable的子类对象 Thread t = new Thread(mr); //5,将Runnable的子类对象作为参数传入到Thread的构造方法 t.start(); //6,开启线程 for (int i = 0; i < 2000; i++){ System.out.println("yoyoyoyoyoyoyoyoyyooyoy"); } } } class MyRunnable implements Runnable{ //1,定义一个类实现Runnable接口 @Override public void run() { //2,重写run()方法 for (int i = 0; i < 1000; i++){ //3,将要执行的代码写在run()方法中 System.out.println("啊啊啊啊啊啊"); } } }
-
方式三:使用Callable和Future创建线程
a. 创建Callable接口的实现类 b. 实现call()方法 c. 把新线程要做的事情写在call()方法中 d. 创建Callable实现类的实例 e. 使用FutureTask类来包装Callable对象 f. 使用FutureTask对象作为Thread对象的target创建并启动新线程 g. 调用FutureTask对象的get()方法来获得子程序执行结束后的返回值
代码示例:通过实现Callable接口来实现线程类,并启动该线程。
public class ThirdThread{ public static void main(String[] args){ //创建Callable对象 ThirdThread rt = new ThirdThread(); //先使用Lambda表达式创建Callable<Integer>对象 //使用FutureTask来包装Callable对象 //使用匿名内部类 FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{ for(int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName() + " 的循环变量i的值: " + i); } //call()方法可以有返回值 return i; }); for(int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName() + " 的循环变量i的值: " + i); if(i == 20){ //实际上还是以Callable对象来创建并启动线程的 new Thread(task,"有返回值的线程").start(); } } try{ //获取线程返回值 System.out.println("子线程的返回值:" + task.get()); } catch (Exception e){ e.printStackTrace(); } } }
注意:Callable接口有泛型限制,Callable接口里的泛型参数类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可以使用Lambda表达式创建Callable对象。
-
创建线程的三种方式对比
通过集成Thread类或者实现Runnable、Callable接口都可以实现多线程,不过实现Runnabe接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此,可以把实现Runnable接口和实现Callable接口看做一种方式。
-
采用实现Runnable、Callable接口方式的优缺点:
- 线程类不仅实现了Runable接口和Callable接口,还可以继承其他类。
- 多个线程可以共享同一个target对象。因此非常适合多个相同线程共同处理同一份资源的情况,从而将CPU、代码、数据分开,较好地体现了面向对象的思想。
- 劣势是,程序稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
-
采用继承Thread类的方式的优缺点:
- 程序简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
- 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
-
多线程的常用方法
-
获取名字
通过getName()方法获取线程对象的名字。
new Thread(){ public void run(){ System.out.println(this.getName() + ".....aa"); } }.start();
-
设置名字
通过构造函数可以传入String类型的线程对象的名字。
new Thread("凤姐"){ //向构造方法传入参数 public void run(){ System.out.println(this.getName() + ".....aa"); } }.start();
通过setName(String s)方法可以设置线程对象的名字。
new Thread(){ public void run(){ this.setName("张三"); //调用setName方法设置名称 System.out.println(this.getName() + "aa"); } }.start();
-
获取当前线程的对象
Thread.currentThread(),主线程也可以获取。
new Thread(new Runnable(){ public void run(){ System.out.println( Thread.currentThread().getName() + "......cc"); } }).start(); Thread.currentThread().setName("我是主线程"); //获取主线程的引用并改名字 System.out.println(Thread.currentThread().getName()); //获取主线程的引用并获取名字
-
休眠线程
Thread.sleep(毫秒,纳秒),控制当前线程休眠若干毫秒。
for (int i = 20; i >= 0; i--){ Thread.sleep(1000); //休眠一秒 System.out.println("倒计时" + i + "秒"); }
-
守护线程/后台线程
setDaemon(),设置一个线程为守护/后台线程,该线程不会单独执行。当其他非守护线程执行结束后,该线程自动退出。
//线程1 Thread t1 = new Thread(){ public void run(){ for (int i = 0; i < 2; i++){ System.out.println(this.getName() + ".....aaaaaaaaaa"); } } }; //线程2 Thread t2 = new Thread(){ public void run(){ for (int i = 0; i < 50; i++){ System.out.println(this.getName() + ".....bb"); } } }; //设置线程2为守护线程 t2.setDaemon(true); //当传入true就是意味着设置为守护线程 t1.start(); t2.start();
-
加入线程
join,当前线程暂停,等待指定的线程执行结束后,当前线程再继续;join(int),等待指定的毫秒之后再继续当前线程。
final Thread t1 = new Thread(){ //用final修饰是为了可以让匿名内部类调用 public void run(){ for (int i = 0; i < 20; i++){ System.out.println("aa"); } } }; Thread t2 = new Thread(){ public void run(){ for (int i = 0; i < 10; i++){ if (i == 2){ try { //t1.join(); //方法的匿名内部类中只能调用方法中使用final修饰的局部变量 t1.join(1); //t1插队1毫秒 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("bb"); } } }; t1.start(); t2.start();
-
礼让线程(知道即可)
yield,让出cpu。
-
设置线程的优先级(知道即可)
setPriority(),设置线程优先级,优先级越高,执行的概率越大。
多线程的同步
多线程编程是一件让人欲罢不能的事情,但是总是容易发生一些意外。这是由于系统的线程调度具有一定的随机性,可能这个线程还没执行完又跑去执行另一个线程,一旦这两个线程共享一个资源,那么意外就发生咯。
-
什么情况下需要同步?
- 如果多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中CPU不要切换到其他的线程工作,这时就需要同步。
- 如果两段代码是同步的,那么同一时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码。
-
什么是同步代码块?
- 使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块。
- 多个同步代码块如果使用相同的锁对象,那么他们就是同步的。
同步代码块代码示例:
public class Demo1_Synchronized {
public static void main(String[] args){
final Printer p1 = new Printer();
new Thread(){
public void run(){
while (true){
p1.print1();
}
}
}.start();
new Thread(){
public void run(){
while (true){
p1.print2();
}
}
}.start();
}
}
class Printer{
Object o = new Object();
public void print1(){ //加了锁对象之后,就不会出现某个方法未执行完,就执行其他线程的情况
synchronized (o){ //锁对象是任意的
System.out.print("老");
System.out.print("师");
System.out.print("\n\r");
}
}
public void print2(){
synchronized (o){ //锁对象是任意的
System.out.print("教");
System.out.print("学");
System.out.print("生");
System.out.print("\n\r");
}
}
}
注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。
多线程的死锁
当两个线程相互等待对方释放同步监视器的时候就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。
死锁代码示例:
private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args){
new Thread(){
public void run(){
while (true){
synchronized (s1){
System.out.println(getName() + "...获取" + s1 + "等待" + s2);
synchronized (s2){
System.out.println(getName() + "...拿到" + s2 + "开整");
}
}
}
}
}.start();
new Thread(){
public void run(){
while (true){
synchronized (s2){
System.out.println(getName() + "...获取" + s2 + "等待" + s1);
synchronized (s1){
System.out.println(getName() + "...拿到" + s1 + "开整");
}
}
}
}
}.start();
}
注意:由于Thread类的suspend()方法也容易导致死锁,所以不推荐使用该方法来暂停线程的执行。