JavaSE基础——(24)多线程

目录

一、多线程概述

二、Java程序运行原理

三、多线程实现

3.1继承Thread类

3.2实现Runnable接口

3.3两种方式的区别

3.4匿名内部类实现线程

3.4.1继承Thread

3.4.2实现Runnable

四、线程的方法

4.1获取与设置名字

4.2线程的休眠

4.3守护线程setDaemon()

4.4加入线程join()

4.5线程的优先级

五、同步

5.1同步代码块

5.2同步方法

5.3线程安全问题

5.3.1继承Thread

5.3.2实现Runnable接口

5.4死锁问题

六、单例设计模式

6.1单例设计模式概述与实现

6.2 Runtime类

七、Timer类

八、线程之间的通信

8.1两个线程之间的通信

8.2多个线程之间的通信

8.3线程间通信注意问题

8.4互斥锁

九、线程组

十、线程的状态

十一、线程池

11.1线程池概述与使用

11.2 Callable

十二、工厂模式

12.1简单工厂模式概述与使用

12.2工厂方法模式概述与使用


一、多线程概述

线程是操作系统能够进行运算调度的最小单位,

它被包含在进程之中,是进程中的实际运作单位,

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,

而多线程则是并发执行程序,提高程序的效率,达到同时完成多项工作的目的。

二、Java程序运行原理

java命令会启动java虚拟机JVM,等于启动了一个应用程序,也就是启动了一个进程。

该进程会自动启动一个“主线程”,然后主线程去带调用某个类中的main方法。

注意:JVM的启动至少启动了垃圾回收线程和主线程,所以说JVM启动是多线程的。

三、多线程实现

3.1继承Thread类

我们可以定义自己的类,继承Thread,并且重写run方法,

然后把新线程要做的事情写在run方法中,然后创建线程对象,调用start方法开启新线程,内部会自动执行run方法。 

public class ThreadTest {
    public static void main(String[] args) {
        myThread mt=new myThread();//创建线程子类对象
        mt.start();//创建并启动子线程,执行run方法

        for (int i = 0; i < 1000; i++) {
            System.out.println("run main method!");
        }
    }
}

class myThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("run thread method!");
        }
    }
}

可以看到主方法线程和子线程的语句交替执行输出语句,

3.2实现Runnable接口

我们还可以通过实现Runnable接口的方法来完成多线程,并重写其中的run方法,

把新线程要做的事写在run方法中,创建Runnable的子类对象,然后创建Thread对象,传入Runnable子类对象,

接着调用start方法开启线程,内部会自动调用Runnable子类对象的run方法,

public class ThreadTest {
    public static void main(String[] args) {
        myRunnable mr=new myRunnable();
        Thread t=new Thread(mr);
        t.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main method");
        }
    }
}

class myRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Runnable method");
        }
    }
}

我们可以看到两个线程并发执行,交替输出, 

3.3两种方式的区别

源码区别:

  • 继承Thread:由于子类重写了Thread类的run()方法,当调用start()方法时,直接找子类的run()方法执行
  • 实现Runnable:通过构造函数传入Runnable的引用,并将该引用赋值给了成员变量,start()方法调用run()方法时内部会判断成员变量的Runnable引用是否为空,不为空那么编译的时候看Runnable的run()方法,运行时执行的是Runnable子类的run()方法

一般区别:

  • 继承Thread
    • 优点:可以直接使用Thread类中的方法,代码简单
    • 缺点:如果已经有了其他的父类,那么就不可以使用这种方法(java中仅支持单继承)
  • 实现Runnable
    • 优点:即使自定义的线程类有了别的父类,也可以实现Runnable接口(java中接口是支持多实现的)
    • 缺点:不可以直接使用Thread中的方法,需要先获取到线程对象后才可以得到Thread的方法,代码复杂

3.4匿名内部类实现线程

使用匿名内部类就不用自定义一个线程类,在主方法中可以直接使用,代码整体感好。

我们来看看这两种方式如何用匿名内部类实现。

3.4.1继承Thread

public class ThreadTest {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("thread method");
                }
            }
        }.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main method");
        }
    }
}

3.4.2实现Runnable

public class ThreadTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("runnable method");
                }
            }
        }).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main method");
        }
    }
}

四、线程的方法

4.1获取与设置名字

