Java:26-多线程

多线程

程序和进程的概念------------------------------
程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件
进程 - 主要指运行在内存中的可执行文件
目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务
但进程是重量级的,也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限
通常一个进程包含一个或者多个线程,而一个进程可能占用一个或者多个端口
但一个端口基本只能由一个进程占用(如果以后说明一个进程基本占用一个端口,那么就是这个意思,虽然可以占用多个)
要注意:只有与网络有关的进程才需要占用端口号,因为端口只是操作网络的而已,对于内部的其他操作,进程并非一定操作端口哦,反正是线程组(当然,进程也包含其他东西,如资源分配操作(因为我们可以继续创建线程),所以也不只是线程组,这也是为什么我们大多会称为进程,而不是线程组的原因)
线程的概念------------------------------
为了解决上述问题就提出线程的概念,线程就是进程内部的程序流
也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的
线程是轻量的,新建线程会共享所在进程的系统资源(如CPU和内存空间),也就是说一个进程的总资源是有限的,一般是该进程获取的(具体如何获取可以百度,这不是java要做的事情),因此目前主流的开发都是采用多线程
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制(单核来说)
对于单核CUP来说:多个线程就是看起来是一起运行的,但是实际上没有一起运行
因为运行速度太快,看不出来(仔细一看不是一起运行的,粗略一看是一起运行的)
对于多核CUP来说(一个cpu可以有多个核心,各个核心可以操作线程,也可以存在多个cpu):多个线程若分配与两个或者两个以上的CPU同时运行,一般核心也算,那么就有可能真正的同时运行
如两个线程分别由各自的CPU来操作,只是占资源,操作由CPU操作,一般我们以多核为主,因为现在的电脑基本上都是多核
对于进程来说,我们通常在进程里运行多个线程,且线程占用的CPU和内存空间资源都是由进程的CPU和内存空间资源分配的
而进程的CPU和内存资源都是由系统资源分配,如一些电脑的内存空间大小有8G或者16G内存
而CPU资源也要看型号或者主频,外频,总线频率
线程的创建------------------------------
public class Thread
extends Object
implements Runnable
java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性
创建方式------------------------------
自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象
然后使用Thread类型的对象调用start方法
相关的方法------------------------------
Thread(),使用无参的方式构造对象
Thread(String name),根据参数指定的名称来构造对象
Thread(Runnable target),根据参数指定的引用来构造对象,其中Runnable是个接口类型
Thread(Runnable target,String name),根据参数指定引用和名称来构造对象
void run(),若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本
若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做
void start(),用于启动线程,Java虚拟机会自动调用该线程的run方法
package com.lagou.task18;

public class ThreadTest {

    public static void main(String[] args) {

        // 1.使用无参方式构造Thread类型的对象
        // 由源码可知:Thread类中的成员变量target的数值为null。
        //Thread(),使用无参的方式构造对象
        Thread t1 = new Thread();
        Thread t2 = new Thread("1"); //最后run也是不运行,target还是null
        // 2.调用run方法进行测试
        // 由源码可知:由于成员变量target的数值为null,因此条件if (target != null)不成立
        //跳过{}中的代码不执行
        //  而run方法中除了上述代码再无代码,因此证明run方法确实啥也不干
        //void run(),若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本
        //若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做
        t1.run();
        // 3.打印一句话
        System.out.println("我想看看你到底是否真的啥也不干!");
    }
}

执行流程------------------------------
执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个
新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响,当run方法执行完毕后子线程结束
当main方法执行完毕后主线程结束,一般两个线程(一般不包括主线程)执行没有明确的先后执行次序,由操作系统调度算法来决定
可以看作谁先用CPU执行,即也可以认为是随机的,这里之所以不包括主线程,因为主线程必然是先占用cpu的,就相当于在线程里进行操作创建线程,而不是同时进行操作,所以主线程基本会优先
即对于单核来说:虽然在main里面创建的线程,但还会与main主线程抢占CPU资源,即谁先抢占(操作系统调度算法),谁先执行
不会因为该线程是主线程的子线程而不进行抢夺
如:它先分时间片给第一个调用,然后时间到了,把第一个踢出去
再把时间片分给第二个调用,时间到了又把第二个踢出去
又让第一个来,就好像2个人排队玩游戏,一人5分钟
即时间片是先分配好的,然后再继续分配,通常叫做抢占,即它通常会依次运行,若你运行时,程序有输出多条的
基本上是多核或者程序运行时间的差异,最有可能是优先级的关系(优先级大分配的时间多,即一般总体可以运行的时间久)
若还是会有一个输出两次的,则应该是内部运行时间的差异
那么就有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
但对于多核来说:就会同时运行了,但一开始还是会先占CPU的先运行(注意:一般不是抢占对方的,极少数情况下,与单核一样,抢占同一个,或者所有的CPU都有线程了,那么在对应同一个CPU下,可以认为是单核了,该cpu可以指核心或者不同cpu),只不过之后不会再依次运行,因为由不同CPU操作了(不同的核,可以称为不同的CPU),也就是说,多核情况下,一个线程占用一个核心,如果核心够多,其他核心是没有被使用的
package com.lagou.task18;

public class SubThreadRun extends Thread {

    @Override
    public void run() {
        // 打印1 ~ 20之间的所有整数
        for (int i = 1; i <= 20; i++) {
            System.out.println("run方法中:i = " + i); // 1 2 ... 20
        }
    }
}

package com.lagou.task18;

public class SubThreadRunTest {

    public static void main(String[] args) {

        // 1.声明Thread类型的引用指向子类类型的对象
        Thread t1 = new SubThreadRun();
        // 2.调用run方法测试,本质上就是相当于对普通成员方法的调用因
        //此执行流程就是run方法的代码执行完毕后才能继续向下执行
        //t1.run();
        // 用于启动线程,Java虚拟机会自动调用该线程类中的run方法(子类的版本,因为对象的原因,而不用考虑是否有Runnable的引用了)
        // 相当于又启动了一个线程,加上执行main方法的线程是两个线程
        t1.start();

        // 打印1 ~ 20之间的所有整数
        for (int i = 1; i <= 20; i++) {
            System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20
        }
    }
}

