目录
1.专业术语
程序:静态的代码。
进程:动态的代码,一次程序的运行就包括一个进程的创建、运行和消亡。
线程:一个进程包含多个线程,各个进程之间是互相不相关,但是各个线程是相互紧密联系的。
为什么使用线程?
因为线程的调用比进程的调用所消耗的资源要少,所需要的时间要少,并且也方便管理
另外随着多核CPU的出现,各个线程可以共同运行,减少了线程的上下文切换
2.线程基本状态
new创建-------start()方法表示进入到线程模式-------获取到CPU的执行权以后,进入运行状态------遇到sleep()或者其他的阻塞方式进入阻塞状态---------阻塞时间结束又进入到就绪状态-------run()方法运行结束进入到线程结束
start()方法和run()方法的区别:
start()方法才是使用线程的方式进行,而run()方法是属于Object方法的,用它的话,就是在main主线程上运行。
sleep()方法和wait()方法的区别:
sleep()方法没有释放了锁,wait()方法释放了锁;
sleep()方法是用来进行线程等待的,wait()方法是用来进行线程之间的交互的;
sleep()方法可以用在任意地方,wait()方法用在Lock锁上;
sleep()方法可以自己苏醒,wait()方法需要其他线程调用notify或者notifyAll()方法才能进行;
3.多线程的创建
3.1 继承Thread
public class OneThread {
public static void main(String[] args) {
new Thread1().start();
}
static class Thread1 extends Thread{
@Override
public void run() {
System.out.println("Thread1.........");
}
}
}
3.2 实现Runnable
public class TwoThread {
public static void main(String[] args) {
new Thread2().run();
}
static class Thread2 implements Runnable{
@Override
public void run() {
System.out.println("Thread2.........");
}
}
}
3.3 实现了Callable
public class ThreeThread {
public static void main(String[] args) {
Thread3 thread3 = new Thread3(); //Callable对象
FutureTask futureTask = new FutureTask(thread3);//FutureTask对象
Thread thread = new Thread(futureTask,"线程1.......");//Thread对象
thread.start();
}
static class Thread3 implements Callable{
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+"正在运行");
return true;
}
}
}
3.4 使用线程池
1.用人家的线程池
ExecutorService executorService1 = Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
第一个线程池:缓存线程池:可以根据任务数量创建相对应的线程数,但是核心线程数:0-2的31次-1 个 可能会造成内存泄漏
第二个线程池:定长线程池:一个固定线程数量的线程池,但是等待队列是链表,可能链表太长造成内存溢出
第三个线程池:单例线程池:就是定长线程池的单一化,一次能创建一个线程,但是等待队列是链表,也可能链表太长造成内存溢出
2.自定义线程池
BlockingQueue<Runnable> BlockingDeque = null;
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, BlockingDeque);
new ThreadPoolExecutor(参数1,参数2........)
参数1:corePoolSize 核心线程数
参数2:maxmumPoolSize 最大线程数
参数3:keepAliveTime 线程存活时间
参数4:timeUnit 线程存活的时间的单元
参数5:workQueue 等待队列
参数6:ThreadFactory 线程工厂
参数7:defaultHandler 饱和策略
4.线程同步问题
4.1 synchronized
synchronized关键字解决了多个线程之间访问资源的同步性,synchronized可以保证被它修饰的方法或代码块,在任何时候都只能有一个线程执行它。
synchronzied关键字的底层原理:
1)synchronized同步语句块的实现:
使用的是monitorenter和mointorexit指令,其中mointorenter指令指向同步代码块开始的位置,monitorexit指令则指向同步代码块的结束位置。
在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。在执行mointorexit指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
2)synchronized修饰方法的实现:
acc_synchronized标识,该标识指明了该方法是一个同步方法。JVM通过该标识来辨别一个方法是否是同步方法,从而执行相应的同步调用。
不过两者的本质都是获取对象监视器monitor。
4.2 volatile
volatile关键字除了防止JVM的指令重排,保证变量的可见性。
synchronized关键字和volatile关键字的区别
1)synchronized和volatile关键字是两个互补的存在,而不是对立的存在。
2)volatile关键字是线程同步的轻量级实现,所以它的性能比synchronized要好,但是volatile只能用于变量,而synchronized可以修饰方法以及代码块。
3)volatile能保证数据的可见性,但不能保证数据的原子性。synchronized两者都能保证。
4)volatile主要用于解决变量在多个线程之间的可见性。而synchronized解决的是多个线程之间访问资源的同步性。
4.3 ThreadLocal
ThreadLocal类主要解决的就是让每个线程都拥有自己本地的变量。
ThreadLocal内存泄露问题:
ThreadLocal里面有一个ThreadLocalMap,Map有key 和 value,其中key是弱引用,value是强引用,因此会导致出现带有null的value值。这就是内存泄漏。
弱引用:不管有没有内存,都会被垃圾回收器处理;
强引用:不管有没有内存,都不会被垃圾回收器处理
4.4 Lock
一般会使用它的子类ReentrantLock,并且使用lock()方法和unlock()方法。
Lock的原理:
lock是一个接口,里面只定义了lock、trylock、unlock等方法,所以实现原理我们直接从ReentrentLock来看。
ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AQS。
线程使用ReentrantLock获取锁分为两个阶段:
第一个阶段是初次竞争(ReentrantLock默认使用非公平锁,当我们调用ReentrantLock的lock方法的时候,实际上它调用的是非公平锁的lock(),这个方法先用CAS操作,去尝试抢占该锁。如果成功,就把当前线程设置在这个锁上,表示抢占成功,如果失败,就调用LockSupport.park将当前线程阻塞,将其加入CLH队列中,等待抢占),
第二个阶段是基于CLH队列的竞争。(然后进入CLH队列的抢占模式,当持有锁的那个线程调用unlock的时候,会将CLH队列的头结点的下一个节点线程唤醒,调用的是LockSupport.unpark()方法。)
在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CLH队列的锁竞争中,依靠CAS操作来抢占锁,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。
/**
* @packageName: PACKAGE_NAME
* @user: lixi
* @date: 2022/11/14 18:06
* @email 1831309074@qq.com
* @description: 100张票,4个窗口
*/
public class LockDemo implements Runnable{
public static void main(String[] args) {
//创建线程对象
LockDemo lockDemo = new LockDemo();
new Thread(lockDemo,"售票窗口1").start();
new Thread(lockDemo,"售票窗口2").start();
new Thread(lockDemo,"售票窗口3").start();
new Thread(lockDemo,"售票窗口4").start();
}
//100张票
private static int count = 100;
//锁
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (count > 0){
sellTickets();
}
}
private void sellTickets() {
try {
//设置休眠时间
Thread.sleep(30);
}catch (Exception e){
//在命令行打印异常信息在程序中出错的位置及原因
e.printStackTrace();
}
try {
//上锁
lock.lock();
if (count > 0){
System.out.println(Thread.currentThread().getName()+",正在售票:"+(100 - count + 1));
count --;
}
}catch (Exception e){
//获取异常信息
e.getCause();
}finally {
lock.unlock();
}
}
}
5.使用多线程造成的问题
5.1 上下文切换
多线程编程中⼀般线程的个数都大于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
简单来说,当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次在切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换对系统来说意味着消耗大量的CPU时间,事实上,它就是操作系统中消耗时间最大的操作。Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。
5.2 线程死锁
线程死锁描述的是,多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于被无限期地阻塞,因此不可能正常终止。
死锁的条件:
1)互斥条件:该资源任意一个时刻只由一个线程占用。
2)请求与保持条件:一个进程因请求资源而阻塞时,对已获取的资源保持不放。
3)不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4)循环等待条件:若干进程之间形成一种头尾相接的等待资源关系。
死锁的解决:
1)破坏互斥条件:这个条件无法破坏,因为用锁本来就是想让它们互斥。
2)破坏请求与保持条件:一次性申请所有的资源。
3)破坏不剥夺条件:占用部分资源的线程,进一步申请其他资源时,如果申请不到,可以主动释放它当Sy前占用的资源。
4)破坏循环等待条件:使用按序申请资源来预防。按某种顺序申请资源,释放资源则反序释放。
5.3 内存泄漏
参看ThreadLocal内存泄漏