Java多线程

多线程学习实践

Posted by CDz on November 27, 2017

1.多线程概述

多线程的实现由两种,一种是时间片,一种是锁机制。Java中的多线程使用的锁机制。每一个对象都有会有一个锁,通过锁对象来控制调度多线程,也正因为有锁机制,才会使线程有很多的状态。

多进程与多线程:

每一个程序就是一个进程(有点抽象举个栗子,在电脑上我们同时开QQ、微信、chrome,这个三个分别是程序(进程)),而在程序中的每个功能又是一个线程(再举个栗子,qq可以同时与多个人聊天),所以线程是在进程里面的。

1.1java线程中生命周期

多线程的生命周期:

多线程的生命周期图

这张图中详细的描述了线程的生命周期。

2.synchronized关键字

synchronized关键字,在多线程中同步的关键,其可以修饰方法或者,直接在代码块中使用,注意,不能修饰变量与类使用时必定其中是有一个锁对象的,这样才有同步的意义,而在修饰方法时其锁对象是方法所在类本身(this),使用到代码块中可以任意传入对象,但是要根据场景来做选择,传入什么样的对象才可以实现线程同步。

3.Thread类API

3.1sleep()与wait()

sleep():

  • sleep方法使当前线程休眠,是Thread类中方法,需要传入时间
  • 调用sleep方法后,当前线程处于休眠状态,并不释放锁,其他线程不可能抢到CPU执行权
  • sleep当休眠时间到后自动醒来执行剩下(同步)代码,然后才回释放锁。
  • sleep最好不要使用在步代码块中。

wait():

  • wait方法是Object类中,也就是说每个类都有此方法,也证明了每个对象都有一把锁
  • wait方法可以传入时间,也可以不传。线程等待时,是释放锁的,其他线程的同步代码块可以执行
  • wait方法当不传入时间时,需要notify或者notifAll方法主动唤醒
  • wait方法调用只能在同步代码块中(也就是synchronized关键字修饰的域中)

注意:这里我有一个疑惑的地方,在多线程的生命周期图中,我们发现经过运行状态到堵塞,堵塞完了之后又回到了,可运行状态(这个状态是不能保证一定被执行的,需要抢到锁了才有执行权),而从使用与描述中。发现sleep好像不是这个过程,而是sleep堵塞完之后又回到的运行状态,这里我的想法是,可能是线程生命周期的概念混淆了,当线程sleep之后确实是进入了堵塞状态,但是没有释放其线程所拥有的锁,导致其他线程不能运行,而当其休眠时间到后,这时确实是回到了可运行状态,但是sleep所在的同步代码块还是没有释放锁的,也就说是一个可运行状态且拥有锁的线程,这时就直接进入了运行状态,执行完后才释放锁,其他线程才可以抢锁执行。

3.2join()

join的作用:当前线程等待子线程运行完后在向下执行。

应用场景:在主线程中开启了多个子线程一起完成一项任务处理,而可能子线程处理的时间过长,主线程早已结束,但是主线程却需要子线程计算的结果。这时就需要用到join()方法。

测试join()

public class ThreadTest {
    //测试类
    static class MyThreadOne extends Thread{
        //构造函数,传入线程名
        public MyThreadOne(String name) {
            //调用父类构造函数
            super(name);
        }
        @Override
        public void run() {
            System.out.println("线程:"+getName()+"开始");
            for (int i = 0; i < 4; i++) {
                System.out.println(i+" "+getName());//getName()可以获取所在线程的名称
                try {
                    //这里线程睡眠,模拟此线程有很多业务逻辑在执行
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程:"+getName()+"结束");
        }
    }
}

测试一:不加join时,线程的执行过程

public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        MyThreadOne a = new MyThreadOne("a");
        a.start();
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }

输出的结果:

main线程开始
main线程结束 //可以看出主线程运行的非常快,执行完创建a线程的代码就结束了,这时连JVM还没有初始化线程
线程:a开始
0 a
1 a
2 a
3 a
线程:a结束

测试二:加入join不传参数