我们可以通过getName和setName获取和设置线程的名字,还可以直接使用构造方法传入字符串设置名字,

如果不设置线程的名字,线程的默认名字是Thread-x,其中x为编号,默认从0开始,按照创建的顺序以此类推,

public class ThreadTest {
    public static void main(String[] args) {
        new Thread(){//不认为设置名字,使用默认名字
            @Override
            public void run() {
                System.out.println(getName());
            }
        }.start();

        new Thread("thread1"){//通过构造方法设置名字
            @Override
            public void run() {
                System.out.println(getName());
            }
        }.start();

        new Thread(){//通过setName方法设置名字
            @Override
            public void run() {
                this.setName("thread2");
                System.out.println(getName());
            }
        }.start();
    }
}

4.2线程的休眠

我们还可以使用静态方法sleep来控制线程暂停多少毫秒,使用方法为Thread.sleep(毫秒值),

public class ThreadTest {
    public static void main(String[] args) {
        new Thread("thread1"){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }.start();

        new Thread("thread2"){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

我们可以看到线程1执行一次后就暂停休息1s钟,然后程序会执行线程2,线程2执行完一次后也休息了1s钟,如此反复交替输出,

4.3守护线程setDaemon()

守护线程就是当程序没有其他线程时,守护线程会立即退出,即使没有执行完,我们可以通过setDaemon()的方法设置线程为守护线程,

守护线程不会单独执行,如果有其他线程时则并发执行,如果其他线程都执行完了,守护线程也会立马退出不会继续执行,

我们设置一下线程1为守护线程,输出名字100次,线程2为普通线程,输出名字5次,让线程2先启动,然后看看线程1会输出几次名字,

public class ThreadTest {
    public static void main(String[] args) {
        Thread t1=new Thread("thread1"){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName());
                }

            }
        };

        Thread t2=new Thread("thread2"){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(getName());
                }
            }
        };

        t1.setDaemon(true);//设置线程1为守护线程
        t2.start();
        t1.start();
    }
}

我们可以看到当线程2执行完毕之后,线程1还是输出了几条语句,但是远没有100条,

看来即使设置了守护线程,也不会一点都不运行了,在退出的时间里,守护线程还是执行了一小段时间的,

4.4加入线程join()

使用join方法可以使当前线程暂停,等待调用join方法的线程执行结束之后,当前线程继续执行,

还可以给join方法传入int类型的毫秒值,指定等待的时间,我们看看如何具体使用join()方法,

public class ThreadTest {
    public static void main(String[] args) {
        Thread t1=new Thread("thread1"){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(getName());
                }
            }
        };

        Thread t2=new Thread("thread2"){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    if(i==1){//当线程输出一次后,让t1线程加入,暂停t2线程
                        try {
                            t1.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(getName());
                }
            }
        };
        
        t2.start();//让t2线程先执行
        t1.start();
    }
}

可以看到t2线程输出一次后,让t1线程加入,暂停了t2线程,执行完了t1线程之后才继续输出的t2,

4.5线程的优先级

线程的优先级即线程优先执行的等级,在java线程中,优先级范围为1-10,

数字越大代表优先级越高,程序也就越有可能更多的占用CPU时间,当然也不是完全不允许执行别的线程,

优先级高代表更有可能执行该线程,我们通过setPriority()方法来设置线程的优先级,下面我们看看具体如何使用,

public class ThreadTest {
    public static void main(String[] args) {
        Thread t1=new Thread("thread1"){
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(getName());
                }
            }
        };

        Thread t2=new Thread("thread2"){
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(getName());
                }
            }
        };

        t1.setPriority(1);//设置t1优先级为1
        t2.setPriority(10);//设置t2优先级为10
        t1.start();
        t2.start();
    }
}

可以看到虽然是t1线程先启动,但是t2的线程优先级比较高,所以程序一开始让t2执行,

五、同步

5.1同步代码块

当多线程并发时,有多段代码同时执行,我们希望执行某一段代码过程中不要切换到其他的线程工作,这个时候就需要同步,

如果两段代码是同步的,那么同一时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码,这就叫做同步。

在java中,我们使用关键字synchronized加上一个锁对象来定义一段代码,这样的代码我们称同步代码块,

其中锁对象可以是任意的对象,但是不可以是匿名对象,

