多线程(Thread 类、Runnable 接口、Callable 接口、synchronized 、Lock、线程池)

1 Thread

  先创建 ThreadTest 类并继承 Thread 类,然后重写 run() 方法。

public class ThreadTest extends Thread {

    @Override
    public void run() {
        for(int i = 0; i < 5000; i++) {
            System.out.println("ThreadTest 的 run 方法:" + i);
        }
    }
}

  创建 main 方法,然后创建 ThreadTest 对象,通过对象调用 start() 方法启动线程。

public class MainTest {

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();	// 创建线程对象
        threadTest.start();	// 启动子线程

        for(int i = 0; i < 5000; i++) {
            System.out.println("main 方法:" + i);
        }
    }
}

控制台输出:

	main 方法:674
	main 方法:675
	ThreadTest 的 run 方法:555
	main 方法:676
	ThreadTest 的 run 方法:556
	main 方法:677
	ThreadTest 的 run 方法:557
	ThreadTest 的 run 方法:558
	ThreadTest 的 run 方法:559

可以看出,主线程和子线程是交替运行的。

2 Runnable

  创建 RunableTest 类并实现 Runnable 接口,重写 run() 方法。

public class RunnableTest implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 5000; i++) {
            System.out.println("RunnableTest 的 run 方法:" + i);
        }
    }
}

  创建 main 方法,首先创建 RunnableTest 对象,然后将 RunnableTest 对象作为参数来创建 Thread 线程对象,最后调用 start() 方法启动线程。

public class MainTest {

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest(); // 1.创建 Runnable 实现类对象
        Thread runnableTestThread = new Thread(runnableTest);   // 2.将 Runnable 实现类对象作为参数来创建线程对象
        runnableTestThread.start(); // 3.启动线程

        for(int i = 0; i < 5000; i++) {
            System.out.println("main方法:" + i);
        }
    }
}

控制台输出:

	main方法:1914
	RunnableTest 的 run 方法:1660
	RunnableTest 的 run 方法:1661
	RunnableTest 的 run 方法:1662
	RunnableTest 的 run 方法:1663
	RunnableTest 的 run 方法:1664
	RunnableTest 的 run 方法:1665
	RunnableTest 的 run 方法:1666
	main方法:1915
	RunnableTest 的 run 方法:1667
	main方法:1916

可以看出,主线程和子线程是交替运行的。

3 Callable

  创建 CallableTest 实现 Callable 接口,并重写 call() 方法,call() 方法有返回值,默认 Object,可自定义。

public class CallableTest implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        for(int i = 0; i < 10; i++) {
            if(i == 5) {
                return true;
            }
            System.out.println(i);
        }
        return false;
    }
}

   创建 main 方法,首先创建 Callable 实现类对象,其次创建执行服务池(线程池),然后通过线程池执行提交执行,接着可以获取返回值,最后关闭服务。

public class MainTest {

    public static void main(String[] args) throws Exception {
        CallableTest callableTest = new CallableTest();
        // 创建执行服务
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 提交执行
        Future<Boolean> submit = executorService.submit(callableTest);
        // 获取结果
        System.out.println(submit.get());
        // 关闭服务
        executorService.shutdown();
    }
}

4 线程安全问题

  用 Runnable 买票为例,总共 5 张票。。

  • Runnable 实现类
public class RunnableTest implements Runnable {

    private Integer ticket = 5; // 票数

    @Override
    public void run() {
        for(;ticket > 0; ticket--) {
            try {
                Thread.sleep(10);	// 让线程休眠,放大问题的发生性。即为了更好的测试出问题,不然根据 CPU 的情况,有时看不出问题。
            }
            catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + "窗口卖出 " + ticket + "号票");
        }
    }
}
  • main 方法
public class MainTest {

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest(); // 创建 Runnable 实现类对象
        new Thread(runnableTest,"顾客1").start();
        new Thread(runnableTest,"顾客2").start();
        new Thread(runnableTest,"顾客3").start();
    }
}

控制台输出:

