Java多线程
1、多线程的创建
1.1 继承于Thread类
- 创建一个继承于Thread类的子类。
- 重写Thread类的run()。
- 创建Thread类的子类的对象。
- 通过此对象调用start()。
例子:
1 | //1. 创建一个继承于Thread类的子类 |
- 不可以还让已经start()的线程去执行。会报IllegalThreadStateException。
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
1.1.1 Thread类中的常用方法
- start():启动当前线程;调用当前线程的run()。
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
- currentThread():静态方法,返回执行当前代码的线程。
- getName():获取当前线程的名字。
- setName():设置当前线程的名字。
- yield():释放当前cpu的执行权。
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- isAlive():判断当前线程是否存活。
- getPriority():获取线程的优先级。
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 –>默认优先级
- setPriority(int p):设置线程的优先级。(说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。)
例子:
1 | class HelloThread extends Thread{ |
1.2 实现Runnable接口
- 创建一个实现了Runnable接口的类。
- 实现类去实现Runnable中的抽象方法:run()。
- 创建实现类的对象。
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
- 通过Thread类的对象调用start()。
例子:
1 | //1. 创建一个实现了Runnable接口的类 |
1.3 实现Callable接口
- 与使用Runnable相比, Callable功能更强大些。
- 相比run()方法,可以有返回值。
- 方法可以抛出异常。
- 支持泛型的返回值。
- 需要借助FutureTask类,比如获取返回结果。
1 | //1.创建一个实现Callable的实现类 |
1.4 使用线程池
- 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors:
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable。
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable。 - void shutdown() :关闭连接池。
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池。
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池。
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
- 好处:
- 提高响应速度(减少了创建新线程的时间)。
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
- 便于线程管理
- corePoolSize:核心池的大小。
- maximumPoolSize:最大线程数。
- keepAliveTime:线程没有任务时最多保持多长时间后会终止。
1 | class NumberThread implements Runnable{ |
2、线程的生命周期
3、线程安全问题
- 例子:创建三个窗口卖票,总票数为100张。
- 问题:卖票过程中,出现了重票、错票 –>出现了线程的安全问题。
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
3.1 同步代码块解决实现Runnable接口线程安全问题
1 | synchronized(同步监视器){ |
- 操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求多个线程必须要共用同一把锁。
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
1 | class Window1 implements Runnable{ |
3.2 同步代码块解决继承实现Thread类线程安全问题
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
1 | class Window2 extends Thread{ |
3.3 同步方法解决实现Runnable接口线程安全问题
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this。
- 静态的同步方法,同步监视器是:当前类本身。
1 | class Window3 implements Runnable { |
3.4 同步方法解决继承实现Thread类线程安全问题
1 | class Window4 extends Thread { |
3.5 Lock锁解决线程安全问题
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
1 | class Window implements Runnable{ |
4、死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
1 | public class ThreadTest { |
5、线程的通信
- 使用两个线程打印 1-100。线程1, 线程2 交替打印。
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
1 | class Number implements Runnable{ |
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常。
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
- sleep() 和 wait()的异同:
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()。
- 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中。
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。