如果希望多段代码是同步的,那么他们的锁对象必须相同,我们举个例子看看如何使用synchronized关键字,

首先我们不用同步,看看输出这么一个程序会有什么结果,

public class ThreadTest {
    public static void main(String[] args) {
        Printer p=new Printer();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    p.print1();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    p.print2();
                }
            }
        }.start();
    }
}

class Printer{
    public void print1(){
        System.out.print("j");
        System.out.print("a");
        System.out.print("v");
        System.out.print("a");
        System.out.print("\r\n");
    }
    public void print2(){
        System.out.print("c");
        System.out.print("+");
        System.out.print("+");
        System.out.print("\r\n");
    }
}

结果可以看到有些字符输出的不完整或者是拼接的错误字符,

很明显是线程输出执行到一半切换到了别的线程导致的 ,这种问题我们就可以通过加入关键字synchronized来定义同步代码块解决,

class Printer{
    public void print1(){
        synchronized (this) {
            System.out.print("j");
            System.out.print("a");
            System.out.print("v");
            System.out.print("a");
            System.out.print("\r\n");
        }
    }
    public void print2(){
        synchronized (this) {
            System.out.print("c");
            System.out.print("+");
            System.out.print("+");
            System.out.print("\r\n");
        }
    }
}

这里我们用this本对象为锁,将两个方法变成了同步代码块,输出的时候就没有乱序的情况出现了。

5.2同步方法

我们还可以使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的,

同步方法只需要在方法上加synchronized关键字即可,而锁对象有两种,

非静态的同步方法锁对象为this对象,静态的同步方法锁对象为该类的字节码对象(例Printer.class)。

class Printer{
    public synchronized void print1(){
        System.out.print("j");
        System.out.print("a");
        System.out.print("v");
        System.out.print("a");
        System.out.print("\r\n");
    }
    public synchronized void print2(){
        System.out.print("c");
        System.out.print("+");
        System.out.print("+");
        System.out.print("\r\n");
    }
}

5.3线程安全问题

当多线程并发操作同一个数据时,就有可能出现线程安全问题(比如数据的更改覆盖等),使用同步就可以解决这个问题,

把操作数据的代码进行同步,不允许多个线程一起操作,我们以卖火车票为例子来看看如何使用同步解决这个问题,

现在有100张火车票,通过四个窗口进行销售,请用线程模拟窗口进行火车票售票,

5.3.1继承Thread

我们先使用继承Thread类的方法来写一下代码,

public class ThreadTest {
    public static void main(String[] args) {
        //创建四个线程模拟窗口进行售票
        new Ticket().start();
        new Ticket().start();
        new Ticket().start();
        new Ticket().start();
    }
}

class Ticket extends Thread{
    private static int ticket=100;//火车票余量,必须用静态修饰,否则四个窗口剩余票数不共享
    @Override
    public void run() {
        while(true){
            if(ticket<=0){
                break;
            }

            //使用线程睡眠10ms模拟卖票的时间消耗
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "还有" + --ticket + "张票");
        }
    }
}

我们可以看到已经卖到负数了,很明显这是不符合逻辑的,

这是因为多个线程在短时间内同时对ticket余量进行了操作,这样直接访问票数是不安全的, 我们需要在该代码上加入同步,

class Ticket extends Thread{
    private static int ticket=100;//火车票余量,必须用静态修饰,否则四个窗口剩余票数不共享
    @Override
    public void run() {
        while(true){
            //注意这里的锁对象不可以使用this,因为主程序中创建了四个对象,也就有四个this
            //所以这里我们使用本类的字节码对象,也可以使用在Ticket类中声明的静态对象
            synchronized (Ticket.class) {
                if (ticket <= 0) {
                    break;
                }

                //使用线程睡眠10ms模拟卖票的时间消耗
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + "还有" + --ticket + "张票");
            }
        }
    }
}

然后我们会发现没有负数票,安全问题完美解决,

5.3.2实现Runnable接口

实现Runnable接口方法类似,其主要代码如下,

