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,所以两个线程才能共享产品数量。

posted @ 2020-08-02 10:41  HainChen  阅读(203)  评论(0编辑  收藏  举报