多线程&JUC

什么是多线程


可以理解成每一个在运行的软件都是一个进程

360这个软件运行就是一个进程而360里面的没项功能就是每一个线程
多线程的理解

每次在运行代码的时候CPU会进行等待不会切换到执行别的程序
在多线程中CPU不会等待,会将等待的时间充分利用起来


并发和并行

并发

1个CPU交替执行多条线程
并行

2个线程在2个CPU上同时执行


上面线程的意思是你的电脑能同时运行多少条线程


当我们只用执行4条线程将不用切换,将并行执行每一条线程。而当我们的线程数量多于4条,将在这些线程中随机切换执行,即并发执行所以说在我们计算机中并发和并行其实都存在

多线程的第一种实现

package com.thread.case1;
//多线程的第一种实现方式
public class ThreadDemo1 extends Thread{

    @Override
    public void run() {
        //将线程要执行的代码写到重写后的run中
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName()+"hello world");
        }
    }
}

package com.thread.case1;
//多线程的第一种实现方式
public class MythreadTest {
    /*
    1.自己定义一个类继承自Thread
    2.重写Thread的run方法
    3.创建子类对象并启动线程
     */
    public static void main(String[] args) {
        ThreadDemo1 t1 = new ThreadDemo1();//创建线程对象t1
        ThreadDemo1 t2 = new ThreadDemo1();//创建线程对象t2
        //给线程命名
        t1.setName("线程1");
        t2.setName("线程2");
        //启动线程要调用start方法,如果直接调用run方法就不是以多线程的方式运行了
        t1.start();
        t2.start();
    }
}

多线程的第二种实现方式

package com.thread.case1;
//多线程的第二种实现方式
public class ThreadDemo2 implements Runnable {
/*
1.自己定义一个类实现Rannble接口
2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread对象并开启线程
 */

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            final Thread thread = Thread.currentThread();//该方法获取当前正在调用的对象
            System.out.println(thread.getName()+"hello world");//该处该类和Thread没有继承关系不能直接获取类名
        }
    }
}

package com.thread.case1;

public class MythreadTest2 {
    public static void main(String[] args) {

        ThreadDemo2 td = new ThreadDemo2();
        ThreadDemo2 tt = new ThreadDemo2();
        Thread t1 = new Thread(td);//创建线程1
        Thread t2 = new Thread(tt);//创建线程2
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

多线程的第三种实现方式


前面方法的弊端:我们将线程要执行的内容放到run方法中,有一个弊端因为run方法只能返回void,当我们要执行的内容是有返回值的时候这2种创建多线程的方式就不管用了

package com.thread.case1;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {//泛型表示线程要返回的类型
    @Override
    public Integer call() throws Exception {//这个相当于run方法里面写执行多线程的代码
        //计算1-100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

package com.thread.case1;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

       /* 多线程第三种实现:可以获取多线程的运行结果
        1.创建一个类MyCallable实现Callable接口
        2.重写call(是有返回值的,表示多线程的运行结果)
        3.创建MyCallable的对象(表示多线程要执行的任务)
         4.创建FutureTask的对象(作用管理多线程运行的结果)
         5.创建Thread类的对象,并启动线程*/
       MyCallable ma = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(ma);
        Thread td = new Thread(ft);
        td.start();
        final Integer result = ft.get();//获取线程的值
        System.out.println(result);

    }
}

多线程中常用成员方法

  • 如果没有把我们的线程命名,系统将会按照Thread-序号自动命名
package com.thread.mothod;

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

package com.thread.mothod;

public class TheadDemo4 {
    //多线程方法的演示
    /*
    String getName() 返回此线程的名称
    void setName(String name)设置线程的名称(通过构造方法也可以设置)
    static currentThread()获取当前线程的对象
    static void sleep(long time)让线程休眠执行的时间,单位为毫秒
     */
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        mt1.start();
        mt2.start();
    }
}


此时我们的线程并没有手动进行命名,这时我们的java将会按照一定的规律进行自动命名

  • 除了使用setName还可以用构造方法命名线程
package com.thread.mothod;

public class MyThread extends Thread{
    public MyThread(){//空参构造
        super();
    }
    public MyThread(String name){//有参构造
        super(name);
    }
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+" "+i);
        }
    }
}

  */
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("飞机");
        MyThread mt2 = new MyThread("坦克");
        mt1.start();
        mt2.start();
    }
}

  • 线程停留

    当我们的代码执行停留的代码时将会停留相应的时间,然后继续执行后面的代码

    String getName() 返回此线程的名称
    void setName(String name)设置线程的名称(通过构造方法也可以设置)
    细节:
    1.如果我们没有给线程设置名字,线程是有默认名字的
    格式:Thread-X(X序号从0开始)(因为父类的空参构造是这样规定的)
    2.如果我们要给线程设置名字,可以用set方法进行设置,也可以使用构造方法进行设置
    static currentThread()获取当前线程的对象
    细节:
    当我们的JVM虚拟机启动之后,会自动启动多条线程
    其中有一条线程就叫main线程
    他的作用就是去调用main方法,并执行里面的代码
    在以前我们写的所以代码都是运行在main线程中的
    static void sleep(long time)让线程休眠执行的时间,单位为毫秒
    细节:
    1.哪条线程执行到这个方法,那么那条线程就会在这里停留对应的时间
    2.方法的参数:表示休眠的时间单位毫秒
    3.当时间到了之后,线程会自动醒来,继续执行下面的代码

线程的优先级


