十八、多线程

1 程序、进程与线程

  1. 基本概念

    • 程序(program):是完成特定任务,使用编程语言编写的一组计算机指令的集合。静态对象。
    • 进程(process): 是程序的执行的一次过程,或是正在运行的一个程序。动态的过程。是程序运行的过程。包括启动、运行、消亡的生命周期。
      • 程序是静态的,进程是动态的
      • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
    • 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径
      • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程之间可以进行相互切换,线程间的切换的开销比较小
      • 在一个进程中的线程,可以共享进程的内存,共享数据。可以访问相同的变量和对象。在多线程的执行中,可以提高计算机的运行的效率 和程序执行的速度,但是由此也会产生线程间的安全问题。

    每一个 Java 线程可以看成由代码、一个真实的 CPU 以及数据三部分组成

  2. 单核CPU与多核CPU

    -单核CPU:CPU中只有一个可执行单元,在同一时刻,只能执行一个线程任务
    -多核CPU:CPU中有多个可执行单元,在同一时刻,可以执行多个线程任务

  3. 并行和并发

    • 并行:多个CPU同时执行多个任务
    • 并发:一个CPU同时执行多个任务
  4. CPU的执行机制

    • 抢占式
    • 轮询式
  5. 多线程的优点

    • 提高程序的相应的速度,提升用户体验
    • 提高CPU的使用率
    • 改善程程序的结构,让程序结构更加的合理,可以独立运行

2 多线程的实现

2.1 多线程的实现一:继承Thread 类

  • Java 支持多线程编程。多线程的实现是通过Thread类来实现

  • Thead 类:

    • 线程实现需要依赖于Thread类
    • 对于线程的任务的完成,需要通过run()方法来实现,是线程的主体
    • start() 是线程的启动方法,而非直接调用run()。启动之后会自动调用线程的run方法

    对同一个Thread对象两次调用start()方法,会抛出异常

    Thread 属于java.lang 下的程序包

  1. 对象的创建

|Thread() 分配一个新的 Thread对象。 |
|–|–|
|Thread(Runnable target) 分配一个新的 Thread对象。 |
|Thread(Runnable target, String name) 分配一个新的 Thread对象。 |
|Thread(String name) 分配一个新的 Thread对象。|

  1. 实现方式一
public class MyThread extends Thread{
    //一个称为多线程的类,要继承Thread
    //必须要重写run 方法,run 方法是该线程所需要完成的任务

    @Override
    public void run() {
        for (int i = 0 ; i < 100 ; i++){
            System.out.println(i);
        }
    }
}
public class THreadTest {
    public static void main(String[] args) {
        //创建线程对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();
        //启动线程
        my1.start();
        my2.start();
    }
}

  • 为什么要重写run() 方法

    • run方法是线程的核心方法 是封装了线程的核心任务。
  • run和start的区别

    • run方法是线程执行的核心任务的代码的封装而来的方法。使用中,我们自己不能去调用run,如果自己调用run方法,那么此时的run方法就是一个普通方法,与多线程无关
    • start方法启动线程,线程自己回去调用run(run方法是由jvm来调用)
  1. 实现方式二(内部类)
public class ThreadDemo {
    public static void main(String[] args) {
        //使用内部类实现Thread类的对象
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(i);
                }
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(i);
                }
            }
        };
        //启动线程,一个线程只能启动一次
        t1.start();
        t2.start();
    }
}

运行结果:

0
1
2
3
4
5
6
7
8
9
···

也可以这样写:

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

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

当程序中存在多个线程的时候,线程间的执行采用的是CPU的执行机制。多个线程之间执行会呈现一种交叉执行的现象。

如这样:

在这里插入图片描述

2.2 线程的基本属性

  1. 基本方法
