三. 多线程
- 线程创建方式
方式1:继承Thread类
步骤:
A:自定义类MyThread继承Thread类。
B:MyThread类里面重写run():不是类中的所有代码都需要被线程执行的。而这个时候,
为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
C:创建对象
D:启动线程 start()
方式2:实现Runnable接口
步骤:
A:自定义类MyRunnable实现Runnable接口
B:重写run()方法
C:创建MyRunnable类的对象
D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
方式3:实现Callable<T>接口,T表示返回类型。必须与线程池结合使用。
- 哪种方式更常用?实现接口的方法更好,因为:
-
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
-
- 方便同一个对象被多个线程使用
-
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
- 启动线程为什么用start()
- 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
- run():仅仅是封装被线程执行的代码,直接调用是普通方法;start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
- synchronized 关键字:解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。4种使用方法:(看不懂)
1 synchronized(this):当a线程执行到该语句时,锁住该语句所在对象object,其它线程
无法访问object中的所有synchronized代码块。
2 synchronized(obj):锁住对象obj,其它线程对obj中的所有synchronized代码块的访问
被阻塞。
3 synchronized method():与(1)类似,区别是(1)中线程执行到某方法中的该语句才会
获得锁,而对方法加锁则是当方法调用时立刻获得锁。
4 synchronized static method():当线程执行到该语句时,获得锁,所有调用该方法的其
它线程阻塞,但是这个类中的其它非static声明的方法可以访问,即使这些方法是用
synchronized声明的,但是static声明的方法会被阻塞;注意,这个锁与对象无关。
- Synchronized和Lock的区别
- 首先synchronized是java内置关键字在jvm层面,Lock是个java类。
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
- synchronized的实现原理
-
- jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。
-
- 方法级的同步:JVM利用ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。如果该访问标志被设置,执行线程将先持有monitor, 然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。
-
- 代码块的同步:利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
- 锁升级
- 锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
- 为什么要有线程池:
- 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下。
- ThreadPoolExecutor构造函数重要参数分析
- corePoolSize:指定了线程池中的线程数量
- maximumPoolSize:指定了线程池中的最大线程数量
- keepAliveTime:线程池维护线程所允许的空闲时间
- unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。
ThreadPoolExecutor thpool = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,...)
- 线程池的执行流程
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException
- sleep()和wait()方法的区别
- sleep():必须指定时间;它不释放锁。 sleep(long millis)
- wait():可以不指定时间,也可以指定时间;它释放锁。
9 线程提交submit()和execute()有什么区别
- submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
- ThreadLocal
- ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
- volatile
- 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 2)禁止进行指令重排序。