文章目录
一、理论学习部分
第一步:理解串行与并发的概念
- 举个栗子:
- 乡村道路一般为单车道,所有行驶在这条道路上的汽车都是串行的关系;
- 高速公路一般为多车道,车道上并排行驶的车辆是并发的关系。
第二步:认识进程和线程
- 进程:程序运行的基本单位。包括程序所需的所有资源和数据,不同进程之间的数据不共享;
- 线程:一个进程中不同任务的划分。同一个进程中的不同线程之间可共享进程中的同一个数据。
- 举个栗子:
- 不同工厂之间不能共享生产原料,而同一工厂中的不同生产车间之间可以共享生产原料。工厂就相当于进程,其中的生产车间就相当于线程。
第三步:了解线程的生命周期
- 线程的生命周期是指:一个线程被实例化完成,到该线程被销毁,中间经过的时间就是该线程的生命周期。
- 线程的状态:
- 新生态:指线程实例化完成,但还未执行任何操作的状态(new);
- 就绪态:指线程已经被开启,开始去争抢CPU的使用权的状态(start);
- 运行态:指线程抢到了CPU使用权,开始执行该线程中的逻辑的状态(run);
- 阻塞态:指线程由于执行某些操作(I/O、抢占临界资源失败等),放弃了自身CPU的使用权的状态(Interrupt);
- 死亡态:指线程中的逻辑已执行完毕或出现了未经处理的异常,等待被销毁的状态(dead)。
- Java中线程状态之间的切换:
- 新生态:new Thread()
- 新生态到就绪态:start()
- 就绪态到运行态:run()
- 运行态到就绪态:yield()
- 运行态到阻塞态:I/O、Thread.sleep()、join()、wait()(较为特殊)
- 阻塞态到就绪态:I/O完成、sleep结束、join的线程结束、获取到线程锁标记(对应上方wait()方式)
二、实践练习部分
第一步:了解线程创建的方法
线程创建主要有两种创建方式:
-
继承Thread类方式:
public class ThreadTest { public static void main(String[] args) { MyThread mt = new MyThread(); //实例化子线程 mt.start(); //子线程进入就绪态 for (int i = 0; i < 5; i++){ //执行主线程逻辑 System.out.println("主线程的第" + (i + 1) + "次执行"); try{ Thread.sleep(50); }catch (InterruptedException e){ System.out.println("Main thread interrupted"); } } } } class MyThread extends Thread{ //继承Thread类 @Override public void run(){ //需要重写run方法 for (int i = 0; i < 5; i++){ //执行子线程逻辑 System.out.println("子线程的第" + (i + 1) + "次执行"); try{ Thread.sleep(50); }catch (InterruptedException e){ System.out.println("Child thread interrupted"); } } } }
代码执行结果:
主线程的第1次执行 子线程的第1次执行 主线程的第2次执行 子线程的第2次执行 主线程的第3次执行 子线程的第3次执行 主线程的第4次执行 子线程的第4次执行 主线程的第5次执行 子线程的第5次执行
-
实现Runable接口方式:
public class ThreadTest { public static void main(String[] args) { MyThread mt = new MyThread(); //实例化子线程 for (int i = 0; i < 5; i++){ //执行主线程逻辑 System.out.println("主线程的第" + (i + 1) + "次执行"); try{ Thread.sleep(50); }catch (InterruptedException e){ System.out.println("Main thread interrupted"); } } } } class MyThread implements Runnable{ //实现Runable接口 Thread t; //需要使用一个Thread对象 public MyThread(){ t = new Thread(this); //在自定义线程类的构造方法中实例化这个Thread对象 t.start(); //并给予执行 } @Override public void run(){ //并重写run方法 for (int i = 0; i < 5; i++){ //执行子线程逻辑 System.out.println("子线程的第" + (i + 1) + "次执行"); try{ Thread.sleep(50); }catch (InterruptedException e){ System.out.println("Child thread interrupted"); } } } }
代码执行结果:
主线程的第1次执行 子线程的第1次执行 主线程的第2次执行 子线程的第2次执行 主线程的第3次执行 子线程的第3次执行 主线程的第4次执行 子线程的第4次执行 主线程的第5次执行 子线程的第5次执行
注意: 为了提高代码的可扩展性,一般情况下我们通常使用实现Runable接口的方式来创建子线程。因为Java的单根继承特性,若该线程类继承了Thread类,则无法继承其他自定义的类。
第二步:了解线程的常用方法
- 线程的命名
方法1:使用Thread类的构造方法直接命名。如:
Thread t = new Thread("Child_1")
;
方法2:使用Thread对象的setName方法。如:
方法3:对于自定义的线程类,可以借用父类的构造方法来命名。如:Thread t = new Thread(); t.setName("Child_1");
class MyThread extends Thread{ public MyThread(String name){ super(name); } }
- 线程的休眠
使用Thread.sleep()方法设定线程的休眠。注意: 因为Java的健壮性原则,JVM要求我们必须要加try-catch块来处理线程休眠过程中无法预估的异常。如:@Override public void run(){ for (int i = 0; i < 5; i++){ System.out.println("子线程的第" + (i + 1) + "次执行"); try{ Thread.sleep(50);//单位是毫秒 }catch (InterruptedException e){ System.out.println("Child thread interrupted"); } } }
- 线程的优先级设定
设置线程的优先级是为了修改这个线程能够抢到CPU使用权的概率,但并不是优先级高就一定能抢到CPU使用权,这是概率问题。Java中线程的优先级是介于1-10之间的整数(1和10可取),默认所有线程的优先级都为5。设定线程的优先级可以使用setPriority方法。如:Thread t = new Thread(); t.setPriority(9);//设置线程t的优先级为9
- 线程的礼让
线程礼让是指让当前运行中的线程释放自己的CPU使用权,由运行状态转到就绪状态。Java中可以通过使用Thread.yield()方法来实现线程礼让。如:@Override public static void run(){ for(int i = 0; i < 10; i++){ if(i == 3){ Thread.yield();//让出当前线程的CPU使用权 } } }
第三步:理解临界资源问题
- 售票员售票问题
代码执行结果:public class TicketCenter { public static int tickets = 8; public static void main(String[] args) { //实例化4个售票员 Conductor conductor1 = new Conductor("1号售票员"); Conductor conductor2 = new Conductor("2号售票员"); Conductor conductor3 = new Conductor("3号售票员"); Conductor conductor4 = new Conductor("4号售票员"); } } class Conductor implements Runnable{//售票员类 Thread t; public Conductor(String name){ t = new Thread(this,name); t.start(); } @Override public void run(){//售票逻辑 while (TicketCenter.tickets > 0){ System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余" + --TicketCenter.tickets + "张票"); } try { Thread.sleep(10);//模拟售票过程中消耗的时间 } catch (InterruptedException e) { e.printStackTrace(); } } }
显然不符合实际情况。因为在本例中tickets属于临界资源,四个Conductor线程同时访问了临界资源,所以会产生意想不到的结果。1号售票员卖出1张票,剩余6张票 1号售票员卖出1张票,剩余3张票 1号售票员卖出1张票,剩余2张票 1号售票员卖出1张票,剩余1张票 1号售票员卖出1张票,剩余0张票 3号售票员卖出1张票,剩余4张票 4号售票员卖出1张票,剩余7张票 2号售票员卖出1张票,剩余5张票
- 临界资源问题的解决方案
解决的方法其实很简单,只需要在某个线程访问临界资源时由并发执行转变成同步执行,不让其他线程在同一时刻访问即可。
第四步:掌握线程同步的方式
- 使用同步代码段实现线程同步
还是售票员售票问题:使用synchronized同步代码段的解决方案
代码执行结果:public class TicketCenter { public static int tickets = 10; public static void main(String[] args) { //实例化4个售票员 Conductor conductor1 = new Conductor("1号售票员"); Conductor conductor2 = new Conductor("2号售票员"); Conductor conductor3 = new Conductor("3号售票员"); Conductor conductor4 = new Conductor("4号售票员"); } } class Conductor implements Runnable{ Thread t; public Conductor(String name){ t = new Thread(this,name); t.start(); } @Override public void run(){ while (TicketCenter.tickets > 0){ synchronized (TicketCenter.class){//定义锁标记,抢到锁标记的线程进入代码块,未抢到的线程则进入锁池(线程阻塞),等待解锁后再争抢锁标记 //需要注意的是。上面小括号里面的锁标记是对象锁或者类锁都行,只要保证所有线程争抢的是同一把锁即可 if (TicketCenter.tickets <= 0){//判断tickets的情况,也就是双重检查的思想 return; } System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余" + --TicketCenter.tickets + "张票"); } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1号售票员卖出1张票,剩余9张票 2号售票员卖出1张票,剩余8张票 3号售票员卖出1张票,剩余7张票 4号售票员卖出1张票,剩余6张票 2号售票员卖出1张票,剩余5张票 1号售票员卖出1张票,剩余4张票 4号售票员卖出1张票,剩余3张票 3号售票员卖出1张票,剩余2张票 2号售票员卖出1张票,剩余1张票 1号售票员卖出1张票,剩余0张票
- 使用同步方法实现线程同步
还是售票员售票问题:使用synchronized同步方法的解决方案
代码执行结果:public class TicketCenter { public static int tickets = 10; public static void main(String[] args) { //实例化4个售票员 Conductor conductor1 = new Conductor("1号售票员"); Conductor conductor2 = new Conductor("2号售票员"); Conductor conductor3 = new Conductor("3号售票员"); Conductor conductor4 = new Conductor("4号售票员"); } } class Conductor implements Runnable{ Thread t; public Conductor(String name){ t = new Thread(this,name); t.start(); } public static synchronized void soldTicket(){//设置同步方法,与同步代码段类似 //若此方法为静态,则设置的锁为类锁:Conductor.class //若此方法不为静态,则设置的锁为对象锁:this,当然此处并不适用,因为此时this指向不同的Conductor对象 if (TicketCenter.tickets <= 0){ return; } System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余" + --TicketCenter.tickets + "张票"); } @Override public void run(){ while (TicketCenter.tickets > 0){ soldTicket();//在run方法中调用同步方法即可 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2号售票员卖出1张票,剩余9张票 3号售票员卖出1张票,剩余8张票 4号售票员卖出1张票,剩余7张票 1号售票员卖出1张票,剩余6张票 4号售票员卖出1张票,剩余5张票 2号售票员卖出1张票,剩余4张票 3号售票员卖出1张票,剩余3张票 1号售票员卖出1张票,剩余2张票 4号售票员卖出1张票,剩余1张票 1号售票员卖出1张票,剩余0张票
- 直接使用ReentrantLock对象实现同步
还是售票员售票问题:直接使用ReentrantLock锁对象的解决方案
代码执行结果:import java.util.concurrent.locks.ReentrantLock; public class TicketCenter { public static int tickets = 10; public static void main(String[] args) { //实例化4个售票员 Conductor conductor1 = new Conductor("1号售票员"); Conductor conductor2 = new Conductor("2号售票员"); Conductor conductor3 = new Conductor("3号售票员"); Conductor conductor4 = new Conductor("4号售票员"); } } class Conductor implements Runnable{ Thread t; ReentrantLock lock = new ReentrantLock();//实例化一个ReentrantLock对象 public Conductor(String name){ t = new Thread(this,name); t.start(); } @Override public void run(){ while (TicketCenter.tickets > 0){ lock.lock();//加锁 if (TicketCenter.tickets <= 0){ return; } System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余" + --TicketCenter.tickets + "张票"); lock.unlock();//解锁 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1号售票员卖出1张票,剩余9张票 3号售票员卖出1张票,剩余8张票 2号售票员卖出1张票,剩余7张票 4号售票员卖出1张票,剩余6张票 1号售票员卖出1张票,剩余5张票 3号售票员卖出1张票,剩余4张票 4号售票员卖出1张票,剩余3张票 2号售票员卖出1张票,剩余2张票 1号售票员卖出1张票,剩余1张票 3号售票员卖出1张票,剩余0张票
第五步:了解线程死锁
死锁:多个线程彼此占有对方所需要的锁对象,而不释放自己的锁。
- 死锁案例:
代码执行结果:public class DeadLock { public static void main(String[] args) { Runnable runnable1 = () -> { synchronized ("A"){ System.out.println("A线程持有了A锁,等待B锁"); synchronized ("B"){ System.out.println("A线程同时持有了A锁和B锁"); } } }; Runnable runnable2 = () -> { synchronized ("B"){ System.out.println("B线程持有了B锁,等待A锁"); synchronized ("A"){ System.out.println("B线程同时持有了A锁和B锁"); } } }; Thread t1 = new Thread(runnable1); Thread t2 = new Thread(runnable2); t1.start(); t2.start(); } }
此时两个线程都无法访问对方手里的那个锁标记,程序进入死锁状态。B线程持有了B锁,等待A锁 A线程持有了A锁,等待B锁
- 解除死锁:
介绍三种用于解决死锁的方法:
wait():等待,是Object类中的一个方法,作用是使当前的线程释放自己的锁标记,进入等待队列中,并让出CPU使用权;
notify():通知,也是Object类中的一个方法,作用是唤醒等待队列中的一个线程,使其进入锁池中;
notifyAll():通知,也是Object类中的一个方法,作用是唤醒等待队列中所有被某个锁约束的线程,使其全部进入锁池
使用这些方法我们就可以解决上面的死锁问题了:
代码执行结果:public class DeadLock { public static void main(String[] args) { Runnable runnable1 = () -> { synchronized ("A"){ System.out.println("A线程持有了A锁,等待B锁"); try { "A".wait();//将先抢走A锁的进程执行wait操作 } catch (InterruptedException e) { e.printStackTrace(); } synchronized ("B"){ System.out.println("A线程同时持有了A锁和B锁"); } } }; Runnable runnable2 = () -> { synchronized ("B"){ System.out.println("B线程持有了B锁,等待A锁"); synchronized ("A"){ System.out.println("B线程同时持有了A锁和B锁"); "A".notifyAll();//等现持有B锁的线程先占用A锁完成任务之后将等待队列中所有被A锁约束的线程唤醒 } } }; Thread t1 = new Thread(runnable1); Thread t2 = new Thread(runnable2); t1.start(); t2.start(); } }
A线程持有了A锁,等待B锁 B线程持有了B锁,等待A锁 B线程同时持有了A锁和B锁 A线程同时持有了A锁和B锁