抢占式调度各个线程自动抢占CPU资源,随机执行。还有非抢占式调度。在java中是抢占式调度

如果线程的优先级设置的越高,抢到CPU的概率就越大。优先级最小是1最大是10,如果线程的优先级没有设置默认是5

package com.thread.mothod1;

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

package com.thread.mothod1;

public class ThreadTest {
    public static void main(String[] args) {
        //设置线程的优先级
        /*
        setPriority(int newPrioity)设置线程的优先级
        final int getPrioity()获取线程的优先级
         */
        MyRunnable ma1 = new MyRunnable();
        MyRunnable ma2 = new MyRunnable();
        //1.获取main方法的优先级
        System.out.println(Thread.currentThread().getPriority());//5
        Thread t1 = new Thread(ma1,"飞机");
        Thread t2 = new Thread(ma1,"坦克");
        //2.获取t1 t2的默认优先级
        System.out.println(t1.getPriority());//5
        System.out.println(t2.getPriority());//5
        //设置线程的优先级
        t1.setPriority(1);
        t2.setPriority(10);
        //启动线程
        t1.start();
        t2.start();


    }
}

守护线程

package com.thread.mothod1;

public class MyThread extends Thread{
    //非守护线程执行代码
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+"@"+i);
        }
    }

}

package com.thread.mothod1;

public class MyThreadplus extends Thread {
    //守护线程执行代码
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

package com.thread.mothod1;

public class MyThreadTest {
    public static void main(String[] args) {
        //验证守护线程
        //final void setDaemon(boolean on)设置为守护线程
        //细节:
            /*
            专业解释:
                当其他的非守护线程执行完毕后,守护线程会陆续结束
            通俗解释:
                当女神线程结束之后,那么备胎也没有存在的必要了
             */
        MyThread t1 = new MyThread();
        MyThreadplus t2 = new MyThreadplus();
        t1.setName("备胎");
        t2.setName("女神");
        t1.setDaemon(true);//将t1设置为守护线程
        t1.start();
        t2.start();
    }
}


守护线程和非守护线程竞争CPU资源执行,但是当非守护线程执行完毕后,我们的守护线程也会陆续结束执行,此时守护线程可能没有执行完毕,也有可能已经执行完毕

守护线程的应用场景


当我们将聊天窗口关闭了,传输文件就没有必要继续执行了

礼让线程

在前面我们的多个线程互相竞争CPU的执行权,如果我们想让CPU的资源分配均匀一点,可以调用礼让线程的方法

package com.thread.mothod1;

public class MyThread1 extends Thread {
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+"@"+i);
            Thread.yield();//当我们的飞机这个线程抢到了cpu并执行完毕了上面的代码,
            //执行了yield后会将cpu出让出去,然后cpu会进行再次争夺,但是不能保证一定是坦克争夺到cup
        }
    }
}

package com.thread.mothod1;

public class MyThreadTest1 {
    public static void main(String[] args) {
        //验证礼让线程
        //public static void yield() 出让线程/礼让线程
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();
        t1.setName("飞机");
        t2.setName("坦克");
        t1.start();
        t2.start();
    }
}

这种礼让资源的方式,只能在一定的程度上保证线程均匀执行,不能完全保证均匀执行

插入线程

package com.thread.mothod1;

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

package com.thread.mothod1;

public class MyThreadTest2 {
    public static void main(String[] args) throws InterruptedException {
        //验证插入线程
        //public final void join()插入线程
        MyThread2 t = new MyThread2();
        t.setName("土豆");
        t.start();
        //表示把t这个线程插入到当前线程之前
        //t 土豆
        //当前线程:main线程
        t.join();

        for (int i = 0; i < 10; i++) {
            System.out.println("main线程"+i);
        }
    }
}

进行上述代码的插入线程的话,将会把土豆线程的100次打印完成之后,才会打印main线程的10次打印

线程的生命周期

线程的安全问题

举例说明

当我们的票数不进行共享,发现我们的创建各自独立的卖出自己的100张票

static修饰后的代码

package com.thread.mothod1;

public class MyThread3 extends Thread{
    static int ticket = 0;//记录票数
    @Override
    public void run(){
        while (true){
            if(ticket<100){
                ticket++;
                System.out.println(getName()+"正在卖出第"+ticket+"张票");
            }else {
                break;
            }
        }
    }

}

package com.thread.mothod1;

public class MyThreaTest3 {
    public static void main(String[] args) {
        //某电影院正在上映国产大片,共有100张票,而他有3个窗口买票,请设计一个程序模拟电影院买票
        //3个窗口很明显可以设计3个线程来实现
       MyThread3 t1 = new MyThread3();
       MyThread3 t2 = new MyThread3();
       MyThread3 t3 = new MyThread3();
       t1.setName("窗口1");
       t2.setName("窗口2");
       t3.setName("窗口3");
       t1.start();
       t2.start();
       t3.start();

    }
}

此时发现当我们的ticket用static修饰后还是会出现买的票号还是会重复,而且还可能票号超过100。这样将会导致代码的安全问题

同步代码块

**
当多个线程操作同一个数据的时候,就会出现问题**


票号重复的解释

当我们的的线程1在票号自增后还没有来得及打印cpu被线程2抢夺去了,线程2还是同样的情况cpu被线程3抢夺了,然后打印的时候,打印的都是卖了了票号3
出现超出范围的票的问题的解释