顾客1顾客抢到 5号票
顾客2顾客抢到 5号票
顾客3顾客抢到 5号票
顾客1顾客抢到 2号票
顾客3顾客抢到 1号票
顾客2顾客抢到 1号票
顾客1顾客抢到 -1号票

  很显然,最终结果并不是我们想要的结果,这就是线程的安全问题。当顾客 1 抢到 5 好票时,系统没有及时更新更新信息。此时顾客 2、3都以为 5 号票依然存在,因此都去抢 5 好票,导致以上结果。

  为了避免发生线程安全问题,需要对资源上锁,线程进行排队获取。即当第一个线程想要获取资源时,先获取锁,将资源锁上。完事后,解锁。第二个线程重复第一个线程的操作。

5 synchronized 与 Lock

5.1 synchronized

  synchronized 也称同步锁。synchronized 锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。可以修饰以下几种:

  • 修饰实例方法,对当前实例对象加锁
  • 修饰静态方法,多当前类的 Class 对象加锁
  • 修饰代码块,对 synchronized 括号内的对象加锁(括号内的为 Object 对象)

  修饰代码块时。

public class RunnableTest implements Runnable {

    private Integer ticket = 5; // 票数

    @Override
    public void run(){
        synchronized (ticket) {
            for(;ticket > 0; ticket--) {
                try {
                    Thread.sleep(10);
                }
                catch (Exception e) {
                }
                System.out.println(Thread.currentThread().getName() + "顾客抢到 " + ticket + "号票");
            }
        }
    }
}

控制台输出:

顾客1顾客抢到 5号票
顾客1顾客抢到 4号票
顾客2顾客抢到 3号票
顾客1顾客抢到 2号票
顾客3顾客抢到 1号票

  修饰方法时。

public class RunnableTest implements Runnable {

    private Integer ticket = 5; // 票数

    @Override
    public synchronized void run(){
        for(;ticket > 0; ticket--) {
            try {
                Thread.sleep(10);
            }
            catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + "顾客抢到 " + ticket + "号票");
        }
    }
}
5.2 Lock
public class RunnableTest implements Runnable {

    private Integer ticket = 5; // 票数

    private final ReentrantLock lock = new ReentrantLock(); // 定义 lock 锁

    @Override
    public synchronized void run(){
        try {
            lock.lock(); // 加锁
            for(;ticket > 0; ticket--) {
                try {
                    Thread.sleep(10);
                }
                catch (Exception e) {
                }
                System.out.println(Thread.currentThread().getName() + "顾客抢到 " + ticket + "号票");
            }
        }
        finally {
            lock.unlock(); // 解锁
        }
    }
}
5.3 对比
  • Lock 是显示锁(手动开启与关闭,记得一定要关锁,因为如果发生异常,那就无法释放锁,这就是为啥上面在 finally 中关锁的原因);synchronized 是隐式锁,出了作用域就会自动解锁。
  • Lock 只作用于代码块,synchronized 可作用于代码块和方法。
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供很多子类)。
  • 优先使用顺序:
    • Lock > 同步代码块 > 同步方法

6 线程池

  为了解决线程经常创建和销毁等影响性能的繁琐过程,减少内存的消耗,Java 中开辟出了一种管理线程的概念,这个概念叫做线程池。

  首先创建号多个线程,放入到线程池中,使用时直接获取,使用完后放回池中。可以避免创建销毁、实现重复利用。好处如下:

  • 提高了响应的速度(减少了创建新线程的时间)。
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)。
  • 便于线程管理。
6.1 图解

在这里插入图片描述

6.2 其中的一个构造方法
public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)
  • corePoolSize:线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收。
  • maximumPoolSize:线程池中可以容纳的最大线程的数量。
  • keepAliveTime:线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的。
  • util:非核心线程可以保留的最长的空闲时间。
  • workQueue:等待队列,任务可以储存在任务队列中等待被执行,执行的是 FIFIO 原则(先进先出)。
  • threadFactory:就是创建线程的线程工厂。
  • handler:是一种拒绝策略,我们可以在任务满了的时候,拒绝执行某些任务。
6.3 执行流程

  任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用 handler 实现拒绝策略。

6.4 拒绝策略
  • AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满。
  • DisCardPolicy:不执行新任务,也不抛出异常。
  • DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行。
  • CallerRunsPolicy:直接调用 execute 来执行当前任务
6.5 常见线程池
  • CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为 Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
  • SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
  • SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
  • FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值