为了面试而战--多线程(上)

什么是多线程

多线程是Java面试中常考的,我看到很多人简历上包括公司职位要求都单独写出会多线程,所以就来学习学习,之前学过一点,也记不住多少了,借此机会好好学学。

多线程拆看来看,多和线程。什么是线程呢?线程是操作系统能够进行运算调用的最小单位。被包含在进程中,是进程中的实际运行单位。简单来说,线程就是应用软件互相独立并且可以同时运行的功能,如果这种功能很多,就成为多线程。

那多线程有什么用呢?之前我们学习的代码,都是从上往下依次执行,执行一句话的时候Cpu无法执行下面的话,Cpu只能等着,这样执行效率低,这种程序被称为单线程程序。多线程程序的特点就是能同时执行多个事情,可以在多个程序之间进行切换,把等待的时间利用起来,提高了执行效率。

最后总结一下,想三个问题:

  • 什么是多线程:不用背概念,就知道多线程可以让程序同时做多件事情
  • 多线程的作用:提高执行效率
  • 多线程的应用场景:比如你想让多个事情同时进行就会用到多线程

再来学习两个概念,并发和并行。

并发是指在同一时刻,有多个指令在单个CPU上交替执行,也就是CPU在多个线程之间交替执行;并行是说在同一时刻,有多个指令在多个CPU上同时执行,比如两个CPU两个线程同时执行。

多线程的实现

学会了什么是线程,现在就来学学怎么写线程,多线程一共有三种实现方式:

  1. 继承Thread类的方式进行实现
  2. 实现Runnable接口的方式进行实现
  3. 利用Callable接口和Future接口方式实现

/*
	多线程的第一种启动方式:
		1.自己定义一个类继承Thread
		2.重写run方法
		3.创建子类的对象,并启动线程
*/
public class MyThread extends Thread{

    @Override
    public void run() {
        //书写线程要执行代码
        for (int i = 0; i < 100; i++) {
		  	//先获取
            System.out.println(getName() + "HelloWorld");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        
	  	//一会儿执行线程1,一会儿执行线程2
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
		
	    //起名字,为了区分
        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();//启动线程,不可以直接调用run()方法,那就只是调用方法了
        t2.start();

    }
}

/*
	多线程的第一种启动方式:
		1.自己定义一个类继承Runnable接口
		2.重写run方法
		3.创建自己类的对象
		4.创建Thread类的对象,并开启线程
*/
public class MyRun implements Runnable{

    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread():获取到当前线程的对象,那个线程执行了这个方法,就把这个线程对象返回
            /*Thread t = Thread.currentThread();
            System.out.println(t.getName() + "HelloWorld!");*/
		  	//不能直接调用getName()了,因为getName()是Thread类中的方法,之前子类调用父类方法完全没问题,现在不是子类了,就不能直接用了。
            System.out.println(Thread.currentThread().getName() + "HelloWorld!");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        
        //创建MyRun的对象
        //表示多线程要执行的任务,理解为任务对象
        MyRun mr = new MyRun();

        //创建线程对象,把任务传递给线程,并且线程1,2的任务是一样的
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.start();
        t2.start();
    }
}
/*
	第三种是对前两种的补充,第一种的run()是没有返回值的,所以这个时候就不能获取多线程运行的结果,第二种也是如此,没有返回值。那我就想要线程运行后的结果,该怎么办呢?
	第三种实现的特点:可以获取多线程运行的结果
	
	1.创建一个类MyCallable实现Callable接口
	2.重写call(是有返回值的,表示多线程运行的结果)
	3.创建MyCallable的对象(表示多线程要执行的任务)
	4.创建FutureTask对象(作用管理多线程运行结果)
	5.创建Thread类对象,并启动,表示线程
*/
//Callable接口是有泛型的,用于指定返回值的类型
public class MyCallable implements Callable<Integer> {

    @Override //Integer表示多线程运行的结果
    public Integer call() throws Exception {
        //求1~100之间的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        //创建FutureTask的对象(作用管理多线程运行的结果),要把mc放进去
        FutureTask<Integer> ft = new FutureTask<>(mc);
        //创建线程的对象,把多线程返回值结果方法Thread中
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

        //获取多线程运行的结果,FutureTask就是用来管理结果的,肯定从这里拿
        Integer result = ft.get();
        System.out.println(result);
    }
}

最后总结一下这三种多线程的创建方式,前两种无法获取到多线程运行结果,第三种是可以的。前两种也是有区别的,继承Thread的这种方式代码简单,可以直接用Thread中的方法(getName()),缺点就是可扩展性差,无法再继承其他类。

多线程常用方法

下面我们来看看多线程中常用的成员方法。

