Featured image of post 第九章 JAVA 多线程机制

第九章 JAVA 多线程机制

什么是多线程

线程 是操作系统能够进行运算调度的最小单位。它被包含在 进程 之中,是进程中的实际运作单位。

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。

程序是指令、数据及其组织形式的描述,进程是程序的实体 —— 进程就是正在运行中的程序(进程是驻留在内存中的)。

  • 是系统执行资源分配和调度的独立单位。

  • 每一进程都有属于自己的存储空间和系统资源。

  • 注意:进程 A 和进程 B 的内存独立不共享。

线程

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

可以简单的将多线程理解为应用软件中的多个相互独立,可以同时运行的功能,有了多线程,我们就可以同时让程序做多件事情,提高效率。

  • 单线程:一个进程中包含一个顺序控制流(一条执行路径)。

  • 多线程:一个进程中包含多个顺序控制流(多条执行路径)。

  • 在 java 语言中:线程 A 和线程 B,堆内存和方法区内存共享;但是栈内存独立,一个线程一个栈。

  • 假设启动 10 个线程,会有 10 个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。java中之所以有多线程机制,目的就是为了提高程序的处理效率。

  • 对于单核的 CPU 来说,不能够做到真正的多线程并发,但是可以做到给人一种 “多线程并发” 的感觉。对于单核的 CPU 来说,在某一个时间点上实际上只能处理一件事情,但是由于 CPU 的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.company.thread;

public class test1 {
    public static void main(String[] args) {
        // 定义一个变量a,在内存中开辟存储空间,存储值10
        int a = 10;     //  -> 等待
        // 定义一个变量b,在内存中开辟存储空间,存储值20
        int b = 20;     //  -> 等待
        // 定义一个变量c,在内存中开辟存储空间,存储值a+b
        int c = a + b;  //  -> 等待
        System.out.println(c);
    }
}

并发和并行

并发:在同一时刻,有多个指令在 单个 CPU 上 交替 执行。

并行:在同一时刻,有多个指令在 多个 CPU 上 同时 执行。

并发

早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。

计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。

为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。

所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。

虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务在视觉上看起来是一起执行的。CPU 的执行速度极快,多任务切换的时间也极短,用户根本感受不到,所以并发执行看起来才跟真的一样。

操作系统负责将有限的 CPU 资源分配给不同的任务,但是不同操作系统的分配方式不太一样,常见的有:

  • 当检测到正在执行的任务进行 I/O 操作时,就将 CPU 资源分配给其它任务。

  • 将 CPU 时间平均分配给各个任务,每个任务都可以获得 CPU 的使用权。 在给定的时间内,即使任务没有执行完成,也要将 CPU 资源分配给其它任务,该任务需要等待下次分配 CPU 使用权后再继续执行。

将 CPU 资源合理地分配给多个任务共同使用,有效避免了 CPU 被某个任务长期霸占的问题,极大地提升了 CPU 资源利用率。

并行

并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了 “同时执行多个任务”。

  • 多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。

多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:

双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的 执行效率更高

并发 + 并行

在上图中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。

例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:

每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。

多线程的实现方式

继承 Thread 类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.company.thread.mt1;

public class MyThread extends Thread {
    // 重写run方法

    @Override
    public void run() {
        // 在里面写想要线程执行的代码
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+ ":i =" + i);
        }
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.company.thread.mt1;

public class ThreadDemo1 {
    public static void main(String[] args) {
        /*
            多线程的第一种实现方式
                1.自己定义一个类继承自Thread类
                2.重写run方法
                3.创建子类的对象,并启动线程
         */
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        // 调用setName方法
        mt1.setName("线程1");
        mt2.setName("线程2");
        // 开启线程
        mt1.start();
        mt2.start();
    }
}

实现 Runnable 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.company.thread.mt2;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 书写线程要执行的代码
        for (int i = 0; i < 10; i++) {
            // 此时getName方法是Threa类里的,不能直接调用
            // System.out.println(getName()+ ":i =" + i);
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + ":i =" + i);
        }
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.company.thread.mt2;

public class ThreadDemo2 {
    public static void main(String[] args) {
        /*
            多线程的第二种实现方式:
                1.自己定义一个类实现Runnable接口
                2.重写里面的run方法
                3.创建自己的类的对象
                4.创建一个Thread类的对象,并开启线程
        */

        // 创建MyRun对象
        MyRunnable mr = new MyRunnable();

        // 创建线程对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();

    }
}

接口 implements

implements 是一个类实现一个接口用的关键字。实现一个接口,必须实现接口中的所有方法

(1) 接口可以被多重实现(implements),抽象类只能被单一继承(extends)。

(2) 接口只有定义,抽象类可以有定义和实现。

(3) 接口的字段定义默认为:public static final,抽象类字段默认是 “friendly”(本包可见)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
比如People是一个接口他里面有say这个方法
接口的定义
public interface People{
    public void say();
}

但是接口没有方法体只能通过一个具体的类去实现其中的方法体
比如 Chinese这个类就实现了People这个接口
接口的实现
public class Chinese implements People{
    public void say() {
        System.out.println(" 你好!");
    }
}

接口的调用
People chinese = new Chinese() ;
chinese.say();
接口可以有不同的实现即使用不同的类实现
在java中extends表示子类继承父类如类A继承类B写成
class A extends B {
    //.....
}

implements 的意思更接近实现”,比如实现一个接口的方法

Implements 与 Extends 的区别

① extends 表示对父类的继承,可以实现父类,也可以调用父类初始化 this.parent()。而且会覆盖父类定义的变量或者函数。

② implements 表示对接口的实现,接口通过关键字interface 进行定义。eg:public class S implements F,在接口 F 中对方法进行声明,在类 S 中对这些方法进行实现。

③ 这两种实现的具体使用,是要看项目的实际情况,需要实现,但不可以修改,要求定义接口,用 implements。需要具体实现,或者可以被修改,扩展性好,用 extends。

利用 Callable 接口和 Future 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.company.thread.mt3;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 求1~100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.company.thread.mt3;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
           多线程的第三种实现方式:
           这种方式可以获取到多线程运行的结果
               1.创建一个类MyCallable实现callable接口
                2.重写call(是有返回值的,表示多线程运行的结果〉
                3.创建MyCallable的对象(表示多线程要执行的任务)
                4.创建Future抽象类的实现类FutureTask的对象(用于管理多线程运行的结果)
                5.创建Thread类的对象,并启动
      */

        // 创建MyCallable的对象
        MyCallable mc = new MyCallable();
        // 创建FutureTask的对象
        FutureTask<Integer> ft = new FutureTask<>(mc);
        // 创建线程对象
        Thread t1 = new Thread(ft);
        t1.start();
        // 获取多线程返回结果
        Integer res = ft.get();
        System.out.println(res);
    }
}

多线程中的常用成员方法

方法名称 说明
string getName() 返回此线程的名称
void setName( string name) 设置线程的名字(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep( long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级(最小是1,最大是10,默认是5)
final int getPriority() 获取线程的优先级
final void setDaemon( boolean on) 设置为守护线程
public static void yield() 出让线程/礼让线程
public static void join() 插入线程/插队线程

线程的基本方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.company.thread;

public class Test1 {
    public static void main(String[] args) {
        /*
            string getName( )               返回此线程的名称
            void setName(String name)       设置线程的名字(构造方法也可以设置名字)
            static Thread currentThread()   获取当前线程的对象
            static void sleep(long time)    让线程休眠指定的时间,单位为毫秒
         */
        MyThread1 mt1 = new MyThread1("构造函数设置的线程名");
        MyThread1 mt2 = new MyThread1();
        MyThread1 mt3 = new MyThread1();
        MyThread1 mt4 = new MyThread1();

        mt4.setName("setName设置的线程名");

        mt1.start();    // 构造函数设置的线程名
        mt2.start();    // Thread-0
        mt3.start();    // Thread-1
        mt4.start();    // setName设置的线程名
    }
}

class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("运行线程——" + getName());
    }

    public MyThread1() {
    }

    public MyThread1(String name) {
        super(name);
    }
}

