Java基础----多线程

0.Intellij Idea

eclipce的输出语句简写是syso,idea是sout
在这里插入图片描述idea中的module相当于eclipce中的project
idea中的project相当于eclipce中的workspace
在这里插入图片描述
若想删掉module,则先remove,再delete
在这里插入图片描述
在这里插入图片描述

快捷键

CTRL+ALT+T:可以直接surround with,然后选择synchronized或try-catch
CTRL+P可以提示构造器的参数是什么类型的
ALT+SHIFT+0:生成构造器或setter、getter

1.基本概念:程序、进程、线程

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码,静态对象
进程(process)是程序的一次执行过程,或是正在执行的一个程序,是一个动态的过程:有它自身的产生、存在和消亡过程。----生命周期
->如运行中的qq。
->可以看出,程序是静态的,进程是动态的
->进程作为资源分配单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径
->若一个进程同一时间并行执行多个线程,就是支持多线程的,如360软件同时可以进行查杀病毒和清理垃圾
->线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
在这里插入图片描述
->一个进程中的多个线程共享相同的内存单元/内存地址空间。他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简单、高效。但多个线程操作共享的系统资源可能会带来安全隐患。

单核cpu:是一种假的多线程,因为在一段时间内只能执行一个线程的任务。但是由于cpu执行时间特别短,所以感觉不出来
多核cpu可以更好的发挥多线程的效率
一个java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程

并行:多个CPU同时执行多个任务。比如多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务。比如:多个人做同一件事,秒杀

多线程有优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验
2.提高计算机系统CPU的利用率
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

何时需要多线程
1.程序需要同时执行两个或多个任务
2.程序需要实现一些必须等待的任务时,如文件读写、用户输入、网络操作弄成不同的线程
3.需要一些后台运行程序时

2.线程的创建和使用

方式一:继承于Thread类

  • 1.创建一个继承于Thread类的子类
  • 2.重写Thread类的run()方法->此线程执行的操作声明在run()中,如遍历100以内所有的偶数
  • 3.创建此子类对象
  • 4.调用start()方法:有两个作用:①启动当前线程 ②调用当前线程的run()
//例子:遍历10000以内所有的偶数
class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10000;i++){
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

public class ThreadTest416 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();//t1对象的创建仍是主线程做的
        t1.start();//调完start()之后,分线程才开始执行
        for(int i=0;i<10000;i++){
            //这是主线程的遍历,由于主线程和分线程是分开执行的,因此不确定"i"和"i主线程**************"谁先输出
            //有可能是输出一部分"i"再输出一部分"i主线程**************",然后再输出"i"......这样交叉的输出
            if(i%2==0){
                System.out.println(i+"主线程****************");
            }
        }
    }
}

必须调用的t1.start(),不能调用t1.run(),因为这样不会创建一个新的线程,而只是简单执行run()方法

默认给分线程起的名字是Thread-0,Thread-1……,可以通过Thread.currentThread().getName()来获取当前线程的名字

若想再起一个遍历10000以内的偶数的线程,不能通过已经start()的线程去再起一个,意思是不能再调用一次t1.start()来再起一个线程。否则会报IllegalThreadStateException异常
这时候可以通过再new一个对象来调用start():MyThread t2=new MyThread(); t2.start();
即要想起多个线程,需要new多个对象

因为一个对象只能调用一次start(),因此经常使用匿名子类的匿名对象来起一个线程:

new Thread(){
    @Override
    public void run() {
        System.out.println("第三个线程");
    }
}.start();

Thread中的一些方法的测试

  • 1.start():启动当前线程;调用当前线程的run()
  • 2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  • 3.currentThread():静态方法,返回执行当前代码的线程
  • 4.getName():获取当前线程的名字
  • 5.setName():设置当前线程的名字,如h1.setName(“第一分线程”);
  • 6.可以通过构造器来给线程命名,需在子类中重载一个带参构造器
  • 7.yield():释放当前CPU的执行权
  • 8.join():在线程A中,调用线程B的join()方法,此时线程A进入阻塞状态,直到线程B执行完以后线程A才结束阻塞状态:
  • 9.stop():当执行此方法时强制结束此线程(这个方法已过时)
  • 10.sleep(long millitime):让当前的线程“睡眠”millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态
  • 11.isAlive():返回boolean类型,判断此线程是否还存活
class HThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            if(i%2==0){
                try {
                    sleep(1000);//每次遇到sleep,就会等一秒。意思就是每一秒执行一次打印i
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }

//            if(i%20==0){
//                //当i=0,20,40,60,80时主要释放CPU执行权,这时很可能CPU会执行主线程,但是还有可能是虽然分线程释放了CPU执行权,
//                //但是仍被分线程抢占了CPU资源,因此接下来仍然是执行分线程
//                this.yield();
//            }
        }
    }

    public HThread(String name){
        super(name);
    }
}

public class ThreadMethod419 {
    public static void main(String[] args) throws InterruptedException {
        HThread h1 = new HThread("Thread------1");
//        h1.setName("第一分线程");
        h1.start();

        //给主线程命名
        Thread.currentThread().setName("主线程");
        for(int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }

            if(i==20){
                //当i=20时,主线程停止,让分线程先执行完主线程再开始执行
                //在i=20之前主线程和分线程也会执行一些,就是说i=20时主线程是i=20,分线程i≠0,可能等于其他值
                try{
                    h1.join();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
        System.out.println(h1.isAlive());
    }
}

线程的优先级

线程的调度
一般调度是按照时间片来调度:
在这里插入图片描述
但是也可以通过抢占式来进行调度:优先级高的线程抢占CPU

Java调度方法:
->同优先级线程按照先进先出(先到先服务),使用时间片策略
->对高优先级,使用优先调度的抢占式策略

线程的优先级等级:
->MAX_PRIORITY:10 最大优先级
->MIN_PRIORITY:1 最小优先级
->NORM_PRIORITY:5 通常的优先级

->getPriority():返回线程的优先级
->setPriority(int p):改变线程的优先级

h1.setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

说明:
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

练习:创建3个窗口卖票,总票数为100张

class Window extends Thread{
    private static int ticket;//票必须是静态的,这样三个窗口的对象共享这100张票
    @Override
    public void run() {
        for(ticket=0;ticket<100;ticket++){
            System.out.println(getName()+"\t"+ticket);
        }
    }

    public Window(String name){
        super(name);
    }
}

public class WindowTest421 {
    public static void main(String[] args) {
        Window w1 = new Window("第1个窗口");
        Window w2 = new Window("第2个窗口");
        Window w3 = new Window("第3个窗口");

        w1.start();
        w2.start();
        w3.start();
    }
}

但是线程不安全:可能会出现三个窗口同时把票1卖了的情况

方式二:实现Runnable接口

  • 1.创建一个实现了Runnable接口的类
  • 2.实现类去实现Runnable接口中的抽象方法:run()
  • 3.创建实现类的对象
  • 4.将此实现类作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 5.通过Thread类的对象调用start()
class MThread implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class ThreadTest422 {
    public static void main(String[] args) {
        MThread m1 = new MThread();
        Thread t1 = new Thread(m1);
        t1.start();//此线程仍然是一个Thread类,调用start():①启动线程  ②调用当前线程的run(),即调用Runnable类型的target的run()

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(m1);
        t2.start();
    }
}

练习:创建3个窗口卖票,总票数为100张

class MaiPiao implements Runnable{
    private int ticket=1;//不用再加static,见下面的main方法中解释
    @Override
    public void run() {
        while(ticket<=100){
            System.out.println(Thread.currentThread().getName()+"卖票,票号:"+ticket);
            ticket++;
        }
    }
}

public class WindowTest423 {
    public static void main(String[] args) {
        MaiPiao m1 = new MaiPiao();
        //由于只需造一个实现类的对象,然后分别传入到三个不同的Thread中,因此t1,t2,t3共享同一个m1对象
        //即这三者共享同一个变量ticket

        Thread t1 = new Thread(m1);
        Thread t2 = new Thread(m1);
        Thread t3 = new Thread(m1);

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

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

虽然不用加static了,但是仍会出现线程不安全问题

比较创建多线程的两种方式:

开发中优先选择方式2(实现Runnable接口),原因是:
1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合处理多个线程有共享数据的情况

由于
public class Thread implements Runnable{}
即Thread类也实现了Runnable接口

因此两种方式的相同点:
->两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
->目前这两种方式要想启动线程,都是要调用Thread类中的start()

Java中的线程分为两类:一种是守护线程,一种是用户线程
守护线程是用来服务用户线程的,当用户线程的生命周期结束时守护线程也会结束
如main是用户线程,gc()是守护线程

3.线程的生命周期

JDK中用Thread.State类(这是Thread类中的一个内部类)定义了线程的几种状态
在Java语言中,一个完整的生命周期中通常要经历如下的五种状态:

  • 1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 2.就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 3.运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  • 4.阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
  • 5.死亡:线程完成了它的全部工作或线程被提前强制性中止或出现异常导致结束
    在这里插入图片描述

4.线程的同步

  • 1.问题:卖票过程中,出现了重票、错票–>出现了线程安全问题
  • 2.问题出现的原因:当某个线程操作车票的过程中,尚未完成时,其他线程参与进来,也操作车票。即当t1还在run()方法中时,此时有些共享的数据还没有改变,t2和t3进入了run(),这时会引起错误
  • 3.如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程t1操作完ticket时,其他线程才可以操作ticket。即当t1还在run()方法中没出来的时候,t2和t3不能进去,这种情况即使线程t1出现了阻塞,也不能被改变
  • 4.在Java中,通过同步机制,来解决线程的安全问题
    方式一:同步代码块
synchronized(同步监视器){
    //需要同步的代码
}

说明:1.操作共享数据的代码,即为需要被同步的代码–>不能把代码包含多了,也不能少了(否则会出现一个窗口把票都卖完,其他窗口没有票的情况)
2.共享数据:多个线程共同操作的变量,如ticket
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁
要求:多个线程必须共用同一把锁
补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当锁。
在继承Tread类创建多线程的方式中,慎用this充当锁;可以考虑使用当前类充当锁(MaiPiao.class)
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
声明同步方法:

权限修饰符 synchronized 返回值类型 方法名(形参列表){}

总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
2.非静态的同步方法,同步监视器是this;静态的同步方法,同步监视器是当前类本身(类.class)

  • 5.同步的方式,虽然解决了线程的安全问题,但是操作同步代码时只能有一个线程参与其他线程等待,这相当于是一个单线程,效率低

使用同步代码块处理继承Runnable接口的线程安全问题

class MaiPiao implements Runnable{
    private int ticket=1;
    Object o=new Object();//锁必须声明在这,不能拿用匿名对象放到synchronized中(synchronized(new Object))
    //因为锁必须只有一把,多个线程必须共用一把锁,因此锁应该声明在共用的区域,上述不合适的方法会导致多把锁,线程不安全
    //如果通过同步代码块之后仍然会出现错票和重票,那么大概率是锁的问题,即锁不止一把
    @Override
    public void run() {
        while(true){//一般循环都放同步机制的外面,判断尽量在同步机制里面,即把while(ticket<=100){}换成while(true){if(ticket<=0){} else break;},这样判断语句就在同步机制以内了
        	// synchronized(o){//方式一:同步代码块
            synchronized(this){//最方便的一把锁就是this,这样不用再new一个对象了,而this就是MaiPiao这个类new出的对象,因为只有一个(m1)
                if(ticket<=100) {
                    //所以可以用来作为锁
                    System.out.println(Thread.currentThread().getName() + "卖票,票号:" + ticket);
                    ticket++;
                }else break;
            }
        }
    }
}

public class MaiPiao430 {
    public static void main(String[] args) {
        MaiPiao m1 = new MaiPiao();

        Thread t1 = new Thread(m1);
        Thread t2 = new Thread(m1);
        Thread t3 = new Thread(m1);

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

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

使用同步代码块处理继承Thread类的线程安全问题

class Window extends Thread{
    private static int ticket=1;
    private static Object o = new Object();//锁要求是一把,所以是static
    @Override
    public void run() {
        while (true){
            synchronized(o) {//这种方式锁不能用this,因为有三个对象,不唯一。可以用Window.class来作为锁,Window.class是Window类
                //也可以看成一个对象,在反射那一部分会了解到
                if(ticket<=100) {
                    System.out.println(getName() + "\t" + ticket);
                    ticket++;
                }else break;
            }
        }
    }

    public Window(String name){
        super(name);
    }
}

public class MaiPiao432 {
    public static void main(String[] args) {
        Window w1 = new Window("第1个窗口");
        Window w2 = new Window("第2个窗口");
        Window w3 = new Window("第3个窗口");

        w1.start();
        w2.start();
        w3.start();
    }
}

使用同步方法来处理实现Runnable接口的线程安全问题

class MaiPiao3 implements Runnable{
    private int ticket=1;
    @Override
    public void run() {
        while(true){
            show();
        }
    }

    private synchronized void show(){//默认同步监视器是this
        if (ticket<=100) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "卖票,票号:" + ticket);
            ticket++;
        }
    }
}

使用同步方法来处理继承Thread类的线程安全问题

class MaiPiao4 extends Thread{
    private static int ticket=1;
    @Override
    public void run() {
        while (true){
            show();
        }
    }

    private static synchronized void show(){//由于同步方法中默认的锁是this,所以处理继承Thread类的方式的线程安全问题时,肯定不对
        //因为MaiPiao4的对象有三个,this不能作为锁
        //所以需要保证同步方法是静态的,此时默认的锁不是this,而是MaiPiao4.class
        if(ticket<=100) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "\t" + ticket);
            ticket++;
        }
    }

    public MaiPiao4(String name){
        super(name);
    }
}

线程安全的懒汉式的单例模式

class Bank{
    private Bank(){}
    private static Bank b=null;
    
    //方式一:1
//    public static synchronized Bank getInstance(){//加上synchronized,直接把此方法设置成同步方法,锁是Bank.class
//        if(b==null){
//            b=new Bank();
//        }
//        return b;
//    }

    //方式一:2
//    public static Bank getInstance(){
//        synchronized (Bank.class) {
//            if(b==null){
//                b=new Bank();
//            }
//            return b;
//        }
//    }

    //以上是方式一:效率较差
    //以下是方式二:效率较高,以后写懒汉式的单例模式直接写这个
    public static Bank getInstance(){
        if(b==null){//因为如果线程1已经new一个对象了,那之后的线程就不用在等这个锁了,直接执行return语句
            synchronized (Bank.class) {
                if(b==null){
                    b=new Bank();
                }
            }
        }
        return b;
    }

}

死锁

->不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
->出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
举例:

/**
 * @description 演示线程的死锁问题
 *
 * 以下代码会出现死锁:线程1拿到锁s1之后睡眠100ms,这时线程2拿到锁s2之后睡眠100ms。等他们醒的时候线程1等着拿锁s2,线程2等着拿锁s1
 * 而这两把锁在对方的手中,这时就僵持住了,因此出现死锁,两个线程都在阻塞,不能进入死亡状态
 *
 */
public class SiSuo436 {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();

        //匿名子类对象继承Thread类来新建一个线程
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        //匿名实现Runnable类对象传入到匿名Thread子类对象中来新建一个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

我们使用同步时要避免出现死锁
如:1.专门的算法
2.尽量减少同步资源的定义
3.尽量避免嵌套同步

线程同步方式3:Lock

从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁

演示:

class Window2 implements Runnable{
    private int ticket=100;

    //1.实例化Lock
    //private ReentrantLock lock = new ReentrantLock(true);  true意味着是公平分配时间片的,即线程1执行一次,线程2执行一次,线程3执行一次,接着再执行一次线程1...
    private ReentrantLock lock = new ReentrantLock();//无参默认是false,意味着线程1,2,3都是抢着时间片的,不是公平的
    //如果是继承Thread类的创建多线程,则是private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){//一般循环都放同步机制的外面,判断尽量在同步机制里面,即把while(ticket>0){}换成while(true){if(ticket<0) break;},这样判断语句就在同步机制以内了
            try{
                //2.调用锁定的方法:lock()方法
                lock.lock();//lock()方法后面的代码被锁定了,和在synchronized代码块中的代码类似

                if(ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
            finally {//开锁放到try中,解锁放到finally中,意思是不管开锁之后的代码是否出现异常,一定要保证必须解锁.所以一旦用Lock的方式,必须用try-finally
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}

public class LockTest437 {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

面试题:synchronized与lock的异同?

同:二者都是用来解决线程安全问题的
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock()),同时结束同步也需要手动释放(unlock())
使用Lock()锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

面试题:如何解决线程安全问题?有几种方式?

三种:Lock,同步代码块,同步方法

优先使用顺序:

Lock>同步代码块>同步方法(这三者的灵活性是依次递减的)
实际上用谁都可以

5.线程的通信

线程通信涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
  • notify():一旦执行此方法,就会唤醒被wait()的一个线程,如果有多个线程被wait,就唤醒优先级高的那个
  • notifyAll():一旦执行此方法,就会唤醒所有被wait()的一个线程

注意点:

  • 1.wait(),notify(),notifyAll()这三个方法必须使用在同步代码块或同步方法中,不能在Lock()中使用
  • 2.wait(),notify(),notifyAll()这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。如果监视器是this,则这三个方法是this.wait()…或省略this。如果监视器是类.class,则调用这三个方法时需要写成类.class.wait()…
    如果不是由同步监视器调用的,则会出现IllegalMonitorStateException异常
  • 3.wait(),notify(),notifyAll()这三个方法是定义在java.lang.Object中的,正是因为如此,所有充当同步监视器的类才都可以调用这三个方法
/**
 * @description 线程通信的例子:使用两个线程打印1-100.线程1,线程2交替打印
 *
 * 以下代码解释:num=1时,线程1进入synchronized代码块,然后忽略notify()方法,然后执行到wait()方法,进而阻塞,并释放锁;这时线程2拿到锁
 * 进入synchronized代码块,执行到notify(),把正在阻塞的线程1唤醒,然后线程2执行到wait()又阻塞,继续等待线程1执行到notify()来唤醒....这样循环
 *
 */

class Number implements Runnable{
    private int num=1;
    @Override
    public void run() {
        while (true){
            synchronized (this) {

                notify();//专门唤醒另一个线程的wait()阻塞

                if(num<=100){
                    try {
                        Thread.sleep(10);//sleep()阻塞的时候不会释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+":"+num++);

                    try {
                        wait();//wait阻塞的时候可以把锁释放了
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }else break;
            }
        }
    }
}

public class CommunicationTest439 {
    public static void main(String[] args) {
        Number n = new Number();
        new Thread(n).start();
        new Thread(n).start();
    }
}

面试题:sleep()和wait()的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
不同点:
1.两个方法声明的位置不同:Thread类中声明的sleep(),Object()类中声明wait()
2.调用的要求不同:sleep()可以在任何需要的场景下调用。Wait()必须使用在同步代码块或同步方法中
3.关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,而wait()会释放锁

经典例题:生产者/消费者问题

/**
 * @author JiaMing
 * @create 08-12 21:54
 * @description 经典例题:生产者/消费者问题
 *
 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
 * 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品
 * 了再通知消费者来取走产品。
 *
 * 分析:
 * 1.是否是多线程问题?是,生产者线程,消费者线程
 * 2.是否有共享数据?有,产品(或店员)
 * 3.如何处理线程安全问题?同步机制,三种方法
 * 4.是否涉及到线程的通信?是
 */

class Clerk{
    private int productCount=0;

    //以下两个方法都在操作productCount,所以会出现线程安全问题,因此都加上synchronized使其变成同步方法。
    //需要注意的是这两个方法必须都设成非静态方法,这样锁默认是this,因为clerk对象只new了一个
    //生产产品
    public synchronized void produceProduct() {
        if(productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");

            notify();
        }else {//等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //消费产品
    public synchronized void consumeProduct() {
        if(productCount>0){
            System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个产品");
            productCount--;

            notify();
        }else {//等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Producer extends Thread{//生产者
    private Clerk clerk;

    public Producer(Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始生产产品......");
        while (true){
            try {
                sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.produceProduct();
        }
    }
}


class Consumer extends Thread{//消费者
    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+"开始消费产品......");
        while (true){
            try {
                sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.consumeProduct();
        }
    }
}


public class ProductTest441 {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);

        producer.setName("生产者");
        consumer.setName("消费者");

        producer.start();
        consumer.start();
    }
}

6.JDK5.0新增线程创建方式

方式三:实现Callable接口

  • 1.创建一个实现Callable接口的实现类
  • 2.实现call()方法,将此线程需要执行的操作声明在call()中
  • 3.创建Callable接口实现类的对象
  • 4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
  • 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  • 6.如果对返回值感兴趣,则通过FutureTask对象的get()方法获取Callable中call()的返回值

如何理解实现Callable接口的方式比实现Runnable接口的方式更强大?

  • 1.call()可以有返回值
  • 2.call()可以抛出异常,被外面的操作捕获,获取异常信息
  • 3.Callable是支持泛型的
class NumberThread implements Callable{//1.
    @Override
    public Object call() throws Exception {//2.
        int sum=0;
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                sum+=i;
            }
        }
        return sum;
    }
}

public class ThreadNew442 {
    public static void main(String[] args) {
        NumberThread n = new NumberThread();//3.
        FutureTask f = new FutureTask(n);//4.
        new Thread(f).start();//5.
        try {
            //get()方法的返回值即为FutureTask构造器参数Callable实现类重写call()方法的返回值
            Object sum = f.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
}

方式四:使用线程池(最常用)

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
好处:
->提高响应速度(减少了创建新线程的时间)
->降低资源消耗(重复利用线程池中线程,不需要每次都创建)
->便于线程管理

JDK5.0提供了线程池相关的API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
->void execute(Runnable command):执行任务,没有返回值,一般用来执行Runnable
-><T>Future<T>submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
->void shutdown():关闭线程池
Executors:工具栏、线程池的工厂类,用于创建并返回不同类型的线程池
->Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
->Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
->Executors.newSingleThreadExecutor(n):创建一个只有一个线程的线程池
->Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期地执行

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);//输出:pool-1-thread-1:i
            }
        }
    }
}

public class ThreadPool444 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);


        //线程池中取一个线程来执行Runnable接口的run(),执行完再放回池中
        service.execute(new NumberThread1());//适合使用于Runnable
//        service.submit(Callable callable);//适合使用于Callable

        service.shutdown();//关闭线程池
    }
}

可以设置corePoolSize(核心池的大小)、maximumPoolSize(最大线程数)、keepAliveTime(线程没有任务时最多保持多长时间后会终止)

//设置线程池的属性
ThreadPoolExecutor service1=(ThreadPoolExecutor)service;//service是ExecutorService接口的类型,肯定不会有属性
//所以要想设置属性,肯定得强转成实现类,然后调属性
service1.setCorePoolSize(15);

面试题:创建多线程有几种方式:4种

多线程一章中需要try-catch的方法:sleep(),join(),wait(),都可能抛出InterruptedException(阻塞异常)。需要try-finally的方法:lock(),unlock()。

等学到框架后可以用更简便的方式来创建多线程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值