zoukankan      html  css  js  c++  java
  • 16 Java的线程

    基本概念:程序-进程-线程

      程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
    进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
      如:运行中的QQ,运行中的MP3播放器
      程序是静态的,进程是动态的


    线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
      若一个程序可同一时间执行多个线程,就是支持多线程的

    多线程,一个进程(一个程序运行时),可以分化为并行执行多个线程(多个子程序)。

    举例:线程相当于一条河,线程就相当于河流的分支。

    什么时候需要多线程呢?

      程序需要同时执行两个或多个任务.

    程序需要实现一些需要等待的任务时,如用户输入,文件读写操作、网络操作、搜索等。

    举例:有一个进程浏览器,看网页,比如用百度搜索(线程),需要等待百度那边的服务器通过网络给你展示搜索的内容,这个过程需要时间,如果网速越慢时间越长。

    在等待过程中,这个浏览器是一直占用CPU的资源,考虑说在浏览器等待百度服务器响应的这段时间,先让这个进程占用的CPU干其他事,等响应回来了数据,再继续使用。

    需要一些后台运行的程序时。

    因为多线程进程支流,当分支之后,就各走各的

    假设在进程上跑的代码是主程序,当其中的第三行代码是开启线程的,那么,开启线程之后线程运行的代码就和主程序并行他们之间互不干扰)。

    多线程的创建和启动

    Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。

    Thread类的特性
      1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。(想要在开启的多线程中运行的代码逻辑,就写到run方法里)。

    注意:run()方法的作用不是启动线程,而是线程体,start()方法调用run()方法时执行线程
      2. 通过该Thread对象的start()方法来调用这个线程

    start()方法用来启动线程,本质上就开始运行run()方法,也就是执行start()方法,start()方法会自动调用run()方法。

     Thread类

    创建线程需要Thread类,Thread类包含4个构造方法,如下

      1. Thread():创建新的Thread对象
      2. Thread(String threadname):创建线程并指定线程实例名
      3. Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
      4. Thread(Runnable target, String name):创建新的Thread对象

    先介绍1,2两种方式。

    继承Thread类

    方式1步骤如下:

      1)定义子类继承Thread类。

         2)子类中重写Thread类中的run方法。
      3) 创建Thread子类对象,即创建了线程对象。
      4) 调用线程对象start方法:启动线程,调用run方法。

    代码:

    /**
     * 继承Thread类方式实现多线程
     * @author leak
     *
     */
    public class TestThread extends Thread{
        
        @Override
        public void run() {
            System.out.println("多线程运行的代码");
            for(int i = 0 ; i < 5 ; i++) {
                System.out.println("这是多线程的逻辑代码:"+i);
            }
        }
    }
    
    
    //测试类
    /**
     * 1.测式线程类
     * @author leak
     *
     */
    public class Test {
        public static void main(String[] args) {
            Thread t = new TestThread();
            t.start();//启动线程
            System.out.println("-------------");
            System.out.println("-------------");
            System.out.println("-------------");
            /**
             * 多次运行这个main方法之后
             * 我们发现main方法中打印的3行于开启线程运行run()方法中的打印语句混合起来,因为主线程和支线程同时运行的结果,但是有可能主线程运行完才运行支线程(CPU特别好的时候)
             * main方法从上到下执行,到t.start()开启线程后,那边的支线程执行语句,这边的主线程还是继续往下执行(无论支线程是否执行完)这就是异步。
             * 
             * 补充: 如果是同步,也就是main方法执行到t.start()的时候,跳去执行start()体内的语句,执行完后,返回来结束t.start()语句,继续往下执行,这就是同步(这里不是多线程运行的方式,只是举例说明同步)。
             * 多线程差不多算异步执行。补充:同步就是从上到下的执行顺序。
             * 
             */
        }
    }
    View Code

    实现Runnable接口

    方式2步骤如下:

      1)定义子类,实现Runnable接口。
      2)子类中重写Runnable接口中的run方法。
      3)通过Thread类含参构造器创建线程对象。
      4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
      5)调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。

    代码:

    /**
     * 2. 通过实现Runnable接口方式实现多线程
     * @author leak
     *
     */
    public class TestRuunable implements Runnable{
        @Override
        public void run() {
            //Thread.currentThread().getName()作用:获取当前线程的线程名
            System.out.println("当前线程名:"+Thread.currentThread().getName()+",Runnable多线程运行的代码");
            for(int i = 0 ; i < 5 ; i++) {
                System.out.println("这是线程:"+Thread.currentThread().getName()+" 的逻辑代码:"+i);
            }
        }
    }
    
    
    //测试类
    /**
     * 这个和方式1有什么区别呢?
     * 方式1是继承了Thread类然后重写run()方法的,因为Java不支持多继承,所以这种方式有限制,
     * 不必为了重写run()方法而继承Thread类。而且Thread类的run()方法也是实现Runnable接口的。
     * 所以方式2直接省略继承Thread类,直接实现Runnable接口,重写run()方法,Java是支持多实现的,所以使用方式2可扩展性好
     * 但是线程启动需要Thread类,所以把实现Runnable接口的类传给Thread类,然后通过Thread的实例对象调用start方法启动线程。
     * 
     * @author leak
     *
     */
    public class Test2 {
        public static void main(String[] args) {
            //2.通过传Runnable对象去实现多线程
            Thread t = new Thread(new TestRuunable());//这里是使用默认的线程名Thread-0,默认0号开始
            t.start();//启动线程1
            
            //另外方式2还可以给线程起名字
            Thread t2 = new Thread(new TestRuunable(),"线程2");//自定义线程名
            t2.start();//启动线程2
            
            //补充:线程可以启动多条,不同的线程实例对象可以开启不同的线程。
        }
    }
    View Code

    继承方式和实现方式的联系和区别

    方式1:继承Thread: 线程代码存放Thread子类run方法中。重写run方法
    方式2:实现Runnable:线程代码存在接口的子类的run方法。实现run方法

     一般使用实现接口方式来实现多线程(方式2),原因如下:

    1)避免了单继承的局限性
    2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
    一般使用实现接口方式来实现多线程

    上面方式2的代码,都是使用匿名实现类来传递给Thread类,十分耗资源,方式2可以共享同一个接口实现类的对象,代码如下:

    代码:

    /**
     * 2. 通过实现Runnable接口方式实现多线程
     * @author leak
     *
     */
    public class TestRuunable implements Runnable{
        int count = 0 ; //测试方式2多线程之间的共享同一个对象,所以方式2才会共享count属性
        
        @Override
        public void run() {
            //Thread.currentThread().getName()作用:获取当前线程的线程名
            System.out.println("当前线程名:"+Thread.currentThread().getName()+",Runnable多线程运行的代码");
            
            for(int i = 0 ; i < 5 ; i++) {//Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10
                count++;
                System.out.println("这是线程:"+Thread.currentThread().getName()+" 的逻辑代码:"+count);
            }
        }
    }
    
    
    //测试类
    public class Test2 {
        public static void main(String[] args) {
            //2.通过传Runnable对象去实现多线程
            Runnable run = new TestRuunable();//下面两个线程实例对象传递的都是同一个Runnable实现类,所以共享对象里面的资源/属性
            
            Thread t = new Thread(run);//这里是使用默认的线程名Thread-0,默认0号开始
            t.start();//启动线程1
            
            //另外方式2还可以给线程起名字
            Thread t2 = new Thread(run,"线程2");//自定义线程名
            t2.start();//启动线程2
            
            //补充:线程可以启动多条,不同的线程实例对象可以开启不同的线程。
        }
    }
    View Code

    注意:上面说过线程之间运行互不干扰,上面的代码的count变量为什么共享了?因为上面的代码采用方式2,方式2通过实现Runnable接口创建线程实例对象,然后方式2需要Thread类,而且需要传递一个实现Runnable的实例对象给Thread类,这样方式2才能启动线程,因为传递的是同一对象,所以不同线程操作的是同一对象,所以属性才会共享。

    使用多线程的优点

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

    第三点是什么意思呢,比如一个方法有1000行代码,前300,中间300,最后400行,这三段代码没有因果关系(也就是代码没有限制执行顺序),这种情况我们就可以使用线程处理,把前中后三段代码分别放在不同线程中去运行,这样三段代码就是并行运行的。

    例子:再比如下载一个视频时,采用多线程方式下载视频,假设视频大小是900M,开启三个线程,线程1从0KB位置下载,线程2从300M位置开始下载,线程3从600M位置开始下载,三个线程下载完成后,再把各自线程下载好的视频合并在一起,这样下载视频是不是快了很多。

    Thread类的有关方法1

      void start(): 启动线程,并执行对象的run()方法
      run(): 线程在被调度时执行的操作
      String getName(): 返回线程的名称
      void setName(String name):设置该线程名称
      static currentThread(): 返回当前线程

    代码:

    /**
     * Thread类的有关方法1
     * 
     * @author leak
     *
     */
    public class Test3 {
        public static void main(String[] args) {
    
            Runnable run = new TestRun();
            Runnable run1 = new TestRun();
    
            // 线程传递不同的实现Runnable接口的实例对象,不会共享属性count这里
            Thread t0 = new Thread(run);
            Thread t1 = new Thread(run1);
            // 1线程对象.setName()设置线程名
            t0.setName("线程1");
    
            // 2不同线程对象.start()都会开启一个线程
            t1.start();
            t0.start();
            // 3线程对象.getName()获取当前线程名,4线程类.currentThread()返回当前线程
            System.out.println(t0.getName() + "当前线程对象:" + Thread.currentThread());
        }
    }
    
    class TestRun implements Runnable {
        int count;// 传递同一TestRun对象给Thread类时,共享对象(包含对象的其他)
    
        @Override
        public void run() {
            // Thread.currentThread().getName()作用:获取当前线程的线程名
            System.out.println("当前线程名:" + Thread.currentThread().getName() + ",Runnable多线程运行的代码");
    
            for (int i = 0; i < 5; i++) {// Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10
                count++;
                System.out.println("这是线程:" + Thread.currentThread().getName() + " 的逻辑代码:" + count);
            }
        }
    }
    View Code

    线程的优先级

    优先级越高,线程执行的概率就越高,但不一定执行。

    MAX_PRIORITY(10); 最高优先级是10级
    MIN _PRIORITY (1); 最低是1级
    NORM_PRIORITY (5); 默认是5级
    涉及的方法:
      getPriority() :返回线程优先值
      setPriority(int newPriority) :改变线程的优先级
    线程创建时继承父线程的优先级

    代码:

    /**
     * 线程的优先级,线程优先级有10级,级数越大,优先级越高,优先级高的线程有大概率被先执行,但是不一定都执行
     * 默认优先级是5,子类继承父类线程,优先级也会被继承
     * 
     * @author leak
     *
     */
    public class Test4 {
        public static void main(String[] args) {
    
            Runnable run = new TestRun();
    
            // 线程传递不同的实现Runnable接口的实例对象,不会共享属性count,这里是共享
            Thread t0 = new Thread(run);
            Thread t1 = new Thread(run);
    
            // 不同线程对象.start()都会开启一个线程
            t1.start();
            t0.start();
            
            //getPriority()获取当前线程优先级
            System.out.println("当前 "+t0.getName()+" 线程的优先级:"+t0.getPriority());
            System.out.println("当前 "+t1.getName()+" 线程的优先级:"+t1.getPriority());
            
            //setPriority(级数)设置线程优先级
            t0.setPriority(10);
            
            //重新获取优先级
            System.out.println("当前 "+t0.getName()+" 线程的优先级:"+t0.getPriority());
        }
    }
    
    class TestRun1 implements Runnable {
        int count;// 传递同一TestRun对象给Thread类时,共享对象(包含对象的其他)
    
        @Override
        public void run() {
            // Thread.currentThread().getName()作用:获取当前线程的线程名
            System.out.println("当前线程名:" + Thread.currentThread().getName() + ",Runnable多线程运行的代码");
    
            for (int i = 0; i < 5; i++) {// Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10
                count++;
                System.out.println("这是线程:" + Thread.currentThread().getName() + " 的逻辑代码:" + count);
            }
        }
    }
    View Code

    Thread类的有关方法2

    static void yield():线程让步
      1. 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
      2. 若队列中没有同优先级的线程,忽略此方法

    join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
      1. 低优先级的线程也可以获得执行

    static void sleep(long millis):(指定时间:毫秒),线程睡眠
      1. 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
      2. 抛出InterruptedException异常


    stop(): 强制线程生命期结束,该方法已经弃用,因为会导致很多问题发生,具体原因百度。这里就不演示了

    interrupt():该方法给线程设置中止状态,但不会马上终止线程,代替上面的stop方法,不过要结合下面两个方法才能停止线程。

    interrupted():判断线程是否中断,并清理线程的中止状态。

    isInterrupted():判断线程是否中断,不会清理线程的中止状态。
    boolean isAlive():返回boolean,判断线程是否还活着

    补充:stop()和interrupt()区别在于,stop()是马上停止线程,interrupt()是把线程停止控制权给你,你想在哪里停止都可以。

    代码:

    /**
     * 1.线程让步yield(),让优先级高的线程大概率优先执行,但不一定都执行
     * 2.线程插队join(),指定线程在哪里执行完(优先级低的也可以)才继续往下执行,该线程执行期间,主线程阻塞状态
     * 3.线程睡眠sleep(),让线程睡眠多久,放弃对CPU控制,使其他线程有机会被执行
     * 4.线程强制停止stop(),立刻停止线程,存在安全问题,该方法已过时,采用interrupt()代替
     * 5.线程停止状态interrupt(),给线程设置停止状态,然后利用isInterrupted()判断线程的停止状态,决定是否停止线程
     * 6.线程判断存活isAlive(),判断线程生命周期是否已经结束
     * @author leak
     *
     */
    public class Test5 {
        public static void main(String[] args) throws InterruptedException {
    
            Runnable run = new TestRun2();
            Runnable run1 = new TestRun2();
            // 传递同一对象,共享里面的变量
            Thread t0 = new Thread(run);
            Thread t1 = new Thread(run);
    
            t0.start();// 启动线程,一直执行run方法
            t1.start();
            
    //        t0.stop();//线程停止,直接终止该线程生命周期,不过现在该方法已过期,存在各种安全问题
            //isAlive()判断线程是否存活,返回布尔值
            System.out.println(t0.isAlive());
            
            System.out.println("---------------------");
            System.out.println("---------------------");
            
            t0.join();//1. 指定线程在哪里执行完,再执行下面的语句,也就是从上往下执行,当前语句没有执行完,就不会往下执行。
            System.out.println("---------------------");
            
            
    //        2. Thread.sleep(300);//只有线程阻塞时,interrupt()才会生效,所以这里手动阻塞线程3秒
            // 或者这里展示开启2个线程,t0开启线程时,t1也开启了,t1开启时,t0线程处在阻塞期间,
            // 所以会执行t0.interrupt()改变t0线程的终止状态为true,因为只有线程处于阻塞期间,interrupt()才会生效
    //        3. t0.interrupt();//判断到线程t0已经阻塞,线程终止状态改为true
        }
    }
    
    class TestRun2 implements Runnable {
        int count = 1;
    
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
    
                // 4. isInterrupted():判断线程的终止状态,如果为终止就返回true,否则返回false
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程停止状态");
                    break;// 线程的终止状态为true,手动停止线程
                } else {
                    if (i % 2 == 0) { //设置线程让步的条件
                        //5. 线程让步yield(),有一定概率让步,和 setPriority(级数)设置线程优先级一样,优先级高的也不一定优先执行
                        Thread.yield();
                    }
    
                    System.out.println("线程: " + Thread.currentThread().getName() + "正在运行: " + count);
                    ++count;// 因为有两个线程开启了,而且是变量共享,所以i<5会被执行2次
                }
            }
        }
    }
    View Code

    线程的生命周期

    JDK中用Thread.State枚举表示了线程的几种状态
    要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态
      1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
      2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
      3. 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
      4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
      5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止

    线程的生命周期执行状态图如下:

    总结:线程从创建,然后start()方法启动线程时,线程处于就绪状态,如果获得CPU执行权就执行run()方法来运行线程,如果没有获得CPU执行权就一直处于就绪状态(可通过yield()方法线程让步就失去了CPU执行权),当然线程处于运行状态时,可以通过4种方法让线程处于阻塞状态,阻塞状态结束后,线程就会回到就绪状态,等待获取CPU的执行权,当然如果线程很顺利的执行一直到线程结束,或是线程异常/其他突发情况导致线程结束,线程就是处于死亡状态

    补充:就绪-运行-阻塞3个状态可以是一个无限循环,直到线程运行状态结束,才会结束循环。

    线程的同步

    上面的例子如果是多线程运行,就算有if判断取款金额不能大于余额,一样没有进入if语句里面,例子代码如下:

    /**
     * 例子:多线程共享资源,导致线程安全问题 提款,判断账号钱够不够
     * 多线程调用这个方法,就有问题,线程共享资源时,一个线程在执行这个方法没有完毕时,另一个线程又开始执行这个方法 导致线程安全问题
     * 
     * @author leak
     *
     */
    public class Test6 {
        public static void main(String[] args) {
            Acount acount = new Acount();// 创建一个账号
    
            Runnable wei_xin = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额
    
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动,看控制台输出,明显最后结果是-1000,为什么没有进入if语句判断呢
            t0.start();
            t1.start();
        }
    }
    
    //账号
    class Acount {
        public static int money = 3000;// 静态变量,全局共享
    
        public void drawing(int m) {
            String name = Thread.currentThread().getName();// 当前线程名称
    
            // 取款金额超出余额,提示金额不足
            if (money < m) {
                System.out.println(name + "操作,账号金额不足:" + money);
            } else {
                System.out.println(name + "操作,账号原有金额:" + money);
                System.out.println(name + "操作,取款金额:" + m);
                System.out.println(name + "取款操作: 原金额" + money + " - 取款金额:" + m);
                money -= m;
                System.out.println(name + "操作,取款后的余额:" + money);
            }
        }
    }
    
    class User implements Runnable {
        Acount acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User(Acount acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            acount.drawing(money);
        }
    
    }
    View Code

    上面的代码运行结果,最后是-1000,并没有进入到if语句判断金额不足里面。

    问题的原因:
    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
    解决思路:
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

    怎么实现解决思路呢,那就要用到synchronized同步锁机制。

    synchronized同步锁机制

    Java对于多线程的安全问题提供了专业的解决方式:
    同步机制
    、synchronized还可以放在方法声明中,表示整个方法
    为同步方法。
    例如:
    public synchronized void show (String name){
    ….
    }


    、synchronized (对象){
    // 需要被同步的代码;
    }

    1)第种的情况1 在方法上加synchronized关键字,锁的是整个对象(这里的对象指的是共享资源的对象),不是锁被修饰的方法。

    2)第种情况2 , 不同的共享对象是不同的锁,如果你传了两个不同的对象给不同的Runnable类,那么就有两个synchronized锁,两个锁是分别运行的,互相不干涉。所以直接在普通方法加synchronized同一个共享对象可以锁住,但是不同共享对象,怎么锁呢,直接在synchronized前面加static修饰符(相当于static修饰的变量共享,所以static synchronized就是共享锁),就是不同对象共享一个锁。

    方法的两种情况代码如下:

    1)情况(被synchronized修饰的普通方法,锁的是同一共享对象,不是方法):

    /**
     * 第1种方法的情况1,同一个共享对象,只要在普通方法加synchronized就可以锁住共享对象
     * 
     * @author leak
     *
     */
    public class Test6 {
        public static void main(String[] args) {
            Acount acount = new Acount();// 创建一个账号,这里就是共享对象
                
            //注意这里的acount是同一个共享对象,所以共享静态变量money=3000
            Runnable wei_xin = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额
            
            //创建2个线程
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动
            t0.start();
            t1.start();
        }
    }
    
    //账号
    class Acount {
        public static int money = 3000;// 静态变量,全局共享
        
        //synchronized同步
        public synchronized void drawing(int m) {
            String name = Thread.currentThread().getName();// 当前线程名称
    
            // 取款金额超出余额,提示金额不足
            if (money < m) {
                System.out.println(name + "操作,取款金额:"+m+", 银行卡账号金额不足剩:" + money);
            } else {
                System.out.println(name + "操作,银行卡账号原有金额:" + money);
                System.out.println(name + "操作,取款金额:" + m);
                System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                money -= m;
                System.out.println(name + "操作,取款后的银行卡余额:" + money);
            }
        }
        
        //这里加了一个不同名字同内容的方法,就是为了说明synchronized锁住的是同一对象,不是锁住方法
        public synchronized void drawing1(int m) {
            String name = Thread.currentThread().getName();// 当前线程名称
    
            // 取款金额超出余额,提示金额不足
            if (money < m) {
                System.out.println(name + "操作,取款金额:"+m+", 银行卡账号金额不足剩:" + money);
            } else {
                System.out.println(name + "操作,银行卡账号原有金额:" + money);
                System.out.println(name + "操作,取款金额:" + m);
                System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                money -= m;
                System.out.println(name + "操作,取款后的银行卡余额:" + money);
            }
        }
    }
    
    class User implements Runnable {
        Acount acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User(Acount acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        /**
         * 重点:这里很详细说明了为什么在普通方法加synchronized是锁对象,而不是锁方法
         * 这里可以在下面的if语句打断点,然后调试模式,
         * 调式模式运行,可以看到左边分别有两个线程,一个是微信,一个是支付宝
         * 可以手动选择哪个线程先进行调试,调试完其中一个线程,发现余额剩1000,
         * 然后再调试另外一个线程发现,虽然是另外一个线程调用的是另一个方法,但是直接跳进了余额不足的if语句里,
         * 所以这里虽然分别调用了两个方法drawing/drawing1,但是最后两个方法的操作的对象资源都是共享的
         */
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            //下面不同线程分别调用不同的被synchronized修饰的普通方法,但是资源属性money=3000,还是被共享了,
            //所以synchronized锁的是共享对象
            if(Thread.currentThread().getName().equals("微信")) {
                acount.drawing(money);
            }else {
                acount.drawing1(money);
            }
            
        }
    }
    View Code

    2) 情况不同的共享对象是不同的锁,还介绍了不同的共享对象怎么使用共享锁static synchronized修饰)

    /**
     * 第1种方法的情况2,不同共享对象,只要在普通方法加static synchronized就可以共享锁 去锁不同共享对象
     * 如果方法没有加static修饰,如果还传递不同的共享对象,那么就有两个不同的锁,控制台最后输出-1000,因为不是同一共享对象,而且是两个锁,所以锁不住
     * 但是如果加了static修饰,就会共享锁,不同共享对象使用共享锁
     * 
     * @author leak
     * 注意:下面的Acount1和User1,不要使用Test6的Acount和User混淆了
     */
    public class Test7 {
        public static void main(String[] args) {
            Acount1 a1 = new Acount1();// 创建一个账号,这里就是共享对象
            Acount1 a2 = new Acount1();
    
            // 重点: 注意这里的a1和a2是不同的共享对象
            Runnable wei_xin = new User1(a1, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User1(a2, 2000);// 传递账号 并初始化,并且设置消费金额
    
            // 创建2个线程
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动
            t0.start();
            t1.start();
        }
    }
    
    //账号
    class Acount1 {
        public static int money = 3000;// 静态变量,全局共享
    
        // synchronized同步
        public static synchronized void drawing(int m) {
            String name = Thread.currentThread().getName();// 当前线程名称
    
            // 取款金额超出余额,提示金额不足
            if (money < m) {
                System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money);
            } else {
                System.out.println(name + "操作,银行卡账号原有金额:" + money);
                System.out.println(name + "操作,取款金额:" + m);
                System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                money -= m;
                System.out.println(name + "操作,取款后的银行卡余额:" + money);
            }
        }
    }
    
    class User1 implements Runnable {
        Acount1 acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User1() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User1(Acount1 acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            Acount1.drawing(money);//静态方法调用,共享锁  锁住不同共享对象
        }
    }
    View Code

    种方法的两种情况代码如下:

    1)情况(如果传递同一共享对象,被synchronized修饰的代码块加了同步锁),这种和上面第一种方法情况1基本一样,只是方式不一样。

    /**
     * 第2种方法的情况1,synchronized(this){代码块}被加了同步锁,前提是传递同一共享对象
     * 
     * @author leak 注意:下面的Acount2和User2,不要使用Test7的Acount1和User1混淆了
     */
    public class Test8 {
        public static void main(String[] args) {
            Acount2 a1 = new Acount2();// 创建一个账号,这里就是共享对象
    
            // 重点:这里传递的是同一a1共享对象
            Runnable wei_xin = new User2(a1, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User2(a1, 2000);// 传递账号 并初始化,并且设置消费金额
    
            // 创建2个线程
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动
            t0.start();
            t1.start();
        }
    }
    
    //账号
    class Acount2 {
        public static int money = 3000;// 静态变量,全局共享
        
        public void drawing(int m) {
            /**
             * synchronized(this){代码块} 给代码块添加同步锁
             * 其实这里和第一种方法的情况1很像,只不过一个是在方法上加synchronized修饰符,
             * 一个是在方法里添加synchronized(this){代码块},
             * 效果都是一样的
             */
            synchronized (this) {
                String name = Thread.currentThread().getName();// 当前线程名称
    
                // 取款金额超出余额,提示金额不足
                if (money < m) {
                    System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money);
                } else {
                    System.out.println(name + "操作,银行卡账号原有金额:" + money);
                    System.out.println(name + "操作,取款金额:" + m);
                    System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                    money -= m;
                    System.out.println(name + "操作,取款后的银行卡余额:" + money);
                }
            }
        }
    }
    
    class User2 implements Runnable {
        Acount2 acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User2() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User2(Acount2 acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            acount.drawing(money);//
        }
    }
    View Code

    2)情况(传递不同的共享对象,利用锁住类 实现不同对象共享锁

    /**
     * 第2种方法的情况2,synchronized(共享类.class){代码块}被加了同步锁,传递不同共享对象实现共享锁
     * 
     * @author leak 注意:下面的Acount3和User3,不要使用Test8的Acount2和User2混淆了
     */
    public class Test9 {
        public static void main(String[] args) {
            Acount3 a1 = new Acount3();// 创建一个账号,这里就是共享对象
            Acount3 a2 = new Acount3();
    
            // 重点: 注意这里的a1和a2是不同的共享对象
            Runnable wei_xin = new User3(a1, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User3(a2, 2000);// 传递账号 并初始化,并且设置消费金额
    
            // 创建2个线程
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动
            t0.start();
            t1.start();
        }
    }
    //账号
    class Acount3{
        public static int money = 3000;// 静态变量,全局共享
    
        // synchronized同步
        public void drawing(int m) {
            synchronized (Acount3.class) {//传递共享类实现不同共享对象使用共享锁
                String name = Thread.currentThread().getName();// 当前线程名称
    
                // 取款金额超出余额,提示金额不足
                if (money < m) {
                    System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money);
                } else {
                    System.out.println(name + "操作,银行卡账号原有金额:" + money);
                    System.out.println(name + "操作,取款金额:" + m);
                    System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                    money -= m;
                    System.out.println(name + "操作,取款后的银行卡余额:" + money);
                }
            }
        }
    }
    
    class User3 implements Runnable {
        Acount3 acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User3() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User3(Acount3 acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            acount.drawing(money);//
        }
    }
    View Code

    3)情况(不同共享对象,不同锁,这里代码和上面情况2,只是改了点代码,这里只放修改的代码)

    //Acount3类的方法
    // synchronized同步,这里形参根据传递不同的共享对象,实现不同锁
        public void drawing(int m,Acount3 a) {
            synchronized (a) {//传递不同共享对象,使用不同锁
                String name = Thread.currentThread().getName();// 当前线程名称
    
                // 取款金额超出余额,提示金额不足
                if (money < m) {
                    System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money);
                } else {
                    System.out.println(name + "操作,银行卡账号原有金额:" + money);
                    System.out.println(name + "操作,取款金额:" + m);
                    System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                    money -= m;
                    System.out.println(name + "操作,取款后的银行卡余额:" + money);
                }
            }
        }
    
    //User3的run方法
        @Override
        public void run() {
            // 把对象初始化的money传递给drawing方法
            acount.drawing(money,acount);//
        }

    总结:两个方法,如果针对对象要加同步锁,那synchronized就加在方法上,如果针对某一段代码需要加同步锁,那就直接在代码块上加同步锁。

    线程的死锁问题

    死锁
      不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    解决方法
      1. 专门的算法、原则,比如加锁顺序一致
      2. 尽量减少同步资源的定义(例如:多个线程使用一个共享资源,如果多个线程使用多个共享资源,导致死锁概率大,也就是尽量保持 多对一,不要多对多关系),尽量避免锁未释放的场景。

    如果上面概念听不懂,那举个例子:

    比如线程a0,需要执行方法f0,线程a1需要执行方法f1,前提:f0和f1都是有同步锁的方法,现在的情况是,a0调用f1方法并且一直没有执行完f1(也就是卡住了),a1调用f0方法并且一直没有执行完f0,导致a0和a1都在等对方释放方法,对方都不释放,这样就形成了线程的死锁。

    线程通信

    wait() 与 notify() 和 notifyAll()
      1. wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
      2. notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
      3. notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
    Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常

    这里三个方法有什么用呢,比如上面synchronized修饰方法,第二种方法情况3运行结果,都是微信先执行线程,然后才是支付宝线程执行(因为微信线程先开启),那怎么先支付宝线程,后执行微信线程呢,这就要使用到线程等待wati()和线程唤醒notify()了。

    代码:

    /**
     * 注意下面的是 Acount5和User5类
     * 
     * @author leak
     *
     */
    public class Test10 {
        public static void main(String[] args) {
            Acount5 acount = new Acount5();// 创建一个账号,这里就是共享对象
    
            // 注意这里的acount是同一个共享对象,所以共享静态变量money=3000
            Runnable wei_xin = new User5(acount, 2000);// 传递账号 并初始化,并且设置消费金额
            Runnable zhifu_bao = new User5(acount, 2000);// 传递账号 并初始化,并且设置消费金额
    
            // 创建2个线程
            Thread t0 = new Thread(wei_xin, "微信");
            Thread t1 = new Thread(zhifu_bao, "支付宝");
    
            // 2个线程启动
            t0.start();// 因为微信线程先启动,所以默认微信先执行完线程
            t1.start();
        }
    }
    
    //账号
    class Acount5 {
        public static int money = 3000;// 静态变量,全局共享
    
        // synchronized同步
        // 重点:wait(),notify(),notifyAll()都要在同步锁中使用,否则报异常。
        public void drawing(int m, Acount5 a) {
            synchronized (a) {
    
                String name = Thread.currentThread().getName();// 当前线程名称
    
                // 首先判断当前线程是否是微信
                if (name.equals("微信")) {
                    // 如果是微信,那么微信线程先等待,进入阻塞状态
                    try {
                        a.wait();// a对象是传过来的共享对象
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
    
                // 取款金额超出余额,提示金额不足
                if (money < m) {
                    System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money);
                } else {
                    System.out.println(name + "操作,银行卡账号原有金额:" + money);
                    System.out.println(name + "操作,取款金额:" + m);
                    System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m);
                    money -= m;
                    System.out.println(name + "操作,取款后的银行卡余额:" + money);
                }
    
                // 因为一开始微信线程处于阻塞状态,而且支付宝线程已经在上面代码执行完,所以要唤醒处于阻塞中的微信线程
                if (name.equals("支付宝")) {
                    a.notify();// 唤醒微信线程继续执行
                    // a.notifyAll()也可以使用,这个方法是唤醒所有的阻塞线程。
                }
            }
        }
    }
    
    class User5 implements Runnable {
        Acount5 acount;// 给用户一个账号属性
        int money; // 消费金额
    
        public User5() {
        }
    
        // 创建线程User类时就初始化一个账号和消费金额给用户
        public User5(Acount5 acount, int money) {
            this.acount = acount;
            this.money = money;
        }
    
        @Override
        public void run() {
            acount.drawing(money, acount);
        }
    }
    View Code

    消费者和生产者模式

      例子:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。


    这里可能出现两个问题:
    生产者比消费者快时,消费者会漏掉一些数据没有取到。
    消费者比生产者快时,消费者会取相同的数据。

    例子代码如下:

    /**
     * 生产者和消费者模式,注意下面为无限循环,建议调试模式看线程运行流程
     * @author leak
     *
     */
    public class Test11 {
        public static void main(String[] args) {
            Clerk c = new Clerk();//共享对象,产品数量
            
            //匿名内部类,生产者
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    //同步锁传入共享对象
                    synchronized(c) {
                        //获取线程名
                        String name = Thread.currentThread().getName();
                        
                        while(true) {
                            if(c.productNum==0) {
                                System.out.println("产品数量为0,"+name+"开始生产");
                                while(c.productNum < 4) {
                                    c.productNum++;
                                    System.out.println("库存为:"+c.productNum);
                                }
                                System.out.println("产品数为:"+c.productNum+",结束生产");
                                c.notify();//2产品生产结束,代表产品数已生产满,唤醒消费者线程,注意这里唤醒了消费者线程,
                                //所以消费者线程处于就绪状态(但不会运行),因为现在生产者线程还在运行,继续循环进入下面的else语句,
                                //然后执行wait()代表当前生产者线程进入阻塞状态释放对CPU的控制权,因为消费者处于就绪状态,所以消费者线程开始运行
                            }else {
                                try {
                                    c.wait(); //1如果产品数量不为0,代表还有产品,所以生产者线程等待
                                } catch (InterruptedException e) {
                                    // TODO Auto-generated catch block
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }},"生产者").start();
            
            
            //匿名内部类,消费者
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    //获取线程名
                    String name = Thread.currentThread().getName();
                    
                    synchronized(c) {
                        while(true) {
                            //判断产品数量是否满了
                            if(c.productNum == 4) {
                                System.out.println("产品数量为4,"+name+"开始消费");
                                while(c.productNum > 0) {
                                    c.productNum--;
                                    System.out.println("库存为:"+c.productNum);
                                }
                                System.out.println("产品数为:"+c.productNum+",结束消费");
                                c.notify();//1产品消费结束,代表产品数已消费完,唤醒生产者线程,
                                //因为上面的生产者现在进入了阻塞状态,现在唤醒生产者处于就绪状态,但是不会马上运行,
                                //因为当前是消费者线程在运行,继续循环进入下面的else语句,执行wait()消费者进入阻塞状态
                                //消费者进入阻塞状态后,放弃对CPU的控制权,因为这里生产者已经被唤醒处于就绪状态,所以执行生产者线程
                            }else {
                                try {
                                    c.wait(); //进入else这里,代表产品已经被消费完,所以消费者线程等待进入阻塞状态
                                } catch (InterruptedException e) {
                                    // TODO Auto-generated catch block
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }},"消费者").start();
        }
    }
    
    class Clerk{
        public static int productNum = 0;//店员持有商品数量
    }
    View Code

    难点:估计在c.wait()和c.notify()那里,会分不清什么时候是消费者线程唤醒还是生产者线程唤醒/ 消费者和生产者进入等待,建议分别在两个匿名类里面的run()方法中,获取线程名/if语句/c.wait() 这三个地方打断点调试,看清楚线程运行情况,注意其中一个线程进入阻塞时,要手动切换到另外一个线程继续调试。

    强调:c.wait()和c.notify()那里我注释写的很详细了,注意看。

    如下图,手动切换线程继续调试。

     如果不会调试的,建议先百度看完eclipse如何调试,还有注意代码的synchronized块,传递的是同一个共享对象c,所以两个线程才能共享产品数量。

  • 相关阅读:
    C#基础:单例模式与多线程
    C#基础:线程同步
    C#基础:C#中的数据结构
    C#基础:GC中什么是代,一共分几代
    C#基础:Dispose方法和Finalize方法在何时被调用
    C#基础:.NET中GC的运行机制
    C#基础:简述.NET中堆栈和堆的特点和差异
    PLSQL_基础系列05_视图控制WITH CHECK OPTION(案例)
    PLSQL_基础系列04_时间间隔INTERVAL(案例)
    PLSQL_基础系列03_合并操作UNION / UNION ALL / MINUS / INTERSET(案例)
  • 原文地址:https://www.cnblogs.com/unlasting/p/13382149.html
Copyright © 2011-2022 走看看