即使没有给线程设置名称,线程也有默认的名称,格式为 Thread-x,x 为序号,从 0 开始。

可以通过生成构造函数的方法,在开启线程时为线程设置名称。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.company.thread;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        // 获取当前线程的对象
        Thread thread = Thread.currentThread();
        String name = thread.getName();
        System.out.println(name);   // main

        // 线程休眠
        // System.out.println("sleep");
        // Thread.sleep(5000);
        // System.out.println("awake");

        MyThread2 mt1 = new MyThread2();
        mt1.start();
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        System.out.println("运行线程——" + getName());
        System.out.println("sleep");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("awake");
    }

    public MyThread2() {
    }

    public MyThread2(String name) {
        super(name);
    }
}

当 JVM 虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫做 main 线程,他的作用就是去调用 main 方法,并执行里面的代码,在以前,我们写的所有的代码,其实都是运行在 main 线程当中的。

可以在线程中使用 Thread.sleep() 方法休眠线程,注意此时异常需要使用 try catch 包裹,因为父类 Thread 中的 run() 方法是不能抛出异常的。

线程的优先级

线程的调度:① 抢占式调度,多个线程抢夺 CPU 的执行权。② 非抢占式调度,轮换执行。

在 Java 中采用的是 抢占式调度 的方式,特点是随机性,线程的优先级越大,则这条线程抢占到 CPU 的概率就越大,就越容易被执行;优先级从 1 到 10,数字越大优先级越高,默认情况下优先级为 5。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.company.thread;

public class Test3 {
    public static void main(String[] args) {
        // 创建 Runnable 对象
        MyRunnable1 mr1 = new MyRunnable1();
        // 创建线程对象
        Thread t1 = new Thread(mr1, "线程1");
        Thread t2 = new Thread(mr1, "线程2");

        System.out.println(t1.getPriority());
        System.out.println(t2.getPriority());
        System.out.println(Thread.currentThread().getPriority());

        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();
    }
}

class MyRunnable1 implements Runnable {
    @Override
    public void run() {
        // System.out.println("运行线程——" + Thread.currentThread().getName());
        for (int i = 0; i <= 100; i++) {
            if (i == 100){
                System.out.println(Thread.currentThread().getName() + ":执行完毕");
            }else {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

守护线程、礼让线程、插入线程

守护线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.company.thread;

public class Test4 {
    public static void main(String[] args) {
        MyThread3 mt1 = new MyThread3();
        MyThread4 mt2 = new MyThread4();

        // 当其他的非守护线程结束后,守护线程会陆续的结束
        mt1.setName("线程");
        mt2.setName("守护线程");

        mt2.setDaemon(true);

        mt1.start();
        mt2.start();
    }
}

class MyThread3 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

class MyThread4 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

守护线程应用场景:传输文件时关闭聊天窗口,则传输线程中断。

▷ 礼让线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.company.thread;

public class Test5 {
    public static void main(String[] args) {
        MyThread4 mt1 = new MyThread4();
        MyThread4 mt2 = new MyThread4();

        mt1.setName("线程1");
        mt2.setName("线程2");

        mt1.start();
        mt2.start();
    }
}

class MyThread5 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + ":" + i);
            // 出让当前CPU的执行权,让执行情况更加均匀
            Thread.yield();
        }
    }
}

▷ 插入线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.company.thread;

public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        MyThread6 mt1 = new MyThread6();
        mt1.setName("线程");
        mt1.start();

        // 把线程 mt1 插入到当前线程前
        mt1.join();

        for (int i = 1; i <= 10; i++) {
            System.out.println("main线程" + i);
        }
    }
}

class MyThread6 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + ":" + i);
            // 出让当前CPU的执行权,让执行情况更加均匀
            Thread.yield();
        }
    }
}

练习:使用三种方式创建线程,分别为其设置名称,并将其中一条设置为守护线程。

线程的生命周期和安全问题

线程的生命周期

