Java多线程

线程的创建与使用

方式1:继承Thread类

  1. 创建一个继承于Thread类的子类

  2. 重写Thread类的run()方法

  3. 创建Thread类的子类对象

  4. 通过此对象调用start()方法

    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
    //1.创建一个继承于Thread类的子类
    class MyThread extends Thread{
    //2.重写Thread类的run()方法
    @Override
    public void run() {
    //此处写要实现的代码
    for (int i = 1; i <= 5; i++) {
    System.out.println(Thread.currentThread().getName() +"--> "+ i);
    }
    }
    }

    public class ThreadTest {
    public static void main(String[] args) {
    //3.创建Thread类的子类对象
    MyThread t1 = new MyThread();
    MyThread t2 = new MyThread();
    //设置线程名
    t1.setName("线程1");
    t2.setName("线程2");
    //4.通过此对象调用start()方法
    t1.start();
    t2.start();
    }
    }
    /*运行结果:
    线程2--> 1
    线程2--> 2
    线程1--> 1
    线程2--> 3
    线程2--> 4
    线程1--> 2
    线程2--> 5
    线程1--> 3
    线程1--> 4
    线程1--> 5 */

方式2:实现Runnable接口

  1. 创建一个实现了Runnable接口的类

  2. 实现Runnable接口的run()方法

  3. 创建实现类的对象

  4. 将该对象作为参数传递到Thread类的构造器中,并创建Thread类的对象

  5. 通过Thread类的对象调用start()方法

    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
    //1.创建一个实现了Runnable接口的类
    class MyThread implements Runnable {
    //2.实现Runnable接口的run()方法
    @Override
    public void run() {
    for (int i = 1; i <= 5; i++) {
    System.out.println(Thread.currentThread().getName() +"--> "+ i);
    }
    }
    }

    public class ThreadTest {
    public static void main(String[] args) {
    //3.创建实现类的对象
    MyThread m = new MyThread();
    //4.将该对象作为参数传递到Thread类的构造器中,并创建Thread类的对象
    Thread t1 = new Thread(m);
    Thread t2 = new Thread(m);
    t1.setName("线程1");
    t2.setName("线程2");
    //5.通过Thread类的对象调用start()方法
    t1.start();
    t2.start();
    }
    }
    /*运行结果:
    线程1--> 1
    线程2--> 1
    线程1--> 2
    线程2--> 2
    线程1--> 3
    线程2--> 3
    线程1--> 4
    线程2--> 4
    线程1--> 5
    线程2--> 5*/

方式3:实现Callable接口

  1. 创建一个实现Callable接口的类

  2. 实现call方法,可以有返回值

  3. 创建Callable接口实现类的对象

  4. 将此对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象

  5. 将FutureTask的对象作为参数传递到Thread构造器中,创建Thread对象且调用start()

  6. 获取实现的call方法的返回值(如果不需要,可以不写)

    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
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    //1.创建一个实现Callable接口的类
    class MyThread implements Callable<Integer> {
    //2.实现call方法,可以有返回值
    @Override
    public Integer call() throws Exception {
    //需要执行的操作
    int sum = 0;
    for (int i = 1; i <= 5; i++) {
    System.out.println(i);
    sum = sum + i;
    }
    return sum;
    }
    }

    public class ThreadTest {
    public static void main(String[] args) {
    //3.创建Callable接口实现类的对象
    MyThread myThread = new MyThread();
    //4.将此对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象
    FutureTask<Integer> futureTask = new FutureTask<>(myThread);
    //5.将FutureTask的对象作为参数传递到Thread构造器中,创建Thread对象且调用start()
    new Thread(futureTask).start();
    try {
    //6.获取实现的call方法的返回值
    Integer num = futureTask.get();
    System.out.println("和为:" + num);
    } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
    }
    }
    }
    /* 1
    2
    3
    4
    5
    和为:15 */
  • 实现Runnable接口和实现Callable接口的区别
    • call()方法相比于run()方法可以有返回值
    • call()方法可以抛出异常
    • Callable支持泛型
    • 需要借助FutureTask类来获取返回结果