 public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        MyThreadOne a = new MyThreadOne("a");
        a.start();
        try {
            //加入
            a.join();
            System.out.println("join执行完了~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }

输出结果为:

main线程开始
线程:a开始
0 a
1 a
2 a
3 a
线程:a结束
join执行完了~ //这里发现当join不传入时间时,主线程会等待子线程全部处理完后才会向下执行
main线程结束

测试三:join传入参数(时间)

public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        MyThreadOne a = new MyThreadOne("a");
        a.start();
        try {
            a.join(1000);
            System.out.println("a join执行完了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }

打印结果为:

main线程开始
线程:a开始
0 a
a join执行完了 //这里看到首先第一点,join是在main之前执行完。
main线程结束   //当join传入时间过了主线程便会向下执行,不再等待a线程。
1 a
2 a
3 a
线程:a结束

测试四:是哪个线程掉用就只等待哪个线程执行完吗,是否等待所有线程?

public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        //创建线程a
        MyThreadOne a = new MyThreadOne("a");
        //创建线程b
        MyThreadOne b = new MyThreadOne("b");
        //开启线程a
        a.start();
        try {
            //模拟 a线程的开启与b线程的开启之间有很多的业务逻辑(保证其不是很快的同时开启)
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //开启线程b
        b.start();
        try {
            //a线程调用join
            a.join();
            System.out.println("a join执行完了~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }

打印结果:

main线程开始
线程:a开始
0 a
1 a
线程:b开始
0 b
2 a
3 a
1 b
线程:a结束     //当a线程结束后
a join执行完了~ //主线程打印 a join执行完了
main线程结束    //主线程向下执行到结束
2 b
3 b
线程:b结束     //b线程由于晚一些开启所以,最后结束。

3.2.1join总结:

  • join是Thread类中的方法,且不是静态方法,所以必须是开启了的线程才能调用
  • join是将调用的线程,会使主线程堵塞,等待被调用线程执行完后再继续主线程执行
  • join若传入时间,那么会主线程会等待调用线程执行指定时间后再继续执行。
  • 多个子线时,那个线程调用join主线程就等待那个线程执行完。

4.多线程编码

测试类:

public class SyncThreadTest{
    static class Printer{
        public void print1(){
                System.out.print("1");
                System.out.print("2");
                System.out.print("3");
                System.out.println();
        }

        public void print2(){
                System.out.print("a");
                System.out.print("b");
                System.out.print("c");
                System.out.println();
        }
    }
}

大致解释一下这个类,SyncThreadTest类中有一个Printer类内类,需要开两个线程来分别执行Printer printer =new Printerprint1print2,想要的效果是这两个方法互斥。例如:当一个线程执行print1()时,另一个线程需要执行的print2()堵塞,后面所以的实验都用到的是这个类。

4.1同步代码块

4.1.1Junit测试多线程问题

    @Test
    public void testSynThread(){
        System.out.println("test开始");
        Printer printer = new Printer();
        new Thread(){
            @Override
            public void run() {
                //无限循环是为了在控制台中打分辨是否方法互斥,因为CPU执行实在太快了。
                while (true){
                    printer.print1();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                while (true){
                    printer.print2();
                }
            }
        }.start();

        System.out.println("test结束");
    }

    //输出结果竟然是;
    //第一次:
    //test开始
    //123
    //test结束
    //abc
    //第二次:
    //test开始
    //test结束

怎么会输出这样的结果?网上查了一下,原来Junit并不支持多线程测试。测试不管子线程是否结束都会回调TestResult的wasSuccessful方法,然后判断结果是成功还是失败,最后调用相应的System.exit()方法~~,太坑了。。还是老老实实用main方法来测试吧。

4.1.2main方法测试

   public static void main(String[] args) {
        System.out.println("test开始");
        Printer printer = new Printer();
        new Thread(){
            @Override
            public void run() {
                //无限循环是为了在控制台中打分辨是否方法互斥,因为CPU执行实在太快了。
                while (true){
                    printer.print1();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                while (true){
                    printer.print2();
                }
            }
        }.start();
        System.out.println("test结束");
    }

原始类不加添加任何同步时:

123
123
123
123

12abc
abc
abc
123
123
1abc
abc
abc

控制台打印的太多,这里只截取出错误的地方。明显这是不符合我们的预期的(想要两线程调用同一对象的两个方法是互斥的)

添加synchronized:

static class Printer{
        String s = new String();
        public void print1(){
            synchronized (s){
                System.out.print("1");
                System.out.print("2");
                System.out.print("3");
                System.out.println();
            }
        }
        public void print2(){
            synchronized (s){
                System.out.print("a");
                System.out.print("b");
                System.out.print("c");
                System.out.println();
            }
        }
    }

输出结果:

abc
abc
abc
abc
123
123
123
//是没有出现一行中有字母与数字的,说明同步成功。读者可以自己去实验一下,看是否正确。

注意:同步代码块是必须要同一对象的锁的,如果是不同对象,那么将不会其效果。

4.2同步方法

        //当一个方法上添加synchronized关键字,如何保证另一个方法的同步?
        public synchronized void print1(){
            System.out.print("1");
            System.out.print("2");
            System.out.print("3");
            System.out.println();
        }
        public void print2(){
            System.out.print("a");
            System.out.print("b");
            System.out.print("c");
            System.out.println();
        }

这时想要这两个方法同步的方法有两种:

  • 同步代码块中锁对象为 this
          public void print2(){
              synchronized (this){
                  System.out.print("a");
                  System.out.print("b");
                  System.out.print("c");
                  System.out.println();
              }
          }
    
  • 同样需要同步的方法上添加 synchronized 关键字
          public synchronized void print2(){
                  System.out.print("a");
                  System.out.print("b");
                  System.out.print("c");
                  System.out.println();
          }
    

总结思考: 我的理解是,当在方法上添加synchronized关键字之后,因为没有像 代码块那样声明 同步对象,所以java将其同步对象默认为此对象,即是this,所以在如果其他代码块需要于此互斥时需要传入this这个锁对象。或者其方法也用synchronized修饰,重点静态方法被synchronized修饰又是不同锁对象,所以需要注意。接下来将会讲到。

4.3同步静态方法

        //当静态方法被synchronized修饰时,如何让其他代码与此互斥?
        public static synchronized void print1() {
            System.out.print("1");
            System.out.print("2");
            System.out.print("3");
            System.out.println();
        }

上小节中说 静态方法 与 非静态方法的锁是不同的,这里就来做一个实验:

        //静态方法 被synchronized
        public static synchronized void print1() {
            System.out.print("1");
            System.out.print("2");
            System.out.print("3");
            System.out.println();
        }
        //非静态方法 被synchronized
        public synchronized void print2() {
            System.out.print("a");
            System.out.print("b");
            System.out.print("c");
            System.out.println();
        }

输出的结果:

123
123
123
123abc
abc
abc
abc
//出现同行,字母与数字 说明 他们两确实不是同一锁。

如果想要同步静态方法的代码块需要如何做呢?

  • 方法同样为静态且被synchronized修饰
          public static synchronized void print2() {
                  System.out.print("a");
                  System.out.print("b");
                  System.out.print("c");
                  System.out.println();
          }
    
  • 代码块中synchronized的锁对象传入其方法所在类的class对象
          public void print2() {
              synchronized (Printer.class){
                  System.out.print("a");
                  System.out.print("b");
                  System.out.print("c");
                  System.out.println();
              }
          }
    

4.4案例一(多窗口售票)

了解了上面的这些知识之后可以用一个案例来加深知识理解。

实现Runnable接口

/**
 * Created by CDz_ on 2017/11/27.
 * 多线程售票
 * 实现Runnable接口写法
 */
public class SyncThread_Demo1 {

    public static void main(String[] args) {
        TicketThread ticketThread = new TicketThread();
        Thread a = new Thread(ticketThread,"A");
        Thread b = new Thread(ticketThread,"B");
        Thread c = new Thread(ticketThread,"C");
        Thread d = new Thread(ticketThread,"D");
        a.start();
        b.start();
        c.start();
        d.start();
    }
    static class TicketThread implements Runnable {
        //定义票的总数
        private int ticket = 100;
        @Override
        public void run() {
            //获取当前线程的名字
            String name = Thread.currentThread().getName();
            while (true) {
                //同步代码块
                synchronized (this) { //this 与 TicketThread.class 都可以
                    if (ticket <= 0) {
                        break;
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "线程窗口售出,一共还剩:" + ticket-- + "张票");
                }
            }
        }
    }
}

以上代码有两点需要注意:

  • 同步代码块是在while里面而非外面,因为这里如果在外面那么就会使一个线程执行完全部的售票操作。
  • 锁对象的使用,在main方法中我们只new出了一个TicketThread对象,故在JVM堆中只有一个次对象。所以使用this是可以,当然TicketThread.class也是可以的,因为JVM中只有一个TicketThread.class对象。

继承Thread类

/**
 * 多线程售票
 * 继承Thread类
 */
public class SyncThread_Demo2 {

    public static void main(String[] args) {
        new TicketThread("A").start();
        new TicketThread("B").start();
        new TicketThread("C").start();
        new TicketThread("D").start();
    }
    static class TicketThread extends Thread{
        //使用静态变量,因为在main方法中new出了多个此类的实例。
        //如果是成员变量,不会对象中共享,只有静态变量多实例中才回共享。
        private static int ticket = 100;
        public TicketThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            while (true) {
                //这里锁对象必须是要此类的.class对象,因为只有他才是唯一的,当然也可以new出一个静态应用变量作为锁对象。
                //这里不能用this的原因同 private static int ticket = 100; 使用静态变量道理一样。(这是JAVA基础方面的,看不懂的同学可以去复习一下JAVA基础知识)
                synchronized (TicketThread.class) {
                    if (ticket <= 0) {
                        break;
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "线程窗口售出,一共还剩:" + ticket-- + "张票");
                }
            }
        }
    }
}

4.5案例二(多线程通信)

为什么要多线程通信?

上列的一些列子可以看出,CPU执行每个任务是分时间片的,而且每个线程抢夺资源也是随机的(注意千万不要过度依赖线程的优先级),那么就会出现这样一个场景,多线程处理,但是又需要线程之间按我们想要的顺序来执行,就需要多线程之间通信,一个线程执行完了别的线程才可以去执行,为了应对这样的场景故需要多线程之间通信其通信的基础是使用到 wait notify notifyAll,这三个Object类中函数。

先简单一点:两个线程通信:

/**
 * @author CDz_
 * @create 2017-11-28 14:44
 * 案例场景,因为CPU执行是无序的,通过wait notify notifyall 控制多线程,使其有序
 * 实现输出顺序为:
 * 123456 
 * abcdef
 **/
public class NotifyThread_Demo1 {

    public static void main(String[] args) {
        Printor printor = new Printor();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    printor.print1();
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    printor.print2();
                }
            }
        }.start();


    }

}

class Printor {
    //通信标识
    private int flag = 1;
    public synchronized void print1() {
        //通过判断是否为1,让线程堵塞
        if (flag != 1) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print(1);
        System.out.print(2);
        System.out.print(3);
        System.out.print(4);
        System.out.print(5);
        System.out.print(6);
        System.out.println();
        //执行完需要输出的代码之后,将标识设置为第二个线程通信的条件,也确保此线程睡眠。
        this.flag = 2;
        //唤醒在等待的线程(也就是第二个线程)
        this.notify();
    }

    public synchronized void print2() {
        //判断是否为2,让线程堵塞
        if (flag != 2) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print("a");
        System.out.print("b");
        System.out.print("c");
        System.out.print("d");
        System.out.print("e");
        System.out.print("f");
        System.out.println();
        //同理执行完之后,第一个线程通信的条件,也确保此线程睡眠
        flag = 1;
        //唤醒在等待的线程(也就是第一个线程)
        this.notify();
    }
}

注意:

此时我使用的是用synchronized修饰方法,也可以使用synchronized代码块,使用synchronized代码块有两点注意

  • 需要将整个方法里的内容都要包括进代码块中。(使用wait notify,必须要在同步代码块中,不然运行时会报错java.lang.IllegalMonitorStateException)。
  • 使用锁中传入的对象可以是this,也可是Printor.class

三个或三个以上线程通信:


/**
 * @author CDz_
 * @create 2017-11-28 21:58
 * 三个线程或三个线程以上通信
 * 实现输出顺序为:
 * 123456 
 * abcdef
 * ABCDEF
 **/
public class NotifyAll_Demo1 {
    public static void main(String[] args) {
        Printor printor = new Printor();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    printor.print1();
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    printor.print2();
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    printor.print3();
                }
            }
        }.start();

    }

    static class Printor {
        private int flag = 1;
        public synchronized void print1() {
            //注意这里为什么要使用 while循环 而不是if做判断
            //因为notifyAll,是唤醒所有的堵塞现象,如果用if那么线程被唤醒后就会直接往下执行代码,
            //而这时可能这个线程是不符合条件的。所以需要用while来做为判断,
            //建议多线程通信时,wait方法都用while来做判断。
            while (flag != 1) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print(1);
            System.out.print(2);
            System.out.print(3);
            System.out.print(4);
            System.out.print(5);
            System.out.print(6);
            System.out.println();
            //使第二线程判断放行
            this.flag = 2;
            //这里使用notifyAll,而不使用notify的原因:
            //因为notify只能随机的唤醒一个线程,他可能唤醒的是不符合条件的线程,
            //这时就不符合条件的线程经过while循环判断,又进入堵塞导致所有线程都在堵塞JVM就会报错
            this.notifyAll();
        }

        public synchronized void print2() {
            while (flag != 2) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("a");
            System.out.print("b");
            System.out.print("c");
            System.out.print("d");
            System.out.print("e");
            System.out.print("f");
            System.out.println();
            //使第三线程判断放行
            flag = 3;
            this.notifyAll();
        }

        public synchronized void print3() {
            while (flag != 3) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("A");
            System.out.print("B");
            System.out.print("C");
            System.out.print("D");
            System.out.print("E");
            System.out.print("F");
            System.out.println();
            //使第一线程判断放行
            flag = 1;
            this.notifyAll();
        }
    }
}

4.6小结:

  • 当需要同步普通多代码块时,可以是任意的对象锁,但是必须保证同一对象
  • 当同步非静态方法时,其锁是this,此对象本身
  • 当同步静态方法时,其锁是方法所在类的class对象
  • 实现Runnable接口与继承Thread类在做代码互斥时,锁对象是的使用是有区别的
  • 实现Runnable接口,在使用多线程时只需要new一次,所以成员变量不需要是静态,锁对象也可以是this或者此类的class对象
  • 继承Thread类,使用多线程时需要new多次,所以成员变量必须是静态的(这里才可以多实例共享),而锁对象必须是此类的class对象