package com.lagou.task18;

public class SubRunnableRun implements Runnable {
    @Override
    public void run() {
        // 打印1 ~ 20之间的所有整数
        for (int i = 1; i <= 20; i++) {
            System.out.println("run方法中:i = " + i); // 1 2 ... 20
        }
    }
}

package com.lagou.task18;

public class SubRunnableRunTest {

    public static void main(String[] args) {

        // 1.创建自定义类型的对象,也就是实现Runnable接口类的对象
        SubRunnableRun srr = new SubRunnableRun();
        // 2.使用该对象作为实参构造Thread类型的对象
        // 由源码可知:经过构造方法的调用之后,Thread类中的成员变量target的数值为srr
        //Thread(Runnable target),根据参数指定的引用来构造对象,其中Runnable是个接口类型
        Thread t1 = new Thread(srr);
        // 3.使用Thread类型的对象调用start方法
        // 若使用Runnable引用构造了线程对象,调用该方法(run)是最终调用接口中的版本
        // 由Thread类的run方法的源码可知:if (target != null) {
        //                         target.run();
        //                    }
        // 此时target的数值不为空这个条件成立,执行target.run()的代码,也就是srr.run()的代码
        t1.start();
        //t1.start();再次的执行会报错,因为只能是一个线程操作该方法,即基本只能分配一个线程操作
        //srr.start();  Error

        // 打印1 ~ 20之间的所有整数
        for (int i = 1; i <= 20; i++) {
            System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20
        }
    }
}

方式的比较------------------------------
继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类
而实现Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式
匿名内部类的方式------------------------------
使用匿名内部类的方式来创建和启动线程
package com.lagou.task18;

public class ThreadNoNameTest {

    public static void main(String[] args) {
        //匿名内部类可以看成没有名字的内部类
        //是内部类的简化写法,它的本质是一个带具体实现的父类或者父接口的匿名的子类对象
        //即相当于指向子类
        // 匿名内部类的语法格式:父类/接口类型 引用变量名 = new 父类/接口类型() { 方法的重写 };
        // 1.使用继承加匿名内部类的方式创建并启动线程
        /*Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("张三说:在吗?");
            }
        };
        t1.start();*/
        new Thread() { //可以凭空创建对象并在当前直接使用,只是我们没有变量可以操作他(地址)了而已
            @Override
            public void run() {
                System.out.println("张三说:在吗?");
            }
        }.start();

        // 2.使用实现接口加匿名内部类的方式创建并启动线程
        Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("李四说:不在。");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("李四说:不在。");
            }
        }).start();*/
        // Java8开始支持lambda表达式: (形参列表)->{方法体;}
        /*Runnable ra = ()-> System.out.println("李四说:不在。");
        new Thread(ra).start();*/

        new Thread(()-> System.out.println("李四说:不在。")).start();
    }
}

线程的生命周期------------------------------

在这里插入图片描述

