Java多线程详解笔记

文章目录

    • 一、理论学习部分
      • 第一步:理解串行与并发的概念
      • 第二步:认识进程和线程
      • 第三步:了解线程的生命周期
    • 二、实践练习部分
      • 第一步:了解线程创建的方法
      • 第二步:了解线程的常用方法
      • 第三步:理解临界资源问题
      • 第四步:掌握线程同步的方式
      • 第五步:了解线程死锁

一、理论学习部分

第一步:理解串行与并发的概念

  • 举个栗子:
    • 乡村道路一般为单车道,所有行驶在这条道路上的汽车都是串行的关系;
    • 高速公路一般为多车道,车道上并排行驶的车辆是并发的关系。

第二步:认识进程和线程

  • 进程:程序运行的基本单位。包括程序所需的所有资源和数据,不同进程之间的数据不共享;
  • 线程:一个进程中不同任务的划分。同一个进程中的不同线程之间可共享进程中的同一个数据。
  • 举个栗子:
    • 不同工厂之间不能共享生产原料,而同一工厂中的不同生产车间之间可以共享生产原料。工厂就相当于进程,其中的生产车间就相当于线程。

第三步:了解线程的生命周期

  • 线程的生命周期是指:一个线程被实例化完成,到该线程被销毁,中间经过的时间就是该线程的生命周期。
  • 线程的状态:
    • 新生态:指线程实例化完成,但还未执行任何操作的状态(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()方式)

二、实践练习部分

第一步:了解线程创建的方法

线程创建主要有两种创建方式:

  1. 继承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次执行
    
  2. 实现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. 线程的命名
    方法1:使用Thread类的构造方法直接命名。如:
    Thread t = new Thread("Child_1");
    方法2:使用Thread对象的setName方法。如:
    Thread t = new Thread();
    t.setName("Child_1");
    
    方法3:对于自定义的线程类,可以借用父类的构造方法来命名。如:
    class MyThread extends Thread{
    	public MyThread(String name){
    		super(name);
    	}
    }
    
  2. 线程的休眠
    使用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");
            }
        }
    }
    
  3. 线程的优先级设定
    设置线程的优先级是为了修改这个线程能够抢到CPU使用权的概率,但并不是优先级高就一定能抢到CPU使用权,这是概率问题。Java中线程的优先级是介于1-10之间的整数(1和10可取),默认所有线程的优先级都为5。设定线程的优先级可以使用setPriority方法。如:
    Thread t = new Thread();
    t.setPriority(9);//设置线程t的优先级为9
    
  4. 线程的礼让
    线程礼让是指让当前运行中的线程释放自己的CPU使用权,由运行状态转到就绪状态。Java中可以通过使用Thread.yield()方法来实现线程礼让。如:
    @Override
    public static void run(){
    	for(int i = 0; i < 10; i++){
    		if(i == 3){
    			Thread.yield();//让出当前线程的CPU使用权
    		}
    	}
    }
    

第三步:理解临界资源问题

  1. 售票员售票问题
    	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();
            }
        }
    }
    
    代码执行结果:
    1号售票员卖出1张票,剩余6张票
    1号售票员卖出1张票,剩余3张票
    1号售票员卖出1张票,剩余2张票
    1号售票员卖出1张票,剩余1张票
    1号售票员卖出1张票,剩余0张票
    3号售票员卖出1张票,剩余4张票
    4号售票员卖出1张票,剩余7张票
    2号售票员卖出1张票,剩余5张票
    
    显然不符合实际情况。因为在本例中tickets属于临界资源,四个Conductor线程同时访问了临界资源,所以会产生意想不到的结果。
  2. 临界资源问题的解决方案
    解决的方法其实很简单,只需要在某个线程访问临界资源时由并发执行转变成同步执行,不让其他线程在同一时刻访问即可。

第四步:掌握线程同步的方式

  1. 使用同步代码段实现线程同步
    还是售票员售票问题:使用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张票
    
  2. 使用同步方法实现线程同步
    还是售票员售票问题:使用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张票
    
  3. 直接使用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张票
    

第五步:了解线程死锁

死锁:多个线程彼此占有对方所需要的锁对象,而不释放自己的锁。

  1. 死锁案例:
    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锁
    
    此时两个线程都无法访问对方手里的那个锁标记,程序进入死锁状态。
  2. 解除死锁:
    介绍三种用于解决死锁的方法:
    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锁
    

你可能感兴趣的:(Java多线程详解笔记)