当我们的ticket是99的时候,线程1抢到cpu执行了ticket++(ticket100)的时候cpu被线程2签到也是执行到ticket++(ticket101)的时候cpu被线程3抢走然后执行ticket++(ticket==102)然后3个线程再次抢夺,打印出来的票号都是102了

以上2种原因的本质是:线程执行时,有随机性

解决思路

我们可以把操作共享数据的这一段代码给锁起来,当一个线程进来之后,当该线程没有执行完毕就算其他线程抢夺到了cup也不能进来,当进去的线程出来,其他线程才能进来

package com.thread.mothod1;

public class MyThread3 extends Thread{
    static int ticket = 0;//记录票数
   static Object obj = new Object();//用static表示该对象为共享
    @Override
    //锁对象是任意的,只需要保证该对象是唯一i的
    public void run(){
      while (true){
          synchronized (obj){

                  if(ticket<100){

                      ticket++;
                      System.out.println(getName()+"正在卖出第"+ticket+"张票");
                  }else {
                      break;
                  }

          }
      }
    }

}

package com.thread.mothod1;

public class MyThreaTest3 {
    public static void main(String[] args) {
        //某电影院正在上映国产大片,共有100张票,而他有3个窗口买票,请设计一个程序模拟电影院买票
        //3个窗口很明显可以设计3个线程来实现
       MyThread3 t1 = new MyThread3();
       MyThread3 t2 = new MyThread3();
       MyThread3 t3 = new MyThread3();
       t1.setName("窗口1");
       t2.setName("窗口2");
       t3.setName("窗口3");
       t1.start();
       t2.start();
       t3.start();

    }
}

注意:此时的锁不能把整个whle循环都锁住,这样的话就会让一个窗口把所有的票都买完了
窗口1正在卖出第1张票
窗口1正在卖出第2张票
窗口1正在卖出第3张票
窗口1正在卖出第4张票
窗口1正在卖出第5张票
窗口1正在卖出第6张票
窗口1正在卖出第7张票
窗口1正在卖出第8张票
窗口1正在卖出第9张票
窗口1正在卖出第10张票
窗口1正在卖出第11张票
窗口1正在卖出第12张票
窗口1正在卖出第13张票
窗口1正在卖出第14张票
窗口1正在卖出第15张票
窗口1正在卖出第16张票
窗口1正在卖出第17张票
窗口1正在卖出第18张票
窗口1正在卖出第19张票
窗口1正在卖出第20张票
窗口1正在卖出第21张票
窗口1正在卖出第22张票
窗口1正在卖出第23张票
窗口1正在卖出第24张票
窗口1正在卖出第25张票
窗口1正在卖出第26张票
窗口1正在卖出第27张票
窗口1正在卖出第28张票
窗口1正在卖出第29张票
窗口3正在卖出第30张票
窗口3正在卖出第31张票
窗口3正在卖出第32张票
窗口3正在卖出第33张票
窗口3正在卖出第34张票
窗口3正在卖出第35张票
窗口3正在卖出第36张票
窗口3正在卖出第37张票
窗口3正在卖出第38张票
窗口3正在卖出第39张票
窗口3正在卖出第40张票
窗口3正在卖出第41张票
窗口3正在卖出第42张票
窗口3正在卖出第43张票
窗口3正在卖出第44张票
窗口3正在卖出第45张票
窗口3正在卖出第46张票
窗口3正在卖出第47张票
窗口3正在卖出第48张票
窗口3正在卖出第49张票
窗口3正在卖出第50张票
窗口3正在卖出第51张票
窗口3正在卖出第52张票
窗口3正在卖出第53张票
窗口3正在卖出第54张票
窗口3正在卖出第55张票
窗口3正在卖出第56张票
窗口3正在卖出第57张票
窗口3正在卖出第58张票
窗口3正在卖出第59张票
窗口3正在卖出第60张票
窗口3正在卖出第61张票
窗口3正在卖出第62张票
窗口3正在卖出第63张票
窗口3正在卖出第64张票
窗口3正在卖出第65张票
窗口3正在卖出第66张票
窗口3正在卖出第67张票
窗口3正在卖出第68张票
窗口3正在卖出第69张票
窗口3正在卖出第70张票
窗口3正在卖出第71张票
窗口3正在卖出第72张票
窗口3正在卖出第73张票
窗口3正在卖出第74张票
窗口3正在卖出第75张票
窗口3正在卖出第76张票
窗口3正在卖出第77张票
窗口3正在卖出第78张票
窗口3正在卖出第79张票
窗口3正在卖出第80张票
窗口3正在卖出第81张票
窗口3正在卖出第82张票
窗口3正在卖出第83张票
窗口3正在卖出第84张票
窗口3正在卖出第85张票
窗口3正在卖出第86张票
窗口3正在卖出第87张票
窗口3正在卖出第88张票
窗口3正在卖出第89张票
窗口3正在卖出第90张票
窗口3正在卖出第91张票
窗口3正在卖出第92张票
窗口3正在卖出第93张票
窗口3正在卖出第94张票
窗口3正在卖出第95张票
窗口3正在卖出第96张票
窗口3正在卖出第97张票
窗口3正在卖出第98张票
窗口3正在卖出第99张票
窗口3正在卖出第100张票

同步代码块的2个细节

1.不能将whle整个循环都放在锁里面
2.锁的对象一定要是唯一的

如果我们的锁的对象不一样,这个锁就相当于没写。

为了确保我们的锁的对象唯一,我们可以时候当前类的字节码文件对象MyThread.class来表示,因为类的字节码文件是唯一的,所以其字节码文件的对象也是唯一的**

同步方法

如果我们想把一个方法都锁上,就没有必要使用前面的同步代码块了,可以用synchronized修饰方法

package com.thread.mothod1;

public class MyThread4 implements Runnable{//用第二种方法实现线程
    int ticket = 0;//记录票数
    //因为我们的这种创建多线程的方式,只会new一个MyThread4的对象,所以ticket是唯一的
    @Override
    public void run() {
        //1.循环
        while (true){
            //设置同步代码块
            try {
                Thread.sleep(100);//因为线程运行的速度过快,可能出现一个线程一下就把票买完的情况
                //进行线程的停顿更容易观察
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(mothod()){
                break;
            }

        }
    }

    private synchronized boolean mothod() {
        //该方法是非静态的,其锁对象是this,因为类只创建了一个对象,所以其锁对象是唯一的
        if(ticket == 100){
          return true;
        }else {
            ticket++;
            System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票!!!");

        }
        return false;

    }
}

package com.thread.mothod1;

public class MyThreadTest4 {
    public static void main(String[] args) {
        /*
        需求:
            某电影院目前正在上映国产大片,公有100张票,而他有3个窗口买票,请设计一个程序模拟卖票(利用同步方法完成)
            技巧:可以先写同步代码块,然后将同步代码块抽取成同步方法
         */
        MyThread4 tt = new MyThread4();
        Thread t1 = new Thread(tt,"窗口1");
        Thread t2 = new Thread(tt,"窗口2");
        Thread t3 = new Thread(tt,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

掌握了前面的知识,我们就可以里面StringBuffer 和StringBuilder2个类的异同点了

StringBuilder和StringBuilder2个类的方法大多一样,但是StringBulder用于多个线程是不安全的。在了解到StringBulder和StringBuffer的源码可以看到StringBuffer的每个方法都使用了sychronized修饰表示是多线程,线程不同步的,线程不安全

  • 2个类的选择问题
    1.如果我们的代码是单线程的,不需要考察多线程中数据安全的问题,就使用StringBuilder
    2.如果是多线程环境下,需要考虑多线程环境下数据安全问题,就选择StringBuffer

Lock锁

在前面的同步代码块中可以看出,当我们的线程进入锁的时候锁会自动关闭,当我们的线程出锁的时候,锁将会自动打开。这个锁的关闭能不能我们手动控制呢?

问题1:当没有使锁对象静态化

在这种情况下我们每一个线程所对应的锁是不一样的,将会导致重复票号和超出范围的票号再次出现

解决方案:静态化锁对象

  • 这次又会出现以下情况:

    没错在这种情况下我们的程序无法停止运行

进行我们对代码的分析可以知道:当ticket==100的时候会执行breaks语句,而没有执行unLock(开锁),而后面的2条线程会一直在锁外面进行等待。本质就是锁的资源没有进行释放

  • 可以使用try...catch..finally
package com.thread.mothod1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread3 extends Thread{
    static int ticket = 0;//记录票数
   static Lock locks = new ReentrantLock();

    @Override
    //锁对象是任意的,只需要保证该对象是唯一i的
    public void run(){
      while (true){

       // synchronized (MyThread3.class){
          locks.lock();
          try {
              if(ticket<100){

                      Thread.sleep(100);

                  ticket++;
                  System.out.println(getName()+"正在卖出第"+ticket+"张票");
              }else {
                  break;
              }
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          } finally {
              locks.unlock();
          }





      }
    }

}

package com.thread.mothod1;

public class MyThreaTest3 {
    public static void main(String[] args) {
        //某电影院正在上映国产大片,共有100张票,而他有3个窗口买票,请设计一个程序模拟电影院买票
        //3个窗口很明显可以设计3个线程来实现
       MyThread3 t1 = new MyThread3();
       MyThread3 t2 = new MyThread3();
       MyThread3 t3 = new MyThread3();
       t1.setName("窗口1");
       t2.setName("窗口2");
       t3.setName("窗口3");
       t1.start();
       t2.start();
       t3.start();

    }
}

这样就不会出现问题了

死锁


死锁指的是一个锁里面嵌套了一个锁,是一种错误

在以后下锁的时候不要让2个锁嵌套起来,如果嵌套将有可能导致死锁的情况


当线程A抢到cpu进入A锁,在还没有打开B锁的时候,线程B抢到cpu打开B锁,当要打开A锁是时候发现A锁关闭,于是在等待A锁打开。此时的线程A也在等待B锁打开。这就是死锁

等待唤醒机制的思路分析


等待唤醒机制可以打破线程随机执行,使得线程的执行变得有序


用生产者和消费者各自表示一个线程,用吃货和厨师来表示,通过桌子来控制执行。如果桌子上有食物即吃货执行,如果没有则厨师执行

生产者和消费者的理想情况

在理想的情况下,桌子上没有面条,厨师做一碗面条,吃货吃一碗,以此反复

情况--消费者等待


当我们的消费者先抢到执行权,桌子上没有面条将会进入等待wite,此时生产者执行,做了一碗面条,此时消费者还在等待,此时生产者需要唤醒消费者执行

  • 消费者和生产者代码执行流程

    情况--生产者等待

当厨师第一次抢到了执行权,桌子上没有食物,然后1.制作食物2.把食物放到桌子上3.叫醒等待的消费者开吃。然后我们在下一次中又是厨师抢到了执行权。但是桌子上有食物,然后厨师进入等待食物被吃掉(厨师进入等待后执行权被吃货抢走)

  • 等待唤醒机制完整的执行过程

等待唤醒机制(消费者代码实现)

  • 消费者
package com.cook.foodie;

public class Foodie extends Thread{
    @Override
    public void run(){
        /*
        1.循环
        2.通过代码块
        3.判断共享数据是否到了末尾(到了末尾)
        4.判断共享数据时候到了默认(没有到默认执行核心逻辑)
         */
        //    1.循环
        while (true){
            //2.通过代码块
            synchronized (Desk.lock){
                //3.判断共享数据是否到了末尾(到了)
                if(Desk.count == 0){
                    break;
                }else {
                    //4.执行核心逻辑
                    //先判断桌子上是否有面条
                    if(Desk.foodFlag == 0){
                        //如果没有面条继续等待
                        try {
                            Desk.lock.wait();//此时的wait方法务必用锁对象调用
                            //表示当前线程和锁进行绑定(唤醒的时候就只唤醒和这把锁绑定的线程)
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else {
                        //如果有面条就吃面条
                        Desk.count--;
                        System.out.println("吃货正在吃面条,还能吃"+Desk.count+"碗面条");
                        //吃完后唤醒厨师继续做面条
                        Desk.lock.notifyAll();//唤醒和这把锁绑定的线程
                        //修改桌子的状态
                        Desk.foodFlag = 0;//将桌子的状态改为没有面条
                    }



                }
            }
        }
    }
}

  • 生产者
package com.cook.foodie;

public class Cooker extends Thread{
    @Override
    public void run(){
         /*
        1.循环
        2.通过代码块
        3.判断共享数据是否到了末尾(到了末尾)
        4.判断共享数据时候到了默认(没有到默认执行核心逻辑)
     */
        while (true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else {
                    //判断桌子上是否有食物
                    if(Desk.foodFlag == 1){
                        //如果有就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else {
                        //如果没有就制作食物
                        System.out.println("制作了一碗食物");
                        //通知(唤醒)消费者吃食物
                        Desk.lock.notifyAll();
                        Desk.foodFlag = 1;
                    }



                    //修改桌子上的食物状态
                }
            }
        }
    }


}

-控制者(Desk)

package com.cook.foodie;

public class Desk {
    //作用:控制生产者和消费者的执行
    //是否有面条 0 表示没有面条 1 表示有面条
    public static int foodFlag = 0;
    //总数量
    public static int count = 10;//一共只能吃10碗面
    //锁对象
    public static Object lock = new Object();
}

  • 测试类
package com.cook.foodie;
//测试类(唤醒机制的演示)
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程对象
        Cooker c = new Cooker();
        Foodie f = new Foodie();
        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");
        //开启线程
        c.start();
        f.start();
    }
}

前面的是等待唤醒机制最基本的实现,下面以一种新的方式来实现

等待换新机制(阻塞队列方式实现)

  • 阻塞队列的继承结构

put方法里面已经实现了锁,所以不用写同步代码块

我们将打印语句放到锁的外面造成了连续打印,其实我们的过程是没有问题的

  • Cookder
package com.cook.foodie1;

import java.util.concurrent.ArrayBlockingQueue;

//要求对于厨师和吃货使用的都是同一个阻塞队列
public class Cooker extends Thread{
    ArrayBlockingQueue<String> queue ;
    public Cooker(ArrayBlockingQueue queue){
        this.queue= queue;
    }
    @Override
    public void run(){
      while (true){
          try {
              //不断的向队列里面放面条

              queue.put("面条");
              System.out.println("厨师放了一碗面条");
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
    }
}

  • Foodie
package com.cook.foodie1;

import java.util.concurrent.ArrayBlockingQueue;

public class Foodie extends Thread {
   ArrayBlockingQueue<String> queue;//定义阻塞队列
   public Foodie(ArrayBlockingQueue<String> queue){
       this.queue = queue;
   }
    @Override
    public void run (){
        while (true){
            try {
               //不断从阻塞队列中获取食物
                final String food = queue.take();//take方法的底层也有锁
                System.out.println(food);

            } catch (InterruptedException e) {
               e.printStackTrace();
            }
        }
    }
}

  • 测试类
package com.cook.foodie1;

import java.util.concurrent.ArrayBlockingQueue;

//用阻塞队列实现多线程的等待唤醒机制
public class FoodieTest {
    public static void main(String[] args) {
    //细节:生产者和消费者必须使用同一个阻塞队列
        //1.创建阻塞队列并指定容量
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        //2.创建线程对象并转递阻塞队列
        Cooker c = new Cooker(queue);
        Foodie f = new Foodie(queue);
        //3.开启线程
        c.start();
        f.start();
    }
}
//没有确定可以吃多少碗,所以程序是个死循环

程序会出现连续打印,这对我们的程序安全没有影响,应为我们的打印语句放到了同步代码块外面了

多线程的6种状态

当抢到cpu的执行权, JVM将会把线程交给操作系统去执行,所以没有定义运行状态

综合练习

package com.cook.prictice;
//1000张票,2个窗口领取,用多线程模拟卖票的过程并打印剩余电影票
public class MyThread1 implements Runnable {
    public  static int ticket = 0;//已经已经卖的票数
    @Override
    public void run() {
        while (true){
            synchronized (MyThread1.class){
                if(ticket<100){
                    ticket++;
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票 "+"还剩下"+(100-ticket)+"张票");
                }else {
                    break;
                }
            }

        }
    }
}

  • 测试类
package com.cook.prictice;

public class MyThreadTest1 {

    public static void main(String[] args) {
        MyThread1 t = new MyThread1();
        Thread t1 = new Thread(t,"窗口1");
        Thread t2 = new Thread(t,"窗口2");
        t1.start();
        t2.start();
    }
}

package com.cook.prictice;

public class MyThread2 implements Runnable{
    public static int count = 100;//还剩余礼品的数量
    @Override
    public void run() {
        while (true){
            synchronized (MyThread2.class){
                if(MyThread2.count < 10){

                    break;
                }else {
                    count--;
                    System.out.println(Thread.currentThread().getName()+"正在发送第 "+(100-count)+"件礼品"+"还剩下"+count+"件礼品");
                }
            }

        }
    }
}

  • 测试类
package com.cook.prictice;
//100份礼物,2人同时发送,当礼品数量小于10的时候不再发送
//利用多线程模拟该过程并将线程的名字和礼物剩余的数量打印出来
public class MyThreadTest2 {
    public static void main(String[] args) {
        MyThread2 t = new MyThread2();
        Thread t1 = new Thread(t,"小刘");
        Thread t2 = new Thread(t,"小李");
        t1.start();
        t2.start();
    }
}

package com.cook.prictice;

public class MyThread3 implements Runnable{
    public static int num = 1;
    @Override
    public void run() {
        while (true){
            synchronized (MyThread2.class){
                if(num>100){
                    break;
                }else {
                    if(num %2 ==1){
                        System.out.println(num);
                    }
                    num++;
                }
            }

        }
    }
}

  • 测试类
package com.cook.prictice;
//同时开启2个线程,共同获取1-100之间的所有数字
//要求输出所有的奇数
public class MyThreadTest3 {
    public static void main(String[] args) {
        MyThread3 t = new MyThread3();
        Thread t1 = new Thread(t,"线程1");
        Thread t2 = new Thread(t,"线程2");
        t1.start();
        t2.start();
    }
}


标记该题

package com.cook.prictice;

import java.util.Random;

public class MyThread4 implements Runnable {
    public static double money = 100.0;//红包总额共享
    public static int count = 3;//红包数量共享
    public static final double MIN = 0.01;//最低红包数额


//需要修改成精确计算
    @Override
    public void run() {
        //同步代码块
        synchronized (MyThread4.class) {
            if (count == 0) {
                //判断共享数据是否到了末尾(已经到了末尾)
                System.out.println(Thread.currentThread().getName() + "没有抢到红包!");
            } else {
                //判断数据是否到了末尾(没有到末尾)
                //定义一个变量表示表示中奖的金额
                double prize = 0;
                if (count == 1) {
                    //此时表示最后一个红包,无需随机(剩下的钱就是中奖金额)
                    prize = money;

                } else {
                    //表示第一 二 次随机
                    Random r = new Random();
                    //必须保证3个包都有钱(第一次最多抽取99.8元)
                    final double value = r.nextInt(10000-(count-1))*0.01 + 1;//0 9998
                   prize = value;





                }
                if (prize < MIN) {
                    prize = MIN;

                }
                money = money - prize;
                count--;


                System.out.println(Thread.currentThread().getName() + "抢了" + prize + "元");
            }
        }

    }
}

package com.cook.prictice;

import java.util.Random;

/*
100块的红包分成3个包,现在5个人去抢
红包是共享数据,5个人是5条线程
打印结果如下:
xxx 抢到了xxx元
xxx 抢到了xxx元
xxx 抢到了xxx元
xxx没有抢到
 */
public class MyThreadTest4 {
    public static void main(String[] args) {
     MyThread4 t = new MyThread4();
     Thread t1 = new Thread(t,"客户1");
     Thread t2 = new Thread(t,"客户2");
     Thread t3 = new Thread(t,"客户3");
     Thread t4 = new Thread(t,"客户4");
     Thread t5 = new Thread(t,"客户5");
     t1.start();
     t2.start();
     t3.start();
     t4.start();
     t5.start();


    }
}


这个代码还是有一点问题,就是当客户3抢的钱不足0.01,将会按照0.01计算,可能会导致抢到的钱的总和>100,将会导致数据不精确。这时我们可以使用大数类来解决这一点

package com.cook.prictice;

import java.util.ArrayList;
import java.util.Random;

//抽奖池里面的奖品原则上可以时候数组或集合储存
//但是考虑到每次抽取的奖品不能重复,用数组不好去重,所有时候集合储存
public class MyThread5 extends  Thread {
 ArrayList<Integer> list = new ArrayList<>();
   //如果将储存集合数组的过程写在run中,每一次都将添加一遍数据
   public MyThread5(ArrayList<Integer> list){
       this.list = list;
   }
    @Override
    public void run() {
        while (true){
            synchronized (MyThread5.class){
                if(list.isEmpty()){
                    break;
                }else {
                    //使用Collections中的shuffle方法进行打乱集合
                    Random r = new Random();
                    final int index = r.nextInt(list.size());//随机产生一个下标
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //获取数据和删除其实可以写在一步
                    final Integer value = list.get(index);//随机得到集合中的元素
                    list.remove(index);//删除该下标对应的元素
                    System.out.println(getName()+"产生了一个"+value+"元的大奖");

                }
            }

        }
    }
}

  • 测试类
package com.cook.prictice;

import java.util.ArrayList;
import java.util.Collections;

/*
在一个抽奖池中存放了任意奖励的金额
设立2个抽奖箱线程,随机从抽奖池中抽奖,并打印在控制台中
 */
public class MyThredTest5 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,15,20,5,50,100,2,80,300,700,1000);
       MyThread5 t1 = new MyThread5(list);
       MyThread5 t2 = new MyThread5(list);
       t1.setName("抽奖箱1");
       t2.setName("抽奖箱2");
       t1.start();
       t2.start();
    }
}

package com.cook.prictice;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

//抽奖池里面的奖品原则上可以时候数组或集合储存
//但是考虑到每次抽取的奖品不能重复,用数组不好去重,所有时候集合储存
public class MyThread5 extends  Thread {
 ArrayList<Integer> list = new ArrayList<>();
 ArrayList<Integer> list1 = new ArrayList<>();//用于储存抽奖箱1的抽奖结果
 ArrayList<Integer> list2 = new ArrayList<>();//用于储存抽奖箱2的抽奖结果