就绪状态: 就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺 CPU 时间片的权力(CPU 时间片就是执行权)。当一个线程抢夺到 CPU 时间片之后,就开始执行 run 方法,run 方法的开始执行标志着线程进入运行状态。

运行状态: run 方法的开始执行标志着这个线程进入运行状态,当之前占有的 CPU 时间片用完之后,会重新回到就绪状态继续抢夺 CPU 时间片,当再次抢到 CPU 时间之后,会重新进入 run 方法接着上一次的代码继续往下执行。

阻塞状态: 当一个线程遇到阻塞事件,例如接收用户键盘输入,或者 sleep 方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的 CPU 时间片。之前的时间片没了 需要再次回到就绪状态 抢夺 CPU 时间片。

锁池: 在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有 CPU 时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺 CPU 时间片(这个进入锁池,可以理解为一种阻塞状态)。

线程的安全问题

同步代码块

练习:某电影院有 3 个售票窗口正在售票,共有 100 张,请使用线程模拟售票情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.company.thread;

public class Pra2 {
    public static void main(String[] args) {
        MyThread8 mt1 = new MyThread8("窗口1", 0);
        MyThread8 mt2 = new MyThread8("窗口2", 0);
        MyThread8 mt3 = new MyThread8("窗口3", 0);

        mt1.start();
        mt2.start();
        mt3.start();
    }
}

class MyThread8 extends Thread {
    // 表示这个类所有的对象都共享ticket这个数据
    static int ticket = 0;

    public MyThread8(int ticket) {
        this.ticket = ticket;
    }

    public MyThread8(String name, int ticket) {
        super(name);
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + ":售出" + ticket + "张票");
            } else {
                break;
            }
        }
    }
}

创建三条线程,通过 static 关键字定义 ticket,使得类中所有的对象都共享 ticket 的值,从而保证三个窗口只售出 100 张票,但此时仍存在一些问题:

① 当线程抢占到 CPU 后,还没来得及打印售票提示信息,CPU 就又被其他线程抢占了,此时会出现多个线程打印同一条售票信息的情况。

② 当执行到 99 时,当前线程如果被其他线程抢占 CPU,也会出现 ticket 自增不及时,无法打印正确的售票信息的情况,此时售票数会超出范围限制。

如果当线程执行到操作数据的代码块时,将这段代码锁起来,使得其他线程即使抢夺到了 CPU 执行权,也需要在外面等待,就可以保证不会出现之前的问题,这种方法叫做 同步代码块,格式:

1
synchronized (锁对象){操作共享数据的代码}

锁默认是打开的,当有线程进入,锁就会关闭,直到里面的进程执行完毕,锁才会打开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.company.thread;

public class Pra2 {
    public static void main(String[] args) {
        MyThread8 mt1 = new MyThread8("窗口1", 0);
        MyThread8 mt2 = new MyThread8("窗口2", 0);
        MyThread8 mt3 = new MyThread8("窗口3", 0);

        mt1.start();
        mt2.start();
        mt3.start();
    }
}

class MyThread8 extends Thread {

    // 表示这个类所有的对象都共享ticket这个数据
    static int ticket = 0;

    // 创建锁对象,必须保证锁对象是唯一的
    // static Object obj = new Object();

    public MyThread8(int ticket) {
        this.ticket = ticket;
    }