/*
	String getName()	返回此线程的名称
	void setName(String name)	设置此线程的名字(构造方法也可以设置)  
	细节:如果我们没给线程名字,默认也是有的,为Thread-X
	
	static Thread currentThread()	获取当前线程的独享
	细节:当JVM虚拟机启动后,会自动的启动多条线程,其中有一条就叫main线程,他的作用就是调用main方法,并执行里面代码。在以前自己写的所有代码都运行在main线程中
	
	static void sleep(long time)	让线程休眠指定的时间,单位为毫秒
	细节:
		1.哪条线程执行到这个方法,那么哪条线程就会在这里停留一段时间
		2.方法的参数:就表示睡眠的时间,单位是ms
		3.当时间到了之后,线程会自动醒来,继续执行下面的其他代码
*/

public class MyThread extends Thread{

    public MyThread() {
    }
	//Thread类可以通过构造方法给线程名称,类的继承中,构造方法不能继承,子类也想像这样通过构造方法起名字就得自己写构造然后用super关键字继承父类构造方法
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "@" + i);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {

        //1.创建线程的对象
        MyThread t1 = new MyThread("飞机");
        MyThread t2 = new MyThread("坦克");

        //2.开启线程
        t1.start();
        t2.start();

        //哪条线程执行到这个方法,此时获取的就是哪条线程的对象
       /* Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name);//main*/

        /*System.out.println("11111111111");
        Thread.sleep(5000);
        System.out.println("22222222222");*/

    }
}

以上是比较简单的成员方法,下面学点复杂的,和线程优先级相关的方法。那什么是线程优先级呢,说道优先级,咱们就要说一下线程的调度。在计算机中,线程的调度有两种,第一种叫抢占式调度。多个线程在抢夺CPU执行权,CPU在什么时候执行哪个线程是不确定的,执行多长时间也是不确定的,主打一手随机性。第二种就是非抢占式调度,所有线程轮流执行,你一次我一次,执行时间也差不多。Java中采用的是抢占式调度,强调随机性,而优先级就跟随机有关系,优先级越大,这条线程抢到优先级的概率就是最大的,优先级最小是1,最大是10,默认是5。

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args){
       /*
            setPriority(int newPriority)        设置线程的优先级
            final int getPriority()             获取线程的优先级
       */

        //创建线程要执行的参数对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t1 = new Thread(mr,"飞机");
        Thread t2 = new Thread(mr,"坦克");
		
	  	//System.out.println(t1.getPriority);默认为5
        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();
    }
}

优先级只是概率,优先级高抢到CPU的概率大,并不是一定能抢到。

接着学习下一个方法,守护线程。

public class MyThread1 extends Thread{

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
public class MyThread2 extends Thread{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
            final void setDaemon(boolean on)    设置为守护线程
            细节:
                当其他的非守护线程执行完毕之后,守护线程会陆续结束
            通俗易懂:
                当女神线程结束了,那么备胎也没有存在的必要了
       */

        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        //把第二个线程设置为守护线程(备胎线程)
        t2.setDaemon(true);

        t1.start();
        t2.start();//肯定是女神线程先结束,然后备胎线程慢慢结束

    }
}

举个例子,我们在用qq聊天框,一边聊天一边发文件,聊天是一个线程,发送文件也是一个线程,把聊天窗口关闭了,也就是线程1结束了,那线程2还有必要存在吗,肯定也结束,所以就可以把线程2设为守护线程。

下一个方法是礼让线程yield(),正常有两个线程要执行的时候,两个线程执行是不均匀的,并不是线程1线程2线程1这样的,那我要是想让结果尽可能均匀一点呢?就可以在run()中直接调用yield()方法。

public class MyThread extends Thread{
    @Override
    public void run() {//"飞机"  "坦克"
        for (int i = 1; i <= 100; i++) {

            System.out.println(getName() + "@" + i);
            //表示出让当前CPU的执行权,让结果尽可能均匀一点 有可能飞机刚出让有抢到了
            Thread.yield();
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
            public static void yield()      出让线程/礼让线程

       */

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("飞机");
        t2.setName("坦克");

        t1.start();
        t2.start();
    }
}

还有一个方法叫插队线程/插入线程,

public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);

        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
       /*
            public final void join()  插入线程/插队线程
       */

		//土豆和main抢夺CPU资源,我想把土豆插入到main之前,等土豆全都执行完了main再执行
		
        MyThread t = new MyThread();
        t.setName("土豆");
        t.start();

        //表示把t这个线程,插入到当前线程之前。
        //t:土豆
        //当前线程: main线程 这段代码运行在main线程上
        t.join();

        //执行在main线程当中的
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程" + i);
        }


    }
}

线程的生命周期

之前的方法已经学习完,现在来学习一下线程的生命周期,存在各种状态。