新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行
就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行(或者说还没有准备执行run方法)
运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行
当线程的时间片执行完毕后,任务没有完成时,回到就绪状态((可以认为是两个线程或者多个线程互相抢夺时间片,注意这个时间片只是一开始的,即与开始的线程进行抢夺,之后就不考虑时间片了,除非再次的抢)
消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止
阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法,阻塞状态解除后进入就绪状态
也就是说:
新建状态:NEW(在Thread里面的,State枚举里面可以知道)
就绪状态和运行状态:RUNNABLE
阻塞状态:BLOCKED,WAITING,TIME_WAITING
消亡状态:TERMINATED
即不管怎样,无论你是阻塞还是没有在运行完,最后都要重新在运行的地方开始,即就绪状态,但并没有清除你以前的行为
且创建时,只要是关于Thread类的,即调用了他的构造方法,基本上就是创建一个线程
注意:若直接输出直接量,且放在开启线程(start方法)后面,基本上就会先运行该输出的值,因为你在运行run方法时
需要输出的时间远远大于直接输出直接量,所以通常会发现你run在运行时,还没输出数值时,他就输出了(多核来说,单核也会)
实际上是他快
但对于单核,则有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
线程的编号和名称------------------------------
long getId(),获取调用对象所表示线程的编号
String getName(),获取调用对象所表示线程的名称
void setName(String name),设置/修改线程的名称为参数指定的数值
static Thread currentThread(),获取当前正在执行线程的引用
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称
然后将线程的名称修改为"zhangfei"后再次打印编号和名称。要求在main方法中也要打印主线程的编号和名称
package com.lagou.task18;

public class ThreadIdNameTest extends Thread {

    public ThreadIdNameTest(String name) {
        super(name); // 表示调用父类的构造方法
    }

    @Override
    public void run() {
        //long getId(),获取调用对象所表示线程的编号
        //String getName(),获取调用对象所表示线程的名称
        System.out.println("子线程的编号是:" + getId() + ",名称是:" + getName()); 
        // 14  Thread-0 guanyu
        // 修改名称为"zhangfei"
        //void setName(String name),设置/修改线程的名称为参数指定的数值
        setName("zhangfei");
        System.out.println("修改后子线程的编号是:" + getId() + ",名称是:" + getName()); 
        // 14  zhangfei
    }

    public static void main(String[] args) {

        ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
        tint.start();

        // 获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用
        //static Thread currentThread(),获取当前正在执行线程的引用,native的方法,具体实现操作可以百度,反正是可以实现,因为都是人写的
        Thread t1 = Thread.currentThread();
        //获取该线程,但并没有创建,即可以说没有调用构造方法
        System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
        System.out.println(System.currentTimeMillis());
    }
}

package com.lagou.task18;

public class RunnableIdNameTest implements Runnable  {
    @Override
    public void run() {
        // 获取当前正在执行线程的引用,也就是子线程的引用
        //必须是start方法调用才可算是真正的执行线程,否则就只有创建线程
        //通过t2执行
        Thread t1 = Thread.currentThread();
        System.out.println("子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName()); 
        // 14 guanyu
        //Thread t2 = new Thread(rint, "guanyu") 源码里,最后还是RunnableIdNameTest来调用run方法
        //若RunnableIdNameTest继承Thread的话,用this.getName()就不是guanyu了(改变的话)
        //因为继承了Thread就是创建了线程,即不是guanyu,但是还没有去执行,则必须要start方法执行
        //否则Thread.currentThread()方法,获得的不是你的引用,而是其他线程到该代码的引用
        t1.setName("zhangfei");
        System.out.println("修改后子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
        // 14 zhangfei
    }

    public static void main(String[] args) {

        RunnableIdNameTest rint = new RunnableIdNameTest();

        //Thread t2 = new Thread(rint);
        Thread t2 = new Thread(rint, "guanyu");
        t2.start();

        // 获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用
        Thread t1 = Thread.currentThread();
        System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
    }
}

常用的方法------------------------------
static void yield(),当前线程让出处理器(离开Running状态),使当前线程进入Runnable状态等待,不占用CPU资源(相当于我直接退出重新抢夺时间片,但是由于他并不是重新调度,所以还是同一个cpu,即占用资源,但是随着时间发展,可能也会在某个时候改变cpu核心,只是可能性很少),但在对应的地方直接等待了(下次到对应等待的yield后面执行)
static void sleep(times),使当前线程从 RunningRunnable,这是一个状态,要注意哦) 放弃处理器进入Block状态,休眠times毫秒
再返回到Runnable如果其他线程打断当前线程的Block(sleep)(还是使用当前线程的引用,比如可以考虑静态的), 就会发生InterruptedException
上面对状态的说明,了解即可,具体可以查看101章博客
因为有阻塞状态,即休眠的毫秒数,所以(间接,实际上是没有的)占用CPU资源(解释:指的是监视,而非sleep自身占用(对与自身,在阻塞时自然没有占用cpu资源,也就会释放对应资源,使得cpu资源回收,大多数博客说明的都是这个),一般监视是其他某些线程考虑的,所以这也是为什么大多数博客说他没有占用的原因,但也正是因为这样才会一直占用,而wait什么的是等待唤醒,没有什么一直操作的执行,因为是手动的,所以他完全不占用),因为休眠不结束的话,就会一直运行
但会有他里面代码的内存占用,但占用内存是必然的,其他程序也都需要内存,所以这里不用考虑内存
但是也要注意:休眠本身也是占用资源的,sleep虽然是休眠,他只是不消耗cpu资源,但是他占用者线程资源(线程没有释放),cpu是描述动的时候的资源,你不操作,就不占用,也就是说消耗cpu等价于占用,除了cpu还有内存等等资源(线程资源属于内存,其他资源基本也都是属于内存,虽然也存在不是内存的一些文件访问,网络连接的其他资源,是否可以访问也是资源,任何东西都可以是资源)
int getPriority(),获取线程的优先级
void setPriority(int newPriority),修改线程的优先级
优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些(一般总体时间更长)
除了程序也有运行时间,过大,则会后出结果
最主要的就是设置的优先级,不会真正的优先,就如概率一样,99%正确也会有1%的错误,可以将优先级看成概率
优先级默认为5 ,当然,优先级是看相对的,如果你的优先级是99,他也是99,那么就与55是一样的,所以是相对的,即我占二分之1,即50%概率,一般来说他们概率都相同(5),所以抢占基本随机
注意:线程优先级的影响对多核核心的情况是减弱的,因为核心充沛的情况下,即当处理器核心数量充足且负载较轻时,线程优先级的直接影响可能确实减少,因为大部分或所有线程都可以同时运行,没有必要竞争处理器资源,在这种情况下,每个线程都有可能获得一个核心,从而减少了优先级对实际运行性能的影响,然而,即使在核心充足的环境中,优先级仍然有其潜在的作用,因为其他系统资源(如内存、I/O操作等)可能仍然存在竞争,并且资源并不是一直充沛的,所以线程优先级的设置还是有一定的作用,并且即使核心充沛,优先级可能也会有作用,比如谁先执行,而不是同时处理了
void join(),等待该线程终止,即主线程会等待,但是主线程里的其他子线程有启动的,那么那个子线程会继续运行
而不会等待
void join(long millis),等待参数指定的毫秒数
boolean isDaemon(),用于判断是否为守护线程
void setDaemon(boolean on),用于设置线程为守护线程
其他程序运行时间,也会导致sleep的操作
如我要一个数值输出8次,每停顿一秒输出一次,而主线程停顿8秒,打印主线程等待结束,通常情况下,都会输出8次
但是我在第一次的时候,就写一个很大的循环,可以循环8秒的循环,那么就会出现,只输出一次的结果
即可以对于一般程序来说,基本不会影响,因为他所占的时间对于一秒来说可以忽略不计
如主线程停5秒,子线程停1秒,主线程的5秒会导致子线程的判断条件为false,如下
//子线程
//run方法里:
white(a == true){
System.out.println(1);
Thread.sleep(1000); // 停一秒
}
//主线程
Thread.sleep(5000);
a = false;
//可以知道如下,我将--------八个-表示一秒钟,那么就会出现以下情况
1-------- 1-------- 1-------- 1-------- 1-------- a == ture(不成立)
-------- -------- -------- -------- -------- a =false
//可以发现子线程里的--------在等待时,主线程就结束了,而实际上,输出的1的时间远远小于一个"-",即忽略不计
//但是若这个1所占的"-"够长,
//----------------------------------------------------------------
//若将子线程的输出位置改变,如下
//子线程
//run方法里:
white(a == true){
Thread.sleep(1000); // 停一秒
System.out.println(1);
}
//主线程
Thread.sleep(5000);
a = false;
//可以知道如下,我将--------八个-表示一秒钟,那么就会出现以下情况
--------1 --------1 --------1 --------1 --------1 a == ture(不成立)
-------- -------- -------- -------- -------- a =false
//可以知道当a的值改变时,由于子线程还在等待,即还没运行完,所以输出1后,在退出循环
//且实际上1所占的时间远远小于一个"-",即也是忽略不计
//但是若有一个大的循环,则会只输出第一个1,然后判断a为false,结束循环
//所以得出一个结论,当等待的时间够长,且程序够少,那么就可以直接看成等待秒数的计算
//如主线程等待5秒,子线程等待1秒,输出的值都是5/1次
//但也有一个解释,就是,只要加起来的程序所占时间(即5秒钟内的程序运行时间,其中有相等的1秒钟)
//大于子线程的等待秒数
//则不可以直接用5/1来算次数(根据大多少具体判断)
//就如大的循环一样
//而对于加起来程序所占时间(即5秒钟内的程序运行时间,其中有相等的1秒钟),与等待秒数是否相等问题:
//若正好占一秒,而你等待一秒,则形成无缝连接
//对于第一种情况,就是子线程等待时间会与主线程等待时间相交,那么就会少输出一个1
//除非主线程在子线程判断结束后,等待时间才到,那么就不会
//对于第二种情况,就是输出的程序时间会与主线程等待时间相交,那么就会少输出一个1
//除非主线程在子线程判断结束后,等待时间才到,那么就不会
//即一个将等待时间交接,一个将程序时间交接
//等待时间交接可以数据先处理,然后再直接让出CPU资源,但最后让出CPU资源
//程序时间交接会将数据后处理,最后操作,可能会对数据重新赋值,但会先让出CPU资源
//这些代码认为是单核,但运行时可能是双核(或多核),我的电脑是双核(或多核),但无论是单核还是多核,结果基本不会变化,因为单核的切换的时间非常小,可以忽略
 //实际上说明了这么多,就是一个问题,即程序代码的运行时间不可忽略,当然,这样的不可忽略所针对的情况是比较少的,所以了解即可
package com.lagou.task18;

public class ThreadPriorityTest extends Thread {
    @Override
    public void run() {
        //int getPriority(),获取线程的优先级
        //System.out.println("子线程的优先级是:" + getPriority()); 
        // 5  10  优先级越高的线程不一定先执行。
        for (int i = 0; i < 20; i++) {
            System.out.println("子线程中:i = " + i);
        }
    }


    public static void main(String[] args) {

        ThreadPriorityTest tpt = new ThreadPriorityTest();
        // 设置子线程的优先级
        //void setPriority(int newPriority),修改线程的优先级
        //优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些
        tpt.setPriority(Thread.MAX_PRIORITY);
        tpt.start();

        Thread t1 = Thread.currentThread();
        //System.out.println("主线程的优先级是:" + t1.getPriority()); // 5 普通的优先级
        for (int i = 0; i < 20; i++) {
            System.out.println("--主线程中:i = " + i);
        }
    }

}

package com.lagou.task18;

public class ThreadJoinTest extends Thread {
    @Override
    public void run() {
        // 模拟倒数10个数的效果
        System.out.println("倒计时开始...");
        for (int i = 10; i > 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("新年快乐!");
    }

    public static void main(String[] args) {

        ThreadJoinTest tjt = new ThreadJoinTest();
        tjt.start();

        // 主线程开始等待
        System.out.println("主线程开始等待...");
        try {
            // 表示当前正在执行的线程对象等待调用线程对象,也就是主线程等待子线程终止(当然了,虽然说是主,不如说是当前线程,在线程中,主和子通常代表父子,而并非一定是main,以后都是如此)
            //void join(),等待该线程终止
            //tjt.join();
            //oid join(long millis),等待参数指定的毫秒数
            tjt.join(5000); // 最多等待5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //System.out.println("终于等到你,还好没放弃!");
        System.out.println("可惜不是你,陪我到最后!");
    }
}

package com.lagou.task18;

public class ThreadDaemonTest extends Thread {
    @Override
    public void run() {
        //boolean isDaemon(),用于判断是否为守护线程
        //System.out.println(isDaemon()? "该线程是守护线程": "该线程不是守护线程"); // 默认不是守护线程
        // 当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止
        // 当子线程是守护线程时,当主线程结束后,则子线程随之结束
         //但是子线程不是立即结束,因为主线程在结束时,子线程开始准备结束(主线程需要通知子线程结束)
        //而在通知时,有一些时间,子线程还在运行,即通常会有子线程在运行一些时间后,再停止
        for (int i = 0; i < 50; i++) {
            System.out.println("子线程中:i = " + i);
        }
    }

    public static void main(String[] args) {

        ThreadDaemonTest tdt = new ThreadDaemonTest();
        // 必须在线程启动之前设置(子)线程为守护线程,这个子是对main来说的,正常情况是在线程启动之前设置为守护线程
        //void setDaemon(boolean on),用于设置线程为守护线程
        tdt.setDaemon(true);
        tdt.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("-------主线程中:i = " + i);
        }
    }
}

//一般main方法是非守护线程,因为我们默认是非守护线程的(默认不是守护线程,所以守护线程基本必然是非守护线程里面的,比如因为main就是非守护线程,所以我们通常基本都是在他这个非守护线程里面的)

主线程:是产生其他子线程的线程,通常它必须最后完成执行比如执行各种关闭动作
即可以说守护线程是给主线程操作子线程的一种设置
案例题目------------------------------
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的所有偶数
在main方法启动上述两个线程同时执行,主线程等待两个线程终止
package com.lagou.task18;

public class SubThread1 extends Thread {
    @Override
    public void run() {
        // 打印1 ~ 100之间的所有奇数
        for (int i = 1; i <= 100; i += 2) {
            System.out.println("子线程一中: i = " + i);
        }
    }
}

package com.lagou.task18;

public class SubThread2 extends Thread {
    @Override
    public void run() {
        // 打印1 ~ 100之间的所有偶数
        for (int i = 2; i <= 100; i += 2) {
            System.out.println("------子线程二中: i = " + i);
        }
    }
}

package com.lagou.task18;

public class SubThreadTest {

    public static void main(String[] args) {

        SubThread1 st1 = new SubThread1();
        SubThread2 st2 = new SubThread2();

        st1.start();
        st2.start();

        System.out.println("主线程开始等待...");
        try {
            st1.join();
            st2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束!");
    }
}

package com.lagou.task18;

public class SubRunnable1 implements Runnable {
    @Override
    public void run() {
        // 打印1 ~ 100之间的所有奇数
        for (int i = 1; i <= 100; i += 2) {
            System.out.println("子线程一中: i = " + i);
        }
    }
}

package com.lagou.task18;

public class SubRunnable2 implements Runnable {
    @Override
    public void run() {
        // 打印1 ~ 100之间的所有偶数
        for (int i = 2; i <= 100; i += 2) {
            System.out.println("------子线程二中: i = " + i);
        }
    }
}

package com.lagou.task18;

public class SubRunnableTest {

    public static void main(String[] args) {

        SubRunnable1 sr1 = new SubRunnable1();
        SubRunnable2 sr2 = new SubRunnable2();

        Thread t1 = new Thread(sr1);
        Thread t2 = new Thread(sr2);

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

        System.out.println("主线程开始等待...");
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束!");
    }
}

线程同步机制------------------------------
当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题
即当多个线程需要访问同一个资源时
它们需要以某种顺序来确保该资源在某时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的
此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制
多个线程并发读写同一个临界资源时会发生线程并发安全问题
异步操作:多线程并发(一起进发)的操作,各自独立运行,可以理解位不同的步骤,即不用排队
同步操作:多线程串行(跟着进行)的操作,先后执行的顺序,可以理解为同样的步骤,即排队
对于同步机制,主要针对于多核,因为单核实际上还是依次运行的
对于多个start方法,先调用的一般先执行,只不过现在的电脑处理很快的,你几乎都感觉不到在并发
但也只是先执行的速度快而已,CPU先运算的还是会先执行
解决方案------------------------------
先看后面的代码:
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率
实现方式------------------------------
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性
即要么我不执行,要么等我执行完,且执行过程不可打断,若有多个在等待,那么他们自己之间进行抢夺资源
来保证谁先进入,即也相当于操作系统调度算法来决定
具体方式如下:
使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized(类类型的引用) { //这里的"类类型的引用"的专业术语是:同步监视器,他不能是null,否则报错,无论是变量还是直接的null都是如此
编写所有需要锁定的代码
} 
使用同步方法的方式实现所有代码的锁定,直接使用synchronized关键字来修饰整个方法即可该方式等价于: 
synchronized(this) { 
整个方法体的代码
} 
即直接用synchronized对方法修饰时,就说明这个方法是谁调用谁来当同步监听器,即等价于this
换言之,就是这个方法被锁了,只对于同一引用,因为其他引用调用的this是不同的
即这就是为什么需要一个同步监听器的原因
由于synchronized(类类型的引用)"类类型的引用"是多变的,即不同的引用也会参与同步,如静态的引用
但基本上都会在类里面新建该引用,防止同一引用的调用,会出现不同步,如synchronized(new 类名())
但是正是因为有可能会没有新建,则出现了直接修饰整个方法,即相当于this,这样就保证了是同一引用的会同步(认为是对该引用加上锁的相关标识,在以后或说明的,比如101章博客中的state状态的线程标识位)
而对于静态来说,由于没有this,但是静态的基本上都是一个类的,即使用了该静态,就一定是使用了同一个类的方法
那么这时使用synchronized修饰方法时等价于使用了synchronized(类名.class)(通常是当前类,即静态方法所在的类),这个类名无论怎么变化,他们在调用这个静态方法时都会同步(串行)
最后可以理解为无论是什么方法或者代码,在有synchronized锁面前,只要()里面的同步监听器是相同的
相同监听器所修饰的锁都必须等待前一个的锁打开(运行完,而Lock必须解锁)
否则,无论你是不同的方法,还是不同的类的方法,都必须等一等
回忆:继承类时相同方法都有重写,一个类的方法是在方法区里,且独一份,并且类里面最好不要创建本类的对象
因为有嵌套,如main方法里创建对象时,运行时有问题,或者静态对象调用时,也会出现问题
当然静态变量在本类方法里,可以直接写出,或者加上类名写出都可以
package com.lagou.task18;

public class AccountThreadTest extends Thread {
    private int balance; // 用于描述账户的余额
    private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个

    public AccountThreadTest() {
    }

    public AccountThreadTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public /*static*/ /*synchronized*/ void run() {
        /*System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
        //synchronized (dm) { // ok
            //synchronized (new Demo()) { // 锁不住  要求必须是同一个对象
            // 1.模拟从后台查询账户余额的过程
            int temp = getBalance(); // temp = 1000  temp = 1000
            // 2.模拟取款200元的过程
            if (temp >= 200) {
                System.out.println("正在出钞,请稍后...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走您的钞票!");
            } else {
                System.out.println("余额不足,请核对您的账户余额!");
            }
            // 3.模拟将最新的账户余额写入到后台
            setBalance(temp); // balance = 800  balance = 800
        //}*/
        test();
    }

    public /*synchronized*/ static void test() {
        synchronized (AccountThreadTest.class) { 
            // 该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步
            System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
            //synchronized (dm) { // ok
            //synchronized (new Demo()) { // 锁不住  要求必须是同一个对象
            //因为这样每来一个都会创建对象,相当于每个人都拿一把锁
            // 1.模拟从后台查询账户余额的过程
            int temp = 1000; //getBalance(); // temp = 1000  temp = 1000
            // 2.模拟取款200元的过程
            if (temp >= 200) {
                System.out.println("正在出钞,请稍后...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走您的钞票!");
            } else {
                System.out.println("余额不足,请核对您的账户余额!");
            }
            // 3.模拟将最新的账户余额写入到后台
            //setBalance(temp); // balance = 800  balance = 800
        }
    }

    public static void main(String[] args) {

        AccountThreadTest att1 = new AccountThreadTest(1000);
        att1.start();

        AccountThreadTest att2 = new AccountThreadTest(1000);
        att2.start();

        System.out.println("主线程开始等待...");
        try {
            att1.join();
            //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
            //不符合多项程的意义,因为没有同时启动了
            att2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终的账户余额为:" + att1.getBalance()); // 800

    }

    }

package com.lagou.task18;

import java.util.concurrent.locks.ReentrantLock;

public class AccountRunnableTest implements Runnable {
    private int balance; // 用于描述账户的余额
    private Demo dm = new Demo();
    private ReentrantLock lock = new ReentrantLock();  // 准备了一把锁

    public AccountRunnableTest() {
    }

    public AccountRunnableTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }
    //使用lock来操作锁,后面会有解释该操作,现在我们先进行大致的了解他的实现方式,在后面会给出其具体的解释的
    @Override
    public /*synchronized*/ void run() {
        // 开始加锁
        lock.lock();

        // 由源码可知:最终是account对象来调用run方法,因此当前正在调用的对象就是account
        //也就是说this就是account
        //synchronized (this) { // ok
        System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
        //synchronized (dm) { // ok
        //synchronized (new Demo()) { // 锁不住  要求必须是同一个对象
            // 1.模拟从后台查询账户余额的过程
            int temp = getBalance(); // temp = 1000  temp = 1000
            // 2.模拟取款200元的过程
            if (temp >= 200) {
                System.out.println("正在出钞,请稍后...");
                temp -= 200;  // temp = 800   temp = 800
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走您的钞票!");
            } else {
                System.out.println("余额不足,请核对您的账户余额!");
            }
            // 3.模拟将最新的账户余额写入到后台
            setBalance(temp); // balance = 800  balance = 800
        //}
        lock.unlock(); // 实现解锁
    }

    public static void main(String[] args) {

        AccountRunnableTest account = new AccountRunnableTest(1000);
        //AccountRunnableTest account2 = new AccountRunnableTest(1000);
        Thread t1 = new Thread(account);
        Thread t2 = new Thread(account);
        //Thread t2 = new Thread(account2);
        t1.start();
        t2.start();

        System.out.println("主线程开始等待...");
        try {
            t1.join();
            //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终的账户余额为:" + account.getBalance()); // 600  800
    }
}

class Demo{}

注意:一个类里面可以写多个类(不是内部类),而文件名通常必须要与有public修饰的类名一样,否则编译时会报错
之所以是通常,是因为若都没有public那么就会编译通过,会出现多个类的字节码文件
即使用java xxx(那些字节码文件的名字,不修改的话即类名),会使用相应的类的main方法
但是若直接用java xxx.java则会执行,他跳过了该错误
而他没有直接的看到出现的字节码文件,那么在运行时,会根据类的执行顺序而运行main方法
且不管你是否为public修饰的类,即只运行放在前面的类的main方法
静态方法的锁定------------------------------
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){.}
那么该方法锁的对象是类对象
每个类都有唯一的一个类对象
获取类对象的方式:类名.class
静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象
静态变量是有引用的连接的,即也可以用引用来调用
所有的修饰都基本上写在返回值前面,而返回值前面的修饰基本上是可以改变位置的
注意事项------------------------------
使用synchronized保证线程同步应当注意:多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
即当使用类实现接口时,由于Thread都使用该类,即调用该类的run方法,最后创建多个Thread时
调用的都是同一个对象的run方法
即synchronized里的引用若使用本类创建的引用时,使用的是同一个
若使用类继承Thread的话,那么调用的是不同的对象的run方法,那么synchronized里的引用若使用本类创建的引用时
使用的不是同一个
在使用同步块时应当尽量减少同步范围以提高并发的执行效率
线程安全类和不安全类------------------------------
StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类,一般我们都会考虑使用不安全的,因为效率高
Collections.synchronizedList() 和 Collections.synchronizedMap()等的返回结果对象中,方法基本都是安全的
死锁的概念------------------------------
线程一执行的代码:
    public void run(){
       synchronized(a){
           //持有对象锁a,等待对象锁b 
           synchronized(b){ 
               编写锁定的代码; 
           } 
       }
}
线程二执行的代码:
    public void run(){ 
        synchronized(b){ 
            //持有对象锁b,等待对象锁a 
            synchronized(a){ 
                编写锁定的代码; 
            } 
        }
}
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用
使用Lock(锁)实现线程同步------------------------------
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性
在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁
常用的方法------------------------------
ReentrantLock(),使用无参方式构造对象
void lock(),获取锁
void unlock(),释放锁
具体代码在前面操作过了
与synchronized方式的比较------------------------------
Lock是显式锁,需要手动实现开启和关闭操作
而synchronized是隐式锁,执行锁定代码后自动释放
Lock只有同步代码块方式的锁,即锁与解锁之间的代码
与synchronized不一样,synchronized是运行完后,才可让其他代码进入,而Lock必须要解锁后才可进入
而synchronized有同步代码块方式和同步方法两种锁
使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好
由于Lock可以实现手动操作,即可以有更多的操作空间
Object类常用的方法------------------------------
void wait(),用于使得线程进入等待状态(也是阻塞状态,类似于sleep方法,他们都是阻塞状态,只是阻塞的方式不同而已),直到其它线程调用notify()notifyAll()方法
且占用CPU资源(针对判断他的位置来说的,否则是没有占用的,就如sleep的监视一样,并且他的唤醒是重新调度,所以可能回到其他cpu核心,在同一个cpu核心上,就代表执行权没有改变,一般的,这都是因为算法来判断的,而重新调度,自然是多核心情况下,所以他都是有可能的,而sleep一般不会这样),并会解除对象锁,而sleep不会
wait()方法会强迫线程先进行释放锁操作,即必须要在锁里面进行,否则报错
即可以知道,他可以随时释放锁,与unlock方法类似(只是类似,因为他释放的是synchronized)
但他也可以让该线程进行等待(对应地方等待,只是其他的线程可以进入,但他还是在哪个地方,并没有离开,即没有结束执行),且必须唤醒或者等待他时间结束(后面的操作方法),这里是必要的
所以也可以看成sleep与unlock的结合体,即可以有更多的操作空间
如互相输出,即线程的来回操作
也可以让两个线程同时操作,如当唤醒后或者等待时间结束后,一个操作wait方法前面的,另一个操作wait方法后面的
但必须要在synchronized锁里面,Lock锁里面可能也不行(实际上也可以,具体可以百度,一般不能直接的操作,需要在里面又加上synchronized,特别的,若要直接实现对应的操作,一般需要Condition,一般是接口,通常使用ReentrantLock来进行创建对象),因为底层代码的缘故,wait()方法会强迫线程先进行释放锁操作
即必须要在synchronized锁里面进行,否则报错,因为wait就是用来操作锁的(包括与他相关的解除,如notify),即在锁里面调用wait方法时
可以隐式的看成监听器.wait(),所以当syschronized的参数为this时,可以直接写wait(),否则需要加参数名.wait()(当然,如果对应的参数是直接new的,那么基本上不能显示调用了),即如果有参数,那么必须参数来调用(比如Object a = new Object();,syschronized参数设置为a,那么就要a.wait(),否则直接的wait()会报错),而new基本不能调用,否则也报错
除非他有隐式的this,如引用调用时,就有this,要不然就报错
即虽然是服务与锁,但实际上服务于监听器,即锁可以称为监听器的锁
那么就会有,当调用方法时,若引用相同(设为引用为c),且该方法里的synchronized的监听器为this
那么该锁里面写上wait或者notify,也就是c.wait或者c.notify,后者会将拥有同样监听器的锁唤醒,而前者不会
因为wait方法作用于本身线程,与其他线程无关,而notify方法可以让其他线程调用,来唤醒同样监听器的线程
其中notify是c.notify可以让该类型锁里面的wait唤醒,而wait虽然也是c.wait
但实际上他并没有让其他该类型锁的线程等待,前者可以通过找到wait方法,而进行唤醒
后者却找不到让其他线程等待的东西,即只可以使得当前线程等待
这里"监听器"实际叫做"同步监听器"
当你认为输出结果与预期不对时,有可能是输出的那个语句没看到了,因为太慢了,即会很少
void wait(long timeout),用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止
即当等待时间到了时,就会自动解除等待,当然,也可以在他等待时直接唤醒
void notify(),用于唤醒等待的单个线程(如果有多个线程,那么随机选中一个来唤醒,并没有什么顺序,可以认为是伪随机),与wait方法一样,有this可以不加,有参数,加参数
void notifyAll(),用于唤醒等待的所有线程,与wait方法一样,有this可以不加,有参数,加参数
解决wait方法和sleep方法是否会释放锁的问题
//当两个线程进入时
public synchronized void run(){
        notify(); //放前面,是防止又调用wait方法,一般来说,要使得wait进行操作,那么wait一般需要更多的时间(唤醒时间,后面会说明的),所以导致,可能生产会直接的生产到顶,然后消费者才会消费,反之是同理的,但是这里由于初始是0,所以必然是生产先操作(因为就算消费者先操作,必然是会导致阻塞的,而生产者先操作,那么对于单核来说,他必然会操作他得到的时间片,使得还是会执行一会,当然,就算是多核,也需要在一开始来决定谁先执行,即进行比抢占,虽然基本不会抢占对方,极少数情况下,与单核一样,抢占同一个,或者所有的CPU(包括核心的说明)都有线程了,那么在对应同一个CPU下,可以认为是单核了,至此,所以最终导致生产者会先操作生产,因为消费者就算先操作,那么也是阻塞的,即并不是每次都是生产者抢到,只是因为消费者阻塞而已),所以基本上生产会生产很多个,无论你是否多次的测试,基本都是生产很多个
          //由于并没有其他操作来抵消wait需要的时间,所以之后,无论是单核还是多核,基本会有一方操作多次
            //单核和多核唯一的区别就是是否真正的依次访问而已,单核的切换时间非常小,可以忽略
            //分开抢占和同一抢占是差不多的,因为抢占间隔是比较大的(或者说"得到最开始的线程是需要时间的",特别是如果一开始就有,那么自然会操作打印,除非打印的时间过大,所以可以在主程序中打印一些值,单纯的打印,主程序(线程)一般会先打印,分开抢占的也是),所以时间基本(远)大于其他操作时间,所以无论是单核还是多核基本一样(但是可能会出现区别,但是因为差不多,所以基本上感受不到的,因为变化的存在,那么该差不多也就忽略的,就如内存是时刻变化的一样,是判断不了的,只能说,在相同的场景,可能会出现区别,即分开抢占块,同一抢占慢,但都有延时,就如我们选中一个人,如果多次的操作他一个人,那么他会比较慢,若有人来分担他,可能会变快,但是都是选中一个家族,即可是会变化的,所以基本感受不到的)
    //当然,对于这样的说明,只要了解即可,因为打印前打印后,与实际业务并没有什么联系,所以可以不用管就行,只需要完成具体需求即可
        System.out.println(1);
        try {
            wait(); //Thread.sleep(1000); //wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(2);

    }
//输出112
//当wait()变为Thread.sleep(1000)时
//输出1212
//即可以知道sleep没有释放锁
//而wait释放了锁
//最后当wait()变成wait(1000)时
//输出1122,最后一个2基本等待1秒出现,除非有其他情况,如卡了等等

//你会发现notify使得可以多个线程同时操作该加锁的资源,但只能操作wait后面的
//但真的是这样吗,经过我的测试,实际上不是这样,在我们唤醒后,他是需要重新获得锁的,也就是说,在wait里面进行获得锁,而这个获得锁之后,由于需要重新获得锁,并且他获得也需要时间(首先是解除阻塞,然后去抢锁),所以我们也会将该时间称为"可以进入"到"完全唤醒"之间的总时间(也就前面说的唤醒时间,在该时间中,主要的时间是解除阻塞,即在这个时间段,他是没有抢锁的,所以导致可以生产多个,当然一开始的cpu抢占也算,所以通常可以发现,他会生产到顶,因为有足够的时间了,虽然他们任意一个基本都能进行生产到顶),即"可以进入"代表从一开始到第一次获得锁,然后运行wait后面的代码,"完全唤醒"是第一次获得锁后到第二次获得锁,然后操作业务代码,这里只是大致的说明具体详细的说明可以到101章博客里查看

package com.lagou.task18;
//这里我决定也来操作一个案例,可以先看代码
public class ConsumerThread extends Thread {
    // 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法   合成复用原则
    private StoreHouse storeHouse;
    // 这里是为了确保两个线程共用同一个仓库,相同对象,自然是操作相同的锁(在这里是,因为是this)
    public ConsumerThread(StoreHouse storeHouse) {
        this.storeHouse = storeHouse;
    }

    public ConsumerThread() {

    }

    @Override
    public void run() {
        while (true) {
            storeHouse.consumerProduct();
            try {
                Thread.sleep(100); //可以去掉该行代码,来测试生产者是否先操作(前面的"必然是生产先操作"的测试),生产者也需要去掉该行代码
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.lagou.task18;

/**
 * 编程实现生产者线程,不断地生产产品
 */
public class ProduceThread extends Thread {
    // 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法   合成复用原则
    private StoreHouse storeHouse;
    // 这里是为了确保两个线程共用同一个仓库,相同对象,自然是操作相同的锁(在这里是,因为是this)
    public ProduceThread(StoreHouse storeHouse) {
        this.storeHouse = storeHouse;
    }

    @Override
    public void run() {
        while (true) {
            storeHouse.produceProduct();
            try {
                Thread.sleep(1000); //可以去掉该行代码,来测试生产者是否先操作(前面的"必然是生产先操作"的测试),消费者也需要去掉该行代码
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.lagou.task18;

/**
 * 编程实现仓库类
 */
public class StoreHouse {
    private int cnt = 0; // 用于记录产品的数量

    public synchronized void produceProduct() {
        notify();
        if (cnt < 10) {
            System.out.println("线程" + Thread.currentThread().getName() + 
            "正在生产第" + (cnt+1) + "个产品...");
            cnt++;
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consumerProduct() {
        notify();
        if (cnt > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品");
            cnt--;
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.lagou.task18;

public class StoreHouseTest {

    public static void main(String[] args) {

        // 创建仓库类的对象
        StoreHouse storeHouse = new StoreHouse();
        // 创建线程类对象并启动
        ProduceThread t1 = new ProduceThread(storeHouse);
        ConsumerThread t2 = new ConsumerThread(storeHouse);
        t1.start();
        t2.start();
    }
}

线程池------------------------------
从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口
常用的方法------------------------------
V call(),计算结果并返回
FutureTask类------------------------------
public class FutureTask<V>
extends Object
implements RunnableFuture<V> //RunnableFuture实现了Runnable接口
java.util.concurrent.FutureTask类用于描述可取消的异步计算
该类提供了Future接口的基本实现,包括启动和取消计算
查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果
常用的方法------------------------------
FutureTask(Callable callable),根据参数指定的引用来创建一个未来任务
V get(),获取call方法计算的结果,并等待线程执行完毕
package com.lagou.task18;

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

public class ThreadCallableTest implements Callable {

    @Override
    //V call(),计算结果并返回
    public Object call() throws Exception {
        // 计算1 ~ 10000之间的累加和并打印返回
        int sum = 0;
        for (int i = 1; i <= 10000; i++) {
            sum +=i;
        }
        System.out.println("计算的累加和是:" + sum); // 50005000
        return sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        //FutureTask(Callable callable),根据参数指定的引用来创建一个未来任务
        FutureTask ft = new FutureTask(tct);
        Thread t1 = new Thread(ft);
        t1.start(); //会执行call方法,与执行run方法类似,实际上是因为FutureTask重写了run方法,而重写的该方法(最终)会导致执行call方法
        Object obj = null; //返回值是FutureTask对应的泛型类型,FutureTask没有设置泛型,则默认为Object
        try {
            //V get(),获取call方法计算的结果,并等待线程执行完毕,如果没有进行启动执行call(即t1.start()),那么他始终等待
            obj = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("线程处理方法的返回值是:" + obj); // 50005000
    }
}

线程池的由来------------------------------
在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了
即每来一个客户端连接,服务器端就要创建一个新线程
如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性能
概念和原理------------------------------
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后
就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程
线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程
任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
相关类和方法------------------------------
public class Executors
extends Object
从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和java.util.concurrent.ExecutorService接口
其中Executors是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池
常用的方法------------------------------
static ExecutorService newCachedThreadPool(),创建一个可根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool(int nThreads),创建一个可重用固定线程数的线程池
static ExecutorService newSingleThreadExecutor(),创建一个只有一个线程的线程池
其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor
常用的方法------------------------------
void execute(Runnable command),执行任务和命令,通常用于执行
Runnable Future<V> submit(Callable<T> task),执行任务和命令,通常用于执行
Callablevoid shutdown(),启动有序关闭
package com.lagou.task18;

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

public class ThreadPoolTest {

    public static void main(String[] args) {

        // 1.创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 2.向线程池中布置任务
        executorService.submit(new ThreadCallableTest());
        // 3.关闭线程池
        executorService.shutdown();
    }
}
对于io流来说,在多线程的情况下,并不共享文件的下标,每个创建的流是自带自己的下标的(注意是创建的流,如果是同一个流,比如赋值给你,那么自然会共享其状态,所以在某种程度上是一个流一个状态,而大多数是多线程状态,所以也可以说成是多线程情况下,但最终的原因还是是因为不同的流,所以在某些情况下,即多线程的情况并不绝对的),唯一共享的是文件,所以在并发情况下,对于文件的写入,可能存在被覆盖的操作,这里注意即可
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值