方式4:使用线程池

  • 优点:

    • 提高响应速度(减少了创建线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理:
      • CorePoolSize:核心池的大小
      • MaximumPoolSize:最大线程数
      • KeepAliveTime:线程没有任务时最多保持多长时间终止
  • 创建步骤:

    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
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadPoolExecutor;

    class MThread implements Runnable {
    @Override
    public void run() {
    for (int i = 1; i <= 5; i++) {
    System.out.println(Thread.currentThread().getName() +"--> "+ i);
    }
    }
    }
    public class ThreadPool {
    public static void main(String[] args) {
    //1.提供指定线程数的线程池
    ExecutorService service = Executors.newFixedThreadPool(10);//创建10个线程
    //设置线程池属性
    //ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
    //service1.setCorePoolSize(15);
    //2.执行指定线程的操作,需要提供实现Runnable或Callable的实现类对象
    service.execute(new MThread()); //适用于Runnable
    //service.submit(); //适用于Callable
    //3.关闭线程池
    service.shutdown();
    }
    }
    /* pool-1-thread-1--> 1
    pool-1-thread-1--> 2
    pool-1-thread-1--> 3
    pool-1-thread-1--> 4
    pool-1-thread-1--> 5 */

线程常见方法:

  • void start():①启动当前线程 ②调用当前线程的run()方法
  • run():线程被调用时执行的方法
  • void setName("线程1");:设置线程名
  • String getName():获取线程名
  • static Thread currentThread():返回当前线程
  • static void yield():暂停正在执行的线程,将执行机会让给同优先级或高优先级的线程
  • join():在线程A执行途中调用线程B的join()方法,线程A阻塞,直到线程B执行完,线程A才能结束阻塞
  • static void sleep(long millis):令当前线程阻塞millis毫秒,时间到后重新排队执行
  • boolean isAlive():判断线程是否活着
  • void setPriority(int newPriority):设置线程优先级,默认是优先级是NORM_PRIORITY = 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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    class Window implements Runnable{
    //票号
    private int ticket = 10;

    @Override
    public void run() {
    while (true){
    if (ticket > 0){
    System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
    //sleep是为了模拟线程阻塞,让票出错的概率变大
    try {
    Thread.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ticket--;
    }else {
    break;
    }
    }
    }
    }

    public class WindowTest {
    public static void main(String[] args) {
    Window window = new Window();
    Thread t1 = new Thread(window);
    Thread t2 = new Thread(window);
    Thread t3 = new Thread(window);
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");
    t1.start();
    t2.start();
    t3.start();
    }
    }
    /* 窗口2卖票,票号是:10
    窗口3卖票,票号是:10
    窗口1卖票,票号是:10
    窗口1卖票,票号是:9
    窗口3卖票,票号是:8
    窗口2卖票,票号是:9
    窗口1卖票,票号是:6
    窗口3卖票,票号是:7
    窗口2卖票,票号是:7
    窗口1卖票,票号是:5
    窗口2卖票,票号是:3
    窗口3卖票,票号是:3
    窗口1卖票,票号是:2
    窗口3卖票,票号是:1 */
  • 发现问题:从结果可以看出,多个线程同时运行,会使共享数据出现我们不愿看到的错误结果,即出现重票、缺票的情况,有时候还会出现0,-1这类错票的情况。因此我们有必要解决线程的安全问题。

  • 问题原因:假设当前票数是5,线程t1成功进入判断if (ticket > 0),因此可以出票,票号为5(即运行到了输出语句),此时t1正好被阻塞,还没来得及运行ticket--;,这时正好t2通过if (ticket > 0),也可以出票,但是t1在t2出票前还没来得及将票数减去1,因此t2仍然认为还剩5张票,因此t2出的票仍然是5号票,这就出现了重票的情况。。。漏票,错票也是差不多情况。

  • 解决思路:操作或判断共享数据(即ticket)时只允许一个线程去进行操作,哪怕正在操作的那个线程阻塞了,其他线程都不可参与进来,直到此线程操作完共享数据,其他线程才可进行操作。

方式1:同步代码块

1
2
3
synchronized (同步监视器){ 
//需要被同步的代码
}
  • 同步监视器,也称:任何一个类的对象,都可以当作锁
    • 要求:多个线程必须用的同一把锁才能实现线程同步

实现Runnable接口

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
class Window implements Runnable{
//票号
private int ticket = 10;
//充当锁
private Object obj = new Object();

@Override
public void run() {
while (true){
//同步代码块
synchronized (obj){
//或者:synchronized (this){
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
/* 窗口1卖票,票号是:10
窗口1卖票,票号是:9
窗口2卖票,票号是:8
窗口2卖票,票号是:7
窗口2卖票,票号是:6
窗口2卖票,票号是:5
窗口2卖票,票号是:4
窗口2卖票,票号是:3
窗口2卖票,票号是:2
窗口2卖票,票号是:1 */

继承Thread类

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
class Window extends Thread{
//票号:注意static
private static int ticket = 10;
//锁:注意static
private static Object obj = new Object();

@Override
public void run() {
while (true){
synchronized (obj){
//或者:synchronized (Window.class){
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else {
break;
}
}
}
}
}

public class WindowTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
/* 窗口1卖票,票号是:10
窗口1卖票,票号是:9
窗口1卖票,票号是:8
窗口1卖票,票号是:7
窗口3卖票,票号是:6
窗口3卖票,票号是:5
窗口3卖票,票号是:4
窗口3卖票,票号是:3
窗口3卖票,票号是:2
窗口3卖票,票号是:1 */

注意:在实现Runnable接口中,第三行和第五行的票和锁并没有加static关键字,而继承Thread类中必须要加上static关键字。因为在main方法中,继承Thread的实现方式中new了三个window对象,如果不加上static,相当于每个对象都有10张ticket和一把单独的锁,这就不是共享的票和单独的锁了,因此必须加上static,而实现Runnable接口中因为只new了一次window,然后三个线程同时使用一个window作为参数,这就已经相当于共享,因此无需static

  • 在实现Runnable接口创建多线程方式中也可以使用this作为锁,即synchronized (this){}
  • 在继承Thread类创建多线程的方式中也可以使用Window.class作为锁,因为类也是一个对象,即synchronized (Window.class){},不可以使用this,原因同上

方式2:同步方法

实现Runnable接口

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
class Window implements Runnable{
//票号
private static int ticket = 10;

@Override
public void run() {
while (true){
myMethod();
}
}
//同步方法(非static),他的锁为this
private synchronized void myMethod(){
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}

public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

继承Thread类

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
class Window extends Thread{
//票号
private static int ticket = 10;

@Override
public void run() {
while (true){
myMethod();
}
}
//同步方法(必须是static方法)
private static synchronized void myMethod(){
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}

public class WindowTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

区别:

  • 在实现Runnable接口创建多线程方式中可以是非静态的同步方法,private synchronized void myMethod(){}
  • 在继承Thread类创建多线程的方式中必须是静态的同步方法,private static synchronized void myMethod(){}

注意:

  • 同步方法仍然涉及到同步监视器,只是不需要显式声明。
  • 非静态同步方法,同步监视器:this
  • 静态同步方法,同步监视器:当前类本身

方式3:Lock锁

  1. 实例化ReentrantLock
  2. 调用lock()加锁
  3. 调用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
41
42
43
44
45
46
class Window implements Runnable{
//票号
private static int ticket = 10;

//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true){
try {
//2.调用lock()加锁
lock.lock();
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号是:"+ticket);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else {
break;
}
}finally {
//3.调用unlock()解锁
lock.unlock();
}
}
}
}

public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
  • synchronized与Lock锁的异同
    • 同:二者都可以解决线程安全问题
    • 不同:synchronized在执行完相应的代码后,会自动释放同步监视器。而Lock需要手动的启动锁,结束后还需要手动的解锁
  • 三种方式优先使用顺序:Lock ——>同步代码块——>同步方法

线程通信

  • 例题:两个线程交替打印1-10

    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
    class MyThread implements Runnable {

    private int num = 1;

    @Override
    public void run() {
    while (true) {
    synchronized (this) {
    notify();//唤醒另一个线程
    if (num <= 10) {
    System.out.println(Thread.currentThread().getName() + ": " + num);
    num++;

    try {
    wait();//让当前线程阻塞
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    }else {
    break;
    }
    }
    }
    }
    }

    public class ThreadTest {
    public static void main(String[] args) {
    MyThread m = new MyThread();
    Thread t1 = new Thread(m);
    Thread t2 = new Thread(m);
    t1.setName("线程1");
    t2.setName("线程2");
    t1.start();
    t2.start();
    }
    }
    /*
    线程1: 1
    线程2: 2
    线程1: 3
    线程2: 4
    线程1: 5
    线程2: 6
    线程1: 7
    线程2: 8
    线程1: 9
    线程2: 10
    */
  • 涉及的三个方法:

    • wait():使当前线程进入阻塞状态,并且释放同步监视器。
    • notify():唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。
    • notifyAll():唤醒所有被wait的线程。
  • 说明:

    1. wait()notify()notifyAll()三个方法必须使用在同步代码块或者同步方法中
    2. wait()notify()notifyAll()三个方法的调用者必须是同步代码块或者同步方法的同步监视器来调用。否则会出现异常。
    3. wait()notify()notifyAll()三个方法是java.lang.Object中声明的方法,不是Thread类中的方法。
  • sleep()wait()方法的异同

    • 相同点:一旦执行方法,都可以使当前线程进入阻塞状态。
    • 不同点:
      1. 所在的类不同:sleep()是Thread类中声明的方法,wait()是Object类中声明的方法。
      2. 调用的要求不同:sleep()可以在任何需要的场景调用。wait()必须使用在同步代码块或者同步方法中。
      3. 是否释放同步监视器:如果都使用在同步代码块中,sleep()不会释放锁,wait()会释放锁。
  • 例题2:生产者消费者问题

    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
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    class Clerk {
    //产品
    private int productNum = 0;
    //生产产品
    public synchronized void produceProduct() {
    if (productNum < 20) {
    productNum++;
    System.out.println(Thread.currentThread().getName() + ":开始生产第" + productNum + "个产品");
    notify();
    } else {
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    //消费产品
    public synchronized void consumeProduct() {
    if (productNum > 0) {
    System.out.println(Thread.currentThread().getName() + ":开始消费第" + productNum + "个产品");
    productNum--;
    notify();
    } else {
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    //生产者
    class Producer extends Thread {

    private Clerk clerk;

    public Producer(Clerk clerk) {
    this.clerk = clerk;
    }

    @Override
    public void run() {
    System.out.println(getName() + ":开始生产产品....");
    while (true) {
    try {
    Thread.sleep(50);//防止运行过快
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    clerk.produceProduct();
    }
    }
    }

    //消费者
    class Customer extends Thread {

    private Clerk clerk;

    public Customer(Clerk clerk) {
    this.clerk = clerk;
    }

    @Override
    public void run() {
    System.out.println(getName() + ":开始消费产品....");
    while (true) {
    try {
    Thread.sleep(50);//防止运行过快
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    clerk.consumeProduct();
    }
    }
    }

    public class ProductTest {
    public static void main(String[] args) {
    Clerk clerk = new Clerk();
    Producer p1 = new Producer(clerk);
    p1.setName("生产者1");
    Customer c1 = new Customer(clerk);
    c1.setName("消费者1");
    p1.start();
    c1.start();
    }
    }
    /*
    消费者1:开始消费产品....
    生产者1:开始生产产品....
    生产者1:开始生产第1个产品
    消费者1:开始消费第1个产品
    生产者1:开始生产第1个产品
    消费者1:开始消费第1个产品
    生产者1:开始生产第1个产品
    消费者1:开始消费第1个产品
    生产者1:开始生产第1个产品
    消费者1:开始消费第1个产品
    生产者1:开始生产第1个产品
    生产者1:开始生产第2个产品
    消费者1:开始消费第2个产品
    消费者1:开始消费第1个产品
    生产者1:开始生产第1个产品
    消费者1:开始消费第1个产品
    .....................
    .....................
    无限运行..............
    */