一个小问题,sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?答案是不会的,时间到了还处在就绪状态,要去抢。

线程安全

以上是多线程的基本知识,多线程可以帮助我们提升效率,但是有一个弊端,就是不安全。下面我们来学习线程安全问题

/*
	这个代码执行结果就是,三个窗口是独立的,各卖各的,相当于卖了三百张。
*/
public class MyThread extends Thread {
    int ticket = 0;//0 ~ 99

    @Override
    public void run() {
            while (true) {
               
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }           
        	}
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
           需求:
                某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
       */


        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //起名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        //开启线程
        t1.start();
        t2.start();
        t3.start();



    }
}
/*
	还是会有重复的,而且还会越界,超出100张
*/
public class MyThread extends Thread {
  //表示这个类所有的对象,都共享ticket数据
    static int ticket = 0;//0 ~ 99

    @Override
    public void run() {
            while (true) {
               
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }           
        	}
    }
}

卖票程序会产生问题,当多个线程操作同一个数据时就会出现问题,第一个问题就是出现重复数据,相同的票出现了多次,第二个问题是数据越界,出现了超出范围的票。到底是为什么呢?

while (true) {
               
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }           
        	}

我们就来看一下上面这块代码。假如有三个线程1、2、3,线程1抢到CPU之后立刻进入if中,然后进入睡眠,其他两个线程开始抢夺。现在线程2抢到了,也去睡眠了,CPU执行权就被线程3抢到了,之后也睡眠了。那三个线程会陆陆续续醒来,继续抢夺CPU往下执行。现在线程1醒了,抢到了CPU,线程1继续执行ticket++,ticket=1,但是还没来得及打印呢,CPU执行权就被线程2抢走了,2也执行ticket++,ticket = 3。线程执行代码的时候,CPU执行权随时都会被其他线程抢走。现在线程3又抢到了,执行ticket++,ticket = 3。所以不管哪个线程打印,都打印的3张票,这就是重复票的由来,这根本原因就是线程在执行的时候,具有随机性

当ticket = 99时,三个线程都处于睡眠状态,线程1醒了开始执行,ticket = 100,还没来得及打印,CPU执行权被线程2抢走,ticket++,ticket = 101。最后线程3抢到,ticket++,ticket = 102。所以最后打印的都是102号票,这就是超出范围的由来,还是由于线程执行的时候具有随机性。

那怎么改呢?假设我能把操作数据的这段代码给锁起来,当有线程进来之后,就算有其他线程抢到CPU执行权,也得在外面等着,进不来。只有当操作共享数据的线程结束后,其他线程才能继续操作。简单来说就是要把共享数据的代码给锁起来,让这些线程轮流执行,就不会出现问题了。

同步代码块

这个锁就被我们称之为同步代码块,把操作共享数据的代码锁起来。

格式:

synchronized(锁){

操作共享数据的代码

}

小括号中的是锁对象,锁对象可以是任意的,但一定要保证这个对象是唯一的

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2;里面所有代码执行完毕,线程出来了,锁才会自动打开

ublic class MyThread extends Thread {

    //表示这个类所有的对象,都共享ticket数据
    static int ticket = 0;//0 ~ 99
	
  	//锁对象是任意的但一定是唯一的,用static修饰即可
  	static Object obj = new Object();
    @Override
    public void run() {
            while (true) {
			  	//小括号里要写锁对象
                synchronized (obj) {
                //同步代码块
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }
            }
        }
    }



}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
           需求:
                某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
       */


        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //起名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        //开启线程
        t1.start();
        t2.start();
        t3.start();



    }
}

小细节注意一下,同步代码块synchronized不能写在循环外面,要不然会导致一个线程把所有票卖完才出来的情况,也就是一个线程把while都执行完了才会出来,其他线程没机会卖。第二个小细节是synchronized小括号里的锁对象一定是唯一的,如果不是唯一的,那有两条线程,他们的是不同的锁,那这锁还有意义吗,根本锁不起来了,synchronized同步代码块就没有任何意义。就像下面这段,synchronized中的对象是this,this的含义就是哪个线程进来了他就代表哪个线程对象本身,所以这个锁对象就是不一样的。