public class ThreadTest {
    public static void main(String[] args) {
        //创建四个线程模拟窗口进行售票
        Ticket t=new Ticket();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

class Ticket implements Runnable{
    private int ticket=100;//我们可以只创建一个对象,所以不用静态修饰
    @Override
    public void run() {
        while(true){
            //因为只创建一个对象,所以也可以直接使用this为锁对象
            synchronized (this) {
                if (ticket <= 0) {
                    break;
                }

                //使用线程睡眠10ms模拟卖票的时间消耗
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "还有" + --ticket + "张票");
            }
        }
    }
}

5.4死锁问题

多线程同步的时候,如果同步代码嵌套,使用了相同锁对象,就有可能出现死锁,

我们现在给两个人一双筷子,一个人给左筷子,一个人给右筷子,如果想要吃饭则必须说服另一个人把筷子给他然后吃饭,

现在我们用线程模拟人,线程0分配了左筷子,线程1分配了右筷子,然后启动这两个线程看看程序会怎样运行,

public class ThreadTest {
    private static String s1="左筷子";
    private static String s2="右筷子";

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    while(true){
                        System.out.println(getName()+"已有"+s1+"等待"+s2);
                        synchronized (s2){
                            System.out.println(getName()+"拿到"+s2+"开始吃饭");
                        }
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                synchronized (s2){
                    while(true){
                        System.out.println(getName()+"已有"+s2+"等待"+s1);
                        synchronized (s1){
                            System.out.println(getName()+"拿到"+s1+"开始吃饭");
                        }
                    }
                }
            }
        }.start();
    }
}

class Ticket implements Runnable{
    private int ticket=100;//我们可以只创建一个对象,所以不用静态修饰
    @Override
    public void run() {
        while(true){
            //因为只创建一个对象,所以也可以直接使用this为锁对象
            synchronized (this) {
                if (ticket <= 0) {
                    break;
                }

                //使用线程睡眠10ms模拟卖票的时间消耗
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "还有" + --ticket + "张票");
            }
        }
    }
}

我们可以看到程序执行到一半不动卡住了,当线程1开始执行的时候,线程0想要右筷子,线程1想要左筷子,

 而线程0想要的右筷子在线程1手上,线程1想要的左筷子在线程0手上,两个线程面对资源都不让步,最终导致了死锁程序卡住,

所以为了避免同步代码块出现死锁的情况,请尽量不要使用同步的嵌套!

六、单例设计模式

6.1单例设计模式概述与实现

单例设计模式即保证类在内存中只有一个对象,主要有以下三种方法实现:

1、饿汉式(开发常用)

  • 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
  • 然后自己创建私有静态本类对象(直接创建对象)
  • 通过get方法返回该对象
class Single{
    private Single(){}
    private static Single s=new Single();
    public static Single getSingle(){
        return s;
    }
}

2、懒汉式(又称单例延迟加载模式,面试常用)

  • 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
  • 然后声明一个本类的引用(使用的时候再创建对象)
  • 提供一个公共的访问方法
class Single{
    private Single(){}
    private static Single s;
    public static Single getSingle(){
        if(s==null){
            s=new Single();
        }
        return s;
    }
}

该方法在多线程访问的时候,有可能会有安全隐患,

因为如果在判断s==null条件满足后,如果切换到别的线程,那么下一个线程该条件也满足,就会创建多个对象,就不是单例模式了。

3、final修饰

  • 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
  • 自己创建公有静态final本类对象,使用final修饰之后就不允许被更改了
class Single{
    private Single(){}
    public static final Single s=new Single();
}

6.2 Runtime类

Runtime类就是一个单例类,该类构造方法被私有化,

并且定义了一个私有的对象,通过getRuntime获取到该对象,

import java.io.IOException;

public class ThreadTest {
    public static void main(String[] args) throws IOException {
        Runtime r=Runtime.getRuntime();//通过静态方法获取对象
        r.exec("shutdown -s -t 600");//执行字符串命令,类似cmd命令行命令,这里我们演示关机命令
        //r.exec("shutdown -a");//取消关机命令
    }
}

七、Timer类

Timer即计时器,是一种工具,用于线程安排在后台执行的任务,

可以安排任务执行一次,或者定期重复执行,它有一个schedule()方法用于安排任务,

void schedule(TimerTask task, Date time)//安排在指定的时间执行指定的任务
void schedule(TimerTask task, Date firsttime, long period)//安排指定的任务在指定时间开始后,每隔一段时间重复执行一次,period的单位为毫秒