返回值类型方法
voidsetName(String name) 将此线程的名称更改为等于参数 name 。
StringgetName() 返回此线程的名称。
longgetId() 返回此线程的标识符。
public class Nature {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(i);
                }
            }
        };
        t1.setName("线程一");  //为线程设置名称
        System.out.println(t1.getName());  //获取线程名称
        System.out.println(t1.getId());  //Id是每个线程的唯一标识,long类型
        t1.start();

        Thread t2 = new Thread("线程二") {  //线程名称也可以设置在这里
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(i);
                }
            }
        };
        //每个线程可以自己命名,也可以使用默认的命名规则由程序给出默认名称 Thread-0  Thread-1 ···
        System.out.println(t2.getName());
        t2.start();
    }
}
  1. 获取系统的当前执行的线程
ThreadcurrentThread() 返回对当前正在执行的线程对象的引用。
public class ThreadDemo_1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0 ; i < 5 ; i++){
                    //Thread.currentThread().getName() 当前线程名称
                    System.out.println(Thread.currentThread().getName() + "-" + i);
                }
            }
        };
        t1.start();
    }
}

2.3 线程的优先级

static intMAX_PRIORITY 线程可以拥有的最大优先级。
static intMIN_PRIORITY 线程可以拥有的最小优先级。
static intNORM_PRIORITY 分配给线程的默认优先级。
intgetPriority() 返回此线程的优先级。
voidsetPriority(int newPriority) 更改此线程的优先级。

线程的调度方式:

  • 分时调度:线程之间轮流使用CPU,平均分配给每个线程占用cpu的时间
  • 抢占调度:对于线程获取cpu的执行权,可以通过线程的优先级来改变。优先级高的线程,将拥有有限获得CPU的执行权,如果线程的优先级相同,那么此时线程获得cpu的执行权是随机。优先级高,指的是线程获取CPU的执行权的几率更大。

Java中使用的是抢占式的调度模式

线程的优先级高 意味着线程获得CPU的执行权的几率大 线程优先级小,意味着线程获得CPU的执行权的几率小

线程优先级的使用(1~10):