   //如果将储存集合数组的过程写在run中,每一次都将添加一遍数据
   public MyThread5(ArrayList<Integer> list){
       this.list = list;
   }
    @Override
    public void run() {
        while (true){
            synchronized (MyThread5.class){
                if(list.isEmpty()){
                    if("抽奖箱1".equals(getName())){
                        System.out.println("抽奖箱1 "+list1);
                       Integer max = Collections.max(list1);
                        System.out.println("最高金额为"+max+"元");
                    }else {
                        System.out.println("抽奖箱2 "+list2);
                        Integer max = Collections.max(list2);
                        System.out.println("最高金额为"+max+"元");
                    }


                    break;
                }else {
                    //使用Collections中的shuffle方法进行打乱集合
                    Random r = new Random();
                    final int index = r.nextInt(list.size());//随机产生一个下标
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //获取数据和删除其实可以写在一步
                    final Integer value = list.get(index);//随机得到集合中的元素
                    list.remove(index);//删除该下标对应的元素
                    if(getName().equals("抽奖箱1")){
                        list1.add(value);
                    }else list2.add(value);


                }
            }

        }
    }
}

package com.cook.prictice;

import java.util.ArrayList;
import java.util.Collections;

/*
在一个抽奖池中存放了任意奖励的金额
设立2个抽奖箱线程,随机从抽奖池中抽奖,并打印在控制台中
 */
public class MyThredTest5 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,15,20,5,50,100,2,80,300,700,1000);
       MyThread5 t1 = new MyThread5(list);
       MyThread5 t2 = new MyThread5(list);
       t1.setName("抽奖箱1");
       t2.setName("抽奖箱2");
       t1.start();
       t2.start();
    }
}


