Java多线程

Java多线程

基本概念

程序:是为完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码,静态对象。

进程:是程序的一次执行过程,或者是正在运行的一个程序。是一个动态的过程,有其自身的产生、存在和消亡的过程。进程是资源分配的基本单位。进程是动态的。

线程:进程进一步细分就可以分为线程,是一个程序内部的一条执行路径。线程是调度和执行的基本单位,每个线程都拥有独立的运行栈和程序计数器(pc)。

进程和线程的关系好比是工厂和工人的关系,工厂中有许多不同的车间,每个车间都有许多不同的设备,好比是进程中的资源,而调度和执行这些设备的就是工人,那么工人就好比是线程,一个车间有多个工人,就好比进程中的多线程。一个车间中的工人是共享这个车间的,好比一个进程中的多个线程是共享进程中的资源的。

什么时候需要多线程

  • 程序需要同时执行两个或多个任务
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
  • 需要一些后台运行的程序时

线程的创建和使用

从创建开始

方式一

继承Thread类

  1. 创建一个Thread类的子类
  2. 重写Thread类的run()方法
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start()

代码示例:

package demo;

/**
 * @Description:多线程的创建和使用--->输出1-100的偶数
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //实例化Thread子类的对象
        MyThread t1 = new MyThread();
        //通过这个对象调用start方法
        t1.start();//启动当前线程,调用当前线程的run()方法
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }

}

这里我们想一下,既然start最终还是调用的线程里重写的run方法,那么我们能不能直接调用run方法呢?我们来试试看

package demo;

/**
 * @Description:多线程的创建和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //实例化Thread子类的对象
        MyThread t1 = new MyThread();
        //通过这个对象调用run方法
        t1.run();
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }

}

这里我们先来直接试试调用run()方法,输出的格式中我们是先得到线程的名字然后再输出数字。

执行结果:

main:0
main:2
main:4
main:6
main:8
main:10
main:12
main:14
main:16
main:18
main:20
main:22
main:24
main:26
main:28
main:30
main:32
main:34
main:36
main:38
main:40
main:42
main:44
main:46
main:48
main:50
main:52
main:54
main:56
main:58
main:60
main:62
main:64
main:66
main:68
main:70
main:72
main:74
main:76
main:78
main:80
main:82
main:84
main:86
main:88
main:90
main:92
main:94
main:96
main:98

我们发现输出的线程的名字其实还是主线程,那么我们要改成start()呢?我们来看看结果。

Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-0:10
Thread-0:12
Thread-0:14
Thread-0:16
Thread-0:18
Thread-0:20
Thread-0:22
Thread-0:24
Thread-0:26
Thread-0:28
Thread-0:30
Thread-0:32
Thread-0:34
Thread-0:36
Thread-0:38
Thread-0:40
Thread-0:42
Thread-0:44
Thread-0:46
Thread-0:48
Thread-0:50
Thread-0:52
Thread-0:54
Thread-0:56
Thread-0:58
Thread-0:60
Thread-0:62
Thread-0:64
Thread-0:66
Thread-0:68
Thread-0:70
Thread-0:72
Thread-0:74
Thread-0:76
Thread-0:78
Thread-0:80
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98

这个时候我们发现线程的名字已经不再是主线程了,而是一个新的线程名字,所以我们了解到通过start()方法是启动的一个新的线程,而不仅仅是调用方法。所以我们是不能直接调用run()方法的方式来启动线程。

那么如果我们要再创建一个线程该如何操作呢?我们应该是再实例化一个新的对象,用这个新的对象来启动一个新的线程。如下所示。

package demo;

/**
 * @Description:多线程的创建和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //实例化Thread子类的对象
        MyThread t1 = new MyThread();
        //通过这个对象调用start方法
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }

}

运行结果

Thread-0:0
Thread-1:0
Thread-0:2
Thread-1:2
Thread-0:4
Thread-1:4
Thread-0:6
Thread-1:6
Thread-0:8
Thread-1:8
Thread-0:10
Thread-1:10
Thread-0:12
Thread-1:12
Thread-1:14
Thread-0:14
Thread-1:16
Thread-0:16
Thread-1:18
Thread-1:20
Thread-0:18
Thread-1:22
Thread-0:20
Thread-1:24
Thread-0:22
Thread-1:26
Thread-0:24
Thread-1:28
Thread-0:26
Thread-1:30
Thread-0:28
Thread-1:32
Thread-0:30
Thread-1:34
Thread-0:32
Thread-1:36
Thread-0:34
Thread-1:38
Thread-0:36
Thread-1:40
Thread-0:38
Thread-1:42
Thread-0:40
Thread-1:44
Thread-0:42
Thread-1:46
Thread-0:44
Thread-1:48
Thread-0:46
Thread-1:50
Thread-1:52
Thread-1:54
Thread-1:56
Thread-1:58
Thread-0:48
Thread-1:60
Thread-0:50
Thread-1:62
Thread-0:52
Thread-1:64
Thread-0:54
Thread-1:66
Thread-0:56
Thread-1:68
Thread-0:58
Thread-1:70
Thread-0:60
Thread-1:72
Thread-0:62
Thread-1:74
Thread-0:64
Thread-1:76
Thread-0:66
Thread-1:78
Thread-0:68
Thread-1:80
Thread-0:70
Thread-1:82
Thread-0:72
Thread-1:84
Thread-0:74
Thread-1:86
Thread-0:76
Thread-1:88
Thread-1:90
Thread-0:78
Thread-1:92
Thread-0:80
Thread-1:94
Thread-1:96
Thread-1:98
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98

我们发现在结果里面,既出现了Thread-0也出现了Thread-1。这里就相当于又新启动了一个新的线程。

匿名对象创建

通过匿名的方式来创建Thread匿名子类。如下所示

package demo;

/**
 * @Description:多线程的创建和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}

这里我们直接创建了一个匿名对象,然后在匿名对象里面重写run()方法,在匿名对象的后面直接调用start()方法来启动线程,从而使代码更简单,而不用每次都需要去写一个自己的类去继承。

方式二

除了通过创建对象的方式来调用线程的方法,我们还可以采用接口的方式来调用线程。通常我们叫做实现Runnable接口的方式。如下所示

package demo;

/**
 * @Description:创建多线程的方式二,实现Runnable接口
 * @Author:Ameng
 * @Create:2020-10-04-11:01
 */