public class PriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread("线程一"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        Thread t2 = new Thread("线程二"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        Thread t3 = new Thread("线程三"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        //获取线程的默认优先级,默认为5
        System.out.println(t1.getPriority());
        System.out.println(t2.getPriority());
        System.out.println(t3.getPriority());
        //设置线程优先级
        t1.setPriority(1);
        t2.setPriority(10);
        t3.setPriority(5);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

5
5
5
线程三0
线程二0
线程三1
线程二1
线程二2
···
···
线程二45
线程三2
线程二46
线程三3
线程二47
线程三4
线程二48
线程三5
线程二49
线程三6
···

可以看到优先级别高的线程,只是获得CPU执行权的几率大,在运行中间也会有优先级别低的线程的执行。

2.4 线程的控制

返回值类型方法
static voidsleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
voidjoin() 等待这个线程死亡。
voidjoin(long millis) 等待这个线程死亡最多 millis毫秒。
static voidyield() 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。
static voidsleep(long millis, int nanos) 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。

2.4.1 sleep 的使用

public class ThreadSleepDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread("线程一"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
                try {
                    Thread.sleep(1000);  //暂停1000毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t2 = new Thread("线程二"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t3 = new Thread("线程三"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}
  1. join的使用(等待加入的进程先执行完,再执行自己)

Join当某个线程执行中调用了其他线程的join方法时,调用线程将被阻塞,直到join方法加入的join线程执行完位为止

public class ThreadJoinDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread("线程一"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        Thread t2 = new Thread("线程二"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    if (i == 50){
                        try {
                            System.out.println(Thread.currentThread().getName() + "---" + i);
                            t1.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        System.out.println(Thread.currentThread().getName() + i);
                    }
                }
            }
        };
        Thread t3 = new Thread("线程三"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        t1.setPriority(1);
        t2.setPriority(10);
        t3.setPriority(1);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

//先执行线程二(虽然为了结果明显设置了优先级,但是线程三也会时不时的蹦出来,以下结果仅供说明join的作用)
线程二0
线程二1
线程二2
线程二3
···
线程二48
线程二49
//当线程二中i=50时,加入了线程一,线程二被阻塞,直到运行完线程一为止,才能继续执行线程二
线程一0
线程一1
线程一2
···
线程一19
线程一20
线程二---50
线程一21
线程一22
···
线程一97
线程一98
线程一99
//当线程一执行完后,继续执行之前的线程二
线程二51
线程二52
线程二53
···

2.4.2 yield 线程让步(礼让线程)

暂停当前正在执行的线程,把执行的机会让给优先级相同的或者更高级别的线程 如果在线程中,没有同优先级的线程,忽略此方法

当在某一个线程中调用了yield 并不意味着当前线程一定会失去CPU的执行权,只是此时其他同级别或高级别的线程将拥有了获取cpu执行权的机会。

public class ThreadYieldDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread("线程一"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        Thread t2 = new Thread("线程二"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    if (i == 50){
                        System.out.println(Thread.currentThread().getName() + "---" + i);
                        Thread.yield();  //线程让步
                    }else {
                        System.out.println(Thread.currentThread().getName() + i);
                    }
                }
            }
        };
        Thread t3 = new Thread("线程三"){
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        //设置线程优先级
        t2.setPriority(10);
        t3.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

···
线程二44
线程二45
线程二46
线程二47
线程二48
线程二49
//当线程二中i=50时,线程让步,让同优先级的线程二先执行
线程三11
线程一14
线程一15
线程一16
线程一17
线程一18
线程一19
线程一20
线程一21
···

2.5 线程的分类

Java中线程的分类:守护线程(setDaemon()) 用户线程

守护线程守护的是用户线程,当用户线程执行结束,守护线程无论是否执行完毕,都将被迫结束。

public class ThreadDaemonDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0 ; i < 1000 ; i++){
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
public class ThreadDaemonTest {
    public static void main(String[] args) {
        Thread td1 = new ThreadDaemonDemo();
        Thread td2 = new ThreadDaemonDemo();

        td1.setName("守护线程一");
        td2.setName("守护线程二");

        //设置td1和td2为守护线程
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        new Thread("用户线程一"){
            @Override
            public void run() {
                for (int i = 0 ; i < 50 ; i++){
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        }.start();
        new Thread("用户线程二"){
            @Override
            public void run() {
                for (int i = 0 ; i < 50 ; i++){
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        }.start();
    }
}

运行结果:

···
用户线程二 0
用户线程二 1
用户线程二 2
用户线程二 3
用户线程二 4
用户线程二 5
···
···
守护线程二50
守护线程二51
守护线程二52
守护线程二53
守护线程二54
守护线程一13

可以看到守护线程是没有执行完的。

2.6 线程的声明周期

线程的生命周期的表示是通过线程的几种状态来表示的

线程的状态:

  • 新建:当一个Thread类或者他的子类对象被创建时,此时的线程就处于新建状态
  • 就绪:处于新建状态的线程被start后,线程就进入到CPU的执行队列,此时的线程将时刻准备着去争抢CPU的执行权,线程已经具备了运行的条件,只是还没有得到CPU的资源。
  • 运行:处于就绪状态的线程 获得了CPU的执行权,那么此时的线程就进入了运行状态,将会执行run方法
  • 阻塞:在运行状态的线程,因为某些原因暂时失去了CPU的执行权,线程被挂起,临时出让了CPU的执行权,终止了当前执行
  • 死亡:线程完成了全部的任务或线程被强制种种执行或者是因为线程在执行过程中出现了异常

线程调用start() 之后,处于就绪状态
在这里插入图片描述
wait 方法会使线程处于等待状态,同时会释放所持有对象的锁

sleep会使得线程处于阻塞状态

yiele会使得线程失去CPU执行权

join ( ) 的作用是阻塞指定线程等到另一个线程完成以后再继续执行

setDaemon( )的作用是将指定的线程设置成守护线程

wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。

2.7 多线程的实现二:实现 Runnable 接口

|Thread(Runnable target) 分配一个新的 Thread对象。 |
|–|–|
|Thread(Runnable target, String name) 分配一个新的 Thread对象。 |

Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run

实现Runnable能够防止出现多父类的问题

public class RunnableThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0 ; i < 10 ; i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class RunnableTest {
    public static void main(String[] args) {
        Runnable r = new RunnableThread();
        Thread t1 = new Thread(r,"线程一");
        Thread t2 = new Thread(r);
        //启动线程
        t1.start();
        t2.start();
    }
}

运行结果:

线程一 0
Thread-0 0
线程一 1
Thread-0 1
线程一 2
Thread-0 2
线程一 3
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
线程一 4
Thread-0 7
线程一 5
线程一 6
线程一 7
线程一 8
线程一 9
Thread-0 8
Thread-0 9
  1. 其他写法
public class Runnable2Demo {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        },"线程一");
        t1.start();

        //使用匿名内部类实现
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 100 ; i++){
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        },"线程二").start();
    }
}

Java类是单继承,多实现

2.8 管道流(Piped Stream)

  • PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流。
    它们的作用是让多线程可以通过管道进行线程间的通讯。在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用。
  • 使用管道通信时,大致的流程是:我们在线程A中向PipedOutputStream中写入数据,这些数据会自动的发送到与PipedOutputStream对应的PipedInputStream中,进而存储在PipedInputStream的缓冲中;此时,线程B通过读取PipedInputStream中的数据。就可以实现,线程A和线程B的通信。

3 线程同步

3.1 售票

需求:模拟火车站售票厅售卖一趟列车车票的场景:售票厅有三个窗口 在同时售卖一列火车的车票,假设该列车共有100张票

思路:

  • 定义一个类SellTicket 实现Runnable 接口,里面定义一个成员变量:private int tickets = 100;
  • 在SellTicket 类中重写run() 方法实现卖票,代码步骤如下:
    • 如果票数大于0,就卖票,并告知是哪个窗口卖的
    • 卖一张票,总票数要减1
    • 票没有了人,也可能有人来,所以是死循环
  • 定义一个测试类SellTicketDemo :
    • 创建SellTicket 类的对象
    • 创建三个Thread 类的对象,把SellTicket 对象作为构造方法的参数,并给出对应的窗口名称

实现:

public class SellTicket implements Runnable{
    private int ticket = 100;  //车票数量
    @Override
    public void run() {
        while (true){  //窗口应该一致处于开的状态,所以为死循环
            if (ticket > 0){
                try {
                    Thread.sleep(10);  //每个窗口售卖一张车票耗费的时间为10毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出了" + ticket-- + "号车票");
            }else {
                break;
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        //三个售票窗口
        SellTicket sellTicket = new SellTicket();
        Thread t1 = new Thread(sellTicket,"一号窗口");
        Thread t2 = new Thread(sellTicket,"二号窗口");
        Thread t3 = new Thread(sellTicket,"三号窗口");
        //三个窗口同时开始售票
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

···
三号窗口售出了89号车票
二号窗口售出了90号车票
二号窗口售出了88号车票
三号窗口售出了88号车票
一号窗口售出了87号车票
一号窗口售出了86号车票
···

结果可以看到,有的票会卖出两次,显然这样是不可以的。

这种问题就是线程安全问题

  • 问题出现的原因:多个线程同时操作共享数据,但是其中一个线程对共享数据的操作还没有完成,此时另一个线程就加入进来执行对共享数据的操作 ,导致了共享数据的错误

  • 解决的方法:对于多个线程操作共享数据时 ,在任何时刻,我们都保证只有一个线程在对共享数据进行操作,如果当前线程的操作没有完成,则不允许其他的线程进入参与执行。

3.2 线程同步的解决方案:同步机制

  1. 同步代码块

格式:

synchronized (同步锁){
   需要同步的代码 
}

实现:

public class SynchronizedDemo {
    public static void main(String[] args) {
        Print p = new Print();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    p.print1();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    p.print2();
                }
            }
        }).start();
    }
}
public class PrintDemo {
    SynchronizedDemo sd = new SynchronizedDemo();
    public void print1(){
        synchronized (sd){  //这就是同步代码块,同步锁可以是任意对象
            System.out.print("中");
            System.out.print("北");
            System.out.print("大");
            System.out.print("学");
            System.out.println();
        }
    }
    public void print2(){
        synchronized (sd){
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
        }
    }
}
  1. 非静态方法中,同步锁可以使用任意对象或this ()推荐

在同步代码块中,可以使用this来表示当前类的对象,而且不用额外生命对象

public class PrintDemo_1 {
    public void print1(){
        synchronized (this){
            System.out.print("中");
            System.out.print("北");
            System.out.print("大");
            System.out.print("学");
            System.out.println();
        }
    }
    public void print2(){
        synchronized (this){ 
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
        }
    }
}
  1. 静态方法中,同步锁可以使用任意静态对象 或者类名.class(推荐)
public class Print2StaticDemo {
    public static void print1(){
        synchronized (Print.class){
            System.out.print("中");
            System.out.print("北");
            System.out.print("大");
            System.out.print("学");
            System.out.println();
        }
    }
    public static void print2(){
        synchronized (Print.class){
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
        }
    }
}

3.3 同步方法

非静态同步方法中的同步锁是this

public class PrintDemo_2 {
    public synchronized void print1() {  //同步方法
        System.out.print("中");
        System.out.print("北");
        System.out.print("大");
        System.out.print("学");
        System.out.println();
    }

    public synchronized void print2() {
        System.out.print("山");
        System.out.print("西");
        System.out.print("太");
        System.out.print("原");
        System.out.println();
        /*synchronized (this){  //也可以这样写,验证了非静态同步方法中的同步锁是this
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
        }*/
    }
}

静态同步方法中的同步锁 是当前类的class对象

public class PrintDemo_2 {
    public static synchronized void print1() {  //同步方法
        System.out.print("中");
        System.out.print("北");
        System.out.print("大");
        System.out.print("学");
        System.out.println();
    }

    public static synchronized void print2() {
        System.out.print("山");
        System.out.print("西");
        System.out.print("太");
        System.out.print("原");
        System.out.println();
        /*synchronized (Print.class){  //也可以这样写,验证了静态同步方法中的同步锁是当前类的class对象
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
        }*/
    }
}

3.4 解决售票问题中的数据安全

  • 同步代码块:
//同步代码块
public class Ticket implements Runnable{
    private int tick = 100;  //车票数量

    @Override
    public void run() {
        while (true){
            synchronized (this){
                if (tick > 0){
                    try {
                        Thread.sleep(10);  //每卖一张车票耗费10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出" + tick-- + "号车票");
                }else {
                    break;
                }
            }
        }
    }
}
  • 同步方法:
//同步方法
public class Ticket_1 implements Runnable{
    private int tick = 100;

    @Override
    public void run() {
        sallTicket();
    }

    public synchronized void sallTicket(){
        while (true){
            if (tick > 0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出" + tick-- + "号车票");
            }else {
                break;
            }
        }
        /* 也可这样写
        while (true){
            synchronized (this){
                if (tick > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出" + tick-- + "号车票");
                }else {
                    break;
                }
            }
        }*/
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        //三个售票窗口
        Ticket_1 ticket  =new Ticket_1();
        Thread t1 = new Thread(ticket,"一号窗口");
        Thread t2 = new Thread(ticket,"二号窗口");
        Thread t3 = new Thread(ticket,"三号窗口");
        //三个窗口同时开始售票
        t1.start();
        t2.start();
        t3.start();
    }
}

3.5 单例设计模式之懒汉式的线程安全问题

//单例设计模式之懒汉式的线程安全问题
public class Singleton {
//将类的构造方法私有化
private Singleton(){

}
//声明当前类的一个对象
private static Singleton instance = null;

//对外提供获取当前类的实例的方法
public static Singleton getInstance(){
    if (instance == null){
        synchronized (Singleton.class){
            if (instance == null){
                instance = new Singleton();
            }
        }
    }
    return instance;
}

}

3.6 线程的死锁问题

  • 死锁:不同的线程分别占用对方需要的同步资源而不释放,都在等待对方放弃自己所需要的资源,就形成了死锁。
  • 出现死锁之后,不会出现任何的异常显示,没有任何的提示,只是所有的线程都处于阻塞状态 此时程序将无法继续执行。
  • 解决死锁问题:
    • 可以使用一些专门的高级算法
    • 尽量减少同步资源的定义
    • 尽量避免同步的嵌套
public class DeadLockDemo {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s2.append("A");
                    synchronized (s2){
                        s2.append("B");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                synchronized (s2){
                    s2.append("C");
                    synchronized (s1){
                        s1.append("D");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

3.7 线程的通信

多个线程在并发执行的过程中,在默认的情况下,CPU对线程的执行是随机切换,如果我们希望线程间的切换是有规律的,此时就可以使用线程的通信。

通信的方式:

返回值类型方法
voidwait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
voidwait(long timeout) 导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
voidnotify() 唤醒正在等待对象监视器的单个线程。
voidnotifyAll() 唤醒正在等待对象监视器的所有线程。
  1. 有两个线程
public class Print {
    private boolean flag = false;  //等待的标记
    public synchronized void print1(){
        if (flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.print("中");
            System.out.print("北");
            System.out.print("大");
            System.out.print("学");
            System.out.println();
            flag = true;  //改变标记状态
            notify();  //随机唤醒一个线程
        }
    }
    
    public synchronized void print2(){
        if (!flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
            flag = false;  //改变标记状态
            notify();  //随机唤醒一个线程
        }
    }
}
  1. 有三个线程
public class Print1 {
    private int flag = 1;

    public synchronized void print1(){
        if (flag == 1){
            System.out.print("中");
            System.out.print("北");
            System.out.print("大");
            System.out.print("学");
            System.out.println();
            flag = 2;
            notifyAll();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void print2(){
        if (flag == 2){
            System.out.print("山");
            System.out.print("西");
            System.out.print("太");
            System.out.print("原");
            System.out.println();
            flag = 3;
            notifyAll();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void print3(){
        if (flag == 3){
            System.out.print("春");
            System.out.print("夏");
            System.out.print("秋");
            System.out.print("冬");
            System.out.println();
            flag = 1;
            notifyAll();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试方法:

public class TestDemo {
    public static void main(String[] args) {
        Print1 p = new Print1();
        new Thread(){
            @Override
            public void run() {
                while (true){
                    p.print1();
                }
            }
        }.start();

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

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

运行结果:

中北大学
山西太原
春夏秋冬
中北大学
山西太原
春夏秋冬
中北大学
山西太原
春夏秋冬
···

3.8 Lock 锁对象

Lock 和Synchronized机制的主要区别:

  • Synchronized提供了对每个对象相关的隐式的监视器锁的访问,并强制所有锁获取和释放均需要出现在同一个快结构中,当获取了多个锁的时候 ,那么在释放的时候,就必须以相反的顺序来释放。对于所得释放也是隐式的,只要代码超出了Synchronized的范围 则就会释放锁。
  • Lock机制的加锁和释放锁都是显式的,通过lock()方法 和unlock()方法来获取锁和释放锁,这样我们由显式的方法可调用,因此我们获取锁和释放锁就将不会在收到代码块结构的限制,可以以更加自由的方式来释放锁和获取锁。

互斥锁 ReentrantLock

一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

构造方法 :

ReentrantLock()创建一个 ReentrantLock的实例。

获得锁和释放锁:

返回值类型方法
voidlock() 获得锁。
voidunlock() 尝试释放此锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable{
    private int tick = 100;

    //创建互斥锁对象
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        sallTicket();
    }

    public synchronized void sallTicket() {
        while (true) {
            lock.lock();  //获得锁
            if (tick > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出" + tick-- + "号车票");
            } else {
                break;
            }
            lock.unlock();  //释放锁
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        //三个售票窗口
        Ticket ticket  =new Ticket();
        Thread t1 = new Thread(ticket,"一号窗口");
        Thread t2 = new Thread(ticket,"二号窗口");
        Thread t3 = new Thread(ticket,"三号窗口");
        //三个窗口同时开始售票
        t1.start();
        t2.start();
        t3.start();
    }
}

3.9 多个线程间的通信

Condition

返回值类型方法
voidawait() 导致当前线程等到发信号或 interrupted 。
booleanawait(long time, TimeUnit unit) 使当前线程等待直到发出信号或中断,或指定的等待时间过去。
voidsignal() 唤醒一个等待线程。
voidsignalAll() 唤醒所有等待线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Print {
    //创建锁对象
    Lock lock = new ReentrantLock();

    //创建线程执行的控制条件
    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    int flag = 1;

    public void print1(){
        lock.lock();  //获得锁
        if (flag != 1){
            try {
                c1.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print("中");
        System.out.print("北");
        System.out.print("大");
        System.out.print("学");
        System.out.println();
        flag = 2;
        c2.signal();  //唤醒一个等待线程
        lock.unlock();  //解锁
    }

    public void print2(){
        lock.lock();
        if (flag != 2){
            try {
                c2.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print("山");
        System.out.print("西");
        System.out.print("太");
        System.out.print("原");
        System.out.println();
        flag = 3;
        c3.signal();  //唤醒一个等待线程
        lock.unlock();  //解锁
    }

    public void print3() {
        lock.lock();
        if (flag != 3) {
            try {
                c3.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("春");
            System.out.print("夏");
            System.out.print("秋");
            System.out.print("冬");
            System.out.println();
            flag = 1;
            c1.signal();  //唤醒一个等待线程
            lock.unlock();  //解锁
        }
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Print p = new Print();
        new Thread(){
            @Override
            public void run() {
                while (true){
                    p.print1();
                }
            }
        }.start();

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

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

运行结果:

中北大学
山西太原
春夏秋冬
中北大学
山西太原
春夏秋冬

3.10 生产者消费者模式

  1. 生产者消费者问题,包含两类线程:

    • 一类是生产者线程用于生产数据
    • 一类是消费者线程用于消费数据

为了解释生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库(我们将其形象为超市)
- 生产者生产数据之后直接放置在共享数据中,并不需要关心消费者的行为
- 消费者只需要从共享数据中去获取数据,并不需要关心生产者的行为

在这里插入图片描述

需要注意的问题:
- 生产者生产速度比消费者消费的速度快,消费者就会获取不到一些数据,漏掉数据
- 消费者比生产者快,消费者就会获取到重复的数据,或者获取不到数据

  • 生产者
public class Productor  implements  Runnable{
    private Clerk clerk;
    public Productor(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while(true){
            System.out.println("生产者开始生产商品");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.clerk.putProduct();
        }

    }
}
  • 消费者
public class Customer  implements  Runnable{
    private Clerk clerk;
    public  Customer(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while(true){
            System.out.println("消费者开始消费商品");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.clerk.getProduct();
        }
    }
}
public class Clerk {
    private int productNum = 0;

    public synchronized void putProduct() {//供货
        if (productNum >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            productNum++;
            System.out.println("生产者生产了第" + productNum + "号商品!");
            notify();
        }
    }

    public synchronized void  getProduct(){//售货
        if(productNum <= 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println("消费者消费了第"+productNum + "号商品");
            productNum--;
            notify();
        }
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        //创建线程
        Thread productor = new Thread(new Productor(clerk));
        Thread customer = new Thread(new Customer(clerk));
        productor.start();
        customer.start();
    }
}

运行结果:

生产者开始生产商品
消费者开始消费商品
生产者生产了第1号商品!
生产者开始生产商品
消费者消费了第1号商品
消费者开始消费商品
生产者生产了第1号商品!
生产者开始生产商品
生产者生产了第2号商品!
生产者开始生产商品
···
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BORN(^-^)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值