上面的方法当我们的线程过多的时候,代码就太复杂了,我们可以时候线程栈来优化(优化重要,理解线程的内存结构)

对于储存所抽取的钱可以不用创建2个集合进行储存,而是可以在run方法中创建一个集合,利用线程栈的特性,代码可用性更高

形成了每一个线程都有自己的集合。当开启线程时,在内存中将出现该线程的栈,并且每一个线程的栈独立

package com.cook.prictice;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

//抽奖池里面的奖品原则上可以时候数组或集合储存
//但是考虑到每次抽取的奖品不能重复,用数组不好去重,所有时候集合储存
public class MyThread5 extends  Thread {
 ArrayList<Integer> list = new ArrayList<>();
 //ArrayList<Integer> list1 = new ArrayList<>();//用于储存抽奖箱1的抽奖结果
 //ArrayList<Integer> list2 = new ArrayList<>();//用于储存抽奖箱2的抽奖结果

   //如果将储存集合数组的过程写在run中,每一次都将添加一遍数据
   public MyThread5(ArrayList<Integer> list){
       this.list = list;
   }
    @Override
    public void run() {
       ArrayList<Integer> list1 = new ArrayList<>();
        while (true){
            synchronized (MyThread5.class){
                if(list.isEmpty()){

                    System.out.println(getName()+list1);


                    break;
                }else {
                    //使用Collections中的shuffle方法进行打乱集合
                    Random r = new Random();
                    final int index = r.nextInt(list.size());//随机产生一个下标
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //获取数据和删除其实可以写在一步
                    final Integer value = list.get(index);//随机得到集合中的元素
                    list.remove(index);//删除该下标对应的元素
                   list1.add(value);


                }
            }

        }
    }
}

  • 测试类