public class ThreadTest1 {
    public static void main(String[] args) {
        //3.创建类的对象
        MThread mThread = new MThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        //5.通过Thread类的对象调用start():①启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();
    }
}

//1.创建一个继承于Thread类的子类
class MThread implements Runnable {
    //2.重写Thread类的run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

运行结果

线程1:0
线程2:0
线程1:2
线程2:2
线程1:4
线程2:4
线程1:6
线程2:6
线程1:8
线程2:8
线程1:10
线程2:10
线程1:12
线程2:12
线程1:14
线程2:14
线程1:16
线程2:16
线程1:18
线程2:18
线程1:20
线程2:20
线程1:22
线程2:22
线程1:24
线程2:24
线程1:26
线程2:26
线程1:28
线程2:28
线程1:30
线程2:30
线程1:32
线程2:32
线程1:34
线程2:34
线程1:36
线程2:36
线程1:38
线程2:38
线程1:40
线程2:40
线程1:42
线程2:42
线程1:44
线程2:44
线程1:46
线程2:46
线程1:48
线程2:48
线程1:50
线程2:50
线程1:52
线程2:52
线程1:54
线程2:54
线程1:56
线程2:56
线程1:58
线程2:58
线程1:60
线程2:60
线程1:62
线程2:62
线程1:64
线程2:64
线程1:66
线程2:66
线程1:68
线程2:68
线程1:70
线程2:70
线程1:72
线程2:72
线程1:74
线程2:74
线程1:76
线程2:76
线程1:78
线程2:78
线程1:80
线程2:80
线程1:82
线程2:82
线程1:84
线程2:84
线程1:86
线程2:86
线程1:88
线程2:88
线程1:90
线程2:90
线程1:92
线程2:92
线程1:94
线程2:94
线程1:96
线程2:96
线程1:98
线程2:98

方式三

实现Callable接口

过程:

  1. 创建一个实现Callbale的类
  2. 实现call()方法,将此线程需要执行的操作声明在这个方法中,类似run()方法
  3. 创建Callbale接口实现类的对象
  4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创造Thread对象,并调用start()

与使用Runnable接口相比,Callbale接口更加强大

  • 相比run()方法,call()方法可以有返回值
  • call()方法可以抛出异常,可以获取到异常的详细信息
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

代码示例

package demo.java2;

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

/**
 * @Description:创建线程的方式三:实现Callbale接口
 * @Author:Ameng
 * @Create:2020-10-08-11:13
 */
public class ThreadNew {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();

        FutureTask futureTask = new FutureTask(numThread);

        new Thread(futureTask).start();


        try {
            Object sum = futureTask.get();
            System.out.println("偶数总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumThread implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

方式四

使用线程池

优点:

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

代码示例

package demo.java2;

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

/**
 * @Description:创建线程的方式四:使用线程池
 * @Author:Ameng
 * @Create:2020-10-08-12:13
 */
public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);

        //线程的设置和管理
//        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        service.execute(new NumberThread());//适合使用于Runnable
        service.execute(new NumberThread1());//适合使用于Runnable
//        service.submit();//适合使用于Callbale
        service.shutdown();
    }
}

class NumberThread implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

线程中常用方法

  • void start():启动线程,并执行对象的run()方法
  • run():线程在被调度时执行的操作,通常需要重写此方法
  • currentThread():静态方法,返回执行当前代码的线程
  • String getName():返回线程的名称
  • void setName(String name):设置线程的名称
  • static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
  • static void yield():线程让步
    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    • 若队列中没有同优先级的线程,忽略此方法
  • join():当某个线程执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
    • 低优先级的线程也可以获得执行
  • static void sleep(long millis):(指定时间:毫秒)
    • 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重新排队
    • 抛出interruptedException异常
  • stop():强制线程生命期结束,不推荐使用
  • boolean isAlive():返回boolean,判断线程是否还活着

线程的调度

调度策略

  • 时间片
  • 抢占式:高优先级的线程抢占CPU

Java的调度方法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级

优先级等级
  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5
获取和设置当前线程优先级涉及的方法
  • getPriority():返回线程的优先值
  • setPriority(int newPriority):改变线程的优先级
说明
  • 线程创建时继承父线程的优先级
  • 低优先级只是获得调度的概率低,并非一定是在高优先级之后才被调用

线程的生命周期

Java中定义了线程的几种状态,一个完整的生命周期通常要经历如下的五种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没有分配到CPU资源
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
  • 死亡:线程完成了它的全部工作或线程被提前强制性地终止或出现异常导致结束

线程的同步(安全问题)

问题:

  1. 多个线程执行的不确定性引起执行结果的不稳定
  2. 多个线程对同一结构的共享,会造成操作的不完整性,可能会破坏数据

出现这些问题的原因主要就是在线程正在操作的过程当中,其他线程参与进来,也参与同样操作从而导致问题出现。所以要解决线程安全问题就是需要当某个线程在进行操作时,其他线程不能参与进来。

同步机制

方法一:同步代码块
synchronized(同步监视器){
//需要被同步的代码,即操作共享数据的代码
}
  • 同步监视器:锁。任何一个类的对象,都可以充当锁,多个线程必须共用同一把锁。
  • 共享数据:多个线程共同操作的数据

代码示例

package demo;

/**
 * @Description:通过加一个锁的方式来进行线程之间的同步
 * @Author:Ameng
 * @Create:2020-10-04-20:29
 */
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

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

class Window1 implements Runnable {
    private int ticket = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;

                } else {
                    break;
                }
            }
        }
    }
}

执行结果