其中TimerTask就是任务,实现了Runnable接口,故任务需要在run方法里面重写,我们来看看具体如何使用该方法,

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Timer t=new Timer();
        t.schedule(new myTimerTask(),new Date(121,0,29,19,50),3000);//从指定时间开始,每隔3s重复执行一次

        //每隔1s输出当前时间
        while(true){
            Thread.sleep(1000);
            System.out.println(new Date());
        }
    }
}

class myTimerTask extends TimerTask{
    @Override
    public void run() {
        System.out.println("学习java");
    }
}

我们可以看到到了我们预设的时间时,程序就执行了run方法,开始学习了java, 

八、线程之间的通信

当多个线程并发执行时,在默认情况下CPU是随机切换现成的,

如果我们希望线程之间有规律的执行,就可以使用通信,例如每个线程执行一次打印。

8.1两个线程之间的通信

两个线程之间的通信可以通过以下方法实现,

  • 如果希望线程等待,那么就调用wait()方法
  • 如果希望唤醒等待的线程,那么就调用notify()方法
  • 这两种方法必须在同步代码块中执行,并且使用同步锁对象来调用

我们来看看具体如何实现,

import java.io.PrintWriter;

public class ThreadTest {
    public static void main(String[] args){
        Printer p=new Printer();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        p.print1();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        p.print2();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

class Printer{
    private int flag=1;

    public void print1() throws InterruptedException {
        synchronized (this) {
            if(flag!=1){
                this.wait();//执行一次后让当前进程等待
            }
            System.out.print("j");
            System.out.print("a");
            System.out.print("v");
            System.out.print("a");
            System.out.print("\r\n");
            flag=2;
            this.notify();//随机唤醒单个等待的线程
        }
    }

    public void print2() throws InterruptedException {
        synchronized (this) {
            if(flag!=2){
                this.wait();//执行一次后让当前进程等待
            }
            System.out.print("c");
            System.out.print("+");
            System.out.print("+");
            System.out.print("\r\n");
            flag=1;
            this.notify();//随机唤醒单个等待的线程
        }
    }
}

可以看到内容交替输出了, 

8.2多个线程之间的通信

多个线程之间的通信可以通过以下方法实现,

  • 如果希望线程等待,那么就调用wait()方法
  • 如果希望唤醒等待的线程,那么就调用notifyAll()方法
  • 这两种方法必须在同步代码块中执行,并且使用同步锁对象来调用

多个线程之间的通信和两个之间的通信在唤醒上有不同,多个使用的是notifyAll()的方法,唤醒全部的等待线程

而两个之间的通信使用的是notify(),随机唤醒一个等待的线程,下面我们看看具体如何实现多个线程的通信,

import java.io.PrintWriter;

public class ThreadTest {
    public static void main(String[] args){
        Printer p=new Printer();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print1();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print2();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print3();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

class Printer{
    private int flag=1;

    public void print1() throws InterruptedException {
        synchronized (this) {
            while(flag!=1){
                this.wait();//执行一次后让当前进程等待
            }
            System.out.print("j");
            System.out.print("a");
            System.out.print("v");
            System.out.print("a");
            System.out.print("\r\n");
            flag=2;
            this.notifyAll();//唤醒所有等待的线程
        }
    }

    public void print2() throws InterruptedException {
        synchronized (this) {
            while(flag!=2){
                this.wait();//执行一次后让当前进程等待
            }
            System.out.print("c");
            System.out.print("+");
            System.out.print("+");
            System.out.print("\r\n");
            flag=3;
            this.notifyAll();//唤醒所有等待的线程
        }
    }

    public void print3() throws InterruptedException {
        synchronized (this) {
            while(flag!=3){
                this.wait();//执行一次后让当前进程等待
            }
            System.out.print("p");
            System.out.print("y");
            System.out.print("t");
            System.out.print("o");
            System.out.print("n");
            System.out.print("\r\n");
            flag=1;
            this.notifyAll();//唤醒所有等待的线程
        }
    }
}

 我们可以看到三个线程交替输出了结果,

8.3线程间通信注意问题

  • 在同步代码块中,用的什么锁对象,就用什么对象调用wait()和notify()方法
  • 为什么wait()和notify()方法定义在了Object类中?
    • 因为锁对象可以是任意对象,Object类是所有对象的基类,所以wait()和notify()方法定义在了Object类中
  • sleep()方法和wait()方法的区别
    • sleep()方法必须传入参数,即睡眠的毫秒值,时间到了自动醒来;在同步中不释放锁,不允许其他线程执行
    • wait()方法可以传入参数,也可以不传,传入就是在参数时间结束后等待,不传入则是直接等待,都不会醒来;在同步中释放锁,可以允许其他线程执行

8.4互斥锁

在JDK1.5版本之后,我们可以使用ReentrantLock类的lock()和unlock()方法来进行同步(不使用synchronized关键字),

还可以实现线程的通信,首先使用ReentrantLock类的newCondition()方法获取Condition对象,

需要等待的时候使用Condition对象的await()方法,唤醒的时候用signal()方法,

不同的线程使用不同的Condition对象,这样就可以唤醒指定的线程了,我们用互斥锁实现一下前面的例子,

import java.io.PrintWriter;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest {
    public static void main(String[] args){
        Printer p=new Printer();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print1();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print2();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        p.print3();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

class Printer{
    private ReentrantLock r=new ReentrantLock();
    private Condition c1=r.newCondition();
    private Condition c2=r.newCondition();
    private Condition c3=r.newCondition();

    private int flag=1;
    public void print1() throws InterruptedException {
        r.lock();//获取锁
        if(flag!=1){
            c1.await();//c1等待
        }
        System.out.print("j");
        System.out.print("a");
        System.out.print("v");
        System.out.print("a");
        System.out.print("\r\n");
        flag=2;
        c2.signal();//唤醒c2
        r.unlock();//释放锁
    }

    public void print2() throws InterruptedException {
        r.lock();//获取锁
        if(flag!=2){
            c2.await();//c2等待
        }
        System.out.print("c");
        System.out.print("+");
        System.out.print("+");
        System.out.print("\r\n");
        flag=3;
        c3.signal();//唤醒c3
        r.unlock();//释放锁
    }

    public void print3() throws InterruptedException {
        r.lock();//获取锁
        if(flag!=3){
            c3.await();//c3等待
        }
        System.out.print("p");
        System.out.print("y");
        System.out.print("t");
        System.out.print("o");
        System.out.print("n");
        System.out.print("\r\n");
        flag=1;
        c1.signal();//唤醒c1
        r.unlock();//释放锁
    }
}

可以看到效果是一样的, 

九、线程组

在java中我们使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,

java允许程序直接对线程组进行控制,在默认情况下,所有线程都属于主线程组,

public final ThreadGroup getThreadGroup()//通过线程对象获取它所属于的组
public final String getName()//通过线程组对象获取它组的名字

我们还可以给线程设置分组,步骤如下:

  1. ThreadGroup(String name) 创建线程组对象并给其赋值名字
  2. 创建线程对象
  3. Thread(ThreadGroup group, Runnable target, String name) 创建线程并分配到指定线程组中
  4. 设置线程组的优先级或者守护线程

下面我们看看具体如何使用线程组,

public class ThreadTest {
    public static void main(String[] args){
        ThreadGroup tg=new ThreadGroup("TG");//创建线程组TG
        myRunnable mr=new myRunnable();

        Thread t1=new Thread(tg,mr,"Thread-1");//创建线程t1,并分配到线程组TG中
        Thread t2=new Thread(tg,mr,"Thread-2");//创建线程t2,并分配到线程组TG中

        System.out.println(t1.getThreadGroup().getName());//输出线程t1所在线程组名字
        System.out.println(t2.getThreadGroup().getName());//输出线程t2所在线程组名字
    }
}

class myRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

十、线程的状态

在java中,我们将线程的生命周期分为了五种状态,

1、新建(NEW):新创建了一个线程对象。

2、可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

3、运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

4、阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。阻塞的情况分三种: 

  • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
  • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
  • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

5、死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程状态转换

十一、线程池

11.1线程池概述与使用

程序启动一个新线程成本是比较高的,因为它涉及到要和操作系统进行交互,

而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池,

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,

在JDK5之前我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池,通过Executors工厂类来产生线程池,

public static ExecutorService newFixedThreadPool(int nThreads)//创建指定线程数的线程池
public static ExecutorService newSingleThreadExecutor()//创建单线程的线程池

它们的返回对象都是ExecutorService线程池服务器,然后我们使用ExecutorService类的submit()方法放入线程,

注意submit()方法可以接收Runnable对象或者Callable对象代表的线程,最后使用shutdown()方法即可关闭线程池,否则程序会一直运行,

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

public class ThreadTest {
    public static void main(String[] args){
        ExecutorService pool= Executors.newFixedThreadPool(2);//创建2个线程的线程池
        pool.submit(new myRunnable());//将线程放入线程池中
        pool.submit(new myRunnable());
        pool.shutdown();//关闭线程池
    }
}

class myRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

11.2 Callable

上面我们提到线程池服务器的submit()方法是用来将线程添加到线程池的,其中接收的参数有Runnable和Callable,

Callable接口类似于Runnable,二者都是用于创建线程的方法,Callable是除了继承Thread类、实现Runnable接口外第三种创建线程的方式,

但是Runnable不会返回结果,并且无法抛出经过检查的异常,而Callable中有一个call()方法,用来计算结果,

这个计算的结果就是重写的call方法中用户自己定义的计算功能(比如重写call方法实现两个数相加,返回相加的结果),如果无法计算则抛出异常,

import java.util.concurrent.*;

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool= Executors.newFixedThreadPool(2);//创建2个线程的线程池
        Future<Integer> f1=pool.submit(new myCallable(4));//将线程放入线程池中
        Future<Integer> f2=pool.submit(new myCallable(100));

        System.out.println(f1.get());//输出计算的结果
        System.out.println(f2.get());
        pool.shutdown();//关闭线程池
    }
}

class myCallable implements Callable<Integer>{
    private int num;
    public myCallable(int num){
        this.num=num;
    }
    @Override//在call函数中实现一个求1到任意数的累加和功能
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 1; i <=num ; i++) {
            sum+=i;
        }
        return sum;
    }
}

十二、工厂模式

12.1简单工厂模式概述与使用

简单工厂模式又称为静态工厂方法模式,它定义一个具体的工厂类负责创建一个类的实例,

  • 优点:客户端不需要再负责对象的创建,从而明确了各个类的职责
  • 缺点:这个静态工厂类负责所有对象的创建,如果有新的对象增加或者某些对象的创建方式不同,则需要不断修改工厂类,后期维护困难

我们看看具体如何使用,

public class ThreadTest {
    public static void main(String[] args){
        Dog d=(Dog)AnimalFactory.createAnimal("dog");//利用工厂方法创建狗类
        d.eat();
        Cat c=(Cat)AnimalFactory.createAnimal("cat");//利用工厂方法创建猫类
        c.eat();
    }
}

abstract class Animal{
    public abstract void eat();
}

class Dog extends Animal{
    public void eat(){
        System.out.println("狗吃骨头");
    }
}

class Cat extends Animal{
    public void eat(){
        System.out.println("猫吃鱼");
    }
}

class AnimalFactory{//创建一个动物工厂,用于产生猫和狗对象
    public static Animal createAnimal(String name){
        if("dog".equals(name)){
            return new Dog();
        }else if("cat".equals(name)){
            return new Cat();
        }else{
            return null;
        }
    }
}

12.2工厂方法模式概述与使用

工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现,

换句话说就是先定义一个抽象工厂类,你想创建什么对象就实现什么工厂类,每个类都有对应的工厂类来负责创建对象,

  • 优点:客户端不需要再负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性
  • 缺点:需要额外的编写代码,增加了工作量

我们看看具体如何使用,

public class ThreadTest {
    public static void main(String[] args){
        Dog d=(Dog)new DogFactory().createAnimal();//利用狗工厂方法创建狗类
        d.eat();
        Cat c=(Cat) new CatFactory().createAnimal();//利用猫工厂方法创建猫类
        c.eat();
    }
}

abstract class Animal{
    public abstract void eat();
}

class Dog extends Animal{
    public void eat(){
        System.out.println("狗吃骨头");
    }
}

class Cat extends Animal{
    public void eat(){
        System.out.println("猫吃鱼");
    }
}

interface Factory{//定义抽象工厂类
    public Animal createAnimal();
}

class DogFactory implements Factory{//实现狗工厂
    public Animal createAnimal() {
        return new Dog();
    }
}

class CatFactory implements Factory{//实现猫工厂
    public Animal createAnimal() {
        return new Cat();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_39478524/article/details/113192356