zoukankan      html  css  js  c++  java
  • Java基础之 多线程

    一、创建多线程程序的第一种方式: 继承(extends) Thread类 

      Thread类的子类: MyThread 

        //1.创建一个Thread类的子类
    public class MyThread extends Thread{
        //2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
        @Override
        public void run() {
            for (int i = 0; i <20 ; i++) {
                System.out.println("run:"+i);
            }
        }
    }

      主线程: MyThread 

    public class MainThread {
        public static void main(String[] args) {
            //3.创建Thread类的子类对象
            MyThread mt = new MyThread();
            //4.调用Thread类中的方法start方法,开启新的线程,执行run方法
            mt.start();
    for (int i = 0; i <20 ; i++) { System.out.println("main:"+i); } } }

    结果:随机性打印

    main:0
    run:0
    main:1
    run:1
    main:2
    run:2
    main:3
    run:3
    main:4
    main:5
    。。。。

    原理:

      1、JVM执行  MainThread类的main 方法时,知道OS开辟了一条main方法通向CPU的路径。

        这个路径叫做main线程,主线程。CPU通过这个路径可以执行main方法。

      2、JVM执行到  mt.start();时,开辟了一条通向CPU的新路径来 执行run方法。

      3、对于CPU而言,就有了两条执行的路径,CPU就有了选择权,我们控制不了CPU,

        两个线程,main线程和,新的 MyThread的新线程,一起抢夺CPU的执行权

        (执行时间),谁抢到谁执行。

          多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

        java程序属于抢占式调度,优先级高的线程优先执行;同一个优先级,随机执行

    线程的内存解析图:

    二、创建多线程程序的第二种方式: 实现(implements) Runnable 类

      实现Runable接口的子类:MyRunbale
        //1.创建一个Runnable接口的实现类
    public class MyRunnable implements Runnable {
        //2.在实现类中重写Runnable接口的run方法,设置线程任务
        @Override
        public void run() {
            for (int i = 0; i <20 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }

      主线程: MyThread 

    public class MainThread {
        public static void main(String[] args) {
            //3.创建一个Runnable接口的实现类对象
            MyRunnable run = new MyRunnable();
            //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
            //Thread t = new Thread(run);//打印线程名称
            Thread t = new Thread(run);//打印HelloWorld
            //5.调用Thread类中的start方法,开启新的线程执行run方法
            t.start();
    
            for (int i = 0; i <20 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }

    结果:随机性打印

    main-->0
    Thread-0-->0
    main-->1
    Thread-0-->1
    main-->2
    Thread-0-->2
    main-->3
    Thread-0-->3
    main-->4
    Thread-0-->4
    main-->5
    Thread-0-->5
    main-->6
    Thread-0-->6
    main-->7
    Thread-0-->7

    原理和内存和实现Thread相识。
    实现Runnable接口创建多线程程序的好处:
            1.避免了单继承的局限性
                一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
                实现了Runnable接口,还可以继承其他的类,实现其他的接口
            2.增强了程序的扩展性,降低了程序的耦合性(解耦)
                实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
                实现类中,重写了run方法:用来设置线程任务
                创建Thread类对象,调用start方法:用来开启新线程

     三、匿名内部类实现线程的创建

     匿名内部类作用:简化代码
    把子类继承父类,重写父类的方法,创建子类对象合一步完成
    把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
    匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

    实现代码:

    /*
        匿名内部类方式实现线程的创建
    
        匿名:没有名字
        内部类:写在其他类内部的类
    
        格式:
            new 父类/接口(){
                重复父类/接口中的方法
            };
     */
    public class InnerClassThread {
        public static void main(String[] args) {
            //线程的父类是Thread
            // new MyThread().start();
            new Thread(){
                //重写run方法,设置线程任务
                @Override
                public void run() {
                    for (int i = 0; i <20 ; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"NOT_Copy");
                    }
                }
            }.start();
    
            //线程的接口Runnable
            //Runnable r = new RunnableImpl();//多态
            Runnable r = new Runnable(){
                //重写run方法,设置线程任务
                @Override
                public void run() {
                    for (int i = 0; i <20 ; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"程序员");
                    }
                }
            };
            new Thread(r).start();
    
            //简化接口的方式
            new Thread(new Runnable(){
                //重写run方法,设置线程任务
                @Override
                public void run() {
                    for (int i = 0; i <20 ; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"MWW");
                    }
                }
            }).start();
        }
    }

    四、线程安全

      1、线程安全产生的原因:

       1、单线程不会出现线程安全问题

       2、多个线程,没有访问共享资源,也不会产生线程安全问题

       3、多个线程,且访问了共享资源,就会产生线程安全问题。

      代码示例:

      线程实现类:

    /*
        实现卖票案例
     */
    public class RunnableImpl implements Runnable{
        //定义一个多个线程共享的票源
        private  int ticket = 100;
    //设置线程任务:卖票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ //先判断票是否存在 if(ticket>0){ try { Thread.sleep(10); //提高安全问题出现的概率,让程序睡眠 } catch (InterruptedException e) { e.printStackTrace(); } //票存在,卖票 ticket-- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); ticket--; } } } }

      MainThread类:

    /*
        模拟卖票案例
        创建3个线程,同时开启,对共享的票进行出售
     */
    public class MainThread {
        public static void main(String[] args) {
                //创建Runnable接口的实现类对象
                RunnableImpl run = new RunnableImpl();
                //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
                Thread t0 = new Thread(run);
                Thread t1 = new Thread(run);
                Thread t2 = new Thread(run);
                //调用start方法开启多线程
                t0.start();
                t1.start();
                t2.start();
        }
    }

    结果:

    Thread-1-->正在卖第100张票
    Thread-0-->正在卖第99张票
    Thread-2-->正在卖第98张票
    Thread-1-->正在卖第97张票
    Thread-0-->正在卖第96张票
    Thread-2-->正在卖第95张票
    Thread-1-->正在卖第94张票
    Thread-0-->正在卖第94张票
    Thread-2-->正在卖第92张票
    Thread-1-->正在卖第91张票
    Thread-0-->正在卖第91张票
    Thread-2-->正在卖第89张票
    Thread-0-->正在卖第88张票
    Thread-1-->正在卖第88张票
    Thread-2-->正在卖第86张票
    Thread-0-->正在卖第85张票
    Thread-1-->正在卖第85张票
    Thread-2-->正在卖第83张票
    Thread-1-->正在卖第82张票
    Thread-0-->正在卖第82张票
    Thread-2-->正在卖第80张票
    Thread-0-->正在卖第79张票
    Thread-1-->正在卖第79张票
    Thread-2-->正在卖第77张票
    Thread-1-->正在卖第76张票
    Thread-0-->正在卖第76张票
    Thread-2-->正在卖第74张票
    Thread-1-->正在卖第73张票
    Thread-0-->正在卖第73张票
    Thread-2-->正在卖第71张票
    Thread-1-->正在卖第70张票
    Thread-0-->正在卖第70张票
    Thread-2-->正在卖第68张票
    Thread-0-->正在卖第67张票
    Thread-1-->正在卖第67张票
    Thread-2-->正在卖第65张票
    Thread-0-->正在卖第64张票
    Thread-1-->正在卖第64张票
    Thread-2-->正在卖第62张票
    Thread-1-->正在卖第61张票
    Thread-0-->正在卖第61张票
    Thread-2-->正在卖第59张票
    Thread-0-->正在卖第58张票
    Thread-1-->正在卖第58张票
    Thread-2-->正在卖第56张票
    Thread-1-->正在卖第55张票
    Thread-0-->正在卖第55张票
    Thread-2-->正在卖第53张票
    Thread-1-->正在卖第52张票
    Thread-0-->正在卖第52张票
    Thread-2-->正在卖第50张票
    Thread-0-->正在卖第49张票
    Thread-1-->正在卖第49张票
    Thread-2-->正在卖第47张票
    Thread-0-->正在卖第46张票
    Thread-1-->正在卖第46张票
    Thread-2-->正在卖第44张票
    Thread-1-->正在卖第43张票
    Thread-0-->正在卖第42张票
    Thread-2-->正在卖第41张票
    Thread-2-->正在卖第40张票
    Thread-1-->正在卖第40张票
    Thread-0-->正在卖第40张票
    Thread-1-->正在卖第37张票
    Thread-2-->正在卖第37张票
    Thread-0-->正在卖第37张票
    Thread-0-->正在卖第34张票
    Thread-1-->正在卖第34张票
    Thread-2-->正在卖第34张票
    Thread-1-->正在卖第31张票
    Thread-2-->正在卖第31张票
    Thread-0-->正在卖第31张票
    Thread-1-->正在卖第28张票
    Thread-0-->正在卖第28张票
    Thread-2-->正在卖第28张票
    Thread-2-->正在卖第25张票
    Thread-1-->正在卖第25张票
    Thread-0-->正在卖第25张票
    Thread-1-->正在卖第22张票
    Thread-0-->正在卖第22张票
    Thread-2-->正在卖第22张票
    Thread-1-->正在卖第19张票
    Thread-0-->正在卖第19张票
    Thread-2-->正在卖第19张票
    Thread-0-->正在卖第16张票
    Thread-1-->正在卖第16张票
    Thread-2-->正在卖第16张票
    Thread-1-->正在卖第13张票
    Thread-0-->正在卖第13张票
    Thread-2-->正在卖第13张票
    Thread-0-->正在卖第10张票
    Thread-1-->正在卖第10张票
    Thread-2-->正在卖第10张票
    Thread-0-->正在卖第7张票
    Thread-1-->正在卖第7张票
    Thread-2-->正在卖第7张票
    Thread-2-->正在卖第4张票
    Thread-1-->正在卖第4张票
    Thread-0-->正在卖第4张票
    Thread-0-->正在卖第1张票
    Thread-1-->正在卖第0张票
    Thread-2-->正在卖第-1张票

    结果出现了卖重复的票,卖不存在的票。

    2、线程安全问题的解决方案:

      1)、同步代码块:

    /*
        格式:
            synchronized(锁对象){
                可能会出现线程安全问题的代码(访问了共享数据的代码)
            }
    
        注意:
            1.通过代码块中的锁对象,可以使用任意的对象
            2.但是必须保证多个线程使用的锁对象是同一个
            3.锁对象作用:
                把同步代码块锁住,只让一个线程在同步代码块中执行
     */

      修改线程实现类:

    /*
        实现卖票案例
     */
    public class RunnableImpl implements Runnable{
        //定义一个多个线程共享的票源
        private  int ticket = 100;
        //创建一个锁对象
        Object object=new Object();
        //设置线程任务:卖票
        @Override
        public void run() {
            //使用死循环,让卖票操作重复执行
            while(true){
                //同步代码块
                synchronized (object){
                    //先判断票是否存在
                    if(ticket>0){
                        try {
                            Thread.sleep(10); //提高安全问题出现的概率,让程序睡眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //票存在,卖票 ticket--
                        System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                        ticket--;
                    }
                }
            }
        }
    }

    将可能出现线程安全问题的代码放到   synchronized (object){}  代码块中。

    结果:

    Thread-0-->正在卖第100张票
    Thread-0-->正在卖第99张票
    Thread-0-->正在卖第98张票
    Thread-0-->正在卖第97张票
    Thread-2-->正在卖第96张票
    Thread-2-->正在卖第95张票
    Thread-2-->正在卖第94张票
    Thread-2-->正在卖第93张票
    Thread-2-->正在卖第92张票
    Thread-2-->正在卖第91张票
    Thread-2-->正在卖第90张票
    Thread-2-->正在卖第89张票
    Thread-2-->正在卖第88张票
    Thread-2-->正在卖第87张票
    Thread-2-->正在卖第86张票
    Thread-2-->正在卖第85张票
    Thread-2-->正在卖第84张票
    Thread-2-->正在卖第83张票
    Thread-2-->正在卖第82张票
    Thread-2-->正在卖第81张票
    Thread-2-->正在卖第80张票
    Thread-2-->正在卖第79张票
    Thread-2-->正在卖第78张票
    Thread-2-->正在卖第77张票
    Thread-2-->正在卖第76张票
    Thread-2-->正在卖第75张票
    Thread-2-->正在卖第74张票
    Thread-2-->正在卖第73张票
    Thread-2-->正在卖第72张票
    Thread-2-->正在卖第71张票
    Thread-2-->正在卖第70张票
    Thread-2-->正在卖第69张票
    Thread-2-->正在卖第68张票
    Thread-2-->正在卖第67张票
    Thread-2-->正在卖第66张票
    Thread-2-->正在卖第65张票
    Thread-2-->正在卖第64张票
    Thread-2-->正在卖第63张票
    Thread-2-->正在卖第62张票
    Thread-2-->正在卖第61张票
    Thread-2-->正在卖第60张票
    Thread-2-->正在卖第59张票
    Thread-2-->正在卖第58张票
    Thread-2-->正在卖第57张票
    Thread-2-->正在卖第56张票
    Thread-2-->正在卖第55张票
    Thread-2-->正在卖第54张票
    Thread-2-->正在卖第53张票
    Thread-2-->正在卖第52张票
    Thread-2-->正在卖第51张票
    Thread-2-->正在卖第50张票
    Thread-2-->正在卖第49张票
    Thread-2-->正在卖第48张票
    Thread-2-->正在卖第47张票
    Thread-2-->正在卖第46张票
    Thread-2-->正在卖第45张票
    Thread-2-->正在卖第44张票
    Thread-2-->正在卖第43张票
    Thread-2-->正在卖第42张票
    Thread-2-->正在卖第41张票
    Thread-2-->正在卖第40张票
    Thread-2-->正在卖第39张票
    Thread-2-->正在卖第38张票
    Thread-2-->正在卖第37张票
    Thread-2-->正在卖第36张票
    Thread-2-->正在卖第35张票
    Thread-2-->正在卖第34张票
    Thread-2-->正在卖第33张票
    Thread-2-->正在卖第32张票
    Thread-2-->正在卖第31张票
    Thread-2-->正在卖第30张票
    Thread-2-->正在卖第29张票
    Thread-2-->正在卖第28张票
    Thread-2-->正在卖第27张票
    Thread-2-->正在卖第26张票
    Thread-2-->正在卖第25张票
    Thread-2-->正在卖第24张票
    Thread-2-->正在卖第23张票
    Thread-2-->正在卖第22张票
    Thread-2-->正在卖第21张票
    Thread-2-->正在卖第20张票
    Thread-2-->正在卖第19张票
    Thread-2-->正在卖第18张票
    Thread-2-->正在卖第17张票
    Thread-2-->正在卖第16张票
    Thread-2-->正在卖第15张票
    Thread-2-->正在卖第14张票
    Thread-2-->正在卖第13张票
    Thread-2-->正在卖第12张票
    Thread-2-->正在卖第11张票
    Thread-2-->正在卖第10张票
    Thread-2-->正在卖第9张票
    Thread-1-->正在卖第8张票
    Thread-1-->正在卖第7张票
    Thread-1-->正在卖第6张票
    Thread-1-->正在卖第5张票
    Thread-1-->正在卖第4张票
    Thread-1-->正在卖第3张票
    Thread-1-->正在卖第2张票
    Thread-1-->正在卖第1张票

    不再出现重复票,和不存在的票了。

      实现原理:使用一个锁对象,这个对象叫做同步锁,也叫作对象监视器。

      上述代码中三个线程抢占CPU执行权,

      t0 抢到了CPUd 执行权,执行run方法,遇到synchronized代码块,

      这时候 t0 会检查 synchronized 代码块是否有锁对象,

      发现有就会获取到锁对象进入到同步代码块执行。

      t1 抢到了CPU的执行权,执行  run 方法,遇到 synchronized 代码块

      这时 t1会检查synchronized代码块是否有锁对象

      发现没有 t1 就会进入到阻塞状态,会一直等到 t0 线程归还 锁对象

      一直到 t0 线程执行完同步代码,会把锁对象归还给同步代码块。

      t1 才能获取到 锁对象 进入到同步执行。

      总结:同步中的线程没有执行完毕不会释放锁,没有锁也进不进同步代码,

        这样就保证了只有一个线程在同步中执行共享数据,保证了安全,

        程序频繁的判断锁,获取锁,释放锁,效率会降低。

     

      2)、同步方法

      修改线程实现类:

    /*
        实现卖票案例
     */
    public class RunnableImpl implements Runnable{
        //定义一个多个线程共享的票源
        private  int ticket = 100;
        //创建一个锁对象
        Object object=new Object();
        //设置线程任务:卖票
        @Override
        public void run() {
            //使用死循环,让卖票操作重复执行
            while(true){
               payTicket();
            }
        }
        /*
            定义一个同步方法
            同步方法也会把方法内部的代码锁住
            只让一个线程执行
            同步方法的对象是谁?
            就是实现类对象 new RunnableImpl()
         */
        public synchronized void payTicket(){
            //先判断票是否存在
            if(ticket>0){
                try {
                    Thread.sleep(10); //提高安全问题出现的概率,让程序睡眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }

    还可以用静态的同步代码:

    package DemoThread;
    /*
        实现卖票案例
     */
    public class RunnableImpl implements Runnable{
        //定义一个多个线程共享的票源
        private static int ticket = 100;
        //创建一个锁对象
        Object object=new Object();
        //设置线程任务:卖票
        @Override
        public void run() {
            //使用死循环,让卖票操作重复执行
            while(true){
                payTicketStatic();
            }
        }
        /*
            定义一个静态的同步方法
            对象是谁?
            就是实现类对象 本类的class属相 --> class文件对象(反射)
         */
        public static synchronized void payTicketStatic(){
            //先判断票是否存在
            if(ticket>0){
                try {
                    Thread.sleep(10); //提高安全问题出现的概率,让程序睡眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }

    将可能出现线程安全问题的代码放到   synchronized 修饰的方法中。

    结果:

    Thread-0-->正在卖第100张票
    Thread-2-->正在卖第99张票
    Thread-2-->正在卖第98张票
    Thread-2-->正在卖第97张票
    Thread-2-->正在卖第96张票
    Thread-2-->正在卖第95张票
    Thread-2-->正在卖第94张票
    Thread-2-->正在卖第93张票
    Thread-2-->正在卖第92张票
    Thread-2-->正在卖第91张票
    Thread-2-->正在卖第90张票
    Thread-2-->正在卖第89张票
    Thread-2-->正在卖第88张票
    Thread-2-->正在卖第87张票
    Thread-2-->正在卖第86张票
    Thread-2-->正在卖第85张票
    Thread-2-->正在卖第84张票
    Thread-2-->正在卖第83张票
    Thread-2-->正在卖第82张票
    Thread-2-->正在卖第81张票
    Thread-2-->正在卖第80张票
    Thread-2-->正在卖第79张票
    Thread-2-->正在卖第78张票
    Thread-2-->正在卖第77张票
    Thread-2-->正在卖第76张票
    Thread-2-->正在卖第75张票
    Thread-2-->正在卖第74张票
    Thread-2-->正在卖第73张票
    Thread-2-->正在卖第72张票
    Thread-2-->正在卖第71张票
    Thread-2-->正在卖第70张票
    Thread-2-->正在卖第69张票
    Thread-2-->正在卖第68张票
    Thread-2-->正在卖第67张票
    Thread-2-->正在卖第66张票
    Thread-2-->正在卖第65张票
    Thread-2-->正在卖第64张票
    Thread-2-->正在卖第63张票
    Thread-2-->正在卖第62张票
    Thread-2-->正在卖第61张票
    Thread-2-->正在卖第60张票
    Thread-2-->正在卖第59张票
    Thread-2-->正在卖第58张票
    Thread-2-->正在卖第57张票
    Thread-2-->正在卖第56张票
    Thread-2-->正在卖第55张票
    Thread-2-->正在卖第54张票
    Thread-2-->正在卖第53张票
    Thread-2-->正在卖第52张票
    Thread-2-->正在卖第51张票
    Thread-2-->正在卖第50张票
    Thread-2-->正在卖第49张票
    Thread-2-->正在卖第48张票
    Thread-2-->正在卖第47张票
    Thread-2-->正在卖第46张票
    Thread-2-->正在卖第45张票
    Thread-2-->正在卖第44张票
    Thread-2-->正在卖第43张票
    Thread-2-->正在卖第42张票
    Thread-2-->正在卖第41张票
    Thread-2-->正在卖第40张票
    Thread-2-->正在卖第39张票
    Thread-2-->正在卖第38张票
    Thread-2-->正在卖第37张票
    Thread-2-->正在卖第36张票
    Thread-2-->正在卖第35张票
    Thread-2-->正在卖第34张票
    Thread-2-->正在卖第33张票
    Thread-2-->正在卖第32张票
    Thread-2-->正在卖第31张票
    Thread-2-->正在卖第30张票
    Thread-2-->正在卖第29张票
    Thread-2-->正在卖第28张票
    Thread-2-->正在卖第27张票
    Thread-2-->正在卖第26张票
    Thread-2-->正在卖第25张票
    Thread-2-->正在卖第24张票
    Thread-2-->正在卖第23张票
    Thread-2-->正在卖第22张票
    Thread-2-->正在卖第21张票
    Thread-2-->正在卖第20张票
    Thread-2-->正在卖第19张票
    Thread-2-->正在卖第18张票
    Thread-2-->正在卖第17张票
    Thread-2-->正在卖第16张票
    Thread-2-->正在卖第15张票
    Thread-2-->正在卖第14张票
    Thread-2-->正在卖第13张票
    Thread-2-->正在卖第12张票
    Thread-2-->正在卖第11张票
    Thread-2-->正在卖第10张票
    Thread-2-->正在卖第9张票
    Thread-2-->正在卖第8张票
    Thread-2-->正在卖第7张票
    Thread-2-->正在卖第6张票
    Thread-2-->正在卖第5张票
    Thread-2-->正在卖第4张票
    Thread-2-->正在卖第3张票
    Thread-2-->正在卖第2张票
    Thread-2-->正在卖第1张票

    同步方法和静态同步的方法的不同点在于,同步锁的对象不同。

    同步方法     的锁对象就是 实现类对象  new RunnableImpl()
    静态同步的方法  的锁对象就是 本类的 RunnableImpl.class。

      3)、锁机制

    代码实现:

    /*
        卖票案例出现了线程安全问题
        卖出了不存在的票和重复的票
    
        解决线程安全问题的三种方案:使用Lock锁
        java.util.concurrent.locks.Lock接口
        Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
        Lock接口中的方法:
            void lock()获取锁。
            void unlock()  释放锁。
        java.util.concurrent.locks.ReentrantLock implements Lock接口
    
    
        使用步骤:
            1.在成员位置创建一个ReentrantLock对象
            2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
            3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
     */
    public class RunnableImpl implements Runnable{
        //定义一个多个线程共享的票源
        private int ticket = 100;
        //1.在成员位置创建一个ReentrantLock对象
        Lock lock1 = new ReentrantLock();
        //设置线程任务:卖票
        @Override
        public void run() {
            //使用死循环,让卖票操作重复执行
            while(true){
                lock1.lock(); //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
                if(ticket>0){
                    try {
                        Thread.sleep(10); //提高安全问题出现的概率,让程序睡眠
                        //票存在,卖票 ticket--
                        System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock1.unlock();//3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                        //放在finally{} 中 无论程序是否异常,都会把锁释放。
                    }
                }
            }
        }
    }

    结果同上面的两种方式相同。

     五、线程的状态

    线程状态  导致状态发生条件
    NEW(新建)  线程刚被创建,但是并未启动。还没调用start方法。
    Runnable(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操
    作系统处理器。
    Blocked(锁阻塞)
    当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状
    态;当该线程持有锁时,该线程将变成Runnable状态。
    Waiting(无限等待)
    一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个
    状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
    TimedWaiting(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态
    将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
    Teminated(被终止)
    因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

      1、Timed Waiting 线程状态图: 

      sleep方法的使用还是很简单的。我们需要记住下面几点:
        1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,
          单独的线程也可以调用,不一定非要有协作关系。
        2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。
          这样才能保证该线程执行过程中会睡眠
        3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
     
        注:sleep()中指定的时间是线程不会运行的最短时间。
        因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

      2、Blocked(锁阻塞)

      上面已经讲过了同步机制,那么这个状态也就非常好理解了,比如线程A与线程B代码中使用

      同一把锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B进入到Blocked锁阻塞状态。

      这里是由Runnable状态进入Blocked状态,除此之外Waiting(无限等待)以及

      Time Waiting(计时等待)也会在某种情况下进入到阻塞状态

      3、Waiting(无限等待)

        1、等待与唤醒案例:

    /*
        等待唤醒案例:线程之间的通信
            创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
            创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
    
        注意:
            顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
            同步使用的锁对象必须保证唯一
            只有锁对象才能调用wait和notify方法
    
        Obejct类中的方法
        void wait()
              在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
        void notify()
              唤醒在此对象监视器上等待的单个线程。
              会继续执行wait方法之后的代码
     */
    public class MainThread {
        public static void main(String[] args) {
            //创建锁对象,保证唯一
            Object obj = new Object();
            // 创建一个顾客线程(消费者)
            new Thread(){
                @Override
                public void run() {
                    //一直等着买包子
                    while(true){
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj){
                            System.out.println("告知老板要的包子的种类和数量");
                            //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //唤醒之后执行的代码
                            System.out.println("包子已经做好了,开吃!");
                            System.out.println("---------------------------------------");
                        }
                    }
                }
            }.start();
    
            //创建一个老板线程(生产者)
            new Thread(){
                @Override
                public void run() {
                    //一直做包子
                    while (true){
                        //花了5秒做包子
                        try {
                            Thread.sleep(5000);//花5秒钟做包子
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj){
                            System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                            //做好包子之后,调用notify方法,唤醒顾客吃包子
                            obj.notify();
                        }
                    }
                }
            }.start();
        }
    }

    结果:

    告知老板要的包子的种类和数量
    老板5秒钟之后做好包子,告知顾客,可以吃包子了
    包子已经做好了,开吃!
    ---------------------------------------
    告知老板要的包子的种类和数量
    老板5秒钟之后做好包子,告知顾客,可以吃包子了
    包子已经做好了,开吃!
    ---------------------------------------
    告知老板要的包子的种类和数量

      2、等待与唤醒案例2:wait(long m)和notifyAll() 方法

    代码实现:

    /*
        进入到TimeWaiting(计时等待)有两种方式
        1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
        2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
    
        唤醒的方法:
             void notify() 唤醒在此对象监视器上等待的单个线程。
             void notifyAll() 唤醒在此对象监视器上等待的所有线程。
     */
    public class MainThread {
        public static void main(String[] args) {
            //创建锁对象,保证唯一
            Object obj = new Object();
            // 创建一个顾客线程(消费者)
            new Thread(){
                @Override
                public void run() {
                    //一直等着买包子
                    while(true){
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj){
                            System.out.println("顾客1告知老板要的包子的种类和数量");
                            //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //唤醒之后执行的代码
                            System.out.println("包子已经做好了,顾客1开吃!");
                            System.out.println("---------------------------------------");
                        }
                    }
                }
            }.start();
    
            // 创建一个顾客线程(消费者)
            new Thread(){
                @Override
                public void run() {
                    //一直等着买包子
                    while(true){
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj){
                            System.out.println("顾客2告知老板要的包子的种类和数量");
                            //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //唤醒之后执行的代码
                            System.out.println("包子已经做好了,顾客2开吃!");
                            System.out.println("---------------------------------------");
                        }
                    }
                }
            }.start();
    
            //创建一个老板线程(生产者)
            new Thread(){
                @Override
                public void run() {
                    //一直做包子
                    while (true){
                        //花了5秒做包子
                        try {
                            Thread.sleep(5000);//花5秒钟做包子
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj){
                            System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                            //做好包子之后,调用notify方法,唤醒顾客吃包子
                            //obj.notify();//如果有多个等待线程,随机唤醒一个
                            obj.notifyAll();//唤醒所有等待的线程
                        }
                    }
                }
            }.start();
        }
    }

    结果:

    顾客1告知老板要的包子的种类和数量
    顾客2告知老板要的包子的种类和数量
    老板5秒钟之后做好包子,告知顾客,可以吃包子了
    包子已经做好了,顾客2开吃!
    ---------------------------------------
    顾客2告知老板要的包子的种类和数量
    包子已经做好了,顾客1开吃!
    ---------------------------------------
    顾客1告知老板要的包子的种类和数量
    老板5秒钟之后做好包子,告知顾客,可以吃包子了
    包子已经做好了,顾客1开吃!
    ---------------------------------------
    顾客1告知老板要的包子的种类和数量
    包子已经做好了,顾客2开吃!
    ---------------------------------------

    等待与唤醒机制:又名 线程之间的通讯。

      等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
      1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,
        这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象
        上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
      2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
      3. notifyAll:则释放所通知对象的 wait set 上的全部线程。
     
      注:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而
        此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调
        用 wait 方法之后的地方恢复执行
     
      总结如下:
      如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
      否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

    如下所示:

    调用wait和notify方法需要注意的细节
      1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对
        象调用的wait方法后的线程。
      2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继
        承了Object类的。
      3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。 

       3、生产者和消费者问题

      等待唤醒机制其实就是经典的“生产者与消费者”的问题。

      重新整理代码:

      BaoZi类:

    /*
        资源类:包子类
        设置包子的属性
            皮
            陷
            包子的状态: 有 true,没有 false
     */
    public class BaoZi {
        private String pier ;
        private String xianer ;
        private boolean flag = false ;//包子资源 是否存在 包子资源状态
    
        public String getPier() {
            return pier;
        }
    
        public void setPier(String pier) {
            this.pier = pier;
        }
    
        public String getXianer() {
            return xianer;
        }
    
        public void setXianer(String xianer) {
            this.xianer = xianer;
        }
    
        public boolean isFlag() {
            return flag;
        }
    
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }

      BaoZiPu类:

    /*
        生产者(包子铺)类:是一个线程类,可以继承Thread
        设置线程任务(run):生产包子
        对包子的状态进行判断
        true:有包子
            包子铺调用wait方法进入等待状态
        false:没有包子
            包子铺生产包子
            增加一些趣味性:交替生产两种包子
                有两种状态(i%2==0)
            包子铺生产好了包子
            修改包子的状态为true有
            唤醒吃货线程,让吃货线程吃包子
    
        注意:
            包子铺线程和包子线程关系-->通信(互斥)
            必须同时同步技术保证两个线程只能有一个在执行
            锁对象必须保证唯一,可以使用包子对象作为锁对象
            包子铺类和吃货的类就需要把包子对象作为参数传递进来
                1.需要在成员位置创建一个包子变量
                2.使用带参数构造方法,为这个包子变量赋值
     */
    public class BaoZiPu extends Thread {
        //1.需要在成员位置创建一个包子变量
        private BaoZi bz;
    
        //2.使用带参数构造方法,为这个包子变量赋值
        public BaoZiPu(BaoZi bz) {
            this.bz = bz;
        }
    
        //设置线程任务(run):生产包子
        @Override
        public void run() {
            //定义一个变量
            int count = 0;
            //让包子铺一直生产包子
            while(true){
                //必须同时同步技术保证两个线程只能有一个在执行
                synchronized (bz){
                    //对包子的状态进行判断
                    if(bz.isFlag()==true){
                        //包子铺调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //被唤醒之后执行,包子铺生产包子
                    //增加一些趣味性:交替生产两种包子
                    if(count%2==0){
                        //生产 薄皮三鲜馅包子
                        bz.setPier("薄皮");
                        bz.setXianer("三鲜馅");
                    }else{
                        //生产 冰皮 牛肉大葱陷
                        bz.setPier("冰皮");
                        bz.setXianer("牛肉大葱陷");
                    }
                    count++;
                    System.out.println("包子铺正在生产:"+bz.getPier()+bz.getXianer()+"包子");
                    //生产包子需要3秒钟
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //包子铺生产好了包子
                    //修改包子的状态为true有
                    bz.setFlag(true);
                    //唤醒吃货线程,让吃货线程吃包子
                    bz.notify();
                    System.out.println("包子铺已经生产好了:"+bz.getPier()+bz.getXianer()+"包子,吃货可以开始吃了");
                }
            }
        }
    }

      ChiHuo类:

    /*
        消费者(吃货)类:是一个线程类,可以继承Thread
        设置线程任务(run):吃包子
        对包子的状态进行判断
        false:没有包子
            吃货调用wait方法进入等待状态
        true:有包子
            吃货吃包子
            吃货吃完包子
            修改包子的状态为false没有
            吃货唤醒包子铺线程,生产包子
     */
    public class ChiHuo extends Thread{
        //1.需要在成员位置创建一个包子变量
        private BaoZi bz;
    
        //2.使用带参数构造方法,为这个包子变量赋值
        public ChiHuo(BaoZi bz) {
            this.bz = bz;
        }
        //设置线程任务(run):吃包子
        @Override
        public void run() {
            //使用死循环,让吃货一直吃包子
            while (true){
                //必须同时同步技术保证两个线程只能有一个在执行
                synchronized (bz){
                    //对包子的状态进行判断
                    if(bz.isFlag()==false){
                        //吃货调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //被唤醒之后执行的代码,吃包子
                    System.out.println("吃货正在吃:"+bz.getPier()+bz.getXianer()+"的包子");
                    //吃货吃完包子
                    //修改包子的状态为false没有
                    bz.setFlag(false);
                    //吃货唤醒包子铺线程,生产包子
                    bz.notify();
                    System.out.println("吃货已经把:"+bz.getPier()+bz.getXianer()+"的包子吃完了,包子铺开始生产包子");
                    System.out.println("----------------------------------------------------");
                }
            }
        }
    }

      测试类:

    /*
        测试类:
        包含main方法,程序执行的入口,启动程序
        创建包子对象;
        创建包子铺线程,开启,生产包子;
        创建吃货线程,开启,吃包子;
     */
    public class Demo {
        public static void main(String[] args) {
            //创建包子对象;
            BaoZi bz =new BaoZi();
            //创建包子铺线程,开启,生产包子;
            new BaoZiPu(bz).start();
            //创建吃货线程,开启,吃包子;
            new ChiHuo(bz).start();
        }
    }

    结果:

    包子铺正在生产:薄皮三鲜馅包子
    包子铺已经生产好了:薄皮三鲜馅包子,吃货可以开始吃了
    吃货正在吃:薄皮三鲜馅的包子
    吃货已经把:薄皮三鲜馅的包子吃完了,包子铺开始生产包子
    ----------------------------------------------------
    包子铺正在生产:冰皮牛肉大葱陷包子
    包子铺已经生产好了:冰皮牛肉大葱陷包子,吃货可以开始吃了
    吃货正在吃:冰皮牛肉大葱陷的包子
    吃货已经把:冰皮牛肉大葱陷的包子吃完了,包子铺开始生产包子
    ----------------------------------------------------
    包子铺正在生产:薄皮三鲜馅包子
    包子铺已经生产好了:薄皮三鲜馅包子,吃货可以开始吃了
    吃货正在吃:薄皮三鲜馅的包子
    吃货已经把:薄皮三鲜馅的包子吃完了,包子铺开始生产包子
    ----------------------------------------------------
    包子铺正在生产:冰皮牛肉大葱陷包子

    等待与唤醒机制:又名 线程之间的通讯。

    原理说明:

      通讯:对包子的状态进行判断

      没有包子-->吃货线程 唤醒包子铺线程-->吃货等待-->包子铺线程做包子-->做好包子-->修改包子的状态为有包子

      有包子-->包子铺线程唤醒 吃货线程-->包子铺线程等待-->吃货线程吃包子-->吃完包子-->修改包子的状态为没有包子

      。。。

     六、线程池

      为什么使用线程:

         当并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,

        这样频繁创建线程就会大大的降低系统的效率,因为频繁创建线程和销毁线程需要时间。

      1、线程池的概念

        线程池:其实就是一个容纳多个线程的容器,其中线程可以反复使用,省去了频繁创建线程对象的操作,

        无需反复创建线程池而消耗了过多的资源。

        图解:

     合理利用线程池能够带来三个好处:

      1、降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复使用,可执行多个任务。

      2、提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行

        3、提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内

         存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

      2、线程池的使用

    /*
        2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
     */
    public class RunnableImpl implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
        }
    }

      测试类;

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /*
        线程池:JDK1.5之后提供的
        java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
        Executors类中的静态方法:
            static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
            参数:
                int nThreads:创建线程池中包含的线程数量
            返回值:
                ExecutorService接口,返回的是ExecutorService接口的实现类对象,
           我们可以使用ExecutorService接口接收(面向接口编程)   java.util.concurrent.ExecutorService:线程池接口 用来从线程池中获取线程,调用start方法,执行线程任务 submit(Runnable task) 提交一个 Runnable 任务用于执行 关闭/销毁线程池的方法 void shutdown() 线程池的使用步骤: 1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池 2.创建一个类,实现Runnable接口,重写run方法,设置线程任务 3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法 4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
    */ public class MainThread { public static void main(String[] args) { //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池 ExecutorService es = Executors.newFixedThreadPool(2); //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法 es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行 //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用 es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行 es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行 //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行) es.shutdown(); es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了 } }

    结果:

    pool-1-thread-1创建了一个新的线程执行
    pool-1-thread-2创建了一个新的线程执行
    pool-1-thread-1创建了一个新的线程执行
    pool-1-thread-2创建了一个新的线程执行
    pool-1-thread-2创建了一个新的线程执行

      因为线程使用完了,会自动把线程归还给线程池,线程可以继续使用,所以我只在线程池中设置了两个线程,

      却可以反复使用,执行多次任务。

    注:多线程执行时为什么调用的是start()方法而不是run()方法?
        如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象就不会交给“线程规划器”来进行处理。而是由main主线程来调用run()方法,也就是说必须要等到run()方法中的代码执行完成后才可以执行后面的代码。
      start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程,通过Thead类中start()方法来启动的线程处于就绪状态(可运行状态),此时并没有运行,一旦得到CPU时间片,就自动开始执行run()方法。 

    待续。。。。

  • 相关阅读:
    cgo中调用有函数指针参数的C函数
    为什么要微服务架构服务化?
    学习刘杨人像后期笔记纪要
    错误日志表设计
    通过jstack日志分析和问题排查
    【JVM】jmap命令详解----查看JVM内存使用详情
    【JVM】jstat命令详解---JVM的统计监测工具
    介绍几种常用的监控工具
    【转】Pinpoint分布式系统性能监控工具
    【转】Skywalking概述
  • 原文地址:https://www.cnblogs.com/mww-NOTCOPY/p/11456814.html
Copyright © 2011-2022 走看看