窗口1:卖票,票号为:100
窗口1:卖票,票号为:99
窗口1:卖票,票号为:98
窗口3:卖票,票号为:97
窗口3:卖票,票号为:96
窗口3:卖票,票号为:95
窗口3:卖票,票号为:94
窗口3:卖票,票号为:93
窗口3:卖票,票号为:92
窗口3:卖票,票号为:91
窗口3:卖票,票号为:90
窗口3:卖票,票号为:89
窗口3:卖票,票号为:88
窗口3:卖票,票号为:87
窗口3:卖票,票号为:86
窗口3:卖票,票号为:85
窗口3:卖票,票号为:84
窗口3:卖票,票号为:83
窗口3:卖票,票号为:82
窗口3:卖票,票号为:81
窗口3:卖票,票号为:80
窗口3:卖票,票号为:79
窗口3:卖票,票号为:78
窗口3:卖票,票号为:77
窗口3:卖票,票号为:76
窗口3:卖票,票号为:75
窗口3:卖票,票号为:74
窗口3:卖票,票号为:73
窗口3:卖票,票号为:72
窗口3:卖票,票号为:71
窗口3:卖票,票号为:70
窗口3:卖票,票号为:69
窗口3:卖票,票号为:68
窗口3:卖票,票号为:67
窗口3:卖票,票号为:66
窗口3:卖票,票号为:65
窗口3:卖票,票号为:64
窗口3:卖票,票号为:63
窗口3:卖票,票号为:62
窗口3:卖票,票号为:61
窗口3:卖票,票号为:60
窗口3:卖票,票号为:59
窗口3:卖票,票号为:58
窗口3:卖票,票号为:57
窗口3:卖票,票号为:56
窗口3:卖票,票号为:55
窗口3:卖票,票号为:54
窗口3:卖票,票号为:53
窗口3:卖票,票号为:52
窗口3:卖票,票号为:51
窗口3:卖票,票号为:50
窗口3:卖票,票号为:49
窗口3:卖票,票号为:48
窗口3:卖票,票号为:47
窗口3:卖票,票号为:46
窗口3:卖票,票号为:45
窗口3:卖票,票号为:44
窗口3:卖票,票号为:43
窗口3:卖票,票号为:42
窗口3:卖票,票号为:41
窗口3:卖票,票号为:40
窗口3:卖票,票号为:39
窗口3:卖票,票号为:38
窗口3:卖票,票号为:37
窗口3:卖票,票号为:36
窗口3:卖票,票号为:35
窗口3:卖票,票号为:34
窗口3:卖票,票号为:33
窗口3:卖票,票号为:32
窗口3:卖票,票号为:31
窗口3:卖票,票号为:30
窗口3:卖票,票号为:29
窗口3:卖票,票号为:28
窗口3:卖票,票号为:27
窗口3:卖票,票号为:26
窗口3:卖票,票号为:25
窗口3:卖票,票号为:24
窗口3:卖票,票号为:23
窗口3:卖票,票号为:22
窗口3:卖票,票号为:21
窗口3:卖票,票号为:20
窗口3:卖票,票号为:19
窗口3:卖票,票号为:18
窗口3:卖票,票号为:17
窗口3:卖票,票号为:16
窗口3:卖票,票号为:15
窗口3:卖票,票号为:14
窗口3:卖票,票号为:13
窗口3:卖票,票号为:12
窗口3:卖票,票号为:11
窗口3:卖票,票号为:10
窗口3:卖票,票号为:9
窗口3:卖票,票号为:8
窗口3:卖票,票号为:7
窗口3:卖票,票号为:6
窗口3:卖票,票号为:5
窗口3:卖票,票号为:4
窗口3:卖票,票号为:3
窗口3:卖票,票号为:2
窗口3:卖票,票号为:1

发现在结果中没有出现重票或者错票的情况,这里需要注意一点的是,加的锁必须是多个线程共用的一个锁,任何一个类的对象都可以。

上面我们是使用Runnable接口的方式实现同步机制,那么如果要是继承Thread类的时候应该如何写呢?如下所示

package demo;