package com.cook.prictice;

import java.util.ArrayList;
import java.util.Collections;

/*
在一个抽奖池中存放了任意奖励的金额
设立2个抽奖箱线程,随机从抽奖池中抽奖,并打印在控制台中
 */
public class MyThredTest5 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,15,20,5,50,100,2,80,300,700,1000);
       MyThread5 t1 = new MyThread5(list);
       MyThread5 t2 = new MyThread5(list);
       t1.setName("抽奖箱1");
       t2.setName("抽奖箱2");
       t1.start();
       t2.start();
    }
}


这里同样可以得到一样的结果,并且当我们的抽奖箱数量变化时,我们的代码同样适用


(意思就是run方法返回线程1 和线程2 所抽奖的最大值(只能使用多线程的第三种创建方式)


该代码比较简单,在 后面复习的时候补全

线程池


在前面中我们需要线程就创建,用完线程就消失了,这其实极大得浪费了系统的资源。可以创建线程池将线程储存起来


核心原理:当我们给线程池提交一个任务的时候,线程池将会自动创建一个线程,将会拿着这个线程去执行任务,执行完了将把线程还回去。第二次再提交一个线程的时候,就不用再创建一个线程了,而是拿着已经存在的线程去执行任务

  • 特殊情况

当我们提交任务的时候,线程还没有还回去,此时将会在线程池中创建新的线程以运行新提交的任务

我们线程池可以指定最大的创建线程的数量,当我们的线程数量达到最大值,有新的任务的时候,我们的任务将会等待线程使用完毕


font color =red>注意:在我们提交任务的时候,在线程池的底层会去创建线程或者是复用已经存在的线程,这个在底层自动完成

对于第三步:在我们的实际生活中服务器其实24小时都在运作,24小时都需要工作,所有我们的线程池也需要24小时都运作,所有我们的线程池没有必要关闭

没有上限的线程池并不是真的没有上限,其最大的上限为int取值的最大值

  • 测验线程复用(但用空闲的线程将直接复用而不去创建新的线程)
package com.cook.foodie1;

public class MyThread implements Runnable{
    @Override
    public void run() {

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


    }
}

package com.cook.foodie1;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        /*
        Executors:线程池的工具类,通过调用方法返回不同类型的线程池对象
        public static ExecutorService newCachedThreadPool()创建没有上限的线程池
        public static ExecutorService newCachedThreadPool(int nThreads)创建有上限的线程池

         */
        //创建一个没有上限的线程池
        final ExecutorService pool1 = Executors.newCachedThreadPool();//获取一个没有上限的线程池
        //2.提交任务(在底层会在线程池中创建线程)
        pool1.submit(new MyThread());
        Thread.sleep(1000);
        pool1.submit(new MyThread());
        Thread.sleep(1000);
        pool1.submit(new MyThread());
        Thread.sleep(1000);
        pool1.submit(new MyThread());
        Thread.sleep(1000);
        pool1.submit(new MyThread());





        //3.销毁线程池
       // pool1.shutdown();线程池一般不会销毁
    }

}