    public MyThread8(String name, int ticket) {
        super(name);
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 同步代码块
            synchronized (MyThread8.class){	// 锁对象一般写当前类的字节码文件对象
                if (ticket < 100) {
                    ticket++;
                    System.out.println(getName() + ":售出" + ticket + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

同步方法

1
修饰符 synchronized 返回值类型 方法名(方法参数){...}

同步方法是锁住方法里面所有的代码;同步方法的锁对象不能自己制定,如果当前的方法是非静态的,那么锁对象是当前方法的调用者 this,如果是静态方法,那么锁对象是当前类的字节码文件对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.company.thread;

public class SynchronizedDemo1 {
    public static void main(String[] args) {
        MyRunnable3 mr = new MyRunnable3();
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class MyRunnable3 implements Runnable {
    // 由于此时是通过实现MyRunnable接口来创建线程的
    // MyRunnable只会执行一次,之后使用Thread t1 = new Thread(mr)来创建线程对象
    // 因此不需要将ticket的数据共享,直接使用int ticket = 0就可以了
    int ticket = 0;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
//            synchronized (MyRunnable3.class) {
                // 重构 -> ctrl + alt + m 提取方法
                if (extracted()) break;
//            }
        }
    }

    private synchronized boolean extracted() {
        if (ticket == 100) {
            return true;
        } else {
            ticket++;
            System.out.println(Thread.currentThread().getName() + ":售出" + ticket + "张");
        }
        return false;
    }
}

​ 锁的基本原理是防止竞争条件,保证线程安全性和可见性,避免死锁等问题。

(1) 防止竞争条件

当多个线程同时访问共享资源时,可能会发生竞争条件。竞争条件是指当多个线程同时执行同一段代码时,由于执行顺序的不同而导致结果的不确定性。

锁的作用就是在多个线程访问共享资源时保证同一时刻只有一个线程访问,从而避免竞争条件的发生。当一个线程获取到锁时,其他线程必须等待锁的释放才能继续访问共享资源。

(2) 保证线程安全性和可见性

线程安全性和可见性是 Java 并发编程中非常重要的概念。线程安全性是指当多个线程同时访问共享资源时,不会出现数据损坏或程序崩溃等问题。可见性是指当一个线程修改了共享资源时,其他线程能够立即看到这个修改。

锁机制可以保证线程安全性和可见性。当一个线程获取到锁时,其他线程无法修改共享资源,从而避免了数据损坏和程序崩溃等问题。而锁机制也可以保证共享资源的可见性,因为当一个线程释放锁时,其他线程能够立即看到共享资源的最新状态。

(3) 避免死锁

死锁是指两个或多个线程相互等待对方释放锁,从而导致程序无法继续执行的情况。死锁是 Java 并发编程中一个非常严重的问题,必须避免发生。

为了避免死锁,必须采取一些策略,例如避免嵌套锁、避免长时间占用锁、按照相同的顺序获取锁等。另外,还可以使用专门的工具来检测和避免死锁,例如死锁检测器和避免死锁算法等。

Synchronized 关键字锁

synchronized 关键字是 Java 中最基本和最常用的锁机制。使用 synchronized 关键字可以将一段代码块或一个方法标记为同步代码块,以保证在任何时刻最多只能有一个线程执行它们。synchronized 锁是 Java 内置锁的一种实现。

Lock 锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象 Lock。

Lock 实现提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作,Lock中提供了获得锁和释放锁的方法。

① void lock():获得锁

② void unlock():释放锁

Lock 是接口,不能直接实例化,这里采用它的实现类 ReentrantLock 来实例化 ReentrantLock 的构造方法:

① ReentrantLock():创建一个ReentrantLock的实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.company.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo1 {
    public static void main(String[] args) {
        MyThread9 mt1 = new MyThread9();
        MyThread9 mt2 = new MyThread9();
        MyThread9 mt3 = new MyThread9();

        mt1.setName("窗口1");
        mt2.setName("窗口2");
        mt3.setName("窗口3");

        mt1.start();
        mt2.start();
        mt3.start();
    }
}

class MyThread9 extends Thread {
    static int ticket = 0;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            if (ticket < 100) {
                ticket++;
                System.out.println(getName() + ":售出" + ticket + "张票");
            } else {
                lock.unlock();
                break;
            }
            lock.unlock();
        }
    }
}

死锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.company.thread;

public class Pra3 {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();

        mt1.setName("线程A");
        mt2.setName("线程B");

        mt1.start();
        mt2.start();
    }
}

class MyThread extends Thread {
    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        while (true) {
            if ("线程A".equals(getName()))
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            else if ("线程B".equals(getName())) {
                synchronized (objB) {
                    System.out.println("线程B拿到了B锁,准备拿A锁");
                    synchronized (objA) {
                        System.out.println("线程B拿到了A锁,顺利执行完一轮");
                    }
                }
            }
        }
    }
}
Blog for Sandy Memories