while (true) {
  	//小括号里要写锁对象
    synchronized (this) {
    //同步代码块
    if (ticket < 100) {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ticket++;
        System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
    } else {
        break;
    }
}

一般这个锁对象,我们就用类名.class表示,这里就是MyThread.class,表示当前类的字节码文件对象,这个对象就是唯一,因为在同一个文件夹中,只能有一个MyThread.class。

同步方法

另一种起到线程安全的方法叫做同步方法,可以把整个方法中所有代码都锁起来。把synchronized写在修饰符后面就行,锁住方法里的所有代码,但是这个同步方法不能指定锁对象,是java指定好的。锁对象有两种情况,一个是当前方法为非静态,那么他的锁对象为this,就是当前方法的调用者。另一种是方法为静态方法,那么锁对象就是当前类的字节码文件对象,比如刚刚的MyThread.class。

我们在写同步方法的时候,可能会有一个小疑问,就是不知道什么代码要写进同步方法中,可以用这种技巧:先不写同步方法,先写同步代码块,再把同步代码块里的代码抽取成方法即可。

public class MyRunnable implement Runnable{
  	//这里就不用static修饰了,之前我们通过创建线程类继承Thread的方式创建线程,通过一个线程类创建多个线程对象,这几个对象要共享ticket,ticket属于线程类。但现在我们是通过实现Runnable接口这种方式创建线程对象,我们只需要创建一次MyRunnable作为参数,所以就不需要用static修饰ticket了。
  	int ticket = 0;
	@Override
  	public void run(){
	  	//1.循环
	  	//2.同步代码块(同步方法)
	  	//3.判断共享数据是否到了末尾,如果到了末尾怎么办
	  	//4.如果没到末尾怎么办
		whiel(true){
			synchronized(MyRunnable.class){
				if(ticket == 100){
					break;
				}else{
				  	try {
                		Thread.sleep(10);
            		} catch (InterruptedException e) {
               			e.printStackTrace();
            		}
					ticket++;
				  	System.out.println(Thread.currentThread.getName()+"在卖第"+ticket+"张票!");
				}
			}
		}
	}
}
public class ThreadDemo(){
	MyRunnable mr = new MyRunnable();
  	Thread t1 = new Thread(mr);
  	Thread t2 = new Thread(mr);
  	Thread t3 = new Thread(mr);
  
  	t1.setName("窗口1");
  	t2.setName("窗口2");
	t3.setName("窗口3");
  
  	t1.start();
  	t2.start();
  	t3.start();
}

然后我们把同步代码块中的代码抽取成方法,在IDEA中选中代码,按ctrl+alt+m,就能生成方法。方法是非静态的,锁对象就是this,而这个this就是在测试类中创建的MyRunnable类的对象mr,mr是唯一的。

public class MyRunnable implements Runnable {

    int ticket = 0;

    @Override
    public void run() {
        //1.循环
        while (true) {
            //2.同步代码块(同步方法)
            if (method()) break;
        }
    }

    //this
    private synchronized boolean method() {
        //3.判断共享数据是否到了末尾,如果到了末尾
        if (ticket == 100) {
            return true;
        } else {
            //4.判断共享数据是否到了末尾,如果没有到末尾
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
        }
        return false;
    }
}

延伸一下,我们用以前用过StringBuilder来拼接对象,还有另一个拼接对象的类叫StringBuffer,两个有什么区别呢?当打开API帮助文档后会惊奇的发现,两个类的方法是一样的,但是在StringBuilder中写着,它是线程不安全的,如果想同步最后用StringBuffer。打开两者的原码会发现,StringBuffer中每一个方法都有synchronized修饰,而StringBuilder是没有这个修饰的,所以StringBuffer是线程安全的。那之后我们该如何选择使用这两个类呢?如果我们是单线程执行,不需要考虑多线程的数据安全性,我们就用StringBuilder;如果是多线程的环境下,需要考虑数据安全性,就用StringBuffer。

刚刚我们用同步代码块的时候,它是自动上锁自动关锁,那有没有锁是可以手动加手动关呢?必须有的,Lock锁就是这样的,这也是一种锁对象,比synchronized有更广泛的锁定操作。

Lock锁

Lock中具有获得锁和释放锁的方法:手动上锁、手动释放锁

  • void lock():获得锁
  • void unlock():释放锁

注意的是Lock是一个接口不能直接实例化,要想创建对象要采用他的实现类ReentrantLock实例化,直接用其空参构造。

public class MyThread extends Thread{
	
    static int ticket = 0;
	
 	//这里采用继承Thread类的方式,需要创建很多次MyThread,势必会造成lock锁会有很多个,所以要加staitc,表示只有一个锁,所有对象共享通一把锁。
  	//创建Lock对象
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        //1.循环
        while(true){
            //2.同步代码块
            //synchronized (MyThread.class){
            lock.lock(); //2 //3
            try {
                //3.判断
                if(ticket == 100){
                    break;
                    //4.判断
                }else{
                    Thread.sleep(10);
                    ticket++;
                    System.out.println(getName() + "在卖第" + ticket + "张票!!!");
                }
                //  }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//保证锁一定会被释放,为什么一定要被释放掉呢。假如有三个线程123,当ticket=100的时候,三个线程全在while里,而且是1抢到了资源戴上了锁,现在1进行if判断,然后break退出了循环,假设我们没有finally代码块,那是不是lock.unlock();就不会被执行,也就是说退出循环之后,锁还在1身上。但是,此时线程2、3还在循环里等着拿锁呢,可是所都出去了,他俩就被困在里面出不来,所以程序就不会停止,因此我们必须要保证上锁之后要解锁,因此就在一个finally代码块,保证会解锁。
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
           需求:
                某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
                用JDK5的lock实现
       */


        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start()

    }
}

死锁

这部分我们来学习一下死锁,死锁就是发生了所嵌套,注意奥这是错误,我们学习他是为了不犯这个错误。描述一下这个死锁就是A线程拿到了A锁,B线程拿到了B锁,他们都在等着对方释放锁,这就卡住了,谁都动弹不得。下面代码就出现死锁现象了,A抢到A锁,B抢到B锁,A想释放A锁就必须得到B锁,B想释放B锁就必须得到A锁,关键是A锁在A手里,B锁在B手里。以后写的时候别让锁嵌套起来就行。

public class MyThread extends Thread {

    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        //1.循环
        while (true) {
            if ("线程A".equals(getName())) {
			  	//1.此时A抢到了CPU并打开了A锁
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");//A
				  	//2.假如此刻线程B抢到了CPU,就会到else if中,得到了B锁
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                if ("线程B".equals(getName())) {
				  	//3.B锁此时也打开了
                    synchronized (objB) {
                        System.out.println("线程B拿到了B锁,准备拿A锁");//B
                        synchronized (objA) {
                            System.out.println("线程B拿到了A锁,顺利执行完一轮");
                        }
                    }
                }
            }
        }
    }
}

等待唤醒机制

兄弟们一起继续往下学习多线程中的生产者和消费者,也被叫做等待唤醒机制,是多线程中十分经典的多线程协作模式。那什么是等待唤醒机制呢?我们知道线程的执行时具有随机性的,等待唤醒机制就会打破随机性,会让线程轮流执行,你一次我一次。

两条线程其中一条被称为生产者,用于生产数据。另一条为消费者,消费数据。假设现在有两个人,一个是吃货,负责吃,就是消费者。另一个是厨师,负责做,就是生产者。还需要有第三者桌子,在默认情况下线程的执行是具有随机性的,咱们就需要有一个东西去控制线程的执行,我们现在就用桌子控制,利用桌子来控制线程的执行。假如桌子上有一碗面条,那就是吃货执行。负责吃,那要是没有面条的话,就是厨师执行,负责做。理想情况就是一开始桌子上没面条,厨师抢到CPU执行权,做一碗面条放在桌子上,然后吃货抢到CPU执行权,把面条吃了。

但是肯定不会这么理想啊,会有两种情况出现。第一种,消费者等待。最开始是吃货先抢到了,但是桌子上没吃的啊,就只能等着,在代码中叫做wait,在等待过程中执行权一定会被厨师抢到,做一碗面放桌子上。当然这还没完,因为吃货在等待,厨师要唤醒吃货,代码中叫做notify。这第一种情况主要就看桌子,没面条消费者就得等待。第二种是生产者等待,最开始是初始先抢到,此时桌子上没东西,厨师就做一碗面,放桌子上,notify消费者,就算没人等待,notify一下也没关系。下一次还是厨师抢到执行权,他就不能做面条了,因为桌子上已经有了,他只能等着,此时处于wait。CPU的执行权到了吃货那里,他会判断桌子上是否有食物,如果有就吃,并且notify厨师,没有就等待。

消费者代码逻辑如下:

  1. 判断桌子上是否有食物
  2. 如果没有就等待
  3. 如果有就开吃
  4. 吃完之后,唤醒厨师继续做

生产者代码逻辑如下:

  1. 判断桌子上是否有食物
  2. 如果有就等待
  3. 如果没有就做
  4. 把食物放桌子上
  5. 叫醒等待的吃货

这里涉及三个方法:

  • public void wait():当前线程等待,直到被其他线程唤醒
  • void notify():随机唤醒单个线程
  • void notifyAll():唤醒所有线程 等待线程比较多的时候就用这个
public class Desk {
    /*
    * 作用:控制生产者和消费者的执行
    *
    * */
    //是否有面条  0:没有面条  1:有面条
    public static int foodFlag = 0;

    //总个数
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}
public class Foodie extends Thread{
	@Override
  	public void run(){
		/*
			1.循环
			2.同步代码块
			3.判断共享数据是否到了末尾(先判断已经到了末尾)
			4.判断共享数据是否到了末尾(没到末尾)
		*/
	  	while(true){
			sychronized(
			  {
				if(Desk.count == 0){
					break;
				}else{
					//先判断桌子上是否有面条
				  	if(Desk.foodFlag == 0){
						//如果没有,就等待
						try {
							Desk.lock.wait();//让当前线程跟锁进行绑定
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}else{
					  	//把吃的总数-1 10
					  	Desk.count--;
						//如果有,就开吃
					  	SYstem.out.println("吃货在吃面条,还能吃"+Desk.count+"碗!!!");
					  	//吃完之后,唤醒厨师继续做
					  	//用锁对象调用notifyAll(),表示要唤醒绑定在这把锁上面的所有线程
					  	Desk.lock.notifyAll();
					  	
					  	//修改桌子状态
					  	Desk.foodDlag = 0;
					}
				  	
				  	
				}
			}
		}
	}
  
}
public class Cook extends Thread{
	@Override
  	public void run(){
		/*
			1.循环
			2.同步代码块
			3.判断共享数据是否到了末尾(到了末尾)
			4.判断共享数据是否到了末尾(没到末尾)
		*/
	  	while(true){
			sychronized(Desk.lock){
				id(Desk.count == 0){
					//吃货吃不下了
				  	break;
				}else{
					//厨师的核心逻辑
				  	//判断桌子上是否有食物
				  	if(Desk.foodFlag == 1){
						try {
					  		//如果有就等待 当前线程和锁绑定起来,一旦绑定之后,唤醒的时候就知道唤醒什么线程了
                   	    	Desk.lock.wait();
                   		} catch (InterruptedException e) {
                   	    	e.printStackTrace();
                    	}		
					}else{
						//如果没有就做
					  	System.out.println("厨师做了一碗面条");
				  		//修改食物的状态
					  	Desk.foodFlag = 1;
				  		//叫醒等待的消费者开吃
					  	Desk.lock.notifyAll();
					}				  				  	
				  	
				}
			}
		}
	}
}
public class ThreadDemo {
    public static void main(String[] args) {
       /*
       *
       *    需求:完成生产者和消费者(等待唤醒机制)的代码
       *         实现线程轮流交替执行的效果
       *
       * */
        //创建线程的对象
        Cook c = new Cook();
        Foodie f = new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        //开启线程
        c.start();
        f.start();
    }
}

咱们已经学习完等待唤醒机制的最基本写法,还有一种写法是阻塞队列方式实现。那什么是阻塞队列呢,就像连接生产者和消费者之间的管道,厨师做好面之后就可以放进管道里,吃货从管道里拿出来吃。而且我们可以规定管道中最多可以放多少碗面条。如果最多只有一万,就是跟刚刚一样,做一碗吃一碗。

阻塞队列里的队列,就是说在管道里,面就是在排队,先进先出。阻塞是什么意思呢?put数据时,放不进去,会等着,也叫作阻塞。take数据时,去除第一个数据,取不到也会等着,也叫阻塞。

阻塞结构的体系结构来看下,一共实现了四个接口。最顶层是Iterable,迭代器接口,阻塞队列可以用迭代器或者增强for遍历。本身也实现了Collection,也是一个单列结合。Queue表示是队列,BlockingQueue是阻塞队列。我们要创建的是两个实现类对象。

public class Cook extends Thread{
  	ArrayBlockingQueue<String> queue;
  	public Cook(ArrayBlockingQueue<String> queue){
		this.queus = queus;
	}
	@Override
  	public void run(){
	  while(true){
		  //不断的把面条放进队列中
		  //这里不需要加锁的,put方法底层内置了锁
		  /*
		  	public void put(E e) throws InterruptedException {
				checkNotNull(e);
				final ReentrantLock lock = this.lock;
				lock.lockInterruptibly();
				try {
					//比较队列中的个数和队列长度是否一致
				  	while (count == items.length)
						//装满了就等待
				  		notFull.await();
					//没装满就放数据
				  	enqueue(e);
				} finally {
					//所有逻辑都操作完成就释放锁
					lock.unlock();
				}
			}
		  */
		  try {
			  queue.put("面条");
			  System.out.println("厨师放了一碗面条");
		  } catch (InterruptedException e) {
			  e.printStackTrace();
		  }
	  }
		
	}
}

public class Foodie extends Thread{
  	ArrayBlockingQueue<String> queue;
  	public Foodie(ArrayBlockingQueue<String> queue){
		this.queus = queus;
	}
	@OVerride
  	public void run(){
		while(true){
		  	//千万别再写synchronized,外面一层锁里面一层锁,容易死锁
           	//不断从阻塞队列中获取面条
		  	/*	
				take的底层逻辑
				public E take() throws InterruptedException {
		  			final ReentrantLock lock = this.lock;
		  			lock.lockInterruptibly();
		  			try {
			  			while (count == 0)
				  			notEmpty.await();
			  			return dequeue();
		  			} finally {
			  			lock.unlock();
		  			}
    			}
			*/
           	try {
           	    String food = queue.take();
           	    System.out.println(food);//把输出语句写在锁的外面就会在输出的时候出现一点点问题,不过没关系,数据是对的。
           	} catch (InterruptedException e) {
           	    e.printStackTrace();
           	}
        }
	}
}

public class ThreadDemo {
    public static void main(String[] args) {
	  	/*
			细节:生产者和消费者必须使用同一个阻塞队列
		*/
	  
	  	//1.创建阻塞队列的对象
	  	ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
	  	
	  	//2.创建线程的对象,并把阻塞队列传递过去
	  	Cook c = new Cook(queue);
	  	Foodie f = new Foodie(queue);
	  
	  	//3.开启
	  	c.start();
	  	f.start();
	}
}

线程的状态

之前的线程状态我们学过了,但是不完整,下面是完整的过程。真正在Java中只有六种状态,是没有运行状态的,因为你代码已经运行了就可以不用管了,但是为了方便理解就可以自己知道还有一个运行状态就行。

新建状态(NEW)---> 创建线程对象

就绪状态(RUNNABLE)---> start方法

阻塞状态(BLOCKED)---> 无法获得锁对象

等待状态(WAITING)---> wait方法

计时等待(TIME_WAITING)---> sleep方法

结束状态(TERMINATED)---> 全部代码运行完毕

多线程的内存图

我们来看看下面代码执行的时候,内存图是怎么样的。

我们要记住一件事,main方法就处于main线程中的,代码启动后,并不只是开了两条线程,而是三条,还要算上main方法。

在Java的内存图中,堆内存是唯一的,但是栈内存不唯一,而是跟线程有关系,简单来说每一个线程都有自己的栈,也称为线程栈,以前我们画的栈都是main线程的栈,而程序的入口,主方法main就是运行在main线程中的。这个内存图顺序是这样的,当执行main方法的时候,会开启main线程的栈,main()进入栈中,然后创建两个MyThread对象,存入到堆内存中。之后t1和t2线程启动,就又多了两个栈,线程1的栈和线程2的栈,两个线程中的run方法和局部变量b全部进栈。

线程池

现在我们来学习下一个知识点,线程池。之前我们写多线程的时候,需要一个线程我们就创建一个线程,代码跑完线程消失,但是这种方式是不对的,会浪费操作系统的资源。我们准备一个容器,用来存放线程,这个容器被称为线程池。最开始线程池是空的,当给线程池一个任务的时候,线程池就会自动创建一个线程,拿着这个线程执行任务,执行完之后再把这个线程还给线程池。当第二次再提交任务的时候,就不会再去创建线程池了,而是用刚才的,用完再放回去,这就是多线程的核心原理。假如执行第一个任务,还没执行完,第二个就来了,怎么办?线程池就会再创建一个线程。线程池有上限,可以自己设定。假如设置为3,可以同时执行三个任务,后面再来任务就要排队了。

线程池代码实现过程:

  1. 创建线程池
  2. 提交任务
  3. 所有任务执行完,关闭线程池。实际开发中线程池不会关闭

Excutors:线程池的工具类,通过调用方法返回不同的线程池对象

public class MyThreadPoolDemo{
	public static void main(String[] args){
		/*
			public static ExecutorService newCachedThread():创建一个没有上限的线程池
			public static ExecutorService newFixedThreadPool(int nThreads):创建有上限的进程池
		*/
	  
	  //1.获取线程池对象
	  ExecutorService pool1 = Executors.newCachedThreadPool();
	  
	  //2.提交任务
	  //提交4个任务,就是创建四个线程执行任务
	  pool1.submit(new MyRunnable());
	  pool1.submit(new MyRunnable());
	  pool1.submit(new MyRunnable());
	  pool1.submit(new MyRunnable());
	  
	  //3.销毁线程池
	  pool1.shutdown();
	  
	}
  
}

public class MyRunnable implements Runnable{
	@Override
  	public void run(){
		for(int i = 1;i <= 100;i++){
			System.out.println(Thread.currentThread().getName()+"---"+i);
		}
	}
}

线程池主要核心原理:

  1. 创建一个线程池,是空的
  2. 当提交任务时,线程池会创建一个新的线程对象,任务执行完毕,线程归还池子,下回再提交任务时,不需要创建新的线程,直接复用。
  3. 如果提交任务的时候,池中没有空闲线程,而且无法创建新的线程,任务就会排队等待。

自定义线程池

ExecutorService创建的线程池,不够灵活。当提交的任务比较多,任务就会排队等待,如果说我想修改队伍长度,就修改不了,所以就想能不能不用工具类,自己创建线程池对象呢?这样就能对参数进行设置。这样我们就可以使用自定义线程池,ThreadpoolExecutor类

核心元素一:核心线程数量(除此之外还有临时线程)不能小于0

核心元素二:线程池中最大线程的数量 最大数量>=核心线程数

核心元素三:空闲时间(值)空闲时间到了临时线程要被销毁 不能小于0

核心元素四:空闲时间(单位)用TImeUnit指定

核心元素五:阻塞队列 不能为null

核心元素六:创建线程的方式(线程工厂) 不能为null

核心元素七:要执行的任务过多时的解决方案(任务拒绝策略) 不能为null

例如,刚开始线程池为空,现在设置核心线程为3,临时线程也为3,所以一共是6个线程,最多只有6个。核心线程就跟正式员工一样,不会被销毁,而临时线程就像临时员工,一段时间不工作,就会被销毁。现在往线程池提交任务,提交三个,会创建3个线程。那要是提交5个呢?池会创建3个核心线程来处理,剩余两个任务会排队等待,等待前面的执行完成。那假如有八个呢,还是会等待,如果这时设置队伍长度为3,有两个线程连队伍都排不上,这是线程池才会创建临时线程处理任务。

小细节:什么时候回创建临时线程?核心线程都在忙,而且队伍排满了,这是创建临时线程。任务一定是按照提交顺序执行的吗?不是的。

那如果提交的任务为10,已经超过了核心线程数+临时线程数+队伍长度,该怎么办?所有都满了之后,还剩一个,会触发任务拒绝策略。拒绝策略默认是舍弃不要,一共有四种,其他的了解可。

public class MyThreadPoolDemo1 {
    public static void main(String[] args){
	  	ThreadPoolExecutor pool = new ThreadPoolExecutor(
			corePoolSize:3,	//核心线程数量,不能小于0
		  	maximumPoolSize:6,	//最大线程数,>=corePoolSize
		  	keepAliveTime:60, //空闲线程最大存活时间
		  	TimeUnit.SECONDS, //空闲线程最大存活时间单位
		  	new ArrayBlockingQueue<>(3), //任务队列
		  	Executors.defaultThreadFactory(),	//创建线程工厂
		  	new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略 
			
		);
	  	
	  	//下面就提交任务即可
	}
}

不断提交任务,会有以下三个临界点:

  1. 当核心线程满了,再提交任务就要排队
  2. 当核心线程满,队伍满时,会创建临时线程
  3. 当核心线程满,队伍满,临时线程也满后,会触发任务拒绝策略

那这个线程池多大合适呢?是有公式的,看着很复杂,一点点拆分。

先来解释一下什么是最大并行数。举个例子,一个间谍能窃取一个情报,那同时派出四个间谍就能窃取四个情报了,间谍可以分身,一分为二,所以有八个人,同时做八件事情。例如我们的CPU是4核8线程,4核就是CPU有四个大脑,能同时并行做四件事,但是发明家创造了超线程技术,可以把原来的4个大脑虚拟成8个,就是8线程,最大的并行数为8。可以通过任务管理器中的性能看看自己电脑的线程数,我的是2核四线程。

线程池多大合适呢?看项目时什么类型的,分为两种,一种是CPU密集型运算,另一种是I/O密集型运算。假设项目中计算比较多,但是读取本地文件和数据库文件比较少,就属于CPU密集型运算,根据公式计算即可。如果项目读取本地文件操作比较多,或者读取数据库操作比较多,那就是I/O密集型项目,大多数项目都是I/O密集型。公式中的总时间怎么计算,比如要从本地文件中,读取两个数据并进行相加,包括两步操作:

操作一:读取两个数据(1s)

操作二:相加(1s)

读取是跟硬盘有关系,跟CPU没关系,只有下面的相加是跟CPU有关系的,假设两个操作各耗时1s,套用公式就可以认为是(100%/50%)。所以4核8线程的最大线程数就是8 * 100% * (100%/50%)。

这篇文章到这就结束了,但这不是线程的结束,这里写的是线程的常用基本操作,还有很多操作是面试常问的,下一篇文章我会整理,跟大家一起学习,下篇见啦。

#牛客在线求职答疑中心##牛客解忧铺##如何判断面试是否凉了##你的简历改到第几版了#
java基础知识 文章被收录于专栏

我是一个转码的小白,平时会在牛客中做选择题,在做题中遇到不会的内容就会去找视频或者文章学习,以此不断积累知识。这个专栏主要是记录一些我通过做题所学到的基础知识,希望能对大家有帮助

全部评论
哪个公司的面试啊?
点赞 回复 分享
发布于 2023-04-03 12:18 江西
点赞 回复 分享
发布于 2023-03-07 08:31 辽宁

相关推荐

04-29 22:35
门头沟学院 Java
牛友说改了名字能收到offer:旧图新发查看图片
点赞 评论 收藏
分享
评论
36
22
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务