当我们提交第一个任务的时候,代码还没到执行第二个任务的时候,第一个任务执行完毕,线程归还,后面的任务可以直接复用

当我们的创建指定线程数量的线程池就最多只会创建3个线程

自定义线程池

在前面我们是通过工具类创建线程池,在这种情况下我们不能对线程池的参数进行设置,会不太方便。对此引入了自定义线程池

餐厅实行1对1服务

当我们提交了3个任务,线程池将和会创建3条线程处理这3个任务

当我们提交了5个任务,会创建3条线程处理,剩下的2条线程将会在后面排队


当有8个任务时,队伍已经排满了,将会创建临时线程处理
(核心线程都在忙,队伍排满了,将会创建临时线程)

先提交的任务一定是先执行的吗?

** no 如上图,任务3 5 6 还在队伍中排列,而后提交的7 8 已经在执行了**

  • 任务舍弃策略情况

    当1.核心线程都在工作,2.任务队列都排满了,3.临时线程都在工作 而此时还有任务没有进行,将会触发任务的舍弃策略,经该任务舍弃



内部类是依赖外部类而存在的,单独出现没有任务意义,而且内部类又是一个独立的个体

package com.cook.foodie;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;



//验证自定义线程池的方法
public class MyThreadTest2 {
    public static void main(String[] args) {
        /*
       ThreadPoolExcutor threadPoolExecutor = new ThreadPoolExcutor
       参数1:核心线程数量           不能小于0
       参数2 :最大线程数            不能小于等于0 最大数量>=核心线程数量
       参数3:空闲线程最大存活时间    不能小于0
       参数4:时间单位               用TimeUnit指定
       参数5:任务队列(阻塞队列)               不能为null
       参数6:创建线程工厂           不能为null
       参数7:任务拒绝策略          不能为null
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                3,//核心线程数量           不能小于0
                6,//最大线程数            不能小于等于0 最大数量>=核心线程数量
                60,//空闲线程最大存活时间
                TimeUnit.SECONDS,//空闲线程存活时间的单位(秒)
                new ArrayBlockingQueue<>(3),//阻塞队列并且长度为3
                Executors.defaultThreadFactory(),//创建线程工厂
              new ThreadPoolExecutor.AbortPolicy()//AbortPolicy是内部类

            //之后提交任务,和前面的一样

        );
    }
}

最大并行数



4核就好比cpu有四个大脑能同时去做4件事,但是intel发明了超线程技术,可以将原来的4个大脑虚拟成8个,虚拟成的8给就是8线程。即4核8线程最大的并行数为8

.

  • 查看
    在设备管理器中查看处理器的数量可以发现是8


在任务管理器中查看cpu也可以发现逻辑处理器是8

在极少数的操作系统中不会把不会把cpu所有的资源交给一个软件用,所有光从任务管理器中看,还不是很稳妥

  • 在java中用代码查看
package com.cook.foodie;

public class MyThread2 {
    public static void main(String[] args) {
        //向java虚拟机返回可用处理器的数目
        final int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);//8
    }
}

可以发现可用的逻辑处理器的数目就是8

线程池多大合适

将我们的项目分为2种类型

  • cpu密集型
    我们的项目中运算比较多,读取本地文件 读取数据库的操作比较少

为什么线程池数量要+1?为了防止前面的线程由于故障暂停,这个可用作为候补选手

  • I/O密集型(现在这样的项目比较多)
    我们的项目运算不是很多,读取本地文件或数据可比较多


    这个时间一般通过工具测试得到

多线程额外扩展内容(开发中用的少,面试的时候问的多)

posted @ 2023-03-08 22:41  一往而深,  阅读(56)  评论(0编辑  收藏  举报