/**
 * @Description:使用同步代码块来解决继承Thread类的安全问题
 * @Author:Ameng
 * @Create:2020-10-04-20:51
 */
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        Window2 w2 = new Window2();
        Window2 w3 = new Window2();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window2 extends Thread {

    private static int ticket = 100;
    private static Object obj = new Object();

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

我们需要做的改变就是将对象obj写成静态对象,这样在每次对象实例化的时候,它们用的就是同一个obj对象(同步锁)

那么是不是我们的同步锁就只能是声明一个对象呢?其实不然,除此之外,我们还可以使用this来充当同步监视器(同步锁),但是一定要慎用,否则可能这个锁会不起作用。另外还可以使用类来充当同步监视器(同步锁),如下所示

package demo;

/**
 * @Description:使用同步代码块来解决继承Thread类的安全问题
 * @Author:Ameng
 * @Create:2020-10-04-20:51
 */
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        Window2 w2 = new Window2();
        Window2 w3 = new Window2();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window2 extends Thread {

    private static int ticket = 100;

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

Window.class只会被加载一次,可充当同步锁。

方法二:同步方法

如果操作共享数据的代码正好完整声明在一个方法中,那么就可以将此方法声明为同步方法。如下所示

package demo;

/**
 * @Description:使用同步方法来实现Runnable接口
 * @Author:Ameng
 * @Create:2020-10-05-18:15
 */
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

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


class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show() {//默认同步监视器为this
        if (ticket > 0) {

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

这里用几个使用一个加了锁的方法,从而达到了同步的目的。

package demo;

/**
 * @Description:同步方法实现继承类Thread类的线程安全问题
 * @Author:Ameng
 * @Create:2020-10-05-18:27
 */
public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show1();
        }
    }

    private static synchronized void show1() {
        if (ticket > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

说明:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显示声明
  • 非静态的同步方法,同步监视器:this
  • 静态的同步方法,同步监视器:当前类

线程的死锁

死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。当出现死锁的时候不会出现异常也不会出现提示,而只是所有线程都处于阻塞状态,无法继续。

来看一个简单的死锁示例

package demo.java;

/**
 * @Description:线程的死锁问题
 * @Author:Ameng
 * @Create:2020-10-05-20:31
 */
public class ThreadTest1 {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread() {
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");

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

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1) {
                        s1.append("d");
                        s1.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

这段代码程序将直接静止,无法执行出结果。

Lock(锁)

在JDK5之后增加了通过显示定义同步锁的对象的方法来实现同步。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

代码示例

package demo.java;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description:使用锁的方式来实现线程同步
 * @Author:Ameng
 * @Create:2020-10-07-16:46
 */
public class LockTest {

    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();

    }
}

class Window implements Runnable {
    private int ticket = 100;
    //实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //调用lock方法
                lock.lock();

                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //调用unlock方法
                lock.unlock();
            }
        }
    }
}

执行结果

窗口1售票,票号为:100
窗口1售票,票号为:99
窗口1售票,票号为:98
窗口1售票,票号为:97
窗口1售票,票号为:96
窗口1售票,票号为:95
窗口1售票,票号为:94
窗口1售票,票号为:93
窗口1售票,票号为:92
窗口1售票,票号为:91
窗口1售票,票号为:90
窗口1售票,票号为:89
窗口1售票,票号为:88
窗口1售票,票号为:87
窗口1售票,票号为:86
窗口2售票,票号为:85
窗口2售票,票号为:84
窗口2售票,票号为:83
窗口2售票,票号为:82
窗口2售票,票号为:81
窗口3售票,票号为:80
窗口3售票,票号为:79
窗口3售票,票号为:78
窗口3售票,票号为:77
窗口3售票,票号为:76
窗口3售票,票号为:75
窗口3售票,票号为:74
窗口3售票,票号为:73
窗口3售票,票号为:72
窗口3售票,票号为:71
窗口3售票,票号为:70
窗口3售票,票号为:69
窗口3售票,票号为:68
窗口3售票,票号为:67
窗口3售票,票号为:66
窗口3售票,票号为:65
窗口3售票,票号为:64
窗口3售票,票号为:63
窗口3售票,票号为:62
窗口3售票,票号为:61
窗口3售票,票号为:60
窗口3售票,票号为:59
窗口3售票,票号为:58
窗口3售票,票号为:57
窗口3售票,票号为:56
窗口3售票,票号为:55
窗口3售票,票号为:54
窗口3售票,票号为:53
窗口3售票,票号为:52
窗口3售票,票号为:51
窗口3售票,票号为:50
窗口3售票,票号为:49
窗口3售票,票号为:48
窗口3售票,票号为:47
窗口3售票,票号为:46
窗口3售票,票号为:45
窗口3售票,票号为:44
窗口3售票,票号为:43
窗口3售票,票号为:42
窗口3售票,票号为:41
窗口3售票,票号为:40
窗口3售票,票号为:39
窗口3售票,票号为:38
窗口3售票,票号为:37
窗口3售票,票号为:36
窗口3售票,票号为:35
窗口3售票,票号为:34
窗口3售票,票号为:33
窗口3售票,票号为:32
窗口3售票,票号为:31
窗口3售票,票号为:30
窗口3售票,票号为:29
窗口3售票,票号为:28
窗口3售票,票号为:27
窗口3售票,票号为:26
窗口3售票,票号为:25
窗口3售票,票号为:24
窗口3售票,票号为:23
窗口3售票,票号为:22
窗口3售票,票号为:21
窗口3售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口3售票,票号为:17
窗口3售票,票号为:16
窗口3售票,票号为:15
窗口3售票,票号为:14
窗口3售票,票号为:13
窗口3售票,票号为:12
窗口3售票,票号为:11
窗口3售票,票号为:10
窗口3售票,票号为:9
窗口3售票,票号为:8
窗口3售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口3售票,票号为:1
synchronized和lock的异同

同:

  • 都用来解决线程的安全问题

异:

  • synchronized可以自动释放同步监视器,而Lock需要手动的启动同步(lock())和结束同步(unlock())

线程的通信

线程通信的三个方法

  • wait():执行此方法,那么当前线程进入阻塞状态,并释放同步监视器
  • notify():执行此方法,就会唤醒被wait的线程,若有多个线程是wait状态,那么就唤醒优先级高的线程
  • notify():唤醒所有的wait状态的线程

说明:这三个方法必须使用在同步代码块或同步方法中

package demo.java2;

/**
 * @Description:使用两个线程交替打印1-100
 * @Author:Ameng
 * @Create:2020-10-07-17:15
 */
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

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

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

class Number implements Runnable {

    private int number = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {

                notify();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

执行结果

线程1:1
线程2:2
线程1:3
线程2:4
线程1:5
线程2:6
线程1:7
线程2:8
线程1:9
线程2:10
线程1:11
线程2:12
线程1:13
线程2:14
线程1:15
线程2:16
线程1:17
线程2:18
线程1:19
线程2:20
线程1:21
线程2:22
线程1:23
线程2:24
线程1:25
线程2:26
线程1:27
线程2:28
线程1:29
线程2:30
线程1:31
线程2:32
线程1:33
线程2:34
线程1:35
线程2:36
线程1:37
线程2:38
线程1:39
线程2:40
线程1:41
线程2:42
线程1:43
线程2:44
线程1:45
线程2:46
线程1:47
线程2:48
线程1:49
线程2:50
线程1:51
线程2:52
线程1:53
线程2:54
线程1:55
线程2:56
线程1:57
线程2:58
线程1:59
线程2:60
线程1:61
线程2:62
线程1:63
线程2:64
线程1:65
线程2:66
线程1:67
线程2:68
线程1:69
线程2:70
线程1:71
线程2:72
线程1:73
线程2:74
线程1:75
线程2:76
线程1:77
线程2:78
线程1:79
线程2:80
线程1:81
线程2:82
线程1:83
线程2:84
线程1:85
线程2:86
线程1:87
线程2:88
线程1:89
线程2:90
线程1:91
线程2:92
线程1:93
线程2:94
线程1:95
线程2:96
线程1:97
线程2:98
线程1:99
线程2:100

sleep()和wait()的异同

同:

  • 都可以使得当前线程进入阻塞状态

异:

  • 两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait()
  • 调用的要求不同,sleep()可以在任何需要的情景下调用,而wait()必须使用在同步代码块或同步方法中
  • 若两个方法都使用在同步代码块或同步方法中,slepp()不会释放锁,而wait()会释放同步监视器
posted @ 2021-01-19 21:32  怪味巧克力  阅读(265)  评论(0编辑  收藏  举报