线程时一个程序内部的一条执行流程,java的main方法就是由一条默认的主线程执行
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
许多平台都离不开多线程,因为多线程可以实现并发技术,只有通过多线程,才可能实现多人同时订票等功能,一个人就代表着一个线程
Java是通过java.lang.Thread类的对象来代表线程的
- //1.子类MyThread继承Thread类
- public class MyThread extends Thread{
- //2.重写run方法,run方法的方法体就是新的线程执行体
- @Override
- public void run() {
- for (int i = 0; i < 5; i++) {
- System.out.println("MyThread执行" + i);
- }
- }
- }
- public class Test1 {
- //main方法默认就是由一个主线程负责执行
- public static void main(String[] args) {
- //3.创建一个子类对象即新的线程对象
- MyThread myThread = new MyThread();
- //4.调用start方法,启动新线程,执行run方法
- myThread.start();
-
- //5.主线程中,通过for循环执行主线程的执行体
- for (int i = 0; i < 5; i++) {
- System.out.println("main线程执行" + i);
- }
- }
- }
优点:编码简单
缺点:java是单继承,线程类已经继承了Thread类,就无法继承其他类,不利于功能的扩展
- public class Test2 {
- public static void main(String[] args) {
- MyThread myThread = new MyThread();
- //如果调用run方法,那么就不是启动新的线程,而是在主线程中调用run方法
- myThread.run();
- for (int i = 0; i < 5; i++) {
- System.out.println("main线程执行" + i);
- };
- }
- }
- result:
- MyThread执行0
- MyThread执行1
- MyThread执行2
- MyThread执行3
- MyThread执行4
- main线程执行0
- main线程执行1
- main线程执行2
- main线程执行3
- main线程执行4
- 由结果可以看出变成了单线程,只有主线程负责执行
- public class Test2 {
- public static void main(String[] args) {
- MyThread myThread = new MyThread();
- for (int i = 0; i < 5; i++) {
- System.out.println("main线程执行" + i);
- };
- //主程序的执行体在线程开启之前,所以先执行主程序的执行体,相当于单线程
- myThread.start();
- }
- }
- main线程执行0
- main线程执行1
- main线程执行2
- main线程执行3
- main线程执行4
- MyThread执行0
- MyThread执行1
- MyThread执行2
- MyThread执行3
- MyThread执行4
- //1.创建一个实现了Runnable接口的类
- public class MyRunnable implements Runnable{
- //2.实现类去实现Runnable中的抽象方法run()
- @Override
- public void run() {
- for (int i = 0; i < 5; i++) {
- System.out.println("子线程执行==》" + i);
- }
- }
- }
- public class Test1 {
- public static void main(String[] args) {
- //3.创建一个任务对象
- Runnable myRunnable = new MyRunnable();
- //4.将任务对象封装成一个线程对象并调用start方法
- new Thread(myRunnable).start();
-
- for (int i = 0; i < 5; i++) {
- System.out.println("main线程执行==>" + i);
- }
- }
- }
- result:
- 子线程执行==》0
- main线程执行==>0
- 子线程执行==》1
- main线程执行==>1
- 子线程执行==》2
- main线程执行==>2
- main线程执行==>3
- main线程执行==>4
- 子线程执行==》3
- 子线程执行==》4
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
缺点:需要多创建一个Runnable对象
- public class Test2 {
- public static void main(String[] args) {
- //使用匿名内部类的方式创建线程
- new Thread(new Runnable(){
- @Override
- public void run() {
- for (int i = 0; i < 3; i++) {
- System.out.println("子线程1执行==>" + i);
- }
- }
- }).start();
-
- //使用lambda表达式创建线程
- new Thread(() ->{
- for (int i = 0; i < 3; i++) {
- System.out.println("子线程2执行==>" + i);
- }
- }
- ).start();
-
- for (int i = 0; i < 3; i++) {
- System.out.println("main线程执行==>" + i);
- }
- }
- }
- //1.创建一个实现Callable接口的实现类
- public class MyCallable implements Callable
{ - private int n;
-
- public MyCallable(int n) {
- this.n = n;
- }
- //2.重写call方法,call方法的方法体就是新的线程执行体
- @Override
- public Double call() throws Exception {
- double sum = 0;
- for (int i = 1; i <= n; i++) {
- sum += i;
- }
- return sum;
- }
- }
- public class Test {
- public static void main(String[] args) throws Exception {
- //3.创建一个子类对象
- Callable
myCallable = new MyCallable(100); - //4.创建一个FutureTask对象,将myCallable对象作为构造参数传入
- FutureTask
futureTask = new FutureTask<>(myCallable); - //5.创建一个Thread对象,并将FutureTask对象作为构造参数传入
- new Thread(futureTask).start();
- //创建第二个线程
- FutureTask
futureTask2 = new FutureTask<>(new MyCallable(200)); - new Thread(futureTask2).start();
-
- //6.通过FutureTask对象的get方法获取call方法的返回值
- //如果call方法没有执行完毕,get方法会阻塞等待
- Double v = futureTask.get();
- System.out.println(v);
- Double v2 = futureTask2.get();
- System.out.println(v2);
- }
- }
优点:
缺点:编码比较复杂
方法 | 说明 |
---|---|
public void run( ) | 线程中要执行的方法 |
public void start( ) | 启动线程 |
public String getName( ) | 获取当前线程的名称,线程名称默认是Thread-索引 |
public void setName( String name ) | 修改线程的名称 |
public static Thread currentThread( ) | 获取当前执行的线程对象 |
public static void sleep( long time ) | 让当前执行的线程休眠多少毫秒,1000毫秒=1秒 |
public final void join( ) | 让调用当前这个方法的线程先执行完再让其他线程执行 |
解决线程安全问题的方案
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
作用:把访问共享资源的核心代码块上锁,以此保证线程安全
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug,因为对于不同的线程需要识别同步锁,同步锁一致才会让一个线程加锁,否则起不到加锁的作用。
锁对象的使用规范:使用共享资源作为锁对象
- //修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
- //快捷键:ctrl+alt+t
- public void drawMoney(double drawMoney){
- String name = Thread.currentThread().getName();
- //同步代码块
- synchronized (this) {
- if (money >= drawMoney){
- System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
- money -= drawMoney;
- System.out.println(name + "取钱后的余额为:" + money);
- }else {
- System.out.println(name + "取钱失败,余额不足!");
- }
- }
- }
- /*
- 小明取钱成功,吐出钞票:100000.0
- 小明取钱后的余额为:0.0
- 小红取钱失败,余额不足!
- */
2.对于静态方法建议使用字节码(类名.class)对象作为锁对象
- //示例
- public static void test(){
- //同步代码块
- synchronized (Account.class) {
- }
- }
作用:把访问共享资源的核心方法给上锁,以此保证线程安全
底层原理(与同步代码块一致,只是同步方法锁的范围更大)
- //修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
- //使用synchronized修饰方法,同步方法
- public synchronized void drawMoney(double drawMoney){
- String name = Thread.currentThread().getName();
- //同步代码块
- if (money >= drawMoney){
- System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
- money -= drawMoney;
- System.out.println(name + "取钱后的余额为:" + money);
- }else {
- System.out.println(name + "取钱失败,余额不足!");
- }
- }
- /*
- 小明取钱成功,吐出钞票:100000.0
- 小明取钱后的余额为:0.0
- 小红取钱失败,余额不足!
- */
- //修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
-
- //创建锁对象
- private final Lock lock = new ReentrantLock();
-
- public void drawMoney(double drawMoney){
- String name = Thread.currentThread().getName();
- //使用try\cathc\finally代码块来确保锁一定会被释放,
- //否则一旦代码块中出现异常,锁就不会被释放
- try {
- //加锁
- lock.lock();
- if (money >= drawMoney){
- System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
- money -= drawMoney;
- System.out.println(name + "取钱后的余额为:" + money);
- }else {
- System.out.println(name + "取钱失败,余额不足!");
- }
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- //释放锁
- lock.unlock();
- }
- }
- /*
- 小明取钱成功,吐出钞票:100000.0
- 小明取钱后的余额为:0.0
- 小红取钱失败,余额不足!
- */
线程通信
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺(线程通讯的前提是必须线程安全)
线程通信的常见模型(生产者与消费者模型)
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据。
Object类提供的方法 | 说明 |
---|---|
void wait( ) | 让当前线程等待并释放所占锁,直到另一个线程调用notify( )方法或notifyA11( )方法 |
void notify( ) | 唤醒正在等待的单个线程 |
void notifyAll( ) | 唤醒正在等待的所有线程 |
- public class Desk {
- private List
list=new ArrayList<>(); - public synchronized void put(){
- try {
- String name = Thread.currentThread().getName();
- if(list.isEmpty()){
- list.add(name+" 做的包子");
- System.out.println(name+" 做的包子");
- Thread.sleep(1000);
- }
- this.notifyAll();
- this.wait();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
- public synchronized void get(){
- try {
- String name = Thread.currentThread().getName();
- if(!list.isEmpty()){
- list.clear();
- System.out.println(name+" 吃了包子");
- Thread.sleep(100);
- }
- this.notifyAll();
- this.wait();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
-
-
-
-
-
- }
- public class Test {
- public static void main(String[] args) {
- Desk desk = new Desk(); //创建桌子对象
- //创建厨师线程
- new Thread(()->{
- while (true) {
- desk.put();
- }
- }, "厨师1").start();
- new Thread(()->{
- while (true) {
- desk.put();
- }
- }, "厨师2").start();
- new Thread(()->{
- while (true) {
- desk.put();
- }
- }, "厨师3").start();
- //创建吃货线程
- new Thread(()->{
- while (true) {
- desk.get();
- }
- }, "吃货1").start();
- new Thread(()->{
- while (true) {
- desk.get();
- }
- }, "吃货2").start();
- }
- }
线程池就是一个可以复用线程的技术
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
JDK5.0起提供了代表线程池的接口:ExecutorService
使用实现了ExecutorService接口的实现类ThreadPoolExecutor创建线程池对象
- //格式
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue
workQueue, - ThreadFactory threadFactory,
- RejectedExecutionHandler handler)
- //实例
- public class Test{
- public static void main(String[] args) {
- // 创建线程池方式一
- /*
- 线程池中有三个核心线程
- 线程池中有五个最大线程
- 线程池中有(5-3=2)个临时线程
- 临时线程的存活时间为8秒
- 线程池中有4个任务队列
- 拒绝策略为:抛出异常并丢弃任务
- */
- new ThreadPoolExecutor(3,5,8,
- TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
- Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
- }
- }
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。(默认策略) |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常(不推荐) |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run( )方法从而绕过线程池直接执行 |
- //创建Runnable任务类
- public class MyRunnable implements Runnable{
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName() + "线程==>被调用了");
- try {
- //让线程睡眠一会,从而便于模拟并发环境
- Thread.sleep(Integer.MAX_VALUE);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
- public class Test{
- public static void main(String[] args) {
- // 创建线程池
- ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
- TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
- Executors.defaultThreadFactory(),
- new ThreadPoolExecutor.AbortPolicy());
- // 创建Runnable任务实例对象
- Runnable target = new MyRunnable();
- //下面的三个任务被三个核心线程处理,不占用任务队列中的空间
- pool.execute(target);
- //该方法会在线程池中自动创建一个新线程自动处理任务,前提是线程池中有空闲线程
- pool.execute(target);
- pool.execute(target);
- //下面的四个任务进入任务队列等待处理
- pool.execute(target);
- pool.execute(target);
- pool.execute(target);
- pool.execute(target);
- //当任务队列满了并且核心线程全部被占用,会创建临时线程处理任务,
- //但是只能创建两个临时线程,因为最大线程数为5
- //下面的两个任务会被临时线程处理
- pool.execute(target);
- pool.execute(target);
- // 下面的两个任务因为临时线程已经达到最大线程数,所以会根据拒绝策略进行处理
- pool.execute(target);
- }
- }
- /*
- 结果见下图
- 此时由于三个核心线程被占用,四个任务也在任务队列中等待处理,两个临时线程也被占用,所以会根据拒绝策略进行任务处理
- */
使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
Executors是一个线程池工具类,提供了许多静态方法用于返回不同特点的线程池对象
方法名称 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor( ) | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool( ) | 线程数量随着任务增加而增加,如果线程任务执行完并且空闲了6s则会被回收掉。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolsize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
缺点:大型并发系统环境中使用Executors?如果不注意可能会出现系统风险
FixedThreadPool和SingleThreadPool
允许的请求队列长度为Integer.MAX VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)。
CachedThreadPool
允许的创建线程数量为Integer.MAX VALUE,可能会创建大量的线程,从而导致OOM(内存溢出)。
ExecutorService的常用方法 | 说明 |
---|---|
void execute(Runnable command) | 执行Runnable任务 |
Future | 执行Cal1able任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown( ) | 等全部任务执行完毕后,再关闭线程池! |
List | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
注意:一般不会关闭线程池,如果实在需要关闭,则使用void shutdown( )方法关闭
- //1.创建一个实现Callable接口的实现类
- public class MyCallable implements Callable
{ - private int n;
-
- public MyCallable(int n) {
- this.n = n;
- }
- //2.重写call方法,call方法的方法体就是新的线程执行体
- @Override
- public Double call() throws Exception {
- double sum = 0;
- for (int i = 1; i <= n; i++) {
- sum += i;
- }
- return sum;
- }
- }
- public class Test3 {
- public static void main(String[] args) throws Exception{
- //创建线程池
- ExecutorService pool = Executors.newFixedThreadPool(3);
- //创建Callable任务对象
- Callable
c1 = new MyCallable(100); - Callable
c2 = new MyCallable(200); - Callable
c3 = new MyCallable(300); - //执行任务并获取Future对象
- Future
s1 = pool.submit(c1); - Future
s2 = pool.submit(c2); - Future
s3 = pool.submit(c3); - //输出结果
- System.out.println(s1.get());
- System.out.println(s2.get());
- System.out.println(s3.get());
- }
- }
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
在同一个时刻上,同时有多个线程在被CPU调度执行。
- public enum State {
- NEW,
- RUNNABLE,
- BLOCKED,
- WAITING,
- TIMED_WAITING,
- TERMINATED;
- }
线程状态 | 说明 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动 |
Runnable(可运行) | 线